From 9437e64936af87ff84ed0a4b5f7273e596bcf4a2 Mon Sep 17 00:00:00 2001
From: Ville Rantanen ']
- lines = text.split('\n')
+ html = [" "]
+ lines = text.split("\n")
in_code = False
in_list = False
for l in lines:
- if l == '':
+ if l == "":
in_list = False
if in_code:
- html.append('')
+ html.append("")
in_code = False
- html.append(' ')
+ html.append(" ")
continue
- if l.startswith(' '):
+ if l.startswith(" "):
in_list = False
l = l[2:]
if not in_code:
- html.append('
Date: Sun, 23 Jul 2023 20:23:48 +0300
Subject: [PATCH] use markdown2, add forced login. Added breadcrumbs
---
captcha.py | 13 +-
db/sqlite.py | 427 +++++++++++++-------
init_sqlite.sh | 6 +-
main.py | 772 +++++++++++++++++++++----------------
minimd.py | 50 ++-
password.py | 4 +-
requirements.txt | 1 +
schema.txt | 3 +-
templates/admin/index.html | 4 +
templates/comments.html | 2 +-
templates/forum.html | 2 +-
templates/thread.html | 1 +
tool.py | 13 +-
13 files changed, 771 insertions(+), 527 deletions(-)
diff --git a/captcha.py b/captcha.py
index eea5287..0736c82 100644
--- a/captcha.py
+++ b/captcha.py
@@ -1,20 +1,25 @@
from random import randint
import hashlib, base64
+
# FIXME hash can be reused
def generate(key):
- '''
+ """
Generate a simple CAPTCHA.
It is based on a simple math expression which stops the simplest of bots.
- '''
+ """
# The parameters are chosen such that they are simple to solve on paper.
a = randint(1, 10)
b = randint(1, 10)
c = randint(10, 20)
- return f'{a} * {b} + {c} = ', _hash_answer(key, str(a * b + c))
+ return f"{a} * {b} + {c} = ", _hash_answer(key, str(a * b + c))
+
def verify(key, answer, hash):
return _hash_answer(key, answer) == hash
+
def _hash_answer(key, answer):
- return base64.b64encode(hashlib.sha256((key + answer).encode('utf-8')).digest()).decode('ascii')
+ return base64.b64encode(
+ hashlib.sha256((key + answer).encode("utf-8")).digest()
+ ).decode("ascii")
diff --git a/db/sqlite.py b/db/sqlite.py
index c13fe36..9115374 100644
--- a/db/sqlite.py
+++ b/db/sqlite.py
@@ -1,18 +1,25 @@
import sqlite3
+
class DB:
def __init__(self, conn):
self.conn = conn
pass
def get_config(self):
- return self._db().execute('''
- select version, name, description, secret_key, captcha_key, registration_enabled from config
- '''
- ).fetchone()
+ return (
+ self._db()
+ .execute(
+ """
+ select version, name, description, secret_key, captcha_key, registration_enabled, login_required from config
+ """
+ )
+ .fetchone()
+ )
def get_forums(self):
- return self._db().execute('''
+ return self._db().execute(
+ """
select f.forum_id, name, description, thread_id, title, update_time
from forums f
left join threads t
@@ -23,20 +30,41 @@ class DB:
order by update_time desc
limit 1
)
- '''
+ """
)
def get_forum(self, forum_id):
- return self._db().execute('''
+ return (
+ self._db()
+ .execute(
+ """
select name, description
from forums
where forum_id = ?
- ''',
- (forum_id,)
- ).fetchone()
+ """,
+ (forum_id,),
+ )
+ .fetchone()
+ )
+
+ def get_thread_forum(self, thread_id):
+ """ Returns forum_id of a thread """
+ return (
+ self._db()
+ .execute(
+ """
+ select forum_id
+ from threads
+ where thread_id = ?
+ """,
+ (thread_id,),
+ )
+ .fetchone()[0]
+ )
def get_threads(self, forum_id, offset, limit, user_id):
- return self._db().execute('''
+ return self._db().execute(
+ """
select
t.thread_id,
title,
@@ -70,20 +98,22 @@ class DB:
order by t.update_time desc
limit ?
offset ?
- ''',
- (forum_id, user_id, limit, offset)
+ """,
+ (forum_id, user_id, limit, offset),
)
def get_thread(self, thread):
db = self._db()
- title, text, author, author_id, create_time, modify_time, hidden = db.execute('''
- select title, text, name, author_id, create_time, modify_time, hidden
+ title, text, author, author_id, create_time, modify_time, hidden, forum_id = db.execute(
+ """
+ select title, text, name, author_id, create_time, modify_time, hidden, forum_id
from threads, users
where thread_id = ? and author_id = user_id
- ''',
- (thread,)
+ """,
+ (thread,),
).fetchone()
- comments = db.execute('''
+ comments = db.execute(
+ """
select
comment_id,
parent_id,
@@ -97,59 +127,91 @@ class DB:
left join users
on author_id = user_id
where thread_id = ?
- ''',
- (thread,)
+ """,
+ (thread,),
+ )
+ return (
+ title,
+ text,
+ author,
+ author_id,
+ create_time,
+ modify_time,
+ comments,
+ hidden,
+ forum_id
)
- return title, text, author, author_id, create_time, modify_time, comments, hidden
def get_thread_title(self, thread_id):
- return self._db().execute('''
+ return (
+ self._db()
+ .execute(
+ """
select title
from threads
where thread_id = ?
- ''',
- (thread_id,)
- ).fetchone()
+ """,
+ (thread_id,),
+ )
+ .fetchone()
+ )
def get_thread_title_text(self, thread_id):
- return self._db().execute('''
+ return (
+ self._db()
+ .execute(
+ """
select title, text
from threads
where thread_id = ?
- ''',
- (thread_id,)
- ).fetchone()
+ """,
+ (thread_id,),
+ )
+ .fetchone()
+ )
def get_recent_threads(self, limit):
- return self._db().execute('''
+ return self._db().execute(
+ """
select thread_id, title, modify_date
from threads
order by modify_date
limit ?
- ''',
- (limit,)
+ """,
+ (limit,),
)
def get_comment(self, comment_id):
- return self._db().execute('''
+ return (
+ self._db()
+ .execute(
+ """
select title, c.text
from comments c, threads t
where comment_id = ? and c.thread_id = t.thread_id
- ''',
- (comment_id,)
- ).fetchone()
+ """,
+ (comment_id,),
+ )
+ .fetchone()
+ )
def get_subcomments(self, comment_id):
db = self._db()
- thread_id, parent_id, title = db.execute('''
+ thread_id, parent_id, title = db.execute(
+ """
select threads.thread_id, parent_id, title
from threads, comments
where comment_id = ? and threads.thread_id = comments.thread_id
- ''',
- (comment_id,)
+ """,
+ (comment_id,),
).fetchone()
# Recursive CTE, see https://www.sqlite.org/lang_with.html
- return thread_id, parent_id, title, db.execute('''
+ return (
+ thread_id,
+ parent_id,
+ title,
+ db.execute(
+ """
with recursive
descendant_of(id) as (
select comment_id from comments where comment_id = ?
@@ -171,112 +233,148 @@ class DB:
users
where id = comment_id
and user_id = author_id
- ''',
- (comment_id,)
+ """,
+ (comment_id,),
+ ),
)
def get_user_password(self, username):
- return self._db().execute('''
+ return (
+ self._db()
+ .execute(
+ """
select user_id, password
from users
where name = lower(?)
- ''',
- (username,)
- ).fetchone()
+ """,
+ (username,),
+ )
+ .fetchone()
+ )
def get_user_password_by_id(self, user_id):
- return self._db().execute('''
+ return (
+ self._db()
+ .execute(
+ """
select password
from users
where user_id = ?
- ''',
- (user_id,)
- ).fetchone()
+ """,
+ (user_id,),
+ )
+ .fetchone()
+ )
def set_user_password(self, user_id, password):
- return self.change_one('''
+ return self.change_one(
+ """
update users
set password = ?
where user_id = ?
- ''',
- (password, user_id)
+ """,
+ (password, user_id),
)
def get_user_public_info(self, user_id):
- return self._db().execute('''
+ return (
+ self._db()
+ .execute(
+ """
select name, about, banned_until
from users
where user_id = ?
- ''',
- (user_id,)
- ).fetchone()
+ """,
+ (user_id,),
+ )
+ .fetchone()
+ )
def get_user_private_info(self, user_id):
- return self._db().execute('''
+ return (
+ self._db()
+ .execute(
+ """
select about
from users
where user_id = ?
- ''',
- (user_id,)
- ).fetchone()
+ """,
+ (user_id,),
+ )
+ .fetchone()
+ )
def set_user_private_info(self, user_id, about):
db = self._db()
- db.execute('''
+ db.execute(
+ """
update users
set about = ?
where user_id = ?
- ''',
- (about, user_id)
+ """,
+ (about, user_id),
)
db.commit()
def get_user_name_role_banned(self, user_id):
- return self._db().execute('''
+ return (
+ self._db()
+ .execute(
+ """
select name, role, banned_until
from users
where user_id = ?
- ''',
- (user_id,)
- ).fetchone()
+ """,
+ (user_id,),
+ )
+ .fetchone()
+ )
def get_user_name(self, user_id):
- return self._db().execute('''
+ return (
+ self._db()
+ .execute(
+ """
select name
from users
where user_id = ?
- ''',
- (user_id,)
- ).fetchone()
+ """,
+ (user_id,),
+ )
+ .fetchone()
+ )
def add_thread(self, author_id, forum_id, title, text, time):
db = self._db()
c = db.cursor()
- c.execute('''
+ c.execute(
+ """
insert into threads (author_id, forum_id, title, text,
create_time, modify_time, update_time)
select ?, ?, ?, ?, ?, ?, ?
from users
where user_id = ? and banned_until < ?
- ''',
- (author_id, forum_id, title, text, time, time, time, author_id, time)
+ """,
+ (author_id, forum_id, title, text, time, time, time, author_id, time),
)
rowid = c.lastrowid
if rowid is None:
return None
db.commit()
- return db.execute('''
+ return db.execute(
+ """
select thread_id
from threads
where rowid = ?
- ''',
- (rowid,)
+ """,
+ (rowid,),
).fetchone()
def delete_thread(self, user_id, thread_id):
db = self._db()
c = db.cursor()
- c.execute('''
+ c.execute(
+ """
delete
from threads
-- 1 = moderator, 2 = admin
@@ -284,8 +382,8 @@ class DB:
author_id = ?
or (select 1 from users where user_id = ? and (role = 1 or role = 2))
)
- ''',
- (thread_id, user_id, user_id)
+ """,
+ (thread_id, user_id, user_id),
)
db.commit()
return c.rowcount > 0
@@ -293,7 +391,8 @@ class DB:
def delete_comment(self, user_id, comment_id):
db = self._db()
c = db.cursor()
- c.execute('''
+ c.execute(
+ """
delete
from comments
where comment_id = ?
@@ -304,8 +403,8 @@ class DB:
)
-- Don't allow deleting comments with children
and (select 1 from comments where parent_id = ?) is null
- ''',
- (comment_id, user_id, user_id, comment_id)
+ """,
+ (comment_id, user_id, user_id, comment_id),
)
db.commit()
return c.rowcount > 0
@@ -313,21 +412,23 @@ class DB:
def add_comment_to_thread(self, thread_id, author_id, text, time):
db = self._db()
c = db.cursor()
- c.execute('''
+ c.execute(
+ """
insert into comments(thread_id, author_id, text, create_time, modify_time)
select ?, ?, ?, ?, ?
from threads, users
where thread_id = ? and user_id = ? and banned_until < ?
- ''',
- (thread_id, author_id, text, time, time, thread_id, author_id, time)
+ """,
+ (thread_id, author_id, text, time, time, thread_id, author_id, time),
)
if c.rowcount > 0:
- c.execute('''
+ c.execute(
+ """
update threads
set update_time = ?
where thread_id = ?
- ''',
- (time, thread_id)
+ """,
+ (time, thread_id),
)
db.commit()
return True
@@ -336,16 +437,18 @@ class DB:
def add_comment_to_comment(self, parent_id, author_id, text, time):
db = self._db()
c = db.cursor()
- c.execute('''
+ c.execute(
+ """
insert into comments(thread_id, parent_id, author_id, text, create_time, modify_time)
select thread_id, ?, ?, ?, ?, ?
from comments, users
where comment_id = ? and user_id = ? and banned_until < ?
- ''',
- (parent_id, author_id, text, time, time, parent_id, author_id, time)
+ """,
+ (parent_id, author_id, text, time, time, parent_id, author_id, time),
)
if c.rowcount > 0:
- c.execute('''
+ c.execute(
+ """
update threads
set update_time = ?
where threads.thread_id = (
@@ -353,8 +456,8 @@ class DB:
from comments c
where comment_id = ?
)
- ''',
- (time, parent_id)
+ """,
+ (time, parent_id),
)
db.commit()
return True
@@ -363,7 +466,8 @@ class DB:
def modify_thread(self, thread_id, user_id, title, text, time):
db = self._db()
c = db.cursor()
- c.execute('''
+ c.execute(
+ """
update threads
set title = ?, text = ?, modify_time = ?
where thread_id = ? and (
@@ -371,13 +475,17 @@ class DB:
-- 1 = moderator, 2 = admin
or (select 1 from users where user_id = ? and (role = 1 or role = 2))
)
- ''',
+ """,
(
- title, text, time,
+ title,
+ text,
+ time,
thread_id,
- user_id, user_id, time,
user_id,
- )
+ user_id,
+ time,
+ user_id,
+ ),
)
if c.rowcount > 0:
db.commit()
@@ -387,7 +495,8 @@ class DB:
def modify_comment(self, comment_id, user_id, text, time):
db = self._db()
c = db.cursor()
- c.execute('''
+ c.execute(
+ """
update comments
set text = ?, modify_time = ?
where comment_id = ? and (
@@ -395,13 +504,16 @@ class DB:
-- 1 = moderator, 2 = admin
or (select 1 from users where user_id = ? and (role = 1 or role = 2))
)
- ''',
+ """,
(
- text, time,
+ text,
+ time,
comment_id,
- user_id, user_id, time,
user_id,
- )
+ user_id,
+ time,
+ user_id,
+ ),
)
if c.rowcount > 0:
db.commit()
@@ -409,19 +521,20 @@ class DB:
return False
def register_user(self, username, password, time):
- '''
+ """
Add a user if registrations are enabled.
- '''
+ """
try:
db = self._db()
c = db.cursor()
- c.execute('''
+ c.execute(
+ """
insert into users(name, password, join_time)
select lower(?), ?, ?
from config
where registration_enabled = 1
- ''',
- (username, password, time)
+ """,
+ (username, password, time),
)
if c.rowcount > 0:
db.commit()
@@ -429,12 +542,13 @@ class DB:
# up by name.
# ROWID is *probably* not always consistent (race conditions).
# Ideally we get the ID immediately on insert.
- return c.execute('''
+ return c.execute(
+ """
select user_id
from users
where name = lower(?)
- ''',
- (username,)
+ """,
+ (username,),
).fetchone()
return None
except sqlite3.IntegrityError:
@@ -442,17 +556,18 @@ class DB:
return None
def add_user(self, username, password, time):
- '''
+ """
Add a user without checking if registrations are enabled.
- '''
+ """
try:
db = self._db()
c = db.cursor()
- c.execute('''
+ c.execute(
+ """
insert into users(name, password, join_time)
values (lower(?), ?, ?)
- ''',
- (username, password, time)
+ """,
+ (username, password, time),
)
if c.rowcount > 0:
db.commit()
@@ -463,90 +578,102 @@ class DB:
return False
def get_users(self):
- return self._db().execute('''
+ return self._db().execute(
+ """
select user_id, name, join_time, role, banned_until
from users
- ''',
+ """,
)
def set_forum_name(self, forum_id, name):
- return self.change_one('''
+ return self.change_one(
+ """
update forums
set name = ?
where forum_id = ?
- ''',
- (name, forum_id)
+ """,
+ (name, forum_id),
)
def set_forum_description(self, forum_id, description):
- return self.change_one('''
+ return self.change_one(
+ """
update forums
set description = ?
where forum_id = ?
- ''',
- (description, forum_id)
+ """,
+ (description, forum_id),
)
def add_forum(self, name, description):
db = self._db()
- db.execute('''
+ db.execute(
+ """
insert into forums(name, description)
values (?, ?)
- ''',
- (name, description)
+ """,
+ (name, description),
)
db.commit()
- def set_config(self, server_name, server_description, registration_enabled):
- return self.change_one('''
+ def set_config(
+ self, server_name, server_description, registration_enabled, login_required
+ ):
+ return self.change_one(
+ """
update config
- set name = ?, description = ?, registration_enabled = ?
- ''',
- (server_name, server_description, registration_enabled)
+ set name = ?, description = ?, registration_enabled = ?, login_required = ?
+ """,
+ (server_name, server_description, registration_enabled, login_required),
)
def set_config_secrets(self, secret_key, captcha_key):
- return self.change_one('''
+ return self.change_one(
+ """
update config
set secret_key = ?, captcha_key = ?
- ''',
- (secret_key, captcha_key)
+ """,
+ (secret_key, captcha_key),
)
def set_user_ban(self, user_id, until):
- return self.change_one('''
+ return self.change_one(
+ """
update users
set banned_until = ?
where user_id = ?
- ''',
- (until, user_id)
+ """,
+ (until, user_id),
)
def set_user_role(self, user_id, role):
- return self.change_one('''
+ return self.change_one(
+ """
update users
set role = ?
where user_id = ?
- ''',
- (role, user_id)
+ """,
+ (role, user_id),
)
def set_thread_hidden(self, thread_id, hide):
- return self.change_one('''
+ return self.change_one(
+ """
update threads
set hidden = ?
where thread_id = ?
- ''',
- (hide, thread_id)
+ """,
+ (hide, thread_id),
)
def set_comment_hidden(self, comment_id, hide):
- return self.change_one('''
+ return self.change_one(
+ """
update comments
set hidden = ?
where comment_id = ?
- ''',
- (hide, comment_id)
+ """,
+ (hide, comment_id),
)
def change_one(self, query, values):
diff --git a/init_sqlite.sh b/init_sqlite.sh
index 2709ceb..14009ea 100755
--- a/init_sqlite.sh
+++ b/init_sqlite.sh
@@ -35,7 +35,8 @@ $SQLITE "$1" -init schema.txt "insert into config (
description,
secret_key,
captcha_key,
- registration_enabled
+ registration_enabled,
+ login_required
)
values (
'agreper-v0.1.1',
@@ -43,7 +44,8 @@ values (
'',
'$(head -c 30 /dev/urandom | base64)',
'$(head -c 30 /dev/urandom | base64)',
- 0
+ 0,
+ 0
)"
if [ "$2" != --no-admin ]
then
diff --git a/main.py b/main.py
index df78fb3..d4842bb 100644
--- a/main.py
+++ b/main.py
@@ -1,4 +1,4 @@
-VERSION = 'agreper-v0.1.1'
+VERSION = "agreper-v0.1.1q1"
# TODO put in config table
THREADS_PER_PAGE = 50
@@ -12,50 +12,74 @@ from datetime import datetime
import captcha, password, minimd
app = Flask(__name__)
-db = DB(os.getenv('DB'))
+db = DB(os.getenv("DB"))
# This defaults to None, which allows CSRF attacks in FireFox
# and older versions of Chrome.
# 'Lax' is sufficient to prevent malicious POST requests.
-app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
+app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
+
class Config:
pass
+
+
config = Config()
-config.version, config.server_name, config.server_description, app.config['SECRET_KEY'], config.captcha_key, config.registration_enabled = db.get_config()
+(
+ config.version,
+ config.server_name,
+ config.server_description,
+ app.config["SECRET_KEY"],
+ config.captcha_key,
+ config.registration_enabled,
+ config.login_required
+) = db.get_config()
if config.version != VERSION:
- print(f'Incompatible version {config.version} (expected {VERSION})')
+ print(f"Incompatible version {config.version} (expected {VERSION})")
sys.exit(1)
+
class Role:
USER = 0
MODERATOR = 1
ADMIN = 2
+
+@app.before_request
+def before_request():
+ if config.login_required:
+ user_id = session.get("user_id", -1)
+ if user_id == -1 and request.endpoint not in ("login","static"):
+ return redirect(url_for("login"))
+
+
+
@app.after_request
def after_request(response):
# This forbids other sites from embedding this site in an iframe,
# preventing clickjacking attacks.
- response.headers['X-Frame-Options'] = 'DENY'
+ response.headers["X-Frame-Options"] = "DENY"
return response
-@app.route('/')
+
+@app.route("/")
def index():
return render_template(
- 'index.html',
- title = config.server_name,
- description = config.server_description,
- config = config,
- user = get_user(),
- forums = db.get_forums()
+ "index.html",
+ title=config.server_name,
+ description=config.server_description,
+ config=config,
+ user=get_user(),
+ forums=db.get_forums(),
)
-@app.route('/forum/
Forbidden
', 403)
+ return False, ("Forbidden
", 403)
return True, user
+
def _admin_check():
user = get_user()
if user is None:
- return False, redirect(url_for('login'))
+ return False, redirect(url_for("login"))
if not user.is_admin():
- return False, ('Forbidden
', 403)
+ return False, ("Forbidden
", 403)
return True, user
class Comment:
- def __init__(self, id, parent_id, author_id, author, text, create_time, modify_time, hidden):
+ def __init__(
+ self, id, parent_id, author_id, author, text, create_time, modify_time, hidden
+ ):
self.id = id
self.author_id = author_id
self.author = author
@@ -665,14 +743,16 @@ class Comment:
self.parent_id = parent_id
self.hidden = hidden
+
def create_comment_tree(comments, user):
- start = time.time();
+ start = time.time()
# Collect comments first, then build the tree in case we encounter a child before a parent
- comment_map = { v[0]: Comment(*v) for v in comments }
+ comment_map = {v[0]: Comment(*v) for v in comments}
root = []
# We should keep showing hidden comments if the user replied to them, directly or indirectly.
# To do that, keep track of user comments, then walk up the tree and insert hidden comments.
user_comments = []
+
# Build tree
def insert(comment):
parent = comment_map.get(comment.parent_id)
@@ -680,6 +760,7 @@ def create_comment_tree(comments, user):
parent.children.append(comment)
else:
root.append(comment)
+
for comment in comment_map.values():
if comment.hidden and (not user or not user.is_moderator()):
continue
@@ -692,11 +773,13 @@ def create_comment_tree(comments, user):
if c.hidden:
insert(c)
c = comment_map.get(c.parent_id)
+
# Sort each comment based on create time
def sort_time(l):
l.sort(key=lambda c: c.modify_time, reverse=True)
for c in l:
sort_time(c.children)
+
sort_time(root)
return root
@@ -717,39 +800,41 @@ class User:
def is_banned(self):
return self.banned_until > time.time_ns()
+
def get_user():
- id = session.get('user_id')
+ id = session.get("user_id")
if id is not None:
name, role, banned_until = db.get_user_name_role_banned(id)
return User(id, name, role, banned_until)
return None
+
def register_user(show_password):
- username, passwd = request.form['username'], request.form['password']
+ username, passwd = request.form["username"], request.form["password"]
if any(c in username for c in string.whitespace):
# This error is more ergonomic in case someone tries to play tricks again :)
- flash('Username may not contain whitespace', 'error')
+ flash("Username may not contain whitespace", "error")
elif len(username) < 3:
- flash('Username must be at least 3 characters long', 'error')
+ flash("Username must be at least 3 characters long", "error")
elif len(passwd) < 8:
- flash('Password must be at least 8 characters long', 'error')
+ flash("Password must be at least 8 characters long", "error")
elif not captcha.verify(
config.captcha_key,
- request.form['captcha'],
- request.form['answer'],
+ request.form["captcha"],
+ request.form["answer"],
):
- flash('CAPTCHA answer is incorrect', 'error')
+ flash("CAPTCHA answer is incorrect", "error")
else:
uid = db.register_user(username, password.hash(passwd), time.time_ns())
if uid is None:
- flash('Failed to create account (username may already be taken)', 'error')
+ flash("Failed to create account (username may already be taken)", "error")
else:
- s = 'Account has been created.'
+ s = "Account has been created."
if show_password:
- s += f' Your password is {passwd} (hover to reveal).'
- flash(s, 'success')
- uid, = uid
- session['user_id'] = uid
+ s += f" Your password is {passwd} (hover to reveal)."
+ flash(s, "success")
+ (uid,) = uid
+ session["user_id"] = uid
session.permanent = True
return True
return False
@@ -759,7 +844,7 @@ def register_user(show_password):
def utility_processor():
def _format_time_delta(n, t):
# Try the sane thing first
- dt = (n - t) // 10 ** 9
+ dt = (n - t) // 10**9
if dt < 1:
return "less than a second"
if dt < 2:
@@ -778,70 +863,73 @@ def utility_processor():
return f"{dt // (3600 * 24)} days"
# Try some very rough estimate, whatever
- f = lambda x: datetime.utcfromtimestamp(x // 10 ** 9)
+ f = lambda x: datetime.utcfromtimestamp(x // 10**9)
n, t = f(n), f(t)
+
def f(x, y, s):
return f'{y - x} {s}{"s" if y - x > 1 else ""}'
+
if t.year < n.year:
return f(t.year, n.year, "year")
if t.month < n.month:
return f(t.month, n.month, "month")
- assert False, 'unreachable'
+ assert False, "unreachable"
def format_since(t):
n = time.time_ns()
if n < t:
- return 'in a distant future'
- return _format_time_delta(n, t) + ' ago'
+ return "in a distant future"
+ return _format_time_delta(n, t) + " ago"
def format_until(t):
n = time.time_ns()
if t <= n:
- return 'in a distant past'
+ return "in a distant past"
return _format_time_delta(t, n)
def format_time(t):
- return datetime.utcfromtimestamp(t / 10 ** 9).replace(microsecond=0)
+ return datetime.utcfromtimestamp(t / 10**9).replace(microsecond=0)
def rand_password():
- '''
+ """
Generate a random password.
The current implementation returns 12 random lower- and uppercase alphabet characters.
This gives up to `log((26 * 2) ** 12) / log(2) = ~68` bits of entropy, which should be
enough for the foreseeable future.
- '''
- return ''.join(string.ascii_letters[secrets.randbelow(52)] for _ in range(12))
+ """
+ return "".join(string.ascii_letters[secrets.randbelow(52)] for _ in range(12))
def gen_captcha():
return captcha.generate(config.captcha_key)
return {
- 'format_since': format_since,
- 'format_time': format_time,
- 'format_until': format_until,
- 'minimd': minimd.html,
- 'rand_password': rand_password,
- 'gen_captcha': gen_captcha,
+ "format_since": format_since,
+ "format_time": format_time,
+ "format_until": format_until,
+ "minimd": minimd.html,
+ "rand_password": rand_password,
+ "gen_captcha": gen_captcha,
}
def restart():
- '''
+ """
Shut down *all* workers and spawn new ones.
This is necessary on e.g. a configuration change.
Since restarting workers depends is platform-dependent this task is delegated to an external
program.
- '''
- r = subprocess.call(['./restart.sh'])
+ """
+ r = subprocess.call(["./restart.sh"])
if r == 0:
- flash('Restart script exited successfully', 'success')
+ flash("Restart script exited successfully", "success")
else:
- flash(f'Restart script exited with error (code {r})', 'error')
+ flash(f"Restart script exited with error (code {r})", "error")
+
def trim_text(s):
- '''
+ """
Because browsers LOVE \\r, trailing whitespace etc.
- '''
- return s.replace('\r', '')
+ """
+ return s.replace("\r", "")
diff --git a/minimd.py b/minimd.py
index 3f456b2..40a80b3 100755
--- a/minimd.py
+++ b/minimd.py
@@ -1,58 +1,70 @@
#!/usr/bin/env python3
import re
+import markdown2
# https://stackoverflow.com/a/6041965
-RE_URL = re.compile(r'(https?://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-]))')
-RE_EM = re.compile(r'\*(.*?)\*')
-RE_LIST = re.compile(r'(-|[0-9]\.) .*')
+RE_URL = re.compile(
+ r"(https?://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-]))"
+)
+RE_EM = re.compile(r"\*(.*?)\*")
+RE_LIST = re.compile(r"(-|[0-9]\.) .*")
+
+RE_PLAINURL = re.compile(
+ r"([ |\n])(https?://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-]))[^\)]"
+)
def html(text):
+ text = RE_PLAINURL.sub(r'\1[\2](\2)', text)
+ return markdown2.markdown(text)
+
+
+def html_old(text):
# Replace angle brackets to prevent XSS
# Also replace ampersands to prevent surprises.
- text = text.replace('&', '&').replace('<', '<').replace('>', '>')
+ text = text.replace("&", "&").replace("<", "<").replace(">", ">")
- html = ['')
+ html.append("")
in_code = False
- l = RE_EM.sub(r'\1', l)
+ l = RE_EM.sub(r"\1", l)
l = RE_URL.sub(r'\1', l)
if RE_LIST.match(l):
if in_list:
- html.append('")
in_code = True
html.append(l)
continue
if in_code:
- html.append('')
+ html.append("
')
+ html.append("
")
in_list = True
else:
in_list = False
html.append(l)
if in_code:
- html.append('')
- html.append('
« {{ forum_title }} « {{ title }}
{{ render_comment_pre(reply_comment, thread_id, comments | length == 0) }} {{ reply() }} diff --git a/templates/forum.html b/templates/forum.html index ac7c1f4..896ddc5 100644 --- a/templates/forum.html +++ b/templates/forum.html @@ -11,7 +11,7 @@ {% block content -%}{{ minimd(description) | safe }}
- + {{- nav() -}}