Files
multi-bot-tg/app/commands/dispatcher.py
2026-09-14 14:35:48 +02:00

108 lines
2.9 KiB
Python

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