✨ Implemented a pure Python QOTD server
authorMalte Bublitz <malte@rolltreppe3.de>
Wed, 12 Nov 2025 12:34:14 +0000 (13:34 +0100)
committerMalte Bublitz <malte@rolltreppe3.de>
Wed, 12 Nov 2025 13:33:32 +0000 (14:33 +0100)
Tje previous QOTD server was a shell script executed by inetd, calling
fortune and doing some shitty UTF-8 to ASCII convertion which did not
support special characters like dashes.

The new server in the module `bbs.qotd` is based on Python's
`socketserver.TCPServer` and supports UTF-8 as expected nowadays.

The quotes are still those found in fortunes-mod databases, but we now
read them directly from the data files in `bbs.fortune`.

bbs/fortune.py [new file with mode: 0644]
bbs/qotd.py [new file with mode: 0644]
bin/qotd [deleted file]

diff --git a/bbs/fortune.py b/bbs/fortune.py
new file mode 100644 (file)
index 0000000..ca4d5bc
--- /dev/null
@@ -0,0 +1,65 @@
+"""Parse fortune database files and get fortunes.
+
+Open and parse fortunes-mod database files into a
+cache and get random fortunes from that cache.
+
+Example:
+       for db in ["de/unfug", "de/computer"]:
+               bbs.fortune.load_database(db)
+       print(bbs.fortune.get())
+"""
+
+import os
+import re
+import random
+
+
+# All fortunes read from databases using `load_database()`.
+fortunes = []
+
+
+def load_database(db: str, db_location: str = "/usr/share/games/fortunes"):
+       """Parse a fortunes-mod database and add the fortunes to a cache.
+
+       Read all fortunes from a database file and store them in the global
+       variable *fortunes*.
+
+       Args:
+               db: The database name, like "bofh-excuses" or "de/computer"
+               db_location: The direcory where your fortunes databases are stored.
+                 Defaults to /usr/share/games/fortunes, which is used on Debian.
+       """
+       global fortunes
+
+       dbfile = os.path.join(db_location, db)
+       f = open(dbfile, "r")
+       db_fortunes = re.split(r'\r?\n%\r?\n', f.read())
+       f.close()
+
+       fortunes += db_fortunes
+
+       # # @see https://codeberg.org/jamesansley/fortune
+       # text = [fortune for fortune in text if fortune.strip("\n\r")]
+       # fortunes += text
+
+
+def get():
+       global fortunes
+       if len(fortunes) < 1:
+               load_database()
+
+       return random.choice(fortunes)
+
+
+def main():
+       if len(sys.argv) > 1:
+               for arg in sys.argv[1:]:
+                       if arg[0] != "-":
+                               load_database(arg)
+
+       print(get())
+
+
+if __name__ == "__main__":
+       main()
+
diff --git a/bbs/qotd.py b/bbs/qotd.py
new file mode 100644 (file)
index 0000000..260d556
--- /dev/null
@@ -0,0 +1,51 @@
+"""QOTD server using socketserver.
+
+TODO
+"""
+
+
+import sys
+import os
+import socketserver
+import bbs.fortune
+
+
+class QOTDHandler(socketserver.StreamRequestHandler):
+       def handle(self):
+               quote = bbs.fortune.get()
+               self.wfile.write(quote.encode("utf-8") + b"\n")
+
+
+def main(fortune_databases: list = ["bofh-excuses", "de/unfug", "de/computer"]):
+       try:
+               PORT = int(os.getenv("QOTD_PORT", 17))
+       except ValueError:
+               print("Environment variable QOTD_PORT is not a valid integer. Falling back to port 17.", file=sys.stderr)
+               PORT = 17
+
+       for db in fortune_databases:
+               bbs.fortune.load_database(db)
+
+       listen_on = ("", PORT)
+
+       try:
+               server = socketserver.TCPServer(
+                       listen_on,
+                       QOTDHandler
+               )
+       except OSError:
+               print("Failed to bind to TCP socket " + str(listen_on), file=sys.stderr)
+               sys.exit(2)
+
+       try:
+               server.serve_forever()
+
+       except KeyboardInterrupt:
+               print("\nCtr+C pressed.\nStopping...")
+               server.shutdown()
+               server.server_close()
+
+
+if __name__=='__main__':
+       main()
+
diff --git a/bin/qotd b/bin/qotd
deleted file mode 100755 (executable)
index 140aaef..0000000
--- a/bin/qotd
+++ /dev/null
@@ -1,47 +0,0 @@
-#!/bin/bash
-# 
-# See also:
-#     RFC 865 - Quote of the Day Protocol
-# 
-
-PATH=/usr/games:$PATH
-
-fortune                 \
-       letzteworte         \
-       ms                  \
-       murphy              \
-       namen               \
-       quiz                \
-       regeln              \
-       sicherheitshinweise \
-       sprueche            \
-       stilblueten         \
-       tips                \
-       translations        \
-       unfug               \
-       vornamen            \
-       witze               \
-       woerterbuch         \
-       wusstensie          \
-       zitate              \
-       | sed \
-               -e's/Ä/Ae/g' \
-               -e's/Ö/Oe/g' \
-               -e's/Ü/Ue/g' \
-               -e's/ä/ae/g' \
-               -e's/ö/oe/g' \
-               -e's/ü/ue/g' \
-               -e's/ß/ss/g' \
-       | iconv -f UTF-8 -t US-ASCII
-
-#      | uni2ascii \
-#              -c -d -e -f -x \
-#              -S U+00E4:ae \
-#              -S U+00C4:Ae \
-#              -S U+00F6:oe \
-#              -S U+00D6:Oe \
-#              -S U+00FC:ue \
-#              -S U+00DC:Ue \
-#              -S U+00DF:ss \
-#              -q
-