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) caption = getattr(message, "caption", None) command_text = text if isinstance(text, str) else caption if not isinstance(command_text, str): return False parsed = parse_command(command_text) if parsed is None: return False definition = self.registry.resolve(parsed.name) if definition is None: return True actor_id = getattr(getattr(message, "from_user", None), "id", None) owner_id = getattr(getattr(account, "me", None), "id", None) is_owner = owner_id is not None and actor_id == owner_id expected_prefix = definition.expected_prefix(is_owner) if parsed.prefix != expected_prefix: return True ctx = CommandContext( client=client, manager=manager, account=account, message=message, command=definition, prefix=parsed.prefix, raw_text=command_text, args=parsed.args, alias=parsed.alias, ) for check in definition.checks: result = await check.run(ctx) if not result.allowed: if is_owner: 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