"""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