Add multi-account command ACL system

This commit is contained in:
Codex
2026-05-13 22:19:27 +02:00
commit 27b2760079
14 changed files with 1296 additions and 0 deletions

101
app/commands/dispatcher.py Normal file
View File

@@ -0,0 +1,101 @@
from __future__ import annotations
from dataclasses import dataclass
import logging
from typing import TYPE_CHECKING
from app.commands.core import (
CommandContext,
CommandRegistry,
normalize_command_name,
)
if TYPE_CHECKING:
from app.manager import AccountManager, AccountState
from pyrogram import Client
from pyrogram.types import Message
log = logging.getLogger(__name__)
@dataclass(slots=True)
class ParsedCommand:
prefix: str
name: str
args: str
alias: str
def parse_command(text: str) -> ParsedCommand | None:
stripped = text.lstrip()
if not stripped.startswith("."):
return None
prefix = ".." if stripped.startswith("..") else "."
payload = stripped[len(prefix) :]
if not payload.strip():
return None
parts = payload.strip().split(maxsplit=1)
if not parts:
return None
alias = parts[0]
name = normalize_command_name(alias)
args = parts[1] if len(parts) > 1 else ""
return ParsedCommand(prefix=prefix, name=name, args=args, alias=alias)
class CommandDispatcher:
def __init__(self, registry: CommandRegistry) -> None:
self.registry = registry
async def dispatch(
self,
client: "Client",
message: "Message",
manager: "AccountManager",
account: "AccountState",
) -> bool:
text = getattr(message, "text", None)
if not isinstance(text, str):
return False
parsed = parse_command(text)
if parsed is None:
return False
definition = self.registry.resolve(parsed.name)
if definition is None:
await message.reply_text(f"Unknown command: {parsed.prefix}{parsed.alias}")
return True
if parsed.prefix != definition.prefix:
await message.reply_text(f"Use {definition.display_name} for this command")
return True
ctx = CommandContext(
client=client,
manager=manager,
account=account,
message=message,
command=definition,
prefix=parsed.prefix,
raw_text=text,
args=parsed.args,
alias=parsed.alias,
)
for check in definition.checks:
result = await check.run(ctx)
if not result.allowed:
await message.reply_text(result.reason or "Access denied")
return True
try:
await definition.handler(ctx)
except Exception as exc: # pragma: no cover - defensive logging
log.exception("Command %s failed: %s", definition.display_name, exc)
await ctx.reply("Command failed with an internal error")
return True