aboutsummaryrefslogtreecommitdiffstats
path: root/dev-docs/jeeves.md
diff options
context:
space:
mode:
authorAhmed <git@gumx.cc>2026-06-14 01:46:29 +0300
committerAhmed <git@gumx.cc>2026-06-14 01:46:29 +0300
commit5a8d568931d9b23ce0df1265d05259a7012081c9 (patch)
tree4ea19b8bb1763a38246f342f172c285f6579e09b /dev-docs/jeeves.md
init: mostly vibed
Diffstat (limited to 'dev-docs/jeeves.md')
-rw-r--r--dev-docs/jeeves.md242
1 files changed, 242 insertions, 0 deletions
diff --git a/dev-docs/jeeves.md b/dev-docs/jeeves.md
new file mode 100644
index 0000000..27ba163
--- /dev/null
+++ b/dev-docs/jeeves.md
@@ -0,0 +1,242 @@
+# jeeves - developer documentation
+
+## Overview
+
+Jeeves is a combined-features IRC bot built on Sopel. It merges the functionality
+of herald (admin/follow/keywords), daft (DJ queue), daffy (duck hunting), contessa
+(recipes), shireen (news/weather), and salvador (image art), and adds a new quote
+bank with auto-posting. All tunable parameters are runtime-configurable via `!set`
+and persisted to a single JSON file.
+
+---
+
+## Module structure
+
+```
+jeeves/
+ jv_core.py - shared helpers, CONFIG_KEYS, db I/O, auth utils (not in enable list)
+ helpstrings.py - help string constants and lookup() (not in enable list)
+ admin.py - !join, !part, !chans, !pause, !resume
+ dj.py - YouTube DJ queue
+ duck.py - duck hunting game
+ recipes.py - recipe bot
+ news.py - weather (wttr.in) + RSS news
+ art.py - mIRC colour art renderer (Pillow)
+ respond.py - keyword responder
+ quotes.py - quote bank + auto-post
+ config.py - !set, !config
+ help.py - !help
+ __init__.py - empty
+```
+
+---
+
+## Shared state (bot.memory)
+
+`jv_core.ensure_setup()` initializes all persistent state on first call:
+
+```
+bot.memory["jv_db"] - main data dict loaded from JSON
+bot.memory["jv_db_path"] - path to db.json
+bot.memory["jv_duck_db_path"] - path to scores.db
+bot.memory["jv_recipes_path"] - path to recipes.json
+bot.memory["jv_lock"] - threading.Lock() for db writes
+```
+
+Additional per-module memory keys (non-persistent, set in setup()):
+
+```
+bot.memory["dj_queue"] - list of normalized YouTube URLs
+bot.memory["dj_pos"] - current position (-1 = not playing)
+bot.memory["dj_playing"] - bool
+bot.memory["dj_timer"] - threading.Timer or None
+bot.memory["dj_loop"] - bool
+bot.memory["dj_channel"] - str or None
+bot.memory["dj_lock"] - threading.RLock() (separate from jv_lock)
+bot.memory["dk_channels"] - {channel_lower: state_dict} duck state
+bot.memory["dk_reload_cd"] - {nick: timestamp} reload cooldown
+bot.memory["dk_players"] - {nick: {"ammo": int}}
+bot.memory["dk_lock"] - threading.Lock()
+bot.memory["jv_paused"] - bool (keyword responder)
+bot.memory["jv_recipes"] - dict loaded from recipes.json
+```
+
+---
+
+## Database structure
+
+### JSON database (jv_db)
+
+```json
+{
+ "config": {"dj_track_duration": 90},
+ "channels": {
+ "#example": {
+ "config": {"duck_spawn_min": 30},
+ "news": {"enabled": true, "last_broadcast": 0},
+ "quotes": {"enabled": false, "last_broadcast": 0}
+ }
+ },
+ "quotes": ["quote text", ...],
+ "news_feeds": [...9 feeds...],
+ "news_seen": ["guid1", ...]
+}
+```
+
+Config resolution is a two-level merge: global config override → channel config
+override, applied on top of hardcoded defaults from `CONFIG_KEYS`.
+
+`cfg(bot, channel)` returns the merged dict.
+`cfg_val(bot, key, channel)` returns a single typed value.
+
+### SQLite database (scores.db)
+
+Single `scores` table: `nick`, `channel`, `score`. Primary key `(nick, channel)`.
+
+---
+
+## CONFIG_KEYS
+
+```python
+CONFIG_KEYS = {
+ "dj_track_duration": (int, 120, True, "seconds per track before auto-advance"),
+ "dj_queue_cap": (int, 50, True, "max tracks in queue"),
+ "art_height": (int, 6, True, "default lines for !draw (4-12)"),
+ "duck_spawn_min": (int, 60, False, "min seconds between duck spawns"),
+ "duck_spawn_max": (int, 300, False, "max seconds between duck spawns"),
+ "duck_flee_time": (int, 45, False, "seconds before duck flees"),
+ "duck_idle_rounds": (int, 3, False, "empty rounds before auto-stop (0=off)"),
+ "news_interval": (int, 90, False, "minutes between news broadcasts"),
+ "news_count": (int, 1, False, "headlines per broadcast cycle"),
+ "quotes_interval": (int, 60, False, "minutes between quote auto-posts"),
+}
+```
+
+The tuple is `(type, default, global_only, description)`.
+
+`global_only=True` means the key can only be changed in PM by the owner. Setting
+it in a channel is rejected with a notice. This is used for keys that have no
+per-channel meaning (DJ track duration, art height).
+
+---
+
+## Help system
+
+Same three-level pattern as alfred and osterman:
+
+```
+!help → TOPICS
+!help <topic> → topic line (list of commands)
+!help <cmd> → command description
+!help <topic> <cmd> → same as above
+```
+
+`helpstrings.lookup(args)` in `helpstrings.py` handles all dispatch. `help.py`
+imports it via sys.path insertion. Unknown args fall back to the TOPICS line.
+
+---
+
+## Module import pattern
+
+All plugin files use sys.path insertion to import jv_core:
+
+```python
+import os as _os, sys as _sys
+_d = _os.path.dirname(_os.path.abspath(__file__))
+if _d not in _sys.path:
+ _sys.path.insert(0, _d)
+import jv_core as jv
+```
+
+`jv_core.py` and `helpstrings.py` are not in the `enable` list and are never
+loaded directly by Sopel. They are shared modules, importable via the sys.path
+trick above.
+
+---
+
+## Message routing
+
+| Output | Method | Destination |
+|--------|--------|-------------|
+| Game events (duck spawn/kill/flee) | `bot.say` | channel |
+| Score leaderboard | `bot.say` | channel |
+| Owner-only score queries | `bot.notice` | nick |
+| DJ now-playing, queue | `bot.say` | channel |
+| DJ feedback (queued, removed) | `bot.say` | channel |
+| DJ error feedback | `bot.notice` | nick |
+| News/weather | `bot.say` | channel/sender |
+| News broadcast (tick) | `bot.say` | channel |
+| Quotes auto-post | `bot.say` | channel |
+| Config feedback | `bot.notice` | nick |
+| Admin feedback | `bot.notice` | nick |
+| Help | `bot.notice` | nick |
+
+---
+
+## DJ notes
+
+- Track duration: read fresh on each play via `jv.cfg_val(bot, "dj_track_duration")` —
+ changing `dj_track_duration` affects tracks queued after the change, not any
+ currently running timer.
+- `!loop` (no args): shows state to anyone, no privilege check.
+- `!loop on|off`: requires op (checked manually via `jv.is_authorized`).
+- Queue display: no leading indentation — `{i}. {url}` format.
+
+---
+
+## Duck notes
+
+- Spawn times are read per-tick from `jv.cfg_val(bot, "duck_spawn_min", channel)`.
+ A config change takes effect on the next spawn scheduling.
+- Each channel has independent state in `dk_channels[channel.lower()]`.
+- Hunt is off by default; ops must `!hunt start` per channel.
+- `!hunt` status output includes current `duck_spawn_min`, `duck_spawn_max`, and
+ `duck_flee_time` values. Authorized users also receive a notice reminding them
+ these are adjustable via `!set`.
+
+---
+
+## News notes
+
+- `!news interval` and `!news count` commands from shireen are replaced by
+ `!set news_interval` / `!set news_count`. The `!news` command only handles
+ on/off toggle and status display.
+- Per-channel news state (`enabled`, `last_broadcast`) lives in
+ `jv.chan_db(bot, channel)["news"]`.
+- Broadcast tick runs every 60s; the interval check is done in-tick against
+ `cfg_val(bot, "news_interval", channel) * 60`.
+
+---
+
+## Quotes notes
+
+- Quotes are stored globally in `jv_db["quotes"]` — shared across all channels.
+- Per-channel auto-post state is in `jv.chan_db(bot, channel)["quotes"]`.
+- `!quote del N` uses 1-based indexing for user-facing display.
+
+---
+
+## Pillow dependency (art.py)
+
+`art.py` checks `_PILLOW` at import time. If Pillow is not installed, `!draw`
+responds with a notice and returns immediately rather than crashing.
+
+`CHAR_RATIO = 2.0` in `art.py` corrects for IRC monospace character cells
+being approximately twice as tall as they are wide. `_fit()` multiplies the
+image's pixel aspect ratio by this factor before computing output dimensions,
+preventing the 2× vertical stretch that would otherwise occur.
+
+---
+
+## Known issues and tradeoffs
+
+**DJ timer not persisted.** If the bot restarts mid-track the timer is lost.
+The queue survives (it's in memory only while the bot runs anyway — not stored
+to disk). Reissue `!play` after restart.
+
+**Duck hunt off by default.** Unlike daffy, jeeves does not auto-start the hunt
+on any channel. Ops must `!hunt start`. This avoids unwanted duck spam in channels
+that don't want the game.
+
+**Global quotes bank.** All channels share the same quote pool. There is no
+per-channel quote list.