Проверки, хуки и ошибки¶

Проверка решает, может ли обработчик выполниться. Хуки выполняют код до и после обработчиков. Хуки ошибок отвечают, когда что-то выбрасывает исключение.
Проверки¶
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)
Проверка — это функция, которая принимает контекст и возвращает bool, сразу или асинхронно. False отклоняет запуск с CheckFailure, а проверка может и сама выбросить свой CheckFailure: его сообщение и увидит пользователь. Проверки задаются в настройке checks=[...] команды, группы, подкоманды, команды пользователя или сообщения, точки входа, а также маршрута компонента или модального окна, а @router.check регистрирует проверку для всех обработчиков роутера; бот — тоже роутер.
Сначала выполняются проверки бота, затем проверки каждого роутера снаружи внутрь, затем проверки групп сверху вниз и наконец собственные проверки команды. Первый отказ останавливает запуск. Автодополнение проверки не выполняет.
В bostie.checks есть распространённые проверки:
| Проверка | Отказывает с | Когда |
|---|---|---|
has_permissions(permissions) |
MissingPermissions |
у пользователя в канале нет хотя бы одного из прав permissions или он в личных сообщениях |
bot_has_permissions(permissions) |
BotMissingPermissions |
у бота в канале нет хотя бы одного из них |
Cooldown(rate, seconds, per="user") |
CooldownActive |
ключ уже использовал её rate раз за последние seconds секунд |
MaxConcurrency(number, per="global") |
MaxConcurrencyReached |
для ключа всё ещё идут number запусков |
has_role(role) |
MissingRole |
у пользователя нет роли, заданной ID или именем, или он в личных сообщениях |
has_any_role(*roles) |
MissingRole |
у пользователя нет ни одной из ролей |
is_owner |
NotOwner |
пользователь не входит в bot.owner_ids |
Объект проверки, у которого есть ещё и метод release(ctx), — checks.ReleasingCheck, такой как MaxConcurrency, — освобождается, когда обработчик завершается, как бы он ни завершился.
per — это "user", "guild" (в личных сообщениях — пользователь), "channel", "global" или функция от контекста, возвращающая любой хешируемый ключ. Проверки прав читают права, которые Discord присылает вместе со взаимодействием, поэтому им не нужен ни кэш, ни запрос. Их ошибки несут missing — недостающие Permissions, а CooldownActive несёт retry_after в секундах.
Роли и владельцы¶
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 и has_any_role принимают ID или имена ролей. ID сверяется с ролями, которые Discord присылает вместе со взаимодействием, поэтому кэш не нужен. Имя ищется среди ролей сервера в кэше, поэтому бот без кэша или приложение только на HTTP указывает роли по ID. MissingRole несёт запрошенные roles, а его сообщение упоминает роль, заданную по ID, и Discord показывает её как имя роли.
is_owner — это проверка сама по себе, без скобок. bot.owner_ids хранит пользователей, которых она пропускает: owner_ids, переданные в Bot(...), а иначе владельца приложения, которого бот читает из приложения при запуске. Для приложения, принадлежащего команде, это владелец команды, а также администраторы и разработчики, принявшие приглашение; участники только с правом чтения не учитываются.
Что увидит пользователь¶
Если ни один хук ошибок не обработал CheckFailure, bostie отвечает его сообщением, эфемерно: «You need the Manage Messages permission.», «The bot needs the Manage Messages permission.», «Try again» с относительной меткой времени вроде «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.» или «You cannot use this.» для простого False.
Хуки до и после¶
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 регистрирует async def hook(ctx), который выполняется после прохождения проверок и до обработчика; @router.after_invoke — хук, который выполняется после обработчика, даже если тот выбросил исключение. Они выполняются для каждой команды, маршрута компонента и маршрута модального окна роутера, поэтому хук, который может увидеть компонент, не должен рассчитывать на Context. Хуки «до» у бота выполняются первыми, а хуки «после» — последними. Исключение из хука «после» пишется в лог и не меняет ответ.
Хуки ошибок¶
Исключение из проверки, конвертера, хука «до» или обработчика уходит в хук ошибок. @x.on_error регистрирует async def hook(ctx, error) для любого Exception, а @x.on_error(NotFound, ValueError) — для этих типов и их подклассов. x — это команда, группа, маршрут компонента или модального окна, роутер или бот, и у каждого может быть по одному хуку на тип исключения.
bostie ищет хук в таком порядке, и первый найденный обрабатывает ошибку:
- команда, затем её группа и группа верхнего уровня;
- роутер, которому принадлежит команда, затем каждый роутер, в который он включён, вплоть до бота.
В пределах одного места побеждает хук для ближайшего класса ошибки: хук для NotFound важнее хука для HTTPException. Хук, который выбрасывает ту же ошибку, передаёт её следующему хуку в этом порядке, как делает slow_down выше. Если хук выбрасывает другую ошибку, она пишется в лог.
Тип контекста — ErrorContext: Context, ComponentContext, ModalContext или AutocompleteContext. Контекст автодополнения не может вызвать respond, поэтому проверяйте его тип, как выше.
Если хук завершился без ответа, bostie пишет в лог предупреждение и отвечает «Something went wrong.». Если ни один хук не обработал ошибку, bostie отвечает на CheckFailure его сообщением, а всё остальное пишет в лог с трассировкой и затем отвечает «Something went wrong.». Если обработчик уже ответил, ничего не отправляется, а автодополнение получает пустой список подсказок.
Ошибки вне обработчиков¶
У слушателей и задач нет пользователя, которому можно ответить. Исключение в слушателе отправляется как событие ListenerError, а в задаче — как TaskError; если их никто не слушает, bostie пишет их в лог. См. «События» и «Фоновые задачи».
Типы исключений¶
Всё, что bostie выбрасывает в связи с Discord, сетью или состоянием бота, — это BostieError. Неверный аргумент вместо этого вызывает ValueError или TypeError. Некоторые ошибки являются ещё и стандартными исключениями: StateError — это RuntimeError, MissingIntent — ValueError, StartupError — OSError, а CommandDefinitionError — TypeError. У ошибок команд и взаимодействий общий базовый класс CommandError, а у ошибок голоса — VoiceError.
| Исключение | Когда выбрасывается |
|---|---|
HTTPException |
REST-запрос завершился статусом ошибки; status, code (JSONErrorCode), message, errors и сырое body |
BadRequest, Forbidden, NotFound, ServerError |
400, 403, 404 и 5xx |
RateLimited |
429, или bostie придержал запрос, который ждал бы дольше max_rate_limit_wait; retry_after, is_global |
InvalidToken |
Discord отклонил токен — в REST или в шлюзе |
TransportError |
запрос не удался до того, как Discord ответил |
UnexpectedResponse |
Discord принял запрос, но его ответ не декодируется; он хранится в raw |
RequestBudgetExhausted |
слишком много недавних запросов были недействительными, и bostie отклонил очередной, чтобы защитить IP |
GatewayClosed, PrivilegedIntentsRequired, ShardingRequired |
Discord окончательно закрыл шлюз; последние два — для кодов 4014 и 4011 |
ShardNotRunning, GatewayQueueFull |
команда шлюза для шарда, который этот процесс не запускает, или при слишком большом числе ожидающих команд |
StateError |
вызов, который не допускает состояние объекта, например bot.rest до start() |
MissingIntent |
возможности нужен интент, который бот не запросил |
MemberRequestRateLimited |
Discord отклонил запрос участников; retry_after |
StartupError |
bostie не смог настроить нужный ему системный ресурс |
CommandDefinitionError |
команда, группа или параметр нарушают правила Discord; выбрасывается декоратором |
CommandSyncError |
Discord отклонил команды во время синхронизации |
OptionConversionError |
параметр или цель не преобразовались в тип из аннотации |
InteractionResponded, InteractionNotAnswered, InteractionExpired |
вызов ответа, который не допускает состояние взаимодействия |
CheckFailure и его подклассы |
проверка отклонила запуск |
Ошибки голоса описаны на странице о голосе. Справочник по ошибкам перечисляет все классы с их атрибутами.