153 lines
5.8 KiB
Python
153 lines
5.8 KiB
Python
#!/usr/bin/env python
|
|
|
|
import re
|
|
import secrets
|
|
from pathlib import Path
|
|
import os
|
|
import hashlib
|
|
from uuid import uuid4
|
|
|
|
from quart import Quart, url_for, redirect, session, make_response, render_template, request, send_file, abort, flash, g
|
|
from quart_db import QuartDB
|
|
import datetime
|
|
|
|
app = Quart(__name__)
|
|
app.config.from_envvar('APP_CONFIG', silent=True)
|
|
if app.config['SECRET_KEY'] is None:
|
|
if (p := Path('/tmp/eightserve-secret')).is_file():
|
|
app.config['SECRET_KEY'] = p.read_bytes()
|
|
|
|
else:
|
|
app.config['SECRET_KEY'] = os.urandom(32)
|
|
try:
|
|
p.touch(0o600)
|
|
p.write_bytes(app.config['SECRET_KEY'])
|
|
except:
|
|
pass
|
|
|
|
app.config.update(
|
|
SESSION_COOKIE_SECURE=True,
|
|
SESSION_COOKIE_HTTPONLY=True,
|
|
SESSION_COOKIE_SAMESITE='Lax',
|
|
# SERVER_NAME='8seg.jaseg.de',
|
|
)
|
|
DISPLAY_COLUMNS = app.config.get('DISPLAY_COLUMNS', 32)
|
|
MESSAGE_MAX_LINES = app.config.get('MESSAGE_MAX_LINES', 10)
|
|
|
|
db = QuartDB(app, url=app.config.get('DATABASE_URL', 'sqlite:///db.sqlite3'), )
|
|
|
|
|
|
@app.before_request
|
|
async def ensure_session_id():
|
|
if 'session_id' not in session:
|
|
session['session_id'] = str(uuid4())
|
|
|
|
|
|
@app.route('/', methods=['GET', 'POST'])
|
|
async def index():
|
|
if session.get('consent'):
|
|
return redirect(url_for('post'))
|
|
|
|
elif (await request.form).get('csrf_token') == session.get('csrf_token', -1):
|
|
session['consent'] = True
|
|
return redirect(url_for('post'))
|
|
|
|
else:
|
|
tok = session['csrf_token'] = secrets.token_urlsafe()
|
|
return await render_template('index.html', csrf_token=tok)
|
|
|
|
|
|
@app.route('/post', methods=['GET', 'POST'])
|
|
async def post():
|
|
if not session.get('consent'):
|
|
return redirect(url_for('index'))
|
|
|
|
result = await g.connection.fetch_one(
|
|
'SELECT COUNT(*) AS count FROM messages WHERE timestamp_displayed IS NULL AND suppress_display = 0')
|
|
digest = hashlib.sha3_256(b'eightserve_queue_length_random')
|
|
digest.update(request.remote_addr.encode())
|
|
digest.update(app.config['SECRET_KEY'])
|
|
digest.update(str(result['count']).encode())
|
|
digest.update(datetime.datetime.now().strftime('%y%m%d%H%M%S').encode())
|
|
digest = digest.digest()
|
|
count = round(max(5, (result['count'] * (1 + (digest[23]/255 - 0.5) * 0.1)) + (digest[42//2]%16 - 8))/5) * 5
|
|
|
|
if request.method == 'POST':
|
|
data = await request.form
|
|
message = data.get('message', '')
|
|
sub = re.sub("[^a-zA-Z0-9\',._:<(\[\])>!?^]", "", message).strip()
|
|
|
|
if data.get('csrf_token') != session.get('csrf_token', -1):
|
|
await flash('Incorrect CSRF token.')
|
|
|
|
elif not re.sub('[^a-zA-Z0-9\',._:<(\[\])>!?^]', '', message).strip():
|
|
await flash('Message does not contain any displayable characters.')
|
|
|
|
elif any(len(line.strip()) > DISPLAY_COLUMNS*2 for line in message.splitlines()):
|
|
await flash('Message has one or more lines that are too long.')
|
|
|
|
elif len(message.splitlines()) > MESSAGE_MAX_LINES:
|
|
await flash('Message has too many lines.')
|
|
|
|
else:
|
|
|
|
result = await g.connection.fetch_one(
|
|
'SELECT COUNT(*) AS count FROM shitlist WHERE remote_ip = :ip', {'ip': request.remote_addr})
|
|
on_shitlist = result['count'] > 0
|
|
|
|
result = await g.connection.fetch_one(
|
|
'SELECT COUNT(*) AS count FROM suslist WHERE remote_ip = :ip AND timestamp_added > datetime("now", :timeout)',
|
|
{'ip': request.remote_addr, 'timeout': '-' + app.config.get('SUSLIST_TIMEOUT', '01:00')})
|
|
on_suslist = result['count'] > app.config.get('SUSLIST_THRESHOLD', 5)
|
|
|
|
badwords = {row['word'] for row in await g.connection.fetch_all('SELECT word FROM badwords')}
|
|
words = [re.sub(r'[^a-zA-Z]', '', word).lower().strip() for word in message.split()]
|
|
has_badwords = any(word in badwords for word in words if word)
|
|
|
|
if on_shitlist:
|
|
suppress = True
|
|
moderated = False
|
|
|
|
elif on_suslist or has_badwords:
|
|
moderated = True
|
|
suppress = True
|
|
await g.connection.execute('INSERT INTO suslist (remote_ip, reason) VALUES (:ip, :reason)',
|
|
{'ip': request.remote_addr, 'reason': 'suslist' if on_suslist else 'badwords'})
|
|
|
|
else:
|
|
moderated = False
|
|
suppress = False
|
|
|
|
flags = []
|
|
if on_suslist:
|
|
flags.append('suslist')
|
|
if on_shitlist:
|
|
flags.append('shitlist')
|
|
if moderated:
|
|
flags.append('moderated')
|
|
if suppress:
|
|
flags.append('suppress')
|
|
|
|
flags = f' ({", ".join(flags)})' if flags else ''
|
|
print(f'Message submitted to queue(n={result["count"]}){flags}: {message!r}')
|
|
await g.connection.execute(
|
|
'''INSERT INTO messages (message, suppress_display, moderated, remote_ip)
|
|
VALUES (:message, :suppress_display, :moderated, :remote_ip)''', dict(
|
|
message=message,
|
|
suppress_display=suppress,
|
|
moderated=moderated,
|
|
remote_ip=request.remote_addr))
|
|
|
|
await flash('Message submitted.')
|
|
message = ''
|
|
|
|
tok = session['csrf_token'] = secrets.token_urlsafe()
|
|
return await render_template('post.html', cols=DISPLAY_COLUMNS, rows=MESSAGE_MAX_LINES, message=message, count=count, csrf_token=tok)
|
|
|
|
else:
|
|
tok = session['csrf_token'] = secrets.token_urlsafe()
|
|
return await render_template('post.html', cols=DISPLAY_COLUMNS, rows=MESSAGE_MAX_LINES, message='', count=count, csrf_token=tok)
|
|
|
|
if __name__ == '__main__':
|
|
app.run(debug=True)
|
|
|