aboutsummaryrefslogtreecommitdiffstats
path: root/dev-docs/herald.md
diff options
context:
space:
mode:
Diffstat (limited to 'dev-docs/herald.md')
-rw-r--r--dev-docs/herald.md140
1 files changed, 140 insertions, 0 deletions
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).