# 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).