Zum Inhalt

Router und Module

Ein Router enthält Befehle, Listener, Routen, Tasks, Checks, Hooks und Fehler-Hooks wie der Bot, ohne selbst einer zu sein. Router teilen einen Bot in Module auf, geben einer Gruppe von Befehlen gemeinsame Einstellungen und lassen sich austauschen, während der Bot läuft. Der Bot ist selbst ein Router, also funktioniert alles auf dieser Seite auch mit ihm.

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)

Ein Router hat jeden Decorator des Bots: command, group, user_command, message_command, entry_point, listen, listen_raw, on_component, on_modal, every, check, before_invoke, after_invoke und on_error. bot.include(router) fügt das alles dem Bot hinzu, und ebenso alles, was später am Router registriert wird. Innerhalb seiner Handler ist router.bot der Bot, in den er eingebunden wurde; davor löst es StateError aus.

Router lassen sich verschachteln: router.include(other) funktioniert genauso, und wer den äußeren in den Bot einbindet, bringt beide mit.

Standardwerte

Router(...) nimmt Standardwerte für die an ihm registrierten Befehle und Routen als Schlüsselwortargumente:

Standardwert Gilt für
ephemeral Antworten seiner Befehle und Routen
auto_defer seine Befehle und Routen
default_permissions seine Befehle auf oberster Ebene
nsfw seine Befehle auf oberster Ebene
contexts, integration_types seine globalen Befehle

Ein Befehl oder eine Route verwendet zuerst die eigene Einstellung, dann die Standardwerte seines Routers, dann die jedes Routers, der ihn einbindet, vom nächstliegenden nach außen, dann die des Bots. Im Beispiel ist /ban vor Mitgliedern ohne die Berechtigung „Mitglieder bannen“ verborgen, nur auf Servern verwendbar und wird ephemer beantwortet, sodass nur der Moderator, der ihn ausgeführt hat, den Button sieht und drücken kann.

Die Checks, Before-Hooks und After-Hooks eines Routers gelten für seine Handler; die der Router, die ihn einbinden, laufen ebenfalls, der äußerste zuerst. Fehler-Hooks werden vom nächstliegenden Router nach außen gesucht, wie Checks, Hooks und Fehler erklärt.

Module

bot.include("greetings") importiert das Modul greetings und bindet den Router ein, den es router nennt. Ein Modul ohne einen solchen löst TypeError aus.

bot.exclude("greetings") entfernt alles, was der Router hinzugefügt hat, bricht seine Tasks ab und lässt Python das Modul und seine Untermodule vergessen, sodass ein erneutes Einbinden die Datei frisch importiert. bot.reload("greetings") erledigt beides in einem Aufruf: Schlägt der neue Import oder das Einbinden fehl, bleibt der alte Router eingebunden, und der Fehler wird ausgelöst. Neu laden ändert die Handler sofort; ruf danach await bot.sync_commands() auf, wenn sich Befehlsdefinitionen geändert haben, wie es das modulare Beispiel in seinem Befehl /reload tut.

exclude und reload nehmen auch ein Router-Objekt, für Router, die nicht der router eines Moduls sind.

Konflikte

Zwei Befehle mit demselben Namen, zwei Benutzer- oder Nachrichtenbefehle mit demselben Namen, zwei Routen mit derselben Vorlage oder eine Funktion, die zweimal als Task registriert wird, lösen beim Hinzufügen des zweiten eine Exception aus, auch wenn ein Router ihn mitbringt. Ein Router kann nur einmal eingebunden werden, und nie in sich selbst.

Hineinschauen

router.commands, router.context_menus, router.component_routes, router.modal_routes, router.tasks und router.listeners enthalten, was registriert ist: Befehle nach Name und Servern, etwa ("rules", ()) für einen globalen, Benutzer- und Nachrichtenbefehle nach Typ, Name und Servern, Routen nach Vorlage, Tasks nach Funktion und Listener nach Event. router.walk_commands() liefert jeden Slash-Befehl und Unterbefehl, der einen Handler hat. router.match_component(custom_id) und router.match_modal(custom_id) geben die Route zurück, die eine Custom-ID erreicht, samt ihren Platzhalterwerten, oder None, und router.find_error_handlers(error) liefert die Fehler-Hooks, die eine Exception verarbeiten würden, den nächstliegenden zuerst.