First git commit

This commit is contained in:
root
2026-09-08 18:21:10 +01:00
commit 66911dc450
65 changed files with 7012 additions and 0 deletions

305
bbs/screens.py Normal file
View File

@ -0,0 +1,305 @@
"""Profile + admin screens for the BBS."""
import time
from bbsdb import q, q1, ex, user_by_name, user_by_id, hash_password, ago
PER_PAGE = 10
# ================================================================= profile
def profile_menu(t, con, me):
while True:
u = user_by_id(con, me["id"])
posts = q1(con, "SELECT COUNT(*) c FROM posts WHERE user_id=?", (u["id"],))["c"]
t.header("YOUR PROFILE")
t.line(" User : %s%s" % (u["username"], " [admin]" if u["is_admin"] else ""))
t.line(" Tagline : %s" % (u["tagline"] or "-"))
t.line(" Location: %s" % (u["location"] or "-"))
t.line(" Joined : %s" % time.strftime("%d/%m/%Y",
time.localtime(u["created_at"])))
t.line(" Posts : %d" % posts)
best = q(con, "SELECT game, MAX(score) sc FROM scores WHERE user_id=?"
" GROUP BY game", (u["id"],))
for b in best:
t.line(" Best %-6s: %d" % (b["game"], b["sc"]))
t.line("\n [T]agline [L]ocation [P]assword [W]ho's online [Q]back")
c = t.ask("\nprofile> ").lower()
if c.startswith("t"):
v = t.ask(" New tagline: ", maxlen=80)
ex(con, "UPDATE users SET tagline=? WHERE id=?", (v, u["id"]))
elif c.startswith("l"):
v = t.ask(" New location: ", maxlen=40)
ex(con, "UPDATE users SET location=? WHERE id=?", (v, u["id"]))
elif c.startswith("p"):
change_password(t, con, u)
elif c.startswith("w"):
who(t, con)
elif c.startswith("q") or c == "":
return
def change_password(t, con, u):
from bbsdb import verify_password
old = t.secret(" Current password: ")
if not verify_password(old, u["pass_hash"]):
t.line(" Wrong password.")
return
n1 = t.secret(" New password: ")
if len(n1) < 4:
t.line(" Too short (min 4).")
return
if n1 != t.secret(" Repeat new: "):
t.line(" Did not match.")
return
h = hash_password(n1)
if not h:
t.line(" Could not hash password - unchanged.")
return
ex(con, "UPDATE users SET pass_hash=? WHERE id=?", (h, u["id"]))
t.line(" Password changed. It works on the website too.")
def who(t, con):
rows = q(con, "SELECT username,last_seen,is_admin FROM users "
"ORDER BY last_seen DESC LIMIT 15")
t.header("RECENTLY SEEN")
for r in rows:
t.line(" %-16s %s ago%s" % (r["username"][:16], ago(r["last_seen"]),
" [admin]" if r["is_admin"] else ""))
t.pause()
def userlist(t, con):
page = 0
while True:
rows = q(con, "SELECT username,tagline,last_seen FROM users "
"ORDER BY username LIMIT ? OFFSET ?",
(PER_PAGE + 1, page * PER_PAGE))
more = len(rows) > PER_PAGE
rows = rows[:PER_PAGE]
t.header("MEMBER LIST")
for r in rows:
t.line(" %-16s %-24s %s" % (r["username"][:16],
(r["tagline"] or "")[:24],
ago(r["last_seen"])))
t.line("\n [N]ext [P]rev [Q]back")
c = t.ask("\nusers> ").lower()
if c.startswith("n") and more:
page += 1
elif c.startswith("p") and page > 0:
page -= 1
else:
return
# ================================================================== admin
def admin_menu(t, con, me):
if not me["is_admin"]:
t.line(" Admin only.")
return
while True:
t.header("ADMIN TOOLS")
t.line(" [U]sers [F]orums [T]opics [M]OTD [S]tats [Q]back")
c = t.ask("\nadmin> ").lower()
if c.startswith("u"):
admin_users(t, con, me)
elif c.startswith("f"):
admin_forums(t, con)
elif c.startswith("t"):
admin_topics(t, con)
elif c.startswith("m"):
v = t.ask(" New MOTD: ", maxlen=200)
ex(con, "INSERT INTO settings (k,v) VALUES ('motd',?) "
"ON CONFLICT(k) DO UPDATE SET v=excluded.v", (v,))
t.line(" MOTD updated (site + BBS).")
elif c.startswith("s"):
admin_stats(t, con)
elif c.startswith("q") or c == "":
return
def admin_stats(t, con):
t.header("STATS")
for label, sql in [
("Users", "SELECT COUNT(*) c FROM users"),
("Banned", "SELECT COUNT(*) c FROM users WHERE is_banned=1"),
("Forums", "SELECT COUNT(*) c FROM forums"),
("Topics", "SELECT COUNT(*) c FROM topics"),
("Posts", "SELECT COUNT(*) c FROM posts"),
("Messages", "SELECT COUNT(*) c FROM messages"),
("Matches", "SELECT COUNT(*) c FROM matches"),
]:
t.line(" %-10s %d" % (label, q1(con, sql)["c"]))
t.pause()
def admin_users(t, con, me):
while True:
t.header("ADMIN: USERS")
t.line(" [L]ist [F]ind [A]dd [E]dit [Q]back")
c = t.ask("\nadmin/users> ").lower()
if c.startswith("l"):
rows = q(con, "SELECT id,username,is_admin,is_banned FROM users"
" ORDER BY id LIMIT 40")
for r in rows:
t.line(" #%-4d %-16s %s%s" % (
r["id"], r["username"][:16],
"admin " if r["is_admin"] else "",
"BANNED" if r["is_banned"] else ""))
t.pause()
elif c.startswith("f"):
term = t.ask(" Username contains: ", maxlen=16)
rows = q(con, "SELECT id,username,is_admin,is_banned FROM users"
" WHERE username LIKE ? ORDER BY id LIMIT 40",
("%" + term + "%",))
if not rows:
t.line(" (no matches)")
for r in rows:
t.line(" #%-4d %-16s %s%s" % (
r["id"], r["username"][:16],
"admin " if r["is_admin"] else "",
"BANNED" if r["is_banned"] else ""))
t.pause()
elif c.startswith("a"):
name = t.ask(" New username: ", maxlen=16)
if not name.replace("_", "").isalnum() or not 3 <= len(name) <= 16:
t.line(" Bad username (3-16 alnum/underscore).")
continue
if user_by_name(con, name):
t.line(" Already taken.")
continue
pw = t.secret(" Password: ")
if len(pw) < 4:
t.line(" Too short.")
continue
h = hash_password(pw)
if not h:
t.line(" Hashing failed.")
continue
now = int(time.time())
ex(con, "INSERT INTO users (username,pass_hash,is_admin,created_at)"
" VALUES (?,?,0,?)", (name, h, now))
t.line(" Created %s." % name)
elif c.startswith("e"):
admin_edit_user(t, con, me)
elif c.startswith("q") or c == "":
return
def admin_edit_user(t, con, me):
name = t.ask(" Username to edit: ", maxlen=16)
u = user_by_name(con, name)
if not u:
t.line(" No such user.")
return
while True:
u = user_by_id(con, u["id"])
t.header("EDIT: %s (#%d)" % (u["username"], u["id"]))
t.line(" admin=%d banned=%d tagline=%s" %
(u["is_admin"], u["is_banned"], u["tagline"] or "-"))
t.line("\n [A]dmin toggle [B]an toggle [R]eset pw [D]elete [Q]back")
c = t.ask("\nedit> ").lower()
if c.startswith("a"):
if u["id"] == me["id"] and u["is_admin"]:
t.line(" You cannot remove your own admin rights.")
continue
ex(con, "UPDATE users SET is_admin=1-is_admin WHERE id=?", (u["id"],))
elif c.startswith("b"):
if u["id"] == me["id"]:
t.line(" You cannot ban yourself.")
continue
ex(con, "UPDATE users SET is_banned=1-is_banned WHERE id=?", (u["id"],))
elif c.startswith("r"):
pw = t.secret(" New password: ")
if len(pw) < 4:
t.line(" Too short.")
continue
h = hash_password(pw)
if h:
ex(con, "UPDATE users SET pass_hash=? WHERE id=?", (h, u["id"]))
t.line(" Reset.")
elif c.startswith("d"):
if u["id"] == me["id"]:
t.line(" You cannot delete yourself.")
continue
if t.ask(" Type DELETE to confirm: ") == "DELETE":
ex(con, "DELETE FROM users WHERE id=?", (u["id"],))
t.line(" Deleted.")
return
elif c.startswith("q") or c == "":
return
def admin_forums(t, con):
while True:
rows = q(con, "SELECT f.*, (SELECT COUNT(*) FROM topics x "
"WHERE x.forum_id=f.id) tc FROM forums f "
"ORDER BY f.sort_order, f.id")
t.header("ADMIN: FORUMS")
for r in rows:
t.line(" #%-3d %-24s %3dt %s" % (
r["id"], r["name"][:24], r["tc"],
"[locked]" if r["is_locked"] else ""))
t.line("\n [A]dd [L]ock toggle [R]ename [D]elete [Q]back")
c = t.ask("\nadmin/forums> ").lower()
if c.startswith("a"):
name = t.ask(" Forum name: ", maxlen=60)
if not name:
continue
descr = t.ask(" Description: ", maxlen=120)
order = t.ask(" Sort order (number): ", maxlen=4)
ex(con, "INSERT INTO forums (name,descr,sort_order) VALUES (?,?,?)",
(name, descr, int(order) if order.isdigit() else 0))
t.line(" Added.")
elif c.startswith("l"):
i = t.ask(" Forum id: ", maxlen=6)
if i.isdigit():
ex(con, "UPDATE forums SET is_locked=1-is_locked WHERE id=?", (int(i),))
elif c.startswith("r"):
i = t.ask(" Forum id: ", maxlen=6)
if i.isdigit():
name = t.ask(" New name: ", maxlen=60)
if name:
ex(con, "UPDATE forums SET name=? WHERE id=?", (name, int(i)))
elif c.startswith("d"):
i = t.ask(" Forum id to DELETE (with all topics): ", maxlen=6)
if i.isdigit() and t.ask(" Type DELETE to confirm: ") == "DELETE":
ex(con, "DELETE FROM forums WHERE id=?", (int(i),))
t.line(" Deleted.")
elif c.startswith("q") or c == "":
return
def admin_topics(t, con):
while True:
rows = q(con, "SELECT t.*, f.name fname, u.username FROM topics t "
"JOIN forums f ON f.id=t.forum_id "
"LEFT JOIN users u ON u.id=t.user_id "
"ORDER BY t.bumped_at DESC LIMIT 20")
t.header("ADMIN: TOPICS (latest 20)")
for r in rows:
t.line(" #%-4d [%-10s] %-26s %s%s" % (
r["id"], r["fname"][:10], r["title"][:26],
(r["username"] or "?")[:10],
" LOCKED" if r["is_locked"] else ""))
t.line("\n [L]ock toggle [M]ove [D]elete [Q]back")
c = t.ask("\nadmin/topics> ").lower()
if c.startswith("l"):
i = t.ask(" Topic id: ", maxlen=6)
if i.isdigit():
ex(con, "UPDATE topics SET is_locked=1-is_locked WHERE id=?", (int(i),))
elif c.startswith("m"):
i = t.ask(" Topic id: ", maxlen=6)
f = t.ask(" Move to forum id: ", maxlen=6)
if i.isdigit() and f.isdigit():
if q1(con, "SELECT 1 x FROM forums WHERE id=?", (int(f),)):
ex(con, "UPDATE topics SET forum_id=? WHERE id=?", (int(f), int(i)))
t.line(" Moved.")
else:
t.line(" No such forum.")
elif c.startswith("d"):
i = t.ask(" Topic id to DELETE: ", maxlen=6)
if i.isdigit() and t.ask(" Type DELETE to confirm: ") == "DELETE":
ex(con, "DELETE FROM topics WHERE id=?", (int(i),))
t.line(" Deleted.")
elif c.startswith("q") or c == "":
return