aboutsummaryrefslogtreecommitdiffstats
path: root/dev-docs/herald.md
blob: c23cf444f6b77164aaffdb1d7aa6e140e88fd55f (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
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).