Zum Inhalt

Events

Bostie begrüßt eine klingelnde Glocke

Discord teilt dem Bot über Gateway-Events mit, was passiert: Eine Nachricht wurde gesendet, ein Mitglied ist beigetreten, eine Rolle hat sich geändert. Ein Listener ist eine asynchrone Funktion, die ein Event entgegennimmt.

import logging

import bostie
from bostie import Intents
from bostie.events import (
    MemberJoin,
    MemberRemove,
    MessageDelete,
    MessageUpdate,
    RoleUpdate,
)

log = logging.getLogger("my-bot")
bot = bostie.Bot(intents=Intents.DEFAULT | Intents.GUILD_MEMBERS, max_messages=1000)


@bot.listen
async def membership(event: MemberJoin | MemberRemove) -> None:
    if isinstance(event, MemberJoin):
        log.info("%s joined guild %d", event.member.user.username, event.guild_id)
    else:
        log.info("%s left guild %d", event.user.username, event.guild_id)


@bot.listen
async def role_renamed(event: RoleUpdate) -> None:
    if event.old is not None and event.old.name != event.role.name:
        log.info("role %r is now %r", event.old.name, event.role.name)


@bot.listen(MessageUpdate)
async def edited(event: MessageUpdate) -> None:
    if event.old is not None:
        log.info("edited: %r -> %r", event.old.content, event.message.content)


@bot.listen
async def deleted(event: MessageDelete) -> None:
    if event.message is not None:
        log.info("deleted: %r", event.message.content)

@bot.listen liest den Type Hint des ersten Parameters und registriert die Funktion für diese Event-Klasse oder für jede Klasse einer Union. @bot.listen(MessageUpdate) nennt die Klassen stattdessen ausdrücklich, für eine Funktion ohne Hint oder mit einem weiter gefassten. Die Klassen liegen in bostie.events, und die Event-Liste nennt jede mit ihren Intents und Feldern. Ein Router hat denselben Decorator.

Jeder Listener läuft als eigener Task, sodass ein langsamer Listener weder die anderen noch das nächste Event aufhält. Der Task startet sofort: Der Code eines Listeners bis zu seinem ersten await läuft, bevor das nächste Event verarbeitet wird. Die Ausnahme sind die Listener von Starting und Stopped: start() und close() warten nacheinander auf sie. bot.remove_listener(function) meldet eine Funktion von jedem Event ab, auf das sie hört.

Nur das, worauf du hörst

bostie lässt seinen Rust-Kern nur die Dispatch-Namen weiterleiten, die etwas im Bot verwendet: deine Listener, der Cache, das Routing von Befehlen und Komponenten und laufende Wartevorgänge. Jedes andere Event wird verworfen, bevor es Python erreicht; auf nichts zu hören kostet also nichts. Typisierte Events werden einmal pro Dispatch dekodiert, und nur, wenn jemand sie braucht.

Intents

Die meisten Events sendet Discord nur an einen Bot, der ihre Intents angefordert hat. Bot(intents=...) ist standardmäßig Intents.DEFAULT, jeder Intent außer den drei privilegierten. Registriert sich ein Listener für ein Event, das keiner der Intents des Bots liefert, loggt bostie eine Warnung mit dem Intent, den du hinzufügen musst, und erinnert dich daran, dass ein privilegierter Intent zusätzlich im Developer Portal eingeschaltet werden muss. Intents behandelt sie im Detail.

Alte Werte und der Cache

Update-Events enthalten old, das Objekt, wie es der Cache vor der Änderung hatte, und Delete-Events enthalten das entfernte Objekt: RoleUpdate.old, ScheduledEventUpdate.old, MessageDelete.message, MemberRemove.member. Sie sind None, wenn der Cache das Objekt nicht hatte. Nachrichten werden nur mit Bot(max_messages=...) gecacht, Mitglieder nur mit members=True, Präsenzen nur mit presences=True.

Auf ein Event warten

import asyncio

import bostie
from bostie import Context
from bostie.events import ReactionAdd

bot = bostie.Bot()


@bot.command
async def vote(ctx: Context, question: str) -> None:
    """Count thumbs-up reactions for 60 seconds."""
    message = await ctx.respond(question)
    me = bot.user.id if bot.user is not None else 0
    voters: set[int] = set()

    def counts(event: ReactionAdd) -> bool:
        return (
            event.message_id == message.id
            and event.emoji.name == "\N{THUMBS UP SIGN}"
            and event.user_id != me
        )

    async with bot.stream(ReactionAdd, check=counts) as reactions:
        await bot.rest.reactions.create(
            message.channel_id, message.id, "\N{THUMBS UP SIGN}"
        )
        try:
            async with asyncio.timeout(60):
                async for reaction in reactions:
                    voters.add(reaction.user_id)
        except TimeoutError:
            pass
    await ctx.followup(f"{len(voters)} votes.")

await bot.wait_for(Event, check=None) gibt das nächste Event dieser Klasse zurück, für das check wahr zurückgibt. async with bot.stream(Event, check=None) as events: reiht jedes passende Event ein, solange der Block läuft, für async for; Events, die eintreffen, während der Schleifenrumpf läuft, gehen nicht verloren. Keines von beiden hat ein eigenes Timeout, also begrenze sie mit asyncio.timeout(). Eine Exception, die check auslöst, wird aus dem Warten heraus ausgelöst. bot.wait_for_component und bot.wait_for_modal tun dasselbe für Komponentenverwendungen und abgesendete Modals.

Roh-Events und Events, die bostie nicht modelliert

import logging

import bostie
from bostie import Context
from bostie.events import BotEvent, ListenerError, RawEvent

log = logging.getLogger("my-bot")
bot = bostie.Bot()


class LevelUp(BotEvent, kw_only=True):
    user_id: int
    level: int


@bot.command
async def train(ctx: Context) -> None:
    """Gain a level."""
    bot.dispatch(LevelUp(user_id=ctx.user.id, level=2))
    await ctx.respond("You trained hard.")


@bot.listen
async def congratulate(event: LevelUp) -> None:
    log.info("user %d reached level %d", event.user_id, event.level)


@bot.listen_raw("GUILD_AUDIT_LOG_ENTRY_CREATE", "SOME_FUTURE_EVENT")
async def raw(event: RawEvent) -> None:
    log.info("%s, sequence %d: %r", event.name, event.seq, event.raw)


@bot.listen
async def failed(failure: ListenerError) -> None:
    log.error(
        "%s failed on %r", failure.listener, failure.event, exc_info=failure.error
    )

@bot.listen_raw("NAME", ...) liefert ein RawEvent für jeden angegebenen Dispatch-Namen, ob modelliert oder nicht, und ein Listener mit dem Hint RawEvent bekommt jeden Dispatch. Ein RawEvent hat den Dispatch-Namen name, seine Sequenznummer seq und den Payload als raw (jedes Mal ein neues dict) oder raw_json (Bytes). Jedes typisierte Event hat ebenfalls raw und raw_json.

Sendet Discord einen Payload, der nicht zu seiner Klasse passt, etwa ohne ein Feld, das die Dokumentation als erforderlich bezeichnet, loggt bostie einmal pro Event-Name eine Warnung, und typisierte Listener überspringen dieses Event, während RawEvent-Listener es trotzdem bekommen.

Eigene Events

Leite von BotEvent ab, mit kw_only=True, und sende eine Instanz mit bot.dispatch(event): Ihre Listener, Wartevorgänge und Streams bekommen sie wie jedes andere Event. Eine Unterklasse ohne NAME wird nach ihrem Modul und ihrer Klasse benannt. Event-Klassen sind eingefrorene msgspec-Structs, ihre Felder werden also als Annotationen deklariert.

Lebenszyklus- und Shard-Events

bostie sendet eigene Events über den Bot: Starting, Started, Stopping und Stopped rund um den Lebenszyklus; ShardReady, ShardDisconnect, ShardStop und ShardBackpressure zu jedem Shard; ListenerError und TaskError zu Fehlern. Sprachverbindungen senden Sprach-Events über dasselbe @bot.listen.

Wenn ein Listener eine Exception wirft

Eine Exception in einem Listener wird als ListenerError-Event gesendet, mit listener, event und error. Ohne ListenerError-Listener loggt bostie die Exception samt Traceback; mit einem liegt das Logging bei dir. Eine Exception in einem ListenerError-Listener wird nur geloggt.