Compare commits
14 commits
Author | SHA1 | Date | |
---|---|---|---|
6b2345a726 | |||
|
d7a54202b3 | ||
|
d9ceca8b77 | ||
|
d348ba7a2e | ||
|
6f49244dbd | ||
|
d50b0bd8de | ||
|
d65bcb3e4a | ||
|
b94a3d2ae4 | ||
|
0dfb1fcd4e | ||
|
d5e45f4838 | ||
|
ff5bf4f5d3 | ||
|
236619cd42 | ||
|
39ecd971dc | ||
|
c9a59162c0 |
13 changed files with 210 additions and 141 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -168,3 +168,5 @@ cython_debug/
|
||||||
|
|
||||||
instance/
|
instance/
|
||||||
images/
|
images/
|
||||||
|
|
||||||
|
config.json
|
||||||
|
|
66
README.md
Normal file
66
README.md
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
# imag
|
||||||
|
|
||||||
|
> simple image board, intended for a quotebook of screenshots
|
||||||
|
|
||||||
|
the imag image board is made to be simple, though separated, so you
|
||||||
|
could easily add or remove features, update them, etc
|
||||||
|
|
||||||
|
example instance: https://quotes.everypizza.im/
|
||||||
|
|
||||||
|
# licensing
|
||||||
|
|
||||||
|
this program is under the Strongest Public License, mostly as a joke.
|
||||||
|
this one, Nyx Tutt, gives you full permission to ignore Clause ⑨.
|
||||||
|
|
||||||
|
# prerequisites
|
||||||
|
|
||||||
|
- tesseract: https://github.com/tesseract-ocr
|
||||||
|
- tesseract data
|
||||||
|
|
||||||
|
# bot
|
||||||
|
|
||||||
|
there's a matrix bot at n/quotes-bot and a very WIP rewrite in python at n/quotes-bot-python.s
|
||||||
|
|
||||||
|
# docs & running
|
||||||
|
|
||||||
|
see the [doc directory](/doc) for documentation, it also has an example nginx and caddy config,
|
||||||
|
and you can also run the app using [./scripts/run.sh](./scripts/run.sh) to match that config - but don't run it using
|
||||||
|
the run.sh as the first run.
|
||||||
|
|
||||||
|
running with gunicorn (run.sh) is for production use, for master key generation (first run), please
|
||||||
|
run it in dev mode after making the config:
|
||||||
|
|
||||||
|
make the config:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cp src/imag/config.sample.json src/imag/config.json && nano config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
run it:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 src/main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
(make sure to save the key!)
|
||||||
|
and only then with gunicorn
|
||||||
|
|
||||||
|
if you already ran it in production and don't know where the key is, run the following command:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
rm -rf src/instance
|
||||||
|
```
|
||||||
|
|
||||||
|
and then run it in debug
|
||||||
|
|
||||||
|
### step-by-step
|
||||||
|
|
||||||
|
this comes from an email the original creator got from a user:
|
||||||
|
|
||||||
|
1. clone the repository: `git clone https://git.everypizza.im/n/imag`
|
||||||
|
2. make sure you have virtualenv installed (either through python-virtualenv / python3-virtualenv / py3-virtualenv packages, or by pip - `python3 -m pip install --user --break-system-packages --upgrade virtualenv`)
|
||||||
|
3. ensure you have sqlite3 and memcached installed: `apt install sqlite3 memcached`
|
||||||
|
4. create a new virtual environment: `python3 -m virtualenv venv && source venv/bin/activate`
|
||||||
|
5. install the dependencies in the environment: `pip install -r requirements.txt`
|
||||||
|
6. configure the instance: `cp src/imag/config.sample.json src/imag/config.json && nano src/imag/config.json`
|
||||||
|
6. run the app by either running `scripts/run.sh` or by manually starting memcached and running `src/main.py` with gunicorn (i assume you're reverse proxying it anyway)
|
|
@ -1,64 +0,0 @@
|
||||||
# imag
|
|
||||||
|
|
||||||
> simple and hackable suckless image board
|
|
||||||
|
|
||||||
the imag image board is made to be simple, though separated, so you
|
|
||||||
could easily add or remove features, update them, etc
|
|
||||||
|
|
||||||
this is not sole software, it is suckless ( ? ) software, which you are meant
|
|
||||||
to hack yourself,, you can, of course, use the default settings and whatnot,
|
|
||||||
but it is highly encouraged to make forks and hack it yourself :)
|
|
||||||
|
|
||||||
example instance : https://quotes.ari.lt/
|
|
||||||
|
|
||||||
~~i hate github versioning so much~~
|
|
||||||
|
|
||||||
# licensing
|
|
||||||
|
|
||||||
you can distribute, modify, share, redistribute, etc etc etc with no credit or anything,
|
|
||||||
this project is released under unlicense and i give away all my rights to this project :)
|
|
||||||
|
|
||||||
license : unlicense
|
|
||||||
|
|
||||||
# prerequisites
|
|
||||||
|
|
||||||
- tesseract : <https://github.com/tesseract-ocr>
|
|
||||||
- tesseract english data ( or whatever other languages ) : <https://github.com/tesseract-ocr/tessdata/blob/main/eng.traineddata>
|
|
||||||
|
|
||||||
# bot
|
|
||||||
|
|
||||||
i made a matrix bot to integrate well with this, it is open source : <https://ari.lt/gh/quotes-bot>, mainly for purpose of posting quotes
|
|
||||||
|
|
||||||
# docs & running
|
|
||||||
|
|
||||||
see the [doc directory](/dov) for documentation, it also has an example nginx config,
|
|
||||||
and you can also run the app using [./scripts/run.sh](./scripts/run.sh) to match that config :) - but don't run it using
|
|
||||||
the run.sh as the first run if you ever want to post on it lol
|
|
||||||
|
|
||||||
running with gunicorn ( run.sh ) is for production use, for master key generation ( first run ), please
|
|
||||||
run it in dev mode :
|
|
||||||
|
|
||||||
```sh
|
|
||||||
python3 src/main.py
|
|
||||||
```
|
|
||||||
|
|
||||||
and only then with gunicorn :)
|
|
||||||
|
|
||||||
if you already ran it in production and don't know where the key is, run the following command :
|
|
||||||
|
|
||||||
```sh
|
|
||||||
rm -rf src/images src/instance
|
|
||||||
```
|
|
||||||
|
|
||||||
and then run it in debug
|
|
||||||
|
|
||||||
### step-by-step
|
|
||||||
|
|
||||||
this comes from an email i got from a user :
|
|
||||||
|
|
||||||
1. clone the repository : `git clone https://ari.lt/gh/imag && cd imag`
|
|
||||||
2. make sure you have virtualenv installed ( either through python-virtualenv / python3-virtualenv / py3-virtualenv packages, or by pip - `python3 -m pip install --user --break-system-packages --upgrade virtualenv`
|
|
||||||
3. ensure you have sqlite3 and memcached installed : `apt install sqlite3 memcached`
|
|
||||||
4. create a new virtual environment : `python3 -m virtualenv venv && source venv/bin/activate`
|
|
||||||
5. install the dependencies in the environment : `pip install -r requirements.txt`
|
|
||||||
6. run the app by either running `scripts/run.sh` or by manually starting memcached and running `src/main.py` with gunicorn ( i assume you're reverse proxying it anyway )
|
|
4
doc/Caddyfile
Normal file
4
doc/Caddyfile
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
https://quotes.example.com {
|
||||||
|
tls alice@example.com
|
||||||
|
reverse_proxy http://127.0.0.1:19721
|
||||||
|
}
|
|
@ -1,40 +0,0 @@
|
||||||
# imag: weird image board thingy
|
|
||||||
|
|
||||||
> note: this is a fork of a project that doesn't exist anymore. this document is mostly paraphrased from README.original.md.
|
|
||||||
|
|
||||||
this is intended to be hacked upon! please do make forks and tell the creator what you've made. our "showcase" is at [quotes.everypizza.im](https://quotes.everypizza.im/).
|
|
||||||
|
|
||||||
# prerequisites
|
|
||||||
|
|
||||||
- tesseract
|
|
||||||
- tesseract data
|
|
||||||
- python
|
|
||||||
|
|
||||||
# bot
|
|
||||||
|
|
||||||
there's a very small bot at [n/quotes-bot](https://git.everypizza.im/n/quotes-bot). copied from the original with some very small changes.
|
|
||||||
|
|
||||||
# running
|
|
||||||
|
|
||||||
first run:
|
|
||||||
```sh
|
|
||||||
python3 src/main.py
|
|
||||||
```
|
|
||||||
this generates a key. keep it, you need it to add new images!
|
|
||||||
|
|
||||||
afterwards:
|
|
||||||
```sh
|
|
||||||
rm -rf src/images src/instance
|
|
||||||
```
|
|
||||||
### step-by-step
|
|
||||||
|
|
||||||
> these are directly from the original docs.
|
|
||||||
|
|
||||||
this comes from an email i got from a user :
|
|
||||||
|
|
||||||
1. clone the repository : `git clone https://ari.lt/gh/imag && cd imag`
|
|
||||||
2. make sure you have virtualenv installed ( either through python-virtualenv / python3-virtualenv / py3-virtualenv packages, or by pip - `python3 -m pip install --user --break-system-packages --upgrade virtualenv`
|
|
||||||
3. ensure you have sqlite3 and memcached installed : `apt install sqlite3 memcached`
|
|
||||||
4. create a new virtual environment : `python3 -m virtualenv venv && source venv/bin/activate`
|
|
||||||
5. install the dependencies in the environment : `pip install -r requirements.txt`
|
|
||||||
6. run the app by either running `scripts/run.sh` or by manually starting memcached and running `src/main.py` with gunicorn ( i assume you're reverse proxying it anyway )
|
|
|
@ -4,12 +4,19 @@
|
||||||
|
|
||||||
import secrets
|
import secrets
|
||||||
import typing as t
|
import typing as t
|
||||||
from os import makedirs
|
from os import makedirs, path
|
||||||
|
import json
|
||||||
import flask
|
import flask
|
||||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
|
|
||||||
from . import const
|
from . import const, models
|
||||||
|
from .api import api
|
||||||
|
from .views import views
|
||||||
|
|
||||||
|
configLocation = path.dirname(path.abspath(__file__))
|
||||||
|
with open(configLocation + '/config.json', 'r') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
message = config["message"]
|
||||||
|
|
||||||
__version__: str = "3.1.2-nyx"
|
__version__: str = "3.1.2-nyx"
|
||||||
|
|
||||||
|
@ -28,9 +35,6 @@ def create_app(db: str = "sqlite:///imag.db") -> t.Tuple[flask.Flask, t.Optional
|
||||||
app.config["PREFERRED_URL_SCHEME"] = "https"
|
app.config["PREFERRED_URL_SCHEME"] = "https"
|
||||||
|
|
||||||
app.config["SECRET_KEY"] = secrets.SystemRandom().randbytes(1024 * 16)
|
app.config["SECRET_KEY"] = secrets.SystemRandom().randbytes(1024 * 16)
|
||||||
|
|
||||||
from . import models
|
|
||||||
|
|
||||||
models.limiter.init_app(app)
|
models.limiter.init_app(app)
|
||||||
models.db.init_app(app)
|
models.db.init_app(app)
|
||||||
|
|
||||||
|
@ -55,6 +59,7 @@ def create_app(db: str = "sqlite:///imag.db") -> t.Tuple[flask.Flask, t.Optional
|
||||||
"desc_len": const.DESC_LEN,
|
"desc_len": const.DESC_LEN,
|
||||||
"key_len": const.KEY_LEN,
|
"key_len": const.KEY_LEN,
|
||||||
"imagv": __version__,
|
"imagv": __version__,
|
||||||
|
"imagmessage": message
|
||||||
}
|
}
|
||||||
|
|
||||||
@app.after_request
|
@app.after_request
|
||||||
|
@ -77,9 +82,6 @@ def create_app(db: str = "sqlite:///imag.db") -> t.Tuple[flask.Flask, t.Optional
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
from .api import api
|
|
||||||
from .views import views
|
|
||||||
|
|
||||||
app.register_blueprint(api, url_prefix="/api/")
|
app.register_blueprint(api, url_prefix="/api/")
|
||||||
app.register_blueprint(views, url_prefix="/")
|
app.register_blueprint(views, url_prefix="/")
|
||||||
|
|
||||||
|
|
3
src/imag/config.sample.json
Normal file
3
src/imag/config.sample.json
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"message": "meow"
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ from .util import make_api
|
||||||
|
|
||||||
|
|
||||||
class Bp(Blueprint):
|
class Bp(Blueprint):
|
||||||
|
"""app routing helper"""
|
||||||
def get(self, rule: str, **kwargs: Any) -> Any:
|
def get(self, rule: str, **kwargs: Any) -> Any:
|
||||||
"""wrapper for GET"""
|
"""wrapper for GET"""
|
||||||
return self.route(rule=rule, methods=("GET",), **kwargs)
|
return self.route(rule=rule, methods=("GET",), **kwargs)
|
||||||
|
|
|
@ -7,13 +7,17 @@
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: #fdccd4 !important;
|
||||||
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
color: whitesmoke;
|
color: whitesmoke
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
max-width: 1100px;
|
max-width: 40em;
|
||||||
padding: 2em;
|
padding: 2em;
|
||||||
height: 100%
|
height: 100%
|
||||||
}
|
}
|
||||||
|
@ -110,3 +114,7 @@ hr.thin{
|
||||||
width:50%;
|
width:50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hr.tiny {
|
||||||
|
width:25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -60,6 +60,14 @@
|
||||||
<a href="https://matrix.to/#/@quotes-python:everypizza.im">@quotes-python:everypizza.im</a>
|
<a href="https://matrix.to/#/@quotes-python:everypizza.im">@quotes-python:everypizza.im</a>
|
||||||
</small>
|
</small>
|
||||||
</center>
|
</center>
|
||||||
|
<hr class="tiny">
|
||||||
|
<center>
|
||||||
|
<small>
|
||||||
|
Board message: {{ imagmessage }}
|
||||||
|
</small>
|
||||||
|
</center>
|
||||||
|
<hr class="thin">
|
||||||
|
|
||||||
|
|
||||||
{% with messages = get_flashed_messages(with_categories=True) %}
|
{% with messages = get_flashed_messages(with_categories=True) %}
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
|
@ -85,28 +93,9 @@
|
||||||
|
|
||||||
{% if q is not defined and "s=newest" not in request.url %} <a href="/?s=newest">Sort by newest</a> {% endif %}
|
{% if q is not defined and "s=newest" not in request.url %} <a href="/?s=newest">Sort by newest</a> {% endif %}
|
||||||
|
|
||||||
<details>
|
<form action="upload">
|
||||||
<summary>Or post an image</summary>
|
<input type="submit" value="Upload an image" />
|
||||||
|
|
||||||
<form method="POST" enctype="multipart/form-data">
|
|
||||||
<label for="desc">Image description:</label>
|
|
||||||
<input type="text" name="desc" id="desc" placeholder="description" autocomplete="off" maxlength="{{ desc_len }}" />
|
|
||||||
|
|
||||||
<br />
|
|
||||||
|
|
||||||
<label for="key">Access key:</label>
|
|
||||||
<input type="password" name="key" id="key" placeholder="key" maxlength="{{ key_len }}" required />
|
|
||||||
|
|
||||||
<br />
|
|
||||||
|
|
||||||
<label for="image">Image:</label>
|
|
||||||
<input type="file" accept="image/*" name="image" id="image" placeholder="the image" required />
|
|
||||||
|
|
||||||
<br />
|
|
||||||
|
|
||||||
<button type="post">Post</button>
|
|
||||||
</form>
|
</form>
|
||||||
</details>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
90
src/imag/templates/upload.j2
Normal file
90
src/imag/templates/upload.j2
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
|
{% if title is defined %}
|
||||||
|
<title>Imag - {{ title | escape }}</title>
|
||||||
|
<meta name="description" content="The Imag image board | {{ title | escape }}" />
|
||||||
|
{% else %}
|
||||||
|
<title>Imag</title>
|
||||||
|
<meta name="description" content="The Imag image board" />
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<meta
|
||||||
|
name="keywords"
|
||||||
|
content="imageboard, image board, image, image hosting"
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
name="robots"
|
||||||
|
content="follow, index, max-snippet:-1, max-video-preview:-1, max-image-preview:large"
|
||||||
|
/>
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
|
||||||
|
<meta name="color-scheme" content="dark" />
|
||||||
|
<meta name="theme-color" content="black" />
|
||||||
|
|
||||||
|
<meta name="license" content="WTFPL" />
|
||||||
|
|
||||||
|
<!-- preloads the css ( technically you can replace it with a style tag -->
|
||||||
|
<link
|
||||||
|
href="{{ url_for("static", filename="index.css") }}"
|
||||||
|
rel="preload"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
type="text/css"
|
||||||
|
as="style"
|
||||||
|
onload="this.onload=null;this.rel='stylesheet'"
|
||||||
|
/>
|
||||||
|
<noscript>
|
||||||
|
<link
|
||||||
|
href="{{ url_for("static", filename="index.css") }}"
|
||||||
|
rel="stylesheet"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
type="text/css"
|
||||||
|
/>
|
||||||
|
</noscript>
|
||||||
|
|
||||||
|
<script src="{{ url_for("static", filename="index.js") }}" defer></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>The <a href="https://git.everypizza.im/n/imag/src/branch/main">Imag</a> image board ({{ imagv }}). | upload</h1>
|
||||||
|
<center>
|
||||||
|
Matrix chat: <a href="https://matrix.to/#/#quotes:everypizza.im">#quotes:everypizza.im</a> |
|
||||||
|
Matrix bot: <a href="https://matrix.to/#/@quotes:everypizza.im">@quotes:everypizza.im</a> <br>
|
||||||
|
<hr class="thin">
|
||||||
|
<small>
|
||||||
|
There's a very WIP complete rewrite of the bot in Python currently:
|
||||||
|
<a href="https://matrix.to/#/@quotes-python:everypizza.im">@quotes-python:everypizza.im</a>
|
||||||
|
</small>
|
||||||
|
</center>
|
||||||
|
<hr class="tiny">
|
||||||
|
<center>
|
||||||
|
<small>
|
||||||
|
Board message: {{ imagmessage }}
|
||||||
|
</small>
|
||||||
|
</center>
|
||||||
|
<hr class="thin">
|
||||||
|
<form method="POST" enctype="multipart/form-data">
|
||||||
|
<label for="desc">Image description:</label>
|
||||||
|
<input type="text" name="desc" id="desc" placeholder="description" autocomplete="off" maxlength="{{ desc_len }}" />
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<label for="key">Access key:</label>
|
||||||
|
<input type="password" name="key" id="key" placeholder="key" maxlength="{{ key_len }}" required />
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<label for="image">Image:</label>
|
||||||
|
<input type="file" accept="image/*" name="image" id="image" placeholder="the image" required />
|
||||||
|
|
||||||
|
<br />
|
||||||
|
<button onclick="history.back()">Back</button>
|
||||||
|
<button type="post">Post</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -48,7 +48,6 @@ def with_access(
|
||||||
|
|
||||||
if access and (access.access_level.value >= access_level.value): # type: ignore
|
if access and (access.access_level.value >= access_level.value): # type: ignore
|
||||||
return fn(*args, **kwargs) # type: ignore
|
return fn(*args, **kwargs) # type: ignore
|
||||||
else:
|
|
||||||
flask.abort(403)
|
flask.abort(403)
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
|
@ -25,8 +25,15 @@ def index() -> str:
|
||||||
images=models.Image.query.order_by((models.Image.created if flask.request.args.get("s") == "newest" else models.Image.score).desc()).all(), # type: ignore
|
images=models.Image.query.order_by((models.Image.created if flask.request.args.get("s") == "newest" else models.Image.score).desc()).all(), # type: ignore
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@views.get("/upload")
|
||||||
|
def upload() -> str:
|
||||||
|
"""upload page"""
|
||||||
|
return flask.render_template(
|
||||||
|
"upload.j2",
|
||||||
|
images=models.Image.query.order_by((models.Image.created if flask.request.args.get("s") == "newest" else models.Image.score).desc()).all(), # type: ignore
|
||||||
|
)
|
||||||
|
|
||||||
@views.post("/")
|
@views.post("/upload")
|
||||||
@util.with_access(models.AccessLevel.write)
|
@util.with_access(models.AccessLevel.write)
|
||||||
def post_image() -> Response:
|
def post_image() -> Response:
|
||||||
"""post image"""
|
"""post image"""
|
||||||
|
@ -82,8 +89,10 @@ def image(iid: int) -> flask.Response:
|
||||||
with open(os.path.join(const.IMAGE_DIR, str(iid)), "rb") as fp:
|
with open(os.path.join(const.IMAGE_DIR, str(iid)), "rb") as fp:
|
||||||
file: bytes = fp.read()
|
file: bytes = fp.read()
|
||||||
return flask.Response(file, mimetype=magic.from_buffer(file, mime=True)) # type: ignore
|
return flask.Response(file, mimetype=magic.from_buffer(file, mime=True)) # type: ignore
|
||||||
except Exception:
|
except FileNotFoundError:
|
||||||
flask.abort(404)
|
flask.abort(404)
|
||||||
|
except Exception:
|
||||||
|
flask.abort(500)
|
||||||
|
|
||||||
|
|
||||||
@views.post("/edit/<int:iid>")
|
@views.post("/edit/<int:iid>")
|
||||||
|
|
Loading…
Add table
Reference in a new issue