События¶

Discord сообщает боту о происходящем через события шлюза: отправлено сообщение, присоединился участник, изменилась роль. Слушатель — это асинхронная функция, которая принимает одно событие.
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 читает аннотацию типа первого параметра и регистрирует функцию для этого класса события или для каждого класса из объединения. @bot.listen(MessageUpdate) вместо этого явно называет классы — для функции без аннотации или с более широкой аннотацией. Классы находятся в bostie.events, а список событий называет каждый из них с его интентами и полями. У роутера есть такой же декоратор.
Каждый слушатель выполняется как отдельная задача, поэтому медленный слушатель не задерживает ни остальных, ни следующее событие. Задача запускается сразу: код слушателя до первого await выполняется до обработки следующего события. Исключение — слушатели Starting и Stopped: start() и close() дожидаются их по очереди. bot.remove_listener(function) снимает функцию со всех событий, которые она слушает.
Только то, что вы слушаете¶
bostie просит своё ядро на Rust пересылать только те имена диспетчеризации, которые что-то в боте использует: ваши слушатели, кэш, маршрутизация команд и компонентов и ожидающие вызовы ожидания. Все остальные события отбрасываются до того, как дойдут до Python, поэтому если ничего не слушать, это ничего не стоит. Типизированные события декодируются один раз на диспетчеризацию и только тогда, когда они кому-то нужны.
Интенты¶
Большинство событий Discord присылает только боту, который запросил их интенты. Bot(intents=...) по умолчанию равен Intents.DEFAULT — все интенты, кроме трёх привилегированных. Если слушатель регистрируется на событие, которое не доставляет ни один из интентов бота, bostie пишет в лог предупреждение с именем интента, который нужно добавить, и напоминает, что привилегированный интент нужно ещё и включить в Developer Portal. Подробнее о них — в разделе «Интенты».
Старые значения и кэш¶
События обновления несут old — объект в том виде, в каком он был в кэше до изменения, а события удаления несут удалённый объект: RoleUpdate.old, ScheduledEventUpdate.old, MessageDelete.message, MemberRemove.member. Они равны None, если объекта не было в кэше. Сообщения кэшируются только с Bot(max_messages=...), участники — только с members=True, присутствие — только с presences=True.
Ожидание события¶
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) возвращает следующее событие этого класса, для которого check возвращает истину. async with bot.stream(Event, check=None) as events: ставит в очередь каждое подходящее событие, пока выполняется блок, — для async for; события, пришедшие во время выполнения тела цикла, не теряются. Ни у того, ни у другого нет собственного тайм-аута, поэтому ограничивайте их с помощью asyncio.timeout(). Исключение, выброшенное check, выбрасывается из ожидания. bot.wait_for_component и bot.wait_for_modal делают то же самое для использований компонентов и отправок модальных окон.
Сырые события и события, которые bostie не моделирует¶
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", ...) доставляет RawEvent для каждого указанного имени диспетчеризации, смоделированного или нет, а слушатель с аннотацией RawEvent получает все диспетчеризации. У RawEvent есть имя диспетчеризации name, её порядковый номер seq и полезная нагрузка в виде raw (каждый раз новый dict) или raw_json (байты). У каждого типизированного события тоже есть raw и raw_json.
Если Discord присылает полезную нагрузку, которая не подходит к своему классу, например без поля, которое документация называет обязательным, bostie один раз на имя события пишет предупреждение в лог, и типизированные слушатели пропускают это событие, а слушатели RawEvent по-прежнему его получают.
Собственные события¶
Унаследуйтесь от BotEvent с kw_only=True и отправьте экземпляр через bot.dispatch(event): его слушатели, ожидания и потоки получат его, как любое другое событие. Подкласс без NAME называется по своему модулю и классу. Классы событий — неизменяемые структуры msgspec, поэтому их поля объявляются аннотациями.
События жизненного цикла и шардов¶
bostie отправляет собственные события о боте: Starting, Started, Stopping и Stopped вокруг жизненного цикла; ShardReady, ShardDisconnect, ShardStop и ShardBackpressure о каждом шарде; ListenerError и TaskError о сбоях. Голосовые подключения отправляют голосовые события через тот же @bot.listen.
Когда слушатель выбрасывает исключение¶
Исключение в слушателе отправляется как событие ListenerError с listener, event и error. Если слушателя ListenerError нет, bostie пишет исключение в лог с трассировкой; если он есть, логирование на вашей стороне. Исключение в слушателе ListenerError только пишется в лог.