Files
2026-09-08 18:21:10 +01:00

251 lines
9.2 KiB
Python

"""Private mail + forum screens for the BBS."""
import time
from bbsdb import q, q1, ex, user_by_name, unread, ago
PER_PAGE = 10
# ==================================================================== mail
def mail_menu(t, con, me):
while True:
n = unread(con, me["id"])
t.header("MAIL (%d unread)" % n)
t.line(" [I]nbox [S]ent [W]rite [Q]uit to main")
c = t.ask("\nmail> ").lower()
if c.startswith("i"):
mail_list(t, con, me, "in")
elif c.startswith("s"):
mail_list(t, con, me, "out")
elif c.startswith("w"):
mail_write(t, con, me)
elif c.startswith("q") or c == "":
return
def mail_list(t, con, me, box):
page = 0
while True:
off = page * PER_PAGE
if box == "in":
rows = q(con, "SELECT m.*, u.username who FROM messages m "
"LEFT JOIN users u ON u.id=m.from_id "
"WHERE m.to_id=? ORDER BY m.id DESC LIMIT ? OFFSET ?",
(me["id"], PER_PAGE + 1, off))
else:
rows = q(con, "SELECT m.*, u.username who FROM messages m "
"LEFT JOIN users u ON u.id=m.to_id "
"WHERE m.from_id=? ORDER BY m.id DESC LIMIT ? OFFSET ?",
(me["id"], PER_PAGE + 1, off))
more = len(rows) > PER_PAGE
rows = rows[:PER_PAGE]
t.header("INBOX" if box == "in" else "SENT")
if not rows:
t.line(" (empty)")
for i, r in enumerate(rows, 1):
flag = "*" if (box == "in" and not r["is_read"]) else " "
t.line(" %s%2d. %-12s %-28s %s" % (
flag, i, (r["who"] or "[gone]")[:12], r["subject"][:28],
ago(r["created_at"])))
t.line("\n number=read [N]ext [P]rev [Q]back")
c = t.ask("\nmail> ").lower()
if c.isdigit() and 1 <= int(c) <= len(rows):
mail_read(t, con, me, rows[int(c) - 1]["id"], box)
elif c.startswith("n") and more:
page += 1
elif c.startswith("p") and page > 0:
page -= 1
elif c.startswith("q") or c == "":
return
def mail_read(t, con, me, mid, box):
m = q1(con, "SELECT m.*, f.username fromname, o.username toname FROM messages m "
"LEFT JOIN users f ON f.id=m.from_id "
"LEFT JOIN users o ON o.id=m.to_id WHERE m.id=?", (mid,))
if not m or me["id"] not in (m["to_id"], m["from_id"]):
t.line("Not found.")
return
if m["to_id"] == me["id"] and not m["is_read"]:
ex(con, "UPDATE messages SET is_read=1 WHERE id=?", (mid,))
t.header("MSG: " + m["subject"][:40])
t.line(" From: %s" % (m["fromname"] or "[gone]"))
t.line(" To : %s" % (m["toname"] or "[gone]"))
t.line(" Date: %s" % time.strftime("%d/%m/%Y %H:%M",
time.localtime(m["created_at"])))
t.rule()
for ln in m["body"].splitlines() or [""]:
t.line(" " + ln)
t.rule()
t.line(" [R]eply [D]elete [Q]back")
c = t.ask("\nmsg> ").lower()
if c.startswith("r") and m["fromname"]:
subj = m["subject"]
if not subj.lower().startswith("re:"):
subj = "Re: " + subj
mail_write(t, con, me, to=m["fromname"], subject=subj)
elif c.startswith("d"):
ex(con, "DELETE FROM messages WHERE id=? AND (to_id=? OR from_id=?)",
(mid, me["id"], me["id"]))
t.line("Deleted.")
def mail_write(t, con, me, to=None, subject=None):
t.header("WRITE MAIL")
if to is None:
to = t.ask(" To (username): ", maxlen=16)
else:
t.line(" To: " + to)
if not to:
return
dest = user_by_name(con, to)
if not dest:
t.line(" No such user.")
return
if dest["id"] == me["id"]:
t.line(" You cannot mail yourself.")
return
if subject is None:
subject = t.ask(" Subject: ", maxlen=60)
else:
t.line(" Subject: " + subject)
if not subject:
return
t.line(" Body - end with a single '.' on its own line:")
body = read_body(t)
if not body.strip():
t.line(" Aborted (empty).")
return
ex(con, "INSERT INTO messages (from_id,to_id,subject,body,created_at) "
"VALUES (?,?,?,?,?)",
(me["id"], dest["id"], subject, body[:4000], int(time.time())))
t.line(" Sent to %s." % dest["username"])
def read_body(t, maxlines=40):
"""Multi-line entry terminated by a lone '.' - the classic BBS editor."""
lines = []
while len(lines) < maxlines:
ln = t.read(" | ", maxlen=200)
if ln == ".":
break
lines.append(ln)
return "\n".join(lines)
# ==================================================================== forum
def forum_menu(t, con, me):
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("FORUMS")
for i, r in enumerate(rows, 1):
t.line(" %2d. %-24s %3d topics%s" % (
i, r["name"][:24], r["tc"], " [locked]" if r["is_locked"] else ""))
t.line("\n number=open [Q]back")
c = t.ask("\nforum> ").lower()
if c.isdigit() and 1 <= int(c) <= len(rows):
topic_list(t, con, me, rows[int(c) - 1])
elif c.startswith("q") or c == "":
return
def topic_list(t, con, me, forum):
page = 0
while True:
off = page * PER_PAGE
rows = q(con, "SELECT t.*, u.username, "
"(SELECT COUNT(*) FROM posts p WHERE p.topic_id=t.id) pc "
"FROM topics t LEFT JOIN users u ON u.id=t.user_id "
"WHERE t.forum_id=? ORDER BY t.bumped_at DESC LIMIT ? OFFSET ?",
(forum["id"], PER_PAGE + 1, off))
more = len(rows) > PER_PAGE
rows = rows[:PER_PAGE]
t.header("FORUM: " + forum["name"])
if not rows:
t.line(" (no topics yet)")
for i, r in enumerate(rows, 1):
t.line(" %2d.%s %-30s %3dp %-10s %s" % (
i, "L" if r["is_locked"] else " ", r["title"][:30], r["pc"],
(r["username"] or "?")[:10], ago(r["bumped_at"])))
t.line("\n number=read [N]ew topic [M]ore [P]rev [Q]back")
c = t.ask("\ntopics> ").lower()
if c.isdigit() and 1 <= int(c) <= len(rows):
topic_read(t, con, me, rows[int(c) - 1]["id"])
elif c.startswith("n"):
new_topic(t, con, me, forum)
elif c.startswith("m") and more:
page += 1
elif c.startswith("p") and page > 0:
page -= 1
elif c.startswith("q") or c == "":
return
def new_topic(t, con, me, forum):
if forum["is_locked"] and not me["is_admin"]:
t.line(" That forum is locked.")
return
title = t.ask(" Topic title: ", maxlen=80)
if not title:
return
t.line(" Body - end with '.' on its own line:")
body = read_body(t)
if not body.strip():
t.line(" Aborted.")
return
now = int(time.time())
tid = ex(con, "INSERT INTO topics (forum_id,user_id,title,created_at,bumped_at)"
" VALUES (?,?,?,?,?)", (forum["id"], me["id"], title, now, now))
ex(con, "INSERT INTO posts (topic_id,user_id,body,created_at) VALUES (?,?,?,?)",
(tid, me["id"], body[:4000], now))
t.line(" Topic posted.")
def topic_read(t, con, me, tid):
page = 0
while True:
top = q1(con, "SELECT t.*, f.name fname, f.is_locked flocked FROM topics t "
"JOIN forums f ON f.id=t.forum_id WHERE t.id=?", (tid,))
if not top:
t.line(" Gone.")
return
locked = top["is_locked"] or top["flocked"]
off = page * PER_PAGE
rows = q(con, "SELECT p.*, u.username FROM posts p "
"LEFT JOIN users u ON u.id=p.user_id "
"WHERE p.topic_id=? ORDER BY p.id LIMIT ? OFFSET ?",
(tid, PER_PAGE + 1, off))
more = len(rows) > PER_PAGE
rows = rows[:PER_PAGE]
t.header(top["title"][:50] + (" [LOCKED]" if locked else ""))
for r in rows:
t.line(" --- %s, %s ago ---" % ((r["username"] or "[gone]"),
ago(r["created_at"])))
for ln in r["body"].splitlines() or [""]:
t.line(" " + ln)
t.line("\n [R]eply [M]ore [P]rev [Q]back")
c = t.ask("\ntopic> ").lower()
if c.startswith("r"):
if locked and not me["is_admin"]:
t.line(" Topic is locked.")
continue
t.line(" Reply - end with '.' on its own line:")
body = read_body(t)
if body.strip():
now = int(time.time())
ex(con, "INSERT INTO posts (topic_id,user_id,body,created_at)"
" VALUES (?,?,?,?)", (tid, me["id"], body[:4000], now))
ex(con, "UPDATE topics SET bumped_at=? WHERE id=?", (now, tid))
t.line(" Posted.")
elif c.startswith("m") and more:
page += 1
elif c.startswith("p") and page > 0:
page -= 1
elif c.startswith("q") or c == "":
return