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

@@ -163,3 +163,57 @@ async def cmd_admins(bot, message):
else:
parts.append("Opers: (none)")
await bot.reply(message, " | ".join(parts))
@command("state", help="Inspect plugin state: !state <list|get|del|clear> ...", admin=True)
async def cmd_state(bot, message):
"""Manage the plugin state store.
Usage:
!state list <plugin> List keys
!state get <plugin> <key> Get a value
!state del <plugin> <key> Delete a key
!state clear <plugin> Clear all state
"""
parts = message.text.split()
if len(parts) < 3:
await bot.reply(message, "Usage: !state <list|get|del|clear> <plugin> [key]")
return
action = parts[1].lower()
plugin = parts[2]
if action == "list":
keys = bot.state.keys(plugin)
if keys:
await bot.reply(message, f"{plugin}: {', '.join(keys)}")
else:
await bot.reply(message, f"{plugin}: (no keys)")
elif action == "get":
if len(parts) < 4:
await bot.reply(message, "Usage: !state get <plugin> <key>")
return
key = parts[3]
value = bot.state.get(plugin, key)
if value is not None:
await bot.reply(message, f"{plugin}.{key} = {value}")
else:
await bot.reply(message, f"{plugin}.{key}: not set")
elif action == "del":
if len(parts) < 4:
await bot.reply(message, "Usage: !state del <plugin> <key>")
return
key = parts[3]
if bot.state.delete(plugin, key):
await bot.reply(message, f"Deleted {plugin}.{key}")
else:
await bot.reply(message, f"{plugin}.{key}: not found")
elif action == "clear":
count = bot.state.clear(plugin)
await bot.reply(message, f"Cleared {count} key(s) from {plugin}")
else:
await bot.reply(message, "Usage: !state <list|get|del|clear> <plugin> [key]")