Événements¶

Discord informe le bot de ce qui se passe par des événements de la gateway : un message a été envoyé, un membre a rejoint le serveur, un rôle a changé. Un écouteur est une fonction asynchrone qui prend un événement.
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 lit l'annotation de type du premier paramètre et enregistre la fonction pour cette classe d'événement, ou pour chaque classe d'une union. @bot.listen(MessageUpdate) nomme plutôt les classes, pour une fonction sans annotation ou avec une annotation plus large. Les classes se trouvent dans bostie.events, et la liste des événements les nomme toutes, avec leurs intents et leurs champs. Un routeur dispose du même décorateur.
Chaque écouteur s'exécute dans sa propre tâche : un écouteur lent ne bloque donc ni les autres ni l'événement suivant. La tâche démarre immédiatement : le code d'un écouteur jusqu'à son premier await s'exécute avant que l'événement suivant soit traité. Les écouteurs de Starting et Stopped font exception : start() et close() les attendent l'un après l'autre. bot.remove_listener(function) désenregistre une fonction de tous les événements qu'elle écoute.
Seulement ce que vous écoutez¶
bostie demande à son cœur Rust de ne transmettre que les noms de dispatch que quelque chose dans le bot utilise : vos écouteurs, le cache, le routage des commandes et des composants, et les attentes en cours. Tout autre événement est abandonné avant d'atteindre Python : n'écouter rien ne coûte donc rien. Les événements typés sont décodés une fois par dispatch, et seulement si quelqu'un en a besoin.
Intents¶
Discord n'envoie la plupart des événements qu'à un bot qui a demandé leurs intents. Bot(intents=...) vaut par défaut Intents.DEFAULT, c'est-à-dire tous les intents sauf les trois privilégiés. Quand un écouteur s'enregistre pour un événement qu'aucun des intents du bot ne fournit, bostie journalise un avertissement qui nomme l'intent à ajouter, et vous rappelle qu'un intent privilégié doit aussi être activé dans le Developer Portal. La section Intents les détaille.
Anciennes valeurs et cache¶
Les événements de mise à jour portent old, l'objet tel que le cache le connaissait avant le changement, et les événements de suppression portent l'objet supprimé : RoleUpdate.old, ScheduledEventUpdate.old, MessageDelete.message, MemberRemove.member. Ils valent None quand le cache ne contenait pas l'objet. Les messages ne sont mis en cache qu'avec Bot(max_messages=...), les membres qu'avec members=True, les présences qu'avec presences=True.
Attendre un événement¶
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) renvoie le prochain événement de cette classe pour lequel check renvoie vrai. async with bot.stream(Event, check=None) as events: met en file d'attente chaque événement correspondant pendant l'exécution du bloc, pour async for ; les événements qui arrivent pendant le corps de la boucle ne sont pas perdus. Aucun des deux n'a de délai d'expiration propre : bornez-les avec asyncio.timeout(). Une exception levée par check est relevée par l'attente. bot.wait_for_component et bot.wait_for_modal font de même pour les utilisations de composants et les envois de fenêtres modales.
Événements bruts et événements que bostie ne modélise pas¶
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", ...) fournit un RawEvent pour chaque nom de dispatch indiqué, modélisé ou non, et un écouteur annoté avec RawEvent reçoit tous les dispatchs. Un RawEvent a le name du dispatch, son numéro de séquence seq, et le payload sous forme de raw (un nouveau dict à chaque fois) ou de raw_json (des octets). Chaque événement typé possède aussi raw et raw_json.
Quand Discord envoie un payload qui ne correspond pas à sa classe, par exemple sans un champ que la documentation dit obligatoire, bostie journalise un avertissement une fois par nom d'événement, et les écouteurs typés ignorent cet événement tandis que les écouteurs RawEvent le reçoivent toujours.
Vos propres événements¶
Sous-classez BotEvent, avec kw_only=True, et envoyez une instance avec bot.dispatch(event) : ses écouteurs, attentes et flux la reçoivent comme n'importe quel autre événement. Une sous-classe sans NAME est nommée d'après son module et sa classe. Les classes d'événement sont des structs msgspec figées : leurs champs se déclarent donc sous forme d'annotations.
Événements de cycle de vie et de shard¶
bostie envoie ses propres événements sur le bot : Starting, Started, Stopping et Stopped autour du cycle de vie ; ShardReady, ShardDisconnect, ShardStop et ShardBackpressure pour chaque shard ; ListenerError et TaskError pour les échecs. Les connexions vocales envoient des événements vocaux par le même @bot.listen.
Quand un écouteur lève une exception¶
Une exception dans un écouteur est envoyée sous forme d'événement ListenerError avec le listener, l'event et l'error. Sans écouteur ListenerError, bostie journalise l'exception avec sa trace d'appels ; avec un tel écouteur, la journalisation vous revient. Une exception dans un écouteur ListenerError est seulement journalisée.