{{ title }}
- {% for category, msg in get_flashed_messages(True) %} + {%- for category, msg in get_flashed_messages(True) -%}{{ msg }}
- {% endfor %} - {% block content %}{% endblock %} + {%- endfor -%} + {%- block content %}{% endblock -%}diff --git a/db/sqlite.py b/db/sqlite.py
index 8394006..bf0fc4d 100644
--- a/db/sqlite.py
+++ b/db/sqlite.py
@@ -5,6 +5,12 @@ class DB:
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()
+
def get_forums(self):
return self._db().execute('''
select f.forum_id, name, description, thread_id, title, update_time
@@ -29,15 +35,18 @@ class DB:
(forum_id,)
).fetchone()
- def get_threads(self, forum_id):
+ def get_threads(self, forum_id, offset, limit):
return self._db().execute('''
select t.thread_id, title, t.create_time, t.update_time, t.author_id, name, count(c.thread_id)
from threads t, users
left join comments c on t.thread_id = c.thread_id
where forum_id = ? and user_id = t.author_id
group by t.thread_id
+ order by t.update_time desc
+ limit ?
+ offset ?
''',
- (forum_id,)
+ (forum_id, limit, offset)
)
def get_thread(self, thread):
@@ -129,6 +138,24 @@ class DB:
(username,)
).fetchone()
+ def get_user_password_by_id(self, user_id):
+ return self._db().execute('''
+ select password
+ from users
+ where user_id = ?
+ ''',
+ (user_id,)
+ ).fetchone()
+
+ def set_user_password(self, user_id, password):
+ return self.change_one('''
+ update users
+ set password = ?
+ where user_id = ?
+ ''',
+ (password, user_id)
+ )
+
def get_user_public_info(self, user_id):
return self._db().execute('''
select name, about
@@ -140,7 +167,7 @@ class DB:
def get_user_private_info(self, user_id):
return self._db().execute('''
- select name, about
+ select about
from users
where user_id = ?
''',
@@ -158,6 +185,15 @@ class DB:
)
db.commit()
+ def get_user_name_role_banned(self, user_id):
+ return self._db().execute('''
+ select name, role, banned_until
+ from users
+ where user_id = ?
+ ''',
+ (user_id,)
+ ).fetchone()
+
def get_user_name(self, user_id):
return self._db().execute('''
select name
@@ -173,11 +209,15 @@ class DB:
c.execute('''
insert into threads (author_id, forum_id, title, text,
create_time, modify_time, update_time)
- values (?, ?, ?, ?, ?, ?, ?)
+ select ?, ?, ?, ?, ?, ?, ?
+ from users
+ where user_id = ? and banned_until < ?
''',
- (author_id, forum_id, title, text, time, time, 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('''
select thread_id
@@ -193,9 +233,13 @@ class DB:
c.execute('''
delete
from threads
- where thread_id = ? and author_id = ?
+ -- 1 = moderator, 2 = admin
+ where thread_id = ? and (
+ author_id = ?
+ or (select 1 from users where user_id = ? and (role = 1 or role = 2))
+ )
''',
- (thread_id, user_id)
+ (thread_id, user_id, user_id)
)
db.commit()
return c.rowcount > 0
@@ -206,9 +250,16 @@ class DB:
c.execute('''
delete
from comments
- where comment_id = ? and author_id = ?
+ where comment_id = ?
+ and (
+ author_id = ?
+ -- 1 = moderator, 2 = admin
+ or (select 1 from users where user_id = ? and (role = 1 or role = 2))
+ )
+ -- Don't allow deleting comments with children
+ and (select 1 from comments where parent_id = ?) is null
''',
- (comment_id, user_id)
+ (comment_id, user_id, user_id, comment_id)
)
db.commit()
return c.rowcount > 0
@@ -219,10 +270,10 @@ class DB:
c.execute('''
insert into comments(thread_id, author_id, text, create_time, modify_time)
select ?, ?, ?, ?, ?
- from threads
- where thread_id = ?
+ from threads, users
+ where thread_id = ? and user_id = ? and banned_until < ?
''',
- (thread_id, author_id, text, time, time, thread_id)
+ (thread_id, author_id, text, time, time, thread_id, author_id, time)
)
if c.rowcount > 0:
print('SHIT')
@@ -243,10 +294,10 @@ class DB:
c.execute('''
insert into comments(thread_id, parent_id, author_id, text, create_time, modify_time)
select thread_id, ?, ?, ?, ?, ?
- from comments
- where comment_id = ?
+ from comments, users
+ where comment_id = ? and user_id = ? and banned_until < ?
''',
- (parent_id, author_id, text, time, time, parent_id)
+ (parent_id, author_id, text, time, time, parent_id, author_id, time)
)
if c.rowcount > 0:
c.execute('''
@@ -270,9 +321,18 @@ class DB:
c.execute('''
update threads
set title = ?, text = ?, modify_time = ?
- where thread_id = ? and author_id = ?
+ where thread_id = ? and (
+ (author_id = ? and (select 1 from users where user_id = ? and banned_until < ?))
+ -- 1 = moderator, 2 = admin
+ or (select 1 from users where user_id = ? and (role = 1 or role = 2))
+ )
''',
- (title, text, time, thread_id, user_id)
+ (
+ title, text, time,
+ thread_id,
+ user_id, user_id, time,
+ user_id,
+ )
)
if c.rowcount > 0:
db.commit()
@@ -285,16 +345,51 @@ class DB:
c.execute('''
update comments
set text = ?, modify_time = ?
- where comment_id = ? and author_id = ?
+ where comment_id = ? and (
+ (author_id = ? and (select 1 from users where user_id = ? and banned_until < ?))
+ -- 1 = moderator, 2 = admin
+ or (select 1 from users where user_id = ? and (role = 1 or role = 2))
+ )
''',
- (text, time, comment_id, user_id)
+ (
+ text, time,
+ comment_id,
+ user_id, user_id, time,
+ user_id,
+ )
)
if c.rowcount > 0:
db.commit()
return True
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('''
+ insert into users(name, password, join_time)
+ select lower(?), ?, ?
+ from config
+ where registration_enabled = 1
+ ''',
+ (username, password, time)
+ )
+ if c.rowcount > 0:
+ db.commit()
+ return True
+ return False
+ except sqlite3.IntegrityError:
+ # User already exists, probably
+ return False
+
def add_user(self, username, password, time):
+ '''
+ Add a user without checking if registrations are enabled.
+ '''
try:
db = self._db()
c = db.cursor()
@@ -312,5 +407,81 @@ class DB:
# User already exists, probably
return False
+ def get_users(self):
+ 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('''
+ update forums
+ set name = ?
+ where forum_id = ?
+ ''',
+ (name, forum_id)
+ )
+
+ def set_forum_description(self, forum_id, description):
+ return self.change_one('''
+ update forums
+ set description = ?
+ where forum_id = ?
+ ''',
+ (description, forum_id)
+ )
+
+ def add_forum(self, name, description):
+ db = self._db()
+ db.execute('''
+ insert into forums(name, description)
+ values (?, ?)
+ ''',
+ (name, description)
+ )
+ db.commit()
+
+ def set_config(self, server_name, server_description, registration_enabled):
+ return self.change_one('''
+ update config
+ set name = ?, description = ?, registration_enabled = ?
+ ''',
+ (server_name, server_description, registration_enabled)
+ )
+
+ def set_config_secrets(self, secret_key, captcha_key):
+ return self.change_one('''
+ update config
+ set secret_key = ?, captcha_key = ?
+ ''',
+ (secret_key, captcha_key)
+ )
+
+ def set_user_ban(self, user_id, until):
+ return self.change_one('''
+ update users
+ set banned_until = ?
+ where user_id = ?
+ ''',
+ (until, user_id)
+ )
+
+ def change_one(self, query, values):
+ db = self._db()
+ c = db.cursor()
+ c.execute(query, values)
+ if c.rowcount > 0:
+ db.commit()
+ return True
+ return False
+
+ def query(self, q):
+ db = self._db()
+ c = db.cursor()
+ rows = c.execute(q)
+ db.commit()
+ return rows, c.rowcount
+
def _db(self):
- return sqlite3.connect(self.conn)
+ return sqlite3.connect(self.conn, timeout=5)
diff --git a/init_sqlite.sh b/init_sqlite.sh
new file mode 100755
index 0000000..e0dd679
--- /dev/null
+++ b/init_sqlite.sh
@@ -0,0 +1,28 @@
+#!/usr/bin/env bash
+
+SQLITE=sqlite3
+
+set -e
+
+if [ $# != 1 ]
+then
+ echo "Usage: $0
+Admin panel
+Home page
+ {{ msg }} ⚠ Only use queries if you know what you're doing ⚠
+Forbidden
', 403)
+ return True, user
+
class Comment:
def __init__(self, id, author_id, author, text, create_time, modify_time, parent_id):
@@ -326,43 +587,77 @@ def create_comment_tree(comments):
return root
+class User:
+ def __init__(self, id, name, role, banned_until):
+ self.id = id
+ self.name = name
+ self.role = role
+ self.banned_until = banned_until
+
+ def is_moderator(self):
+ return self.role in (Role.ADMIN, Role.MODERATOR)
+
+ def is_admin(self):
+ return self.role == Role.ADMIN
+
+ def is_banned(self):
+ return self.banned_until > time.time_ns()
+
+def get_user():
+ 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
+
+
@app.context_processor
def utility_processor():
- def format_since(t):
- n = time.time_ns()
- if n < t:
- return 'In a distant future'
-
+ def _format_time_delta(n, t):
# Try the sane thing first
dt = (n - t) // 10 ** 9
if dt < 1:
- return "less than a second ago"
+ return "less than a second"
if dt < 2:
- return f"1 second ago"
+ return f"1 second"
if dt < 60:
- return f"{dt} seconds ago"
+ return f"{dt} seconds"
if dt < 119:
- return f"1 minute ago"
+ return f"1 minute"
if dt < 3600:
- return f"{dt // 60} minutes ago"
+ return f"{dt // 60} minutes"
if dt < 3600 * 2:
- return f"1 hour ago"
+ return f"1 hour"
if dt < 3600 * 24:
- return f"{dt // 3600} hours ago"
+ return f"{dt // 3600} hours"
if dt < 3600 * 24 * 31:
- return f"{dt // (3600 * 24)} days ago"
+ return f"{dt // (3600 * 24)} days"
# Try some very rough estimate, whatever
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 ""} ago'
+ 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")
- # This shouldn't be reachable, but it's still better to return something
- return "incredibly long ago"
+ 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'
+
+ def format_until(t):
+ n = time.time_ns()
+ if t <= n:
+ return 'in a distant past'
+ return _format_time_delta(t, n)
+
+ def format_time(t):
+ return datetime.utcfromtimestamp(t / 10 ** 9).replace(microsecond=0)
def minimd(text):
# Replace angle brackets to prevent XSS
@@ -377,6 +672,8 @@ def utility_processor():
return {
'format_since': format_since,
+ 'format_time': format_time,
+ 'format_until': format_until,
'minimd': minimd,
}
@@ -386,3 +683,24 @@ def hash_password(password):
def verify_password(password, hash):
return passlib.hash.argon2.verify(password, hash)
+
+
+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'])
+ if r == 0:
+ flash('Restart script exited successfully', 'success')
+ else:
+ flash(f'Restart script exited with error (code {r})', 'error')
+
+def trim_text(s):
+ '''
+ Because browsers LOVE \\r, trailing whitespace etc.
+ '''
+ return s.strip().replace('\r', '')
diff --git a/restart.sh b/restart.sh
new file mode 100755
index 0000000..b6023e3
--- /dev/null
+++ b/restart.sh
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+
+# This script is intended for dev environments only.
+touch main.py
diff --git a/schema.txt b/schema.txt
index 596bfca..fdd8869 100644
--- a/schema.txt
+++ b/schema.txt
@@ -1,11 +1,20 @@
+create table config (
+ version text not null,
+ name text not null,
+ description text not null,
+ secret_key text not null,
+ captcha_key text not null,
+ registration_enabled boolean not null
+);
+
create table users (
user_id integer unique not null primary key autoincrement,
name varchar(32) unique not null,
password varchar(128) not null,
- email varchar(254),
about text not null default '',
join_time integer not null,
- role integer not null default 0
+ role integer not null default 0,
+ banned_until integer not null default 0
);
create table threads (
@@ -34,10 +43,9 @@ create table comments (
);
create table forums (
- forum_id integer unique not null primary key autoincrement,
+ forum_id integer unique not null primary key autoincrement,
name varchar(64) not null,
- description text,
- allowed_roles_mask integer not null
+ description text not null default ''
);
-- Both of these speed up searches significantly if there are many threads or comments.
diff --git a/templates/admin/base.html b/templates/admin/base.html
new file mode 100644
index 0000000..a864555
--- /dev/null
+++ b/templates/admin/base.html
@@ -0,0 +1,45 @@
+{# Don't use the default theme to emphasize this page is special -#}
+
+
+{{ title }}
+Query
+Configuration
+
+
+
+ +| ID | +Name | +Description | +Actions | +
|---|---|---|---|
| {{ id }} | ++ + | + + | +Remove | +
| ID | +Name | +Join date | +Role | +Banned | +
|---|---|---|---|---|
| {{ id }} | +{{ name }} | +{{ format_time(join_date) }} | ++ + | ++{%- if banned_until > 0 -%} + +{%- endif -%} + + | +
| {{ c }} | +{%- endfor -%} +
{{ msg }}
- {% endfor %} - {% block content %}{% endblock %} + {%- endfor -%} + {%- block content %}{% endblock -%}