feat: add granular ACL tiers (trusted/oper/admin)

4-tier permission model: user < trusted < oper < admin.
Commands specify a required tier via tier= parameter.
Backward compatible: admin=True maps to tier="admin".

- TIERS constant and Handler.tier field in plugin.py
- _get_tier() method in bot.py with pattern matching
- _is_admin() preserved as thin wrapper
- operators/trusted config lists in config.py
- whoami shows tier, admins shows all configured tiers
- 32 test cases in test_acl.py
This commit is contained in:
user
2026-02-21 17:59:05 +01:00
parent 5bc59730c4
commit 2514aa777d
6 changed files with 480 additions and 35 deletions

View File

@@ -139,34 +139,33 @@ async def cmd_plugins(bot, message):
await bot.reply(message, f"Plugins: {', '.join(parts)}")
@command("whoami", help="Show your hostmask and admin status")
@command("whoami", help="Show your hostmask and permission tier")
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:
tier = bot._get_tier(message)
tags = [tier]
if message.prefix and message.prefix in bot._opers:
tags.append("IRCOP")
await bot.reply(message, f"{prefix} [{', '.join(tags)}]")
@command("admins", help="Show configured admin patterns and detected opers", admin=True)
@command("admins", help="Show configured permission tiers and detected opers", admin=True)
async def cmd_admins(bot, message):
"""Display admin hostmask patterns and known IRC operators."""
"""Display configured permission tiers and known IRC operators."""
parts = []
if bot._admins:
parts.append(f"Patterns: {', '.join(bot._admins)}")
parts.append(f"Admin: {', '.join(bot._admins)}")
else:
parts.append("Patterns: (none)")
parts.append("Admin: (none)")
if bot._operators:
parts.append(f"Oper: {', '.join(bot._operators)}")
if bot._trusted:
parts.append(f"Trusted: {', '.join(bot._trusted)}")
if bot._opers:
parts.append(f"Opers: {', '.join(sorted(bot._opers))}")
parts.append(f"IRCOPs: {', '.join(sorted(bot._opers))}")
else:
parts.append("Opers: (none)")
parts.append("IRCOPs: (none)")
await bot.reply(message, " | ".join(parts))