Verificações, hooks e erros¶

Uma verificação decide se um handler pode rodar. Os hooks rodam código antes e depois dos handlers. Os hooks de erro respondem quando algo levanta uma exceção.
Verificações¶
import bostie
from bostie import CheckFailure, Context, Permissions
from bostie.checks import Cooldown, MaxConcurrency, bot_has_permissions, has_permissions
bot = bostie.Bot()
BANNED_USERS = {80351110224678912}
@bot.check
def not_banned(ctx: Context) -> bool:
if ctx.user.id in BANNED_USERS:
raise CheckFailure("You are banned from using this bot.")
return True
async def in_a_guild(ctx: Context) -> bool:
return ctx.guild_id is not None
@bot.command(
checks=[
in_a_guild,
has_permissions(Permissions.MANAGE_MESSAGES),
bot_has_permissions(Permissions.MANAGE_MESSAGES),
Cooldown(2, 60, per="channel"),
MaxConcurrency(1, per="guild"),
]
)
async def clean(ctx: Context, count: int = 20) -> None:
"""Delete recent messages."""
await ctx.respond(f"Deleting {count} messages.", ephemeral=True)
Uma verificação é uma função que recebe o contexto e retorna um bool, e que também pode ser assíncrona. False recusa a execução com CheckFailure, e uma verificação pode levantar uma CheckFailure própria: a mensagem dela é o que o usuário recebe. As verificações vão na configuração checks=[...] de um comando, grupo, subcomando, comando de usuário ou de mensagem, ponto de entrada, ou rota de componente e de modal, e @router.check registra uma para todos os handlers de um router; o bot é um router.
As verificações do bot rodam primeiro, depois as de cada router, do mais externo para dentro, depois as dos grupos, de cima para baixo, e por fim as do próprio comando. A primeira recusa interrompe a execução. O preenchimento automático não roda verificações.
bostie.checks tem as mais comuns:
| Verificação | Recusa com | Quando |
|---|---|---|
has_permissions(permissions) |
MissingPermissions |
falta ao usuário alguma de permissions no canal, ou ele está em uma mensagem direta |
bot_has_permissions(permissions) |
BotMissingPermissions |
falta ao bot alguma delas no canal |
Cooldown(rate, seconds, per="user") |
CooldownActive |
a chave já a usou rate vezes nos últimos seconds |
MaxConcurrency(number, per="global") |
MaxConcurrencyReached |
number execuções para a chave ainda estão em andamento |
has_role(role) |
MissingRole |
falta ao usuário o cargo, dado por ID ou nome, ou ele está em uma mensagem direta |
has_any_role(*roles) |
MissingRole |
o usuário não tem nenhum dos cargos |
is_owner |
NotOwner |
o usuário não é um dos bot.owner_ids |
Um objeto de verificação que também tem um método release(ctx), um checks.ReleasingCheck como MaxConcurrency, é liberado assim que o handler termina, seja como for que termine.
per é "user", "guild" (o usuário, em mensagens diretas), "channel", "global" ou uma função do contexto que retorna qualquer chave hashable. As verificações de permissão leem as permissões que o Discord envia com a interação, então não precisam de cache nem de requisição. Os erros delas trazem missing, as Permissions que faltam, e CooldownActive traz retry_after em segundos.
Cargos e donos¶
import bostie
from bostie import Context, ResolvedMember, Router
from bostie.checks import has_any_role, has_role, is_owner
bot = bostie.Bot()
owner = Router()
owner.check(is_owner)
MODERATOR_ROLE = 1335174302269571073
@owner.command
async def reload(ctx: Context, module: str) -> None:
"""Load a module of the bot again."""
bot.reload(module)
await ctx.respond(f"Reloaded {module}.", ephemeral=True)
@bot.command(checks=[has_role(MODERATOR_ROLE)])
async def warn(ctx: Context, member: ResolvedMember) -> None:
"""Warn a member."""
await ctx.respond(f"{member.mention}, please follow the rules.")
@bot.command(checks=[has_any_role("Admin", "Event Host")])
async def announce(ctx: Context, text: str) -> None:
"""Post an announcement."""
await ctx.respond(text)
bot.include(owner)
has_role e has_any_role aceitam IDs ou nomes de cargos. Um ID é comparado com os cargos que o Discord envia com a interação, então não precisa de cache. Um nome é procurado entre os cargos do servidor em cache, então um bot sem o cache, ou um app só por HTTP, nomeia os cargos por ID. MissingRole traz os roles pedidos, e a mensagem dela menciona um cargo dado por ID, que o Discord mostra como o nome do cargo.
is_owner é uma verificação do jeito que está, sem parênteses. bot.owner_ids guarda os usuários que ela deixa passar: os owner_ids passados para Bot(...) ou, se não houver, o dono do app, lido do aplicativo quando o bot inicia. Para um app que pertence a uma equipe, eles são o dono da equipe e os administradores e desenvolvedores que aceitaram o convite; os membros somente leitura ficam de fora.
O que o usuário recebe¶
Quando nenhum hook de erro trata uma CheckFailure, o bostie responde com a mensagem dela, de forma efêmera: "You need the Manage Messages permission.", "The bot needs the Manage Messages permission.", "Try again" com um timestamp relativo como "in 42 seconds", "This is already running; try again once it is done.", "You need the Moderator role.", "Only the bot's owners can use this." ou "You cannot use this." para um False simples.
Hooks antes e depois¶
import logging
import time
import bostie
from bostie import (
AutocompleteContext,
Context,
CooldownActive,
ErrorContext,
Forbidden,
NotFound,
)
log = logging.getLogger("my-bot")
bot = bostie.Bot()
started: dict[int, float] = {}
@bot.before_invoke
async def start_timer(ctx: Context) -> None:
started[ctx.interaction.id] = time.monotonic()
@bot.after_invoke
async def log_duration(ctx: Context) -> None:
took = time.monotonic() - started.pop(ctx.interaction.id)
log.info("/%s took %.2f s", ctx.command_name, took)
@bot.command
async def fetch(ctx: Context, message_id: str) -> None:
"""Show a message of this channel."""
if ctx.channel_id is not None:
message = await bot.rest.messages.fetch(ctx.channel_id, int(message_id))
await ctx.respond(message.content or "(no text)")
@fetch.on_error(NotFound, ValueError)
async def no_such_message(ctx: ErrorContext, error: NotFound | ValueError) -> None:
if not isinstance(ctx, AutocompleteContext):
await ctx.respond("No message has that id here.", ephemeral=True)
@bot.on_error(Forbidden)
async def missing_access(ctx: ErrorContext, error: Forbidden) -> None:
if not isinstance(ctx, AutocompleteContext):
await ctx.respond("I cannot see that channel.", ephemeral=True)
@bot.on_error(CooldownActive)
async def slow_down(ctx: ErrorContext, error: CooldownActive) -> None:
log.info("%s hit a cooldown, %.0f s left", ctx.user, error.retry_after)
raise error
@router.before_invoke registra um async def hook(ctx) que roda depois que as verificações passaram e antes do handler; @router.after_invoke, um que roda depois do handler, também quando ele levantou uma exceção. Eles rodam para todos os comandos, rotas de componente e rotas de modal do router, então um hook que pode ver um componente não deve supor um Context. Os hooks "antes" do bot rodam primeiro e os hooks "depois" dele por último. Uma exceção vinda de um hook "depois" vai para o log e não muda a resposta.
Hooks de erro¶
Uma exceção vinda de uma verificação, de um conversor, de um hook "antes" ou do handler vai para um hook de erro. @x.on_error registra um async def hook(ctx, error) para toda Exception, e @x.on_error(NotFound, ValueError) para esses tipos e as suas subclasses. x é um comando, um grupo, uma rota de componente ou de modal, um router ou o bot, e cada um pode ter um hook por tipo de exceção.
O bostie procura um hook nesta ordem, e o primeiro que encontra trata o erro:
- o comando, depois o grupo dele e o grupo de nível superior;
- o router ao qual o comando pertence, depois cada router em que ele está incluído, até o bot.
Dentro de um mesmo lugar, vence o hook para a classe mais próxima do erro: um hook para NotFound ganha de um para HTTPException. Um hook que levanta o mesmo erro o repassa ao próximo hook nessa ordem, como slow_down faz acima. Um hook que levanta outro erro faz com que esse erro vá para o log.
O tipo do contexto é ErrorContext: um Context, ComponentContext, ModalContext ou AutocompleteContext. O contexto de preenchimento automático não pode chamar respond, então verifique esse caso como acima.
Quando um hook retorna sem responder, o bostie escreve um aviso no log e responde "Something went wrong.". Quando nenhum hook trata o erro, o bostie responde a uma CheckFailure com a mensagem dela e escreve qualquer outra coisa no log com o traceback antes de responder "Something went wrong.". Nada é respondido quando o handler já tinha respondido, e um preenchimento automático recebe uma lista vazia de sugestões.
Erros fora dos handlers¶
Listeners e tarefas não têm usuário a quem responder. Uma exceção em um listener é enviada como um evento ListenerError, e uma em uma tarefa como um TaskError; sem um listener para eles, o bostie os escreve no log. Veja Eventos e Tarefas em segundo plano.
Tipos de exceção¶
Tudo o que o bostie levanta sobre o Discord, a rede ou o estado do bot é um BostieError. Um argumento inválido levanta ValueError ou TypeError no lugar disso. Alguns erros também são uma exceção padrão: StateError é um RuntimeError, MissingIntent um ValueError, StartupError um OSError e CommandDefinitionError um TypeError. Os erros sobre comandos e interações compartilham a base CommandError, e os erros de voz a base VoiceError.
| Exceção | Levantada quando |
|---|---|
HTTPException |
uma requisição REST terminou com um status de erro; status, code (um JSONErrorCode), message, errors e o body bruto |
BadRequest, Forbidden, NotFound, ServerError |
400, 403, 404 e 5xx |
RateLimited |
429, ou o bostie segurou uma requisição que esperaria mais que max_rate_limit_wait; retry_after, is_global |
InvalidToken |
o Discord rejeitou o token, no REST ou no gateway |
TransportError |
uma requisição falhou antes de o Discord responder |
UnexpectedResponse |
o Discord aceitou uma requisição, mas a resposta não decodifica; raw a guarda |
RequestBudgetExhausted |
requisições inválidas demais recentemente, e o bostie recusou uma para proteger o IP |
GatewayClosed, PrivilegedIntentsRequired, ShardingRequired |
o Discord fechou o gateway de vez; as duas últimas para os códigos 4014 e 4011 |
ShardNotRunning, GatewayQueueFull |
um comando do gateway para um shard que este processo não roda, ou com comandos demais esperando |
StateError |
uma chamada que o estado do objeto não permite, como bot.rest antes de start() |
MissingIntent |
um recurso precisa de uma intent que o bot não pediu |
MemberRequestRateLimited |
o Discord recusou uma requisição de membros; retry_after |
StartupError |
o bostie não conseguiu preparar um recurso do sistema de que precisa |
CommandDefinitionError |
um comando, grupo ou opção quebra as regras do Discord; levantada pelo decorador |
CommandSyncError |
o Discord rejeitou os comandos durante uma sincronização |
OptionConversionError |
uma opção ou um alvo não foi convertido para a sua anotação |
InteractionResponded, InteractionNotAnswered, InteractionExpired |
uma chamada de resposta que o estado da interação não permite |
CheckFailure e as suas subclasses |
uma verificação recusou uma execução |
Os erros de voz estão na página de voz. A referência de erros lista cada classe com os seus atributos.