Pular para conteúdo

Routers e módulos

Um Router guarda comandos, listeners, rotas, tarefas, verificações, hooks e hooks de erro, como o bot, sem ser um bot. Os routers dividem um bot em módulos, dão configurações compartilhadas a um grupo de comandos e podem ser trocados enquanto o bot roda. O próprio bot é um router, então tudo nesta página funciona nele também.

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)

Um router tem todos os decoradores do bot: command, group, user_command, message_command, entry_point, listen, listen_raw, on_component, on_modal, every, check, before_invoke, after_invoke e on_error. bot.include(router) adiciona tudo isso ao bot, e também o que for registrado no router depois. Dentro dos handlers dele, router.bot é o bot no qual ele foi incluído; antes disso, levanta StateError.

Os routers se aninham: router.include(other) funciona do mesmo jeito, e incluir o externo no bot traz os dois.

Padrões

Router(...) aceita, como argumentos nomeados, padrões para os comandos e rotas registrados nele:

Padrão Vale para
ephemeral as respostas dos seus comandos e rotas
auto_defer os seus comandos e rotas
default_permissions os seus comandos de nível superior
nsfw os seus comandos de nível superior
contexts, integration_types os seus comandos globais

Um comando ou rota usa primeiro a sua própria configuração, depois os padrões do seu router, depois os de cada router que o inclui, do mais próximo para fora, e por fim os do bot. No exemplo, /ban fica oculto para membros sem Banir membros, só pode ser usado em servidores e é respondido de forma efêmera, então só o moderador que o executou vê o botão e pode apertá-lo.

As verificações, os hooks "antes" e os hooks "depois" de um router valem para os handlers dele; os dos routers que o incluem também rodam, do mais externo primeiro. Os hooks de erro são procurados do router mais próximo para fora, como Verificações, hooks e erros explica.

Módulos

bot.include("greetings") importa o módulo greetings e inclui o Router que ele chama de router. Um módulo sem um levanta TypeError.

bot.exclude("greetings") remove tudo o que o router adicionou, cancela as tarefas dele e faz o Python esquecer o módulo e os seus submódulos, então incluí-lo de novo importa o arquivo outra vez. bot.reload("greetings") faz as duas coisas em uma chamada: se a nova importação ou inclusão falhar, o router antigo continua incluído e o erro é levantado. Recarregar troca os handlers na hora; chame await bot.sync_commands() depois quando as definições de comandos mudarem, como o exemplo modular faz no seu comando /reload.

exclude e reload também aceitam um objeto Router, para routers que não são o router de um módulo.

Conflitos

Dois comandos com o mesmo nome, dois comandos de usuário ou de mensagem com o mesmo nome, duas rotas com o mesmo modelo, ou uma função registrada duas vezes como tarefa levantam uma exceção quando o segundo é adicionado, inclusive quando é um router que o traz. Um router pode ser incluído uma vez, e nunca em si mesmo.

Olhando por dentro

router.commands, router.context_menus, router.component_routes, router.modal_routes, router.tasks e router.listeners guardam o que está registrado: comandos por nome e servidores, como ("rules", ()) para um global, comandos de usuário e de mensagem por tipo, nome e servidores, rotas por modelo, tarefas por função e listeners por evento. router.walk_commands() produz cada comando de barra e subcomando que tem um handler. router.match_component(custom_id) e router.match_modal(custom_id) retornam a rota a que um custom id chega e os valores dos marcadores dela, ou None, e router.find_error_handlers(error) produz os hooks de erro que tratariam uma exceção, do mais próximo primeiro.