qdb -- a Gopher <-> IRC quote database ============================================================= qdb bridges IRC and gopherspace. In #main on irc.someodd.zip you type .qdb 10 and the last 10 lines of channel buffer are saved forever as a plain-text file; oddbot replies in-channel with the gopher permalink. In gopherspace those snapshots are browsable as text or rendered as a Microsoft Comic Chat strip. The directory of .txt files IS the database -- no schema, no SQLite. Each file is self-contained: a setext header block then the raw transcript. ------------------------------------------------------------ Directory contents ------------------------------------------------------------ qdbviewer.lhs the gopher applet (read side). Literate Haskell, run by Venusia's .lhs script- extension. qdb-watch.sh capture side: tails ii's channel `out` file, writes quotes/.txt on `.qdb`, announces the permalink back qdb-logprune.sh weekly trim of ii's append-only `out` logs qdb-comic render shim: a snapshot -> a Comic Chat strip PNG via Spittoon. Called by qdbviewer.lhs. comic-config.yaml Spittoon config used by qdb-comic spittoon-compat.rb Ruby compatibility shim that lets Spittoon (2005-era) run on modern RMagick + Ruby 3.x qdb.env deployment config for the systemd units systemd/ qdb-stunnel / qdb-ii / qdb-watch / qdb-logprune .service and .timer files stunnel/qdb.conf TLS tunnel config (only used if the IRC server is remote --- see setup section) setup.sh installs the systemd user units README.txt this file quotes/ data dir: .txt snapshots, cached .comic.png renders, and a .counter file One level up, ../qdb.bcard is the directory sidecar shown when you browse the /applets menu. (Subdirectory applets get exactly one .bcard, at the parent level --- not another one inside.) ------------------------------------------------------------ The .qdb command (IRC side) ------------------------------------------------------------ .qdb save the last 10 lines of channel buffer .qdb N save the last N lines (1..50) oddbot replies with the gopher permalink. The buffer holds the last 50 chat lines the watcher has seen since it started -- joins, parts, and other `-!-` server notices are excluded, so a snapshot is conversation, not channel noise. ------------------------------------------------------------ Endpoints (gopher side) ------------------------------------------------------------ `$SCRIPT` is wherever routes.toml mounts qdbviewer.lhs, e.g. /applets/qdb/qdbviewer.lhs $SCRIPT landing -- latest quotes, browse, random $SCRIPT/browse full index, newest first, plus a link to the raw quotes/ directory $SCRIPT/random a random quote's menu $SCRIPT/q/ per-quote menu: read-as-text / Comic Chat strip / raw .txt, plus a bookmarkable permalink $SCRIPT/q//txt the quote as plain gopher text $SCRIPT/q//comic the quote as a Microsoft Comic Chat strip (PNG). Rendered lazily on first request and cached beside the .txt as .comic.png; later requests are served straight from the cached file by Venusia's [[files]] block. `` is always all-digits; anything else is rejected. ------------------------------------------------------------ From the command line ------------------------------------------------------------ curl gopher://gopher.someodd.zip/1/applets/qdb/qdbviewer.lhs curl gopher://gopher.someodd.zip/1/applets/qdb/qdbviewer.lhs/q/0001 curl gopher://gopher.someodd.zip/0/applets/qdb/qdbviewer.lhs/q/0001/txt curl gopher://gopher.someodd.zip/I/applets/qdb/qdbviewer.lhs/q/0001/comic > 0001.png ------------------------------------------------------------ Snapshot file format (quotes/.txt) ------------------------------------------------------------ qdb #0042 ========= saved-by: alice channel: #main network: irc.someodd.zip date: 2026-05-14T17:30:00Z lines: 10 2026-05-14 17:29 it compiled on my machine i swear 2026-05-14 17:29 your machine is a liar Title line + setext underline, a block of `key: value` headers, a blank line, then the verbatim transcript. The parser is tolerant: missing headers render as "?", a malformed file still shows its body. ------------------------------------------------------------ Setting it up ------------------------------------------------------------ qdb is two halves: a gopher applet (no setup beyond dropping the files in place) and an always-on IRC capture daemon (three small systemd user services). 1. FILES Put this directory at applets/qdb/ under Venusia's gopher root, with ../qdb.bcard beside it. The applet is live immediately -- Venusia's existing /applets [[files]] block already runs .lhs. 2. routes.toml (optional) The applet links cached comics as image items itself, so nothing is strictly required. To also have raw quotes/*.png show as images when the directory is browsed, add to the /applets [[files]] block: [[files.file_type]] extension = "png" item_type = "I" 3. PERMISSIONS quotes/ must be writable by BOTH the watcher (snapshots) and the Venusia daemon (cached PNGs). Make it group-venusia, group- writable, setgid, and run the watcher as a user in group venusia: chgrp venusia applets/qdb applets/qdb/quotes chmod 2775 applets/qdb applets/qdb/quotes 4. IRC CAPTURE DAEMON Edit qdb.env (nick, channel, network display name), then run -- as the user the bot should run as (a member of group venusia): ./setup.sh systemctl --user enable --now qdb-ii qdb-watch setup.sh installs the user units; the two enabled here are: qdb-ii ii: connects to the IRC server, joins the channel qdb-watch tails ii's `out`, writes snapshots, announces (BindsTo qdb-ii) Follow them: journalctl --user -u qdb-ii -u qdb-watch -f For the bot to survive logout: loginctl enable-linger "$USER" ON SIMULACRA the bot runs as `venusia` -- the same user Venusia runs applets as. That makes `venusia` the OWNER of ii's `out` log, so any applet can read it directly with no cross-user group hop; the group-venusia/setgid quotes/ setup in step 3 still applies and is satisfied trivially. (Running as venusia also means applet code could write ii's `in` FIFO -- acceptable here, where only trusted venusia-owned applets run.) SHARED LOG: ii's `out` is a single append-only transcript with two independent readers -- qdb-watch (this: curated `.qdb` quotes) and girc.lhs's `/log` view (full scrollback). One logger, one log, two gopher front-ends; see ../girc.lhs. ii connects to 127.0.0.1:6667. On simulacra the IRC server (ngircd) runs on the same host, so that is a loopback connection -- no TLS needed, the traffic never leaves the machine. Because ii dials 127.0.0.1 its on-disk dir is ~/irc/127.0.0.1/ -- qdb.env's QDB_IRC_NETDIR reflects that, while QDB_IRC_NET is the pretty name written into each snapshot. IF YOUR IRC SERVER IS REMOTE: ii is plaintext-only, so put TLS in front of it. setup.sh also installs qdb-stunnel.service (left disabled). Point the `connect` line in stunnel/qdb.conf at your server, then also: systemctl --user enable --now qdb-stunnel stunnel listens on 127.0.0.1:6667 and tunnels to the remote server; ii's connection is unchanged. Caveats: - ii does not auto-rejoin after an IRC-side reconnect. Run `systemctl --user restart qdb-ii` to rejoin. - NickServ identification, if the nick needs it, can be added as another ExecStartPost in qdb-ii.service. 5. LOG PRUNING (optional) ii has no built-in rotation -- it appends every channel line to one `out` file per channel, forever. qdb-logprune.sh trims those to a recent window (QDB_LOG_KEEP_DAYS in qdb.env, default 14); setup.sh installs it as a weekly timer, left disabled. Turn it on with: systemctl --user enable --now qdb-logprune.timer Because ii holds `out` open, the prune briefly stops qdb-ii (and thus qdb-watch) while it rewrites the files, ~10s once a week. qdb snapshots in quotes/ are NEVER pruned -- they are permanent, which is the whole point. ------------------------------------------------------------ Comic Chat (the /comic render mode) ------------------------------------------------------------ /comic shells out to qdb-comic, which drives Spittoon (statico/ spittoon) -- an implementation of the Microsoft Comic Chat SIGGRAPH '96 layout algorithm. Dependencies (root-level, one-time): - Ruby + ImageMagick + RMagick sudo apt install imagemagick libmagickwand-dev ruby-dev sudo gem install rmagick # must be a SYSTEM gem --- # /comic runs as `venusia` - Spittoon sudo git clone https://github.com/statico/spittoon /opt/spittoon qdb works without these; /comic just returns a graceful "is Spittoon installed?" error until they are in place. Spittoon is 2005-era Ruby and predates several modern API changes (RMagick configuration blocks no longer instance_eval; File.exists? removed in Ruby 3.2). qdb-comic loads spittoon-compat.rb via `ruby -r` ahead of make_comic.rb to bridge those --- without touching the root-owned /opt/spittoon tree. If you upgrade RMagick / Ruby and /comic starts failing, the shim is the first place to look. qdb-comic is zero-config in the standard layout: - Spittoon lib/bin /opt/spittoon/lib, /opt/spittoon/bin/make_comic.rb - Spittoon config comic-config.yaml (beside qdb-comic) - assets dir the dir cd'd into so the config's relative artwork/background paths resolve: ./comic-assets/ if it has an artwork/ subdir, else /opt/spittoon/examples/ All four are overridable: QDB_SPITTOON_LIB, QDB_SPITTOON_BIN, QDB_SPITTOON_CONFIG, QDB_COMIC_ASSETS. Also QDB_COMIC_MAXPANELS (default 24) and QDB_COMIC_RETRIES (default 5). AVATARS -- the authentic look: Out of the box qdb-comic uses Spittoon's bundled example artwork (bucket/easy/melon/nibbles/taff). The real Microsoft Comic Chat cast (TIKI, MAYNARD, ANNA, ...) shipped as .avb files and is NOT a drop-in retrofit; here is the state of the world as of the build of this kit: - AVBuster (the only known .avb extractor) does NOT work on the official MS-shipped characters, only on user-made custom ones. - files.defcon.no/mscc/ has all 22 official characters as PNGs, but only ONE pose each (single portraits, ~50x128 px). - Spittoon expects 17 sprites per character (10 face names composited as the head + 7 pose names composited as the body), so the defcon portraits do not slot in directly. - The .avb v2.1/v2.5 format is documented at , but the official files appear to have additional encoding beyond what is published. Writing a working extractor is real preservation work, not a config swap. Drop-in path if you DO end up with a full per-expression sprite set: put it under a comic-assets/ dir beside qdb-comic and it is picked up automatically. comic-assets/artwork/faces/-.png comic-assets/artwork/poses/-.png comic-assets/backgrounds/*.jpg Every character needs a PNG for each face name in comic-config.yaml (angry, annoyed, bored, grinning, grossed, happy, listening, shocked, speaking, surprised) and each pose name (exclaiming, explaining, pointing, question, standing1, standing2, surprised). List the character names under `characters:` in comic-config.yaml. Spittoon picks layout variations at random and occasionally bails when one doesn't fit; qdb-comic retries to absorb that. The first /comic hit for a quote renders and caches .comic.png beside the .txt; later hits are served straight from the cache. ------------------------------------------------------------ Running the doctests ------------------------------------------------------------ qdbviewer.lhs's pure helpers carry doctest examples: stack exec --resolver lts-22.6 \ --package doctest \ --package text --package bytestring --package directory \ --package filepath --package time --package process \ -- doctest -XOverloadedStrings qdbviewer.lhs ------------------------------------------------------------ Limits ------------------------------------------------------------ `.qdb N` is clamped to 1..50. The ring buffer is 50 lines; right after a watcher restart it is short until the channel talks again. The comic render is capped at 24 panels (QDB_COMIC_MAXPANELS) -- longer quotes show their first 24 lines as a comic; /txt always shows the whole thing. Unrecognised path-info, bad ids, and missing quotes come back as a type-3 row plus a "back to qdb" link.