First git commit
This commit is contained in:
241
bbs/bbsd.py
Normal file
241
bbs/bbsd.py
Normal file
@ -0,0 +1,241 @@
|
||||
#!/usr/bin/env python3
|
||||
"""txt3 BBS - telnet front-end for wap.txt3.net.
|
||||
|
||||
Shares the SQLite database with the WML/XHTML site: one account works on all
|
||||
three front-ends, and multiplayer matches are cross-playable between them.
|
||||
|
||||
Binds to the tailscale interface only by default.
|
||||
"""
|
||||
import os
|
||||
import socket
|
||||
import socketserver
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
import bbsdb # noqa: E402
|
||||
from bbsdb import connect, q1, login, create_user, user_by_name, touch # noqa: E402
|
||||
from term import Term, Hangup # noqa: E402
|
||||
import boards # noqa: E402
|
||||
import games # noqa: E402
|
||||
import mud # noqa: E402
|
||||
import screens # noqa: E402
|
||||
|
||||
HOST = os.environ.get("BBS_HOST", "100.127.96.105")
|
||||
PORT = int(os.environ.get("BBS_PORT", "12300"))
|
||||
MAX_SESSIONS = int(os.environ.get("BBS_MAX", "20"))
|
||||
|
||||
_sessions = threading.BoundedSemaphore(MAX_SESSIONS)
|
||||
|
||||
BANNER = r"""
|
||||
_ _ _____ ____ ____ ____
|
||||
| |___ _| |_|___ / | __ )| __ ) ___|
|
||||
| __\ \/ / __| |_ \ | _ \| _ \___ \
|
||||
| |_ > <| |_ ___) | | |_) | |_) |__) |
|
||||
\__/_/\_\\__|____/ |____/|____/____/
|
||||
"""
|
||||
|
||||
|
||||
def log(msg):
|
||||
sys.stdout.write("[%s] %s\n" % (time.strftime("%Y-%m-%d %H:%M:%S"), msg))
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def welcome(t, con):
|
||||
t.write(BANNER)
|
||||
motd = q1(con, "SELECT v FROM settings WHERE k='motd'")
|
||||
t.line(" " + (motd["v"] if motd else "Welcome!"))
|
||||
users = q1(con, "SELECT COUNT(*) c FROM users")["c"]
|
||||
posts = q1(con, "SELECT COUNT(*) c FROM posts")["c"]
|
||||
t.line(" %d members, %d posts. Also on the web: https://wap.txt3.net" % (users, posts))
|
||||
t.rule("=")
|
||||
|
||||
|
||||
def do_login(t, con):
|
||||
"""Returns a user row, or None if the caller gave up."""
|
||||
for _ in range(3):
|
||||
t.line("\n [L]ogin [N]ew user [G]uest look around [Q]uit")
|
||||
c = t.ask("\n> ").lower()
|
||||
if c.startswith("l"):
|
||||
name = t.ask(" Username: ", maxlen=16)
|
||||
if not name:
|
||||
continue
|
||||
pw = t.secret(" Password: ")
|
||||
u = login(con, name, pw)
|
||||
if u:
|
||||
t.line("\n Welcome back, %s." % u["username"])
|
||||
return u
|
||||
t.line(" Login failed (bad details, or account banned).")
|
||||
elif c.startswith("n"):
|
||||
u = do_signup(t, con)
|
||||
if u:
|
||||
return u
|
||||
elif c.startswith("g"):
|
||||
return "guest"
|
||||
elif c.startswith("q"):
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def do_signup(t, con):
|
||||
t.line("\n New account:")
|
||||
name = t.ask(" Choose a username (3-16, letters/digits/_): ", maxlen=16)
|
||||
if not name:
|
||||
return None
|
||||
if not name.replace("_", "").isalnum() or not 3 <= len(name) <= 16:
|
||||
t.line(" Invalid username.")
|
||||
return None
|
||||
if user_by_name(con, name):
|
||||
t.line(" That name is taken.")
|
||||
return None
|
||||
pw = t.secret(" Choose a password (min 4): ")
|
||||
if len(pw) < 4:
|
||||
t.line(" Too short.")
|
||||
return None
|
||||
if pw != t.secret(" Repeat password: "):
|
||||
t.line(" Passwords did not match.")
|
||||
return None
|
||||
uid = create_user(con, name, pw)
|
||||
if not uid:
|
||||
t.line(" Could not create the account (hashing failed).")
|
||||
return None
|
||||
t.line("\n Account created. It works on https://wap.txt3.net too.")
|
||||
return bbsdb.user_by_id(con, uid)
|
||||
|
||||
|
||||
def guest_menu(t, con):
|
||||
while True:
|
||||
t.header("GUEST - read only")
|
||||
t.line(" [F]orums (read) [U]ser list [S]cores [L]ogin/signup [Q]uit")
|
||||
c = t.ask("\nguest> ").lower()
|
||||
if c.startswith("f"):
|
||||
boards.forum_menu(t, con, {"id": -1, "is_admin": 0, "username": "guest"})
|
||||
elif c.startswith("u"):
|
||||
screens.userlist(t, con)
|
||||
elif c.startswith("s"):
|
||||
games.scores(t, con)
|
||||
elif c.startswith("l"):
|
||||
return "login"
|
||||
elif c.startswith("q") or c == "":
|
||||
return None
|
||||
|
||||
|
||||
def main_menu(t, con, me):
|
||||
while True:
|
||||
me = bbsdb.user_by_id(con, me["id"])
|
||||
if not me or me["is_banned"]:
|
||||
t.line("\n Your account is no longer active. Goodbye.")
|
||||
return
|
||||
touch(con, me["id"])
|
||||
n = bbsdb.unread(con, me["id"])
|
||||
t.header("MAIN MENU - %s%s" % (me["username"],
|
||||
" [admin]" if me["is_admin"] else ""))
|
||||
t.line(" [M]ail %s" % ("(%d unread)" % n if n else ""))
|
||||
t.line(" [F]orums")
|
||||
t.line(" [G]ames")
|
||||
t.line(" [D]ungeon - explore a shared world (also on the web)")
|
||||
t.line(" [P]rofile")
|
||||
t.line(" [U]ser list")
|
||||
if me["is_admin"]:
|
||||
t.line(" [A]dmin tools")
|
||||
t.line(" [Q]uit")
|
||||
c = t.ask("\nmain> ").lower()
|
||||
if c.startswith("m"):
|
||||
boards.mail_menu(t, con, me)
|
||||
elif c.startswith("f"):
|
||||
boards.forum_menu(t, con, me)
|
||||
elif c.startswith("g"):
|
||||
games.games_menu(t, con, me)
|
||||
elif c.startswith("d"):
|
||||
mud.mud_menu(t, con, me)
|
||||
elif c.startswith("p"):
|
||||
screens.profile_menu(t, con, me)
|
||||
elif c.startswith("u"):
|
||||
screens.userlist(t, con)
|
||||
elif c.startswith("a") and me["is_admin"]:
|
||||
screens.admin_menu(t, con, me)
|
||||
elif c.startswith("q"):
|
||||
return
|
||||
|
||||
|
||||
class Handler(socketserver.BaseRequestHandler):
|
||||
def handle(self):
|
||||
if not _sessions.acquire(blocking=False):
|
||||
try:
|
||||
self.request.sendall(b"\r\nBBS is full, try later.\r\n")
|
||||
self.request.close()
|
||||
except OSError:
|
||||
pass
|
||||
return
|
||||
con = None
|
||||
t = None
|
||||
ip = self.client_address[0]
|
||||
log("connect from %s" % ip)
|
||||
try:
|
||||
t = Term(self.request, self.client_address)
|
||||
con = connect()
|
||||
welcome(t, con)
|
||||
while True:
|
||||
who = do_login(t, con)
|
||||
if who is None:
|
||||
break
|
||||
if who == "guest":
|
||||
if guest_menu(t, con) == "login":
|
||||
continue
|
||||
break
|
||||
main_menu(t, con, who)
|
||||
break
|
||||
if t:
|
||||
t.line("\n Goodbye - 73s from txt3 BBS.\n")
|
||||
except Hangup:
|
||||
log("hangup %s" % ip)
|
||||
except Exception as e: # keep one session's bug local
|
||||
log("ERROR %s: %r" % (ip, e))
|
||||
try:
|
||||
t.line("\n Internal error - disconnecting.\n")
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
if con is not None:
|
||||
try:
|
||||
con.close()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self.request.close()
|
||||
except OSError:
|
||||
pass
|
||||
_sessions.release()
|
||||
log("disconnect %s" % ip)
|
||||
|
||||
|
||||
class Server(socketserver.ThreadingTCPServer):
|
||||
allow_reuse_address = True
|
||||
daemon_threads = True
|
||||
address_family = socket.AF_INET
|
||||
|
||||
|
||||
def main():
|
||||
# Fail loudly if the shared database is not reachable.
|
||||
try:
|
||||
con = connect()
|
||||
con.execute("SELECT 1 FROM users LIMIT 1")
|
||||
con.close()
|
||||
except Exception as e:
|
||||
log("FATAL: cannot open database %s: %r" % (bbsdb.DB_PATH, e))
|
||||
return 1
|
||||
srv = Server((HOST, PORT), Handler)
|
||||
log("txt3 BBS listening on %s:%d (db=%s)" % (HOST, PORT, bbsdb.DB_PATH))
|
||||
try:
|
||||
srv.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
log("shutting down")
|
||||
finally:
|
||||
srv.server_close()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user