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