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/herald.md | 140 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 dev-docs/herald.md (limited to 'dev-docs/herald.md') diff --git a/dev-docs/herald.md b/dev-docs/herald.md new file mode 100644 index 0000000..c23cf44 --- /dev/null +++ b/dev-docs/herald.md @@ -0,0 +1,140 @@ +# herald - developer documentation + +## Overview + +Herald (also deployed as bikabot) does two things: it follows the owner around +IRC channels automatically, and it responds to configured keywords in channel +messages. These are implemented in two independent plugin files. + +--- + +## Module structure + +``` +herald/ + commands.py - follow logic, admin commands, owner presence tracking + respond.py - keyword-based auto-responder + __init__.py - empty +``` + +--- + +## commands.py: follow logic + +### How follow works + +When `!follow on` is active, the bot sends a WHOIS for the owner every 30 +seconds via `@plugin.interval(30)`. The server responds with one or more IRC +numeric replies: + +- **319 RPL_WHOISCHANNELS**: lists the channels the owner is in. +- **318 RPL_ENDOFWHOIS**: signals the end of the WHOIS response. + +If 319 arrives, `_whois_channels()` parses the channel list and joins any +channel the owner is in that the bot is not already in. Joined channels are +tracked in `bot.memory["follow_joined"]`. + +If 318 arrives with no preceding 319 (the flag `whois_got_channels` is still +False), the owner is offline. The bot leaves all follow-joined channels. + +### Preconfigured channels + +Channels listed under `channels` in the bot config are never auto-left, even +if the owner leaves or goes offline. `_configured_channels()` reads +`bot.settings.core.channels` and returns a set of lowercase channel names. + +This matters for the PART and QUIT event handlers: if the owner leaves a +channel, the bot only follows if the channel is not preconfigured. + +### PART and QUIT handling + +`on_owner_part()` fires on any PART. If the parting nick is the owner and the +channel is not preconfigured, the bot parts too. + +`on_owner_quit()` fires on QUIT and leaves all non-preconfigured channels +immediately without waiting for the next WHOIS cycle. + +### Why WHOIS instead of tracking JOIN/PART directly + +Tracking JOIN/PART directly would miss channels the owner was already in before +the bot connected. WHOIS gives a complete current picture regardless of when +the bot joined the network. + +### RPL_WHOISCHANNELS parsing + +The 319 reply contains channel names prefixed with membership status characters +(`@`, `+`, `~`, `&`, `%`). The parser strips these prefixes before comparing +channel names: + +```python +chan = part.lstrip("@+~&%") +``` + +--- + +## commands.py: !pause / !resume + +`!pause` sets `bot.memory["herald_paused"] = True`; `!resume` clears it. Both +are owner-only and work in any context (channel or PM). The `respond()` handler +in `respond.py` checks this flag before processing any message. + +--- + +## commands.py: !unfollow + +`!unfollow` leaves all channels in `follow_joined` immediately and clears the +set. It does not disable `follow_enabled`. After `!unfollow`, the bot stops +following for that session but `!follow on` can restart it. + +--- + +## respond.py: keyword responder + +### How it works + +`respond()` fires on every channel message. It loads `keywords` and `responses` +from the `[respond]` config section (both are `ListAttribute`, supporting +newline-separated values in the config file). If any keyword appears anywhere +in the message (case-insensitive using `casefold()`), a random response is sent. + +### Why casefold over lower + +`casefold()` is more aggressive than `lower()` for Unicode. For a bot that +might see messages in Arabic or other scripts, casefold handles more edge +cases correctly. + +### Error handling + +The match is wrapped in a try/except for `UnicodeError` and `AttributeError`. +This guards against malformed trigger data from the IRC layer. + +--- + +## State + +``` +bot.memory["follow_enabled"] - bool, follow mode on/off +bot.memory["follow_joined"] - set of channel names joined via follow +bot.memory["whois_got_channels"] - bool, reset each WHOIS cycle +bot.memory["herald_paused"] - bool, keyword responses paused on/off +``` + +None of this persists across restarts. Both `follow_enabled` and `herald_paused` +are off by default on startup. + +`herald_paused` is set by `!pause` / `!resume` in `commands.py` and read by +`respond()` in `respond.py`. The two plugins share `bot.memory`, so no import +or coupling between files is needed. + +--- + +## Known issues and tradeoffs + +**WHOIS interval is fixed at 30 seconds.** If the owner moves channels quickly, +the bot may lag behind by up to 30 seconds. Shortening the interval increases +WHOIS traffic on the server. + +**Follow only joins, never leaves proactively.** With `!follow on`, the bot +joins channels the owner is in but only leaves them on QUIT/PART events or +when the owner goes offline. It does not automatically leave a channel if the +owner leaves without the bot noticing (network split, for example). -- cgit v1.2.3