from __future__ import annotations
from collections import defaultdict
from contextlib import suppress
from typing import TYPE_CHECKING, NamedTuple
from aiogram import Bot
from aiogram.dispatcher.event.handler import HandlerObject
from aiogram.exceptions import TelegramBadRequest
from aiogram.types import (
BotCommand,
BotCommandScopeChat,
BotCommandScopeDefault,
BotCommandScopeUnion,
)
from aiogram.utils.i18n.context import gettext
from raito.plugins.roles.data import RoleData
from raito.utils import loggers
if TYPE_CHECKING:
from aiogram.filters.command import Command
from aiogram.utils.i18n.lazy_proxy import LazyProxy # type: ignore
from raito.plugins.roles.manager import RoleManager
__all__ = ("register_bot_commands",)
class _CommandMeta(NamedTuple):
"""Command metadata.
:param command: Command name (without prefix)
:type command: str
:param description: Localized or plain description
:type description: str
:param role: Minimum required role (if any)
:type role: Role | None
"""
command: str
description: str
roles: list[RoleData]
def _extract_command_metadata(handler: HandlerObject) -> _CommandMeta | None:
"""Extract command metadata from a handler's flags.
:param handler: Message handler object
:type handler: HandlerObject
:return: CommandMeta instance or None
:rtype: CommandMeta | None
"""
commands: list[Command] | None = handler.flags.get("commands")
if not commands or not commands[0].commands:
return None
if handler.flags.get("raito__hidden"):
return None
description: LazyProxy | str | None = handler.flags.get("raito__description", "—")
roles: list[RoleData] | None = handler.flags.get("raito__roles")
return _CommandMeta(
command=str(commands[0].commands[0]),
description=str(description).strip(),
roles=roles or [],
)
def _format_description(meta: _CommandMeta, text: str) -> str:
"""Format description with role emoji if available.
:param meta: CommandMeta instance
:type meta: CommandMeta
:param text: Description string
:type text: str
:return: Formatted description
:rtype: str
"""
if not meta.roles:
return text
emojis = "".join([r.emoji for r in meta.roles])
return f"[{emojis}] {text}"
async def _apply_bot_commands(
bot: Bot,
meta_entries: list[_CommandMeta],
scope: BotCommandScopeUnion,
locale: str | None = None,
) -> None:
"""Set commands for a given scope and locale.
:param bot: Bot instance
:type bot: Bot
:param meta_entries: List of command metadata
:type meta_entries: list[_CommandMeta]
:param locale: Locale string (e.g., "en", "ru")
:type locale: str
:param scope: Scope for which to set commands
:type scope: BotCommandScopeUnion
"""
loggers.commands.debug(
"Setting %d command(s) for scope=%s, locale='%s'",
len(meta_entries),
getattr(scope, "chat_id", "default"),
locale or "default",
)
bot_commands: list[BotCommand] = []
for meta in meta_entries:
description = meta.description
with suppress(LookupError):
description = gettext(description, locale=locale)
bot_commands.append(
BotCommand(
command=meta.command,
description=_format_description(meta, description),
)
)
try:
await bot.set_my_commands(commands=bot_commands, scope=scope, language_code=locale)
except TelegramBadRequest as exc:
loggers.commands.warning(
"Failed to set commands for scope=%s, locale='%s': %s",
getattr(scope, "chat_id", "default"),
locale or "default",
exc,
)
[документация]
async def register_bot_commands(
role_manager: RoleManager,
bot: Bot,
handlers: list[HandlerObject],
locales: list[str],
) -> None:
"""Register localized bot commands across roles and user scopes.
:param role_manager: RoleManager instance
:type role_manager: RoleManager
:param bot: Aiogram Bot instance
:type bot: Bot
:param handlers: List of message handler objects
:type handlers: list[HandlerObject]
:param locales: List of supported locales (e.g., "en", "ru")
:type locales: list[str]
"""
role_commands: dict[str | None, list[_CommandMeta]] = defaultdict(list)
for handler in handlers:
meta = _extract_command_metadata(handler)
if not meta:
continue
if not meta.roles:
role_commands[None].append(meta)
for role in meta.roles:
role_commands[role.slug].append(meta)
public_commands = role_commands.get(None, [])
user_commands: dict[int, list[_CommandMeta]] = defaultdict(list)
for role_slug in role_commands:
if role_slug is None:
continue
users = await role_manager.get_users(bot.id, role_slug)
commands = public_commands + role_commands.get(role_slug, [])
for user_id in users:
for meta in commands:
if meta not in user_commands[user_id]:
user_commands[user_id].append(meta)
for locale in locales or (None,):
for user_id, commands in user_commands.items():
await _apply_bot_commands(
bot=bot,
meta_entries=commands,
scope=BotCommandScopeChat(chat_id=user_id),
locale=locale,
)
await _apply_bot_commands(
bot=bot,
meta_entries=role_commands.get(None, []),
scope=BotCommandScopeDefault(),
locale=locale,
)
await _apply_bot_commands(
bot=bot,
meta_entries=role_commands.get(None, []),
scope=BotCommandScopeDefault(),
locale=None,
)