Перейти к содержанию

Роутеры и модули

Router хранит команды, слушатели, маршруты, задачи, проверки, хуки и хуки ошибок, как бот, но ботом не является. Роутеры разбивают бота на модули, задают группе команд общие настройки и могут заменяться, пока бот работает. Бот сам является роутером, поэтому всё на этой странице работает и для него.

import logging

import bostie
from bostie import (
    ComponentContext,
    Context,
    ErrorContext,
    Forbidden,
    InteractionContextType,
    Permissions,
    ResolvedMember,
    Router,
    ui,
)
from bostie.checks import bot_has_permissions
from bostie.events import BanAdd

log = logging.getLogger("my-bot")
moderation = Router(
    default_permissions=Permissions.BAN_MEMBERS,
    contexts=[InteractionContextType.GUILD],
    ephemeral=True,
)
moderation.check(bot_has_permissions(Permissions.BAN_MEMBERS))


@moderation.command
async def ban(ctx: Context, member: ResolvedMember) -> None:
    """Ban a member after a confirmation."""
    confirm = ui.button("Ban", custom_id=confirmed.id(user_id=member.id))
    await ctx.respond(f"Ban {member.mention}?", components=[ui.row(confirm)])


@moderation.on_component("ban:{user_id:int}")
async def confirmed(ctx: ComponentContext, user_id: int) -> None:
    if ctx.guild_id is not None:
        reason = f"Requested by {ctx.user.username}"
        await moderation.bot.rest.bans.create(ctx.guild_id, user_id, reason=reason)
    await ctx.update(content="Banned.", components=[])


@moderation.listen
async def banned(event: BanAdd) -> None:
    log.info("%s was banned from guild %d", event.user.username, event.guild_id)


@moderation.on_error(Forbidden)
async def cannot_ban(ctx: ErrorContext, error: Forbidden) -> None:
    if isinstance(ctx, Context | ComponentContext):
        await ctx.respond("That member is above the bot.")


bot = bostie.Bot()
bot.include(moderation)

У роутера есть все декораторы бота: command, group, user_command, message_command, entry_point, listen, listen_raw, on_component, on_modal, every, check, before_invoke, after_invoke и on_error. bot.include(router) добавляет всё это в бота, а также всё, что будет зарегистрировано на роутере позже. Внутри его обработчиков router.bot — это бот, в который он включён; до включения он вызывает StateError.

Роутеры вкладываются: router.include(other) работает так же, и включение внешнего роутера в бота приносит оба.

Значения по умолчанию

Router(...) принимает именованными аргументами значения по умолчанию для команд и маршрутов, зарегистрированных на нём:

Значение по умолчанию Применяется к
ephemeral ответам его команд и маршрутов
auto_defer его командам и маршрутам
default_permissions его командам верхнего уровня
nsfw его командам верхнего уровня
contexts, integration_types его глобальным командам

Команда или маршрут сначала использует собственную настройку, затем значения по умолчанию своего роутера, затем каждого роутера, который его включает, от ближайшего наружу, и наконец бота. В примере /ban скрыта от участников без права «Банить участников», доступна только на серверах и отвечает эфемерно, так что видит ответ и может нажать кнопку только модератор, который её запустил.

Проверки, хуки «до» и хуки «после» роутера применяются к его обработчикам; выполняются и проверки и хуки роутеров, которые его включают, начиная с самого внешнего. Хуки ошибок ищутся от ближайшего роутера наружу, как объясняет раздел «Проверки, хуки и ошибки».

Модули

bot.include("greetings") импортирует модуль greetings и включает Router, названный в нём router. Модуль без него вызывает TypeError.

bot.exclude("greetings") удаляет всё, что добавил роутер, отменяет его задачи и заставляет Python забыть модуль и его подмодули, так что при повторном включении файл импортируется заново. bot.reload("greetings") делает и то и другое одним вызовом: если новый импорт или включение не удаётся, старый роутер остаётся включённым, а ошибка выбрасывается. Перезагрузка сразу меняет обработчики; если изменились определения команд, вызовите после неё await bot.sync_commands(), как делает модульный пример в своей команде /reload.

exclude и reload также принимают объект Router — для роутеров, которые не являются router какого-либо модуля.

Конфликты

Две команды с одинаковым именем, две команды пользователя или сообщения с одинаковым именем, два маршрута с одинаковым шаблоном или одна функция, дважды зарегистрированная как задача, вызывают исключение при добавлении второй, в том числе когда её приносит роутер. Роутер можно включить только один раз и никогда — в самого себя.

Что внутри

router.commands, router.context_menus, router.component_routes, router.modal_routes, router.tasks и router.listeners хранят зарегистрированное: команды — по имени и серверам, например ("rules", ()) для глобальной, команды пользователя и сообщения — по типу, имени и серверам, маршруты — по шаблону, задачи — по функции, а слушателей — по событию. router.walk_commands() выдаёт каждую слэш-команду и подкоманду, у которой есть обработчик. router.match_component(custom_id) и router.match_modal(custom_id) возвращают маршрут, до которого доходит custom id, и значения его плейсхолдеров или None, а router.find_error_handlers(error) выдаёт хуки ошибок, которые обработали бы исключение, начиная с ближайшего.