From 5a8d568931d9b23ce0df1265d05259a7012081c9 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Sun, 14 Jun 2026 01:46:29 +0300 Subject: init: mostly vibed --- dev-docs/jeeves.md | 242 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 dev-docs/jeeves.md (limited to 'dev-docs/jeeves.md') 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 line (list of commands) +!help → command description +!help → 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. -- cgit v1.2.3