feat: add IRCv3 cap negotiation, channel management, state persistence

Implement CAP LS 302 flow with configurable ircv3_caps list, replacing
the minimal SASL-only registration. Parse IRCv3 message tags (@key=value)
with proper value unescaping. Add channel management plugin (kick, ban,
unban, topic, mode) and bot API methods. Add SQLite key-value StateStore
for plugin state persistence with !state inspection command.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
user
2026-02-15 03:07:06 +01:00
parent 4a2960b288
commit f86cd1ad49
14 changed files with 614 additions and 49 deletions

View File

@@ -68,6 +68,37 @@ admins = ["*!~user@trusted.host", "ops!*@*.ops.net"]
IRC operators are auto-detected via WHO. Hostmask patterns use fnmatch.
## Channel Management (admin)
```
!kick nick reason # Kick user from channel
!ban *!*@bad.host # Ban hostmask
!unban *!*@bad.host # Remove ban
!topic New topic text # Set channel topic
!topic # Query current topic
!mode +m # Set channel mode
!mode +o nick # Give ops
```
## State Store (admin)
```
!state list myplugin # List keys
!state get myplugin key # Get value
!state del myplugin key # Delete key
!state clear myplugin # Clear all keys
```
## IRCv3 Capabilities
```toml
# config/derp.toml
[server]
ircv3_caps = ["multi-prefix", "away-notify", "server-time"]
```
SASL auto-added when sasl_user/sasl_pass configured.
## Plugin Management (admin)
```
@@ -232,6 +263,7 @@ msg.is_channel # True if channel
msg.prefix # nick!user@host
msg.command # PRIVMSG, JOIN, etc.
msg.params # All params list
msg.tags # IRCv3 tags dict
```
## Config Locations

View File

@@ -35,6 +35,13 @@ realname = "derp IRC bot" # Real name field
password = "" # Server password (optional)
sasl_user = "" # SASL PLAIN username (optional)
sasl_pass = "" # SASL PLAIN password (optional)
ircv3_caps = [ # IRCv3 capabilities to request
"multi-prefix",
"away-notify",
"server-time",
"cap-notify",
"account-notify",
]
[bot]
prefix = "!" # Command prefix character
@@ -66,6 +73,12 @@ level = "info" # Logging level: debug, info, warning, error
| `!unload <plugin>` | Unload a plugin (admin) |
| `!admins` | Show admin patterns and detected opers (admin) |
| `!plugins` | List loaded plugins with handler counts |
| `!state <action> <plugin> [key]` | Inspect plugin state store (admin) |
| `!kick <nick> [reason]` | Kick user from channel (admin) |
| `!ban <mask>` | Ban a hostmask in channel (admin) |
| `!unban <mask>` | Remove a ban from channel (admin) |
| `!topic [text]` | Set or query channel topic (admin) |
| `!mode <mode> [args]` | Set channel mode (admin) |
| `!dns <target> [type]` | DNS lookup (A, AAAA, MX, NS, TXT, CNAME, PTR, SOA) |
| `!encode <fmt> <text>` | Encode text (b64, hex, url, rot13) |
| `!decode <fmt> <text>` | Decode text (b64, hex, url, rot13) |
@@ -156,7 +169,8 @@ are configured.
| `!whoami` | Show your hostmask and admin status |
| `!admins` | Show configured patterns and detected opers (admin) |
Admin-restricted commands: `!load`, `!reload`, `!unload`, `!admins`.
Admin-restricted commands: `!load`, `!reload`, `!unload`, `!admins`, `!state`,
`!kick`, `!ban`, `!unban`, `!topic`, `!mode`.
### Writing Admin Commands
@@ -166,6 +180,76 @@ async def cmd_dangerous(bot, message):
...
```
## IRCv3 Capability Negotiation
The bot negotiates IRCv3 capabilities using `CAP LS 302` during registration.
This enables richer features on servers that support them.
### Default Capabilities
```toml
[server]
ircv3_caps = ["multi-prefix", "away-notify", "server-time",
"cap-notify", "account-notify"]
```
| Capability | Purpose |
|------------|---------|
| `multi-prefix` | Better IRCOP/voice detection |
| `away-notify` | Receive AWAY status changes |
| `server-time` | Accurate message timestamps |
| `cap-notify` | Dynamic capability updates |
| `account-notify` | Account login/logout notices |
| `sasl` | Auto-added when SASL credentials configured |
The bot only requests caps the server advertises. SASL is automatically
included when `sasl_user` and `sasl_pass` are configured.
### Message Tags
IRCv3 message tags (`@key=value;...`) are parsed automatically and available
on the message object as `message.tags` (a `dict[str, str]`). Values are
unescaped per the IRCv3 spec.
## Channel Management
Channel management commands require admin privileges and must be used in a
channel (not DM).
```
!kick <nick> [reason] Kick a user from the channel
!ban <mask> Set +b on a hostmask (e.g. *!*@bad.host)
!unban <mask> Remove +b from a hostmask
!topic [text] Set topic (empty = query current topic)
!mode <mode> [args] Set raw MODE on the channel
```
The bot must have channel operator status for these commands to take effect.
## Plugin State Persistence
Plugins can persist key-value data across restarts via `bot.state`:
```python
bot.state.set("myplugin", "last_run", "2026-02-15")
value = bot.state.get("myplugin", "last_run")
bot.state.delete("myplugin", "last_run")
keys = bot.state.keys("myplugin")
bot.state.clear("myplugin")
```
Data is stored in `data/state.db` (SQLite). Each plugin gets its own
namespace so keys never collide.
### Inspection Commands (admin)
```
!state list <plugin> List keys for a plugin
!state get <plugin> <key> Get a value
!state del <plugin> <key> Delete a key
!state clear <plugin> Clear all state for a plugin
```
## Local Databases (Wave 3)
Several plugins rely on local data files in the `data/` directory. Use the
@@ -254,6 +338,10 @@ The `bot` object provides:
| `bot.join(channel)` | Join a channel |
| `bot.part(channel [, reason])` | Leave a channel |
| `bot.quit([reason])` | Disconnect from server |
| `bot.kick(channel, nick [, reason])` | Kick a user |
| `bot.mode(target, mode_str, *args)` | Set a mode |
| `bot.set_topic(channel, topic)` | Set channel topic |
| `bot.state` | Plugin state store (get/set/delete/keys/clear) |
The `message` object provides:
@@ -266,3 +354,4 @@ The `message` object provides:
| `message.text` | Trailing text content |
| `message.is_channel` | Whether target is a channel |
| `message.params` | All message parameters |
| `message.tags` | IRCv3 message tags (dict) |