feat: add admin/owner permission system
Hostmask-based admin controls with automatic IRCOP detection via WHO. Permission enforcement in the central dispatch path denies restricted commands to non-admins. Includes !whoami and !admins commands, marks load/reload/unload as admin-only. Also lands previously-implemented SASL PLAIN auth, token-bucket rate limiting, and CTCP VERSION/TIME/PING responses that were staged but uncommitted. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -76,7 +76,7 @@ async def cmd_uptime(bot, message):
|
||||
await bot.reply(message, f"up {' '.join(parts)}")
|
||||
|
||||
|
||||
@command("load", help="Hot-load a plugin: !load <name>")
|
||||
@command("load", help="Hot-load a plugin: !load <name>", admin=True)
|
||||
async def cmd_load(bot, message):
|
||||
"""Load a new plugin from the plugins directory."""
|
||||
parts = message.text.split(None, 2)
|
||||
@@ -91,7 +91,7 @@ async def cmd_load(bot, message):
|
||||
await bot.reply(message, f"Failed to load plugin: {reason}")
|
||||
|
||||
|
||||
@command("reload", help="Reload a plugin: !reload <name>")
|
||||
@command("reload", help="Reload a plugin: !reload <name>", admin=True)
|
||||
async def cmd_reload(bot, message):
|
||||
"""Unload and reload a plugin, picking up file changes."""
|
||||
parts = message.text.split(None, 2)
|
||||
@@ -106,7 +106,7 @@ async def cmd_reload(bot, message):
|
||||
await bot.reply(message, f"Failed to reload plugin: {reason}")
|
||||
|
||||
|
||||
@command("unload", help="Unload a plugin: !unload <name>")
|
||||
@command("unload", help="Unload a plugin: !unload <name>", admin=True)
|
||||
async def cmd_unload(bot, message):
|
||||
"""Unload a plugin, removing all its handlers."""
|
||||
parts = message.text.split(None, 2)
|
||||
@@ -132,3 +132,34 @@ async def cmd_plugins(bot, message):
|
||||
counts[handler.plugin] += 1
|
||||
parts = [f"{name} ({counts[name]})" for name in sorted(counts)]
|
||||
await bot.reply(message, f"Plugins: {', '.join(parts)}")
|
||||
|
||||
|
||||
@command("whoami", help="Show your hostmask and admin status")
|
||||
async def cmd_whoami(bot, message):
|
||||
"""Display the sender's hostmask and permission level."""
|
||||
prefix = message.prefix or "unknown"
|
||||
is_admin = bot._is_admin(message)
|
||||
is_oper = message.prefix in bot._opers if message.prefix else False
|
||||
tags = []
|
||||
if is_admin:
|
||||
tags.append("admin")
|
||||
else:
|
||||
tags.append("user")
|
||||
if is_oper:
|
||||
tags.append("IRCOP")
|
||||
await bot.reply(message, f"{prefix} [{', '.join(tags)}]")
|
||||
|
||||
|
||||
@command("admins", help="Show configured admin patterns and detected opers", admin=True)
|
||||
async def cmd_admins(bot, message):
|
||||
"""Display admin hostmask patterns and known IRC operators."""
|
||||
parts = []
|
||||
if bot._admins:
|
||||
parts.append(f"Patterns: {', '.join(bot._admins)}")
|
||||
else:
|
||||
parts.append("Patterns: (none)")
|
||||
if bot._opers:
|
||||
parts.append(f"Opers: {', '.join(sorted(bot._opers))}")
|
||||
else:
|
||||
parts.append("Opers: (none)")
|
||||
await bot.reply(message, " | ".join(parts))
|
||||
|
||||
Reference in New Issue
Block a user