Checks, Hooks und Fehler¶

Ein Check entscheidet, ob ein Handler laufen darf. Hooks führen Code vor und nach Handlern aus. Fehler-Hooks antworten, wenn etwas eine Exception wirft.
Checks¶
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)
Ein Check ist eine Funktion, die den Kontext entgegennimmt und einen bool zurückgibt oder per await liefert. False verweigert den Lauf mit CheckFailure, und ein Check kann auch selbst eine CheckFailure auslösen: Ihre Meldung ist das, was der Benutzer zu sehen bekommt. Checks gehören in die Einstellung checks=[...] eines Befehls, einer Gruppe, eines Unterbefehls, eines Benutzer- oder Nachrichtenbefehls, eines Einstiegspunkts oder einer Komponenten- oder Modal-Route, und @router.check registriert einen für jeden Handler eines Routers; der Bot ist ein Router.
Zuerst laufen die Checks des Bots, dann die jedes Routers von außen nach innen, dann die der Gruppen von oben nach unten, dann die des Befehls selbst. Die erste Ablehnung beendet den Lauf. Bei der Autovervollständigung laufen keine Checks.
bostie.checks enthält die gängigen:
| Check | Lehnt ab mit | Wann |
|---|---|---|
has_permissions(permissions) |
MissingPermissions |
dem Benutzer fehlt im Kanal eine der permissions, oder er ist in einer DM |
bot_has_permissions(permissions) |
BotMissingPermissions |
dem Bot fehlt im Kanal eine davon |
Cooldown(rate, seconds, per="user") |
CooldownActive |
der Schlüssel hat ihn in den letzten seconds schon rate-mal verwendet |
MaxConcurrency(number, per="global") |
MaxConcurrencyReached |
für den Schlüssel laufen noch number Ausführungen |
has_role(role) |
MissingRole |
dem Benutzer fehlt die Rolle, angegeben per ID oder Name, oder er ist in einer DM |
has_any_role(*roles) |
MissingRole |
der Benutzer hat keine der Rollen |
is_owner |
NotOwner |
der Benutzer gehört nicht zu bot.owner_ids |
Ein Check-Objekt, das zusätzlich eine Methode release(ctx) hat, ein checks.ReleasingCheck wie MaxConcurrency, wird freigegeben, sobald der Handler endet, egal wie er endet.
per ist "user", "guild" (in DMs der Benutzer), "channel", "global" oder eine Funktion des Kontexts, die einen beliebigen hashbaren Schlüssel zurückgibt. Die Berechtigungs-Checks lesen die Berechtigungen, die Discord mit der Interaktion sendet, und brauchen daher weder Cache noch Anfrage. Ihre Fehler enthalten missing, die fehlenden Permissions, und CooldownActive enthält retry_after in Sekunden.
Rollen und Eigentümer¶
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 und has_any_role nehmen Rollen-IDs oder -Namen. Eine ID wird mit den Rollen abgeglichen, die Discord mit der Interaktion sendet, und braucht daher keinen Cache. Ein Name wird unter den Rollen des gecachten Servers gesucht; ein Bot ohne Cache oder eine reine HTTP-App gibt Rollen deshalb per ID an. MissingRole enthält die verlangten roles, und seine Meldung erwähnt eine per ID angegebene Rolle, die Discord als Namen der Rolle anzeigt.
is_owner ist ein Check, so wie er ist, ohne Klammern. bot.owner_ids enthält die Benutzer, die er durchlässt: die owner_ids, die Bot(...) übergeben wurden, sonst den Eigentümer der App, der beim Start des Bots aus der Anwendung gelesen wird. Bei einer App, die einem Team gehört, sind das der Eigentümer des Teams sowie die Admins und Entwickler, die die Einladung angenommen haben; Mitglieder mit reinem Lesezugriff bleiben außen vor.
Was der Benutzer zu sehen bekommt¶
Wenn kein Fehler-Hook eine CheckFailure verarbeitet, antwortet bostie mit ihrer Meldung, ephemer: „You need the Manage Messages permission.“, „The bot needs the Manage Messages permission.“, „Try again“ mit einem relativen Zeitstempel wie „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.“ oder bei einem einfachen False „You cannot use this.“.
Hooks davor und danach¶
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 registriert ein async def hook(ctx), das läuft, nachdem die Checks bestanden sind, und vor dem Handler; @router.after_invoke eines, das nach dem Handler läuft, auch wenn dieser eine Exception geworfen hat. Sie laufen für jeden Befehl, jede Komponentenroute und jede Modal-Route des Routers; ein Hook, der eine Komponente sehen kann, sollte also nicht von einem Context ausgehen. Die Before-Hooks des Bots laufen zuerst, seine After-Hooks zuletzt. Eine Exception aus einem After-Hook wird geloggt und ändert die Antwort nicht.
Fehler-Hooks¶
Eine Exception aus einem Check, einem Konverter, einem Before-Hook oder dem Handler geht an einen Fehler-Hook. @x.on_error registriert async def hook(ctx, error) für jede Exception, und @x.on_error(NotFound, ValueError) für diese Typen und ihre Unterklassen. x ist ein Befehl, eine Gruppe, eine Komponenten- oder Modal-Route, ein Router oder der Bot, und jedes davon kann einen Hook pro Exception-Typ haben.
bostie sucht in dieser Reihenfolge nach einem Hook, und der erste, den es findet, verarbeitet den Fehler:
- der Befehl, dann seine Gruppe und die Gruppe auf oberster Ebene;
- der Router, zu dem der Befehl gehört, dann jeder Router, in den er eingebunden ist, bis hinauf zum Bot.
An einer Stelle gewinnt der Hook für die nächstliegende Klasse des Fehlers: Ein Hook für NotFound schlägt einen für HTTPException. Ein Hook, der denselben Fehler erneut auslöst, reicht ihn in dieser Reihenfolge an den nächsten Hook weiter, wie es slow_down oben tut. Löst ein Hook einen anderen Fehler aus, wird dieser geloggt.
Der Kontexttyp ist ErrorContext: ein Context, ComponentContext, ModalContext oder AutocompleteContext. Der Autovervollständigungs-Kontext kann nicht respond, prüfe also wie oben darauf.
Kehrt ein Hook zurück, ohne zu antworten, loggt bostie eine Warnung und antwortet mit „Something went wrong.“. Verarbeitet kein Hook den Fehler, beantwortet bostie eine CheckFailure mit ihrer Meldung und loggt alles andere samt Traceback, bevor es mit „Something went wrong.“ antwortet. Hatte der Handler bereits geantwortet, wird nichts geantwortet, und eine Autovervollständigung bekommt eine leere Vorschlagsliste.
Fehler außerhalb von Handlern¶
Listener und Tasks haben keinen Benutzer, dem sie antworten könnten. Eine Exception in einem Listener wird als ListenerError-Event gesendet, eine in einem Task als TaskError; ohne Listener dafür loggt bostie sie. Siehe Events und Hintergrund-Tasks.
Exception-Typen¶
Alles, was bostie zu Discord, zum Netzwerk oder zum Zustand des Bots auslöst, ist ein BostieError. Ein falsches Argument löst stattdessen ValueError oder TypeError aus. Manche Fehler sind zugleich eine Standard-Exception: StateError ist ein RuntimeError, MissingIntent ein ValueError, StartupError ein OSError und CommandDefinitionError ein TypeError. Die Fehler zu Befehlen und Interaktionen teilen sich die Basis CommandError, die Fehler zu Sprachverbindungen VoiceError.
| Exception | Ausgelöst, wenn |
|---|---|
HTTPException |
eine REST-Anfrage mit einem Fehlerstatus endete; status, code (ein JSONErrorCode), message, errors und der rohe body |
BadRequest, Forbidden, NotFound, ServerError |
400, 403, 404 und 5xx |
RateLimited |
429, oder bostie hat eine Anfrage zurückgehalten, die länger als max_rate_limit_wait warten müsste; retry_after, is_global |
InvalidToken |
Discord das Token abgelehnt hat, bei REST oder am Gateway |
TransportError |
eine Anfrage fehlschlug, bevor Discord geantwortet hat |
UnexpectedResponse |
Discord eine Anfrage angenommen hat, seine Antwort sich aber nicht dekodieren lässt; raw enthält sie |
RequestBudgetExhausted |
zu viele der letzten Anfragen ungültig waren und bostie eine abgelehnt hat, um die IP zu schützen |
GatewayClosed, PrivilegedIntentsRequired, ShardingRequired |
Discord das Gateway endgültig geschlossen hat; die letzten beiden für die Codes 4014 und 4011 |
ShardNotRunning, GatewayQueueFull |
ein Gateway-Befehl für einen Shard, den dieser Prozess nicht betreibt, oder mit zu vielen wartenden Befehlen |
StateError |
ein Aufruf, den der Zustand des Objekts nicht erlaubt, etwa bot.rest vor start() |
MissingIntent |
eine Funktion einen Intent braucht, den der Bot nicht angefordert hat |
MemberRequestRateLimited |
Discord eine Mitgliederanfrage abgelehnt hat; retry_after |
StartupError |
bostie eine benötigte Systemressource nicht einrichten konnte |
CommandDefinitionError |
ein Befehl, eine Gruppe oder eine Option gegen Discords Regeln verstößt; vom Decorator ausgelöst |
CommandSyncError |
Discord die Befehle bei einer Synchronisierung abgelehnt hat |
OptionConversionError |
eine Option oder ein Ziel sich nicht in seinen Hint umwandeln ließ |
InteractionResponded, InteractionNotAnswered, InteractionExpired |
ein Antwortaufruf, den der Zustand der Interaktion nicht erlaubt |
CheckFailure und seine Unterklassen |
ein Check einen Lauf verweigert hat |
Die Fehler zu Sprachverbindungen stehen auf der Seite zu Sprachverbindungen. Die Fehler-Referenz listet jede Klasse mit ihren Attributen auf.