Перейти к содержанию

Интенты, присутствие и шлюз

Шлюз — это WebSocket, по которому Discord присылает события, а бот отправляет своё присутствие и запросы. Ядро bostie на Rust поддерживает соединение: heartbeat, возобновления, переподключения, сжатие и лимит отправки. Эта страница описывает, что бот у шлюза запрашивает.

Интенты

Discord присылает событие только боту, который запросил его интент. Bot(intents=...) принимает флаг Intents, а если он не указан — Intents.DEFAULT: все интенты, кроме трёх привилегированных.

Привилегированный интент Нужен для
GUILD_MEMBERS событий присоединения, обновления и ухода участников, всех участников в кэше, запроса всех участников
GUILD_PRESENCES обновлений присутствия и присутствия в кэше
MESSAGE_CONTENT содержимого, эмбедов, вложений и компонентов сообщений на серверах, кроме сообщений, которые упоминают бота; для личных сообщений интент не нужен

Привилегированный интент нужно ещё и включить на странице Bot в Developer Portal. Без этого Discord закрывает соединение с кодом 4014, а start() вызывает PrivilegedIntentsRequired, в сообщении которого сказано, где его включить. Интенты складываются через |: Intents.DEFAULT | Intents.MESSAGE_CONTENT. bostie предупреждает, когда слушателю нужен интент, которого у бота нет, и вызывает MissingIntent, когда интент нужен параметру или запросу.

Голосу нужен GUILD_VOICE_STATES, который входит в DEFAULT.

import bostie
from bostie import Intents

bot = bostie.Bot(intents=Intents.ALL & ~Intents.GUILD_PRESENCES)

Рядом с отдельными интентами есть три готовых набора: Intents.DEFAULT, он же Intents.UNPRIVILEGED, содержит все интенты, кроме привилегированных, Intents.PRIVILEGED — три привилегированных, а Intents.ALL — и те и другие. & ~ убирает интенты из набора, как в примере выше.

Присутствие

import bostie
from bostie import Context, Intents, Status, activity

bot = bostie.Bot(
    intents=Intents.DEFAULT | Intents.GUILD_MEMBERS,
    status=Status.IDLE,
    activities=[activity.custom("Starting up")],
)


@bot.command
async def online(ctx: Context) -> None:
    """Switch to online and show what the bot is doing."""
    await bot.update_presence(
        status=Status.ONLINE, activities=[activity.listening("/help")]
    )
    await ctx.respond("Online.", ephemeral=True)


@bot.command
async def find(ctx: Context, name: str) -> None:
    """Find members whose name starts with the text."""
    if ctx.guild_id is None:
        await ctx.respond("Use this in a server.", ephemeral=True)
        return
    found = await bot.request_members(ctx.guild_id, query=name, limit=10)
    names = ", ".join(member.display_name for member in found.members) or "nobody"
    await ctx.respond(f"Found {names}.", ephemeral=True)


@bot.command
async def sounds(ctx: Context) -> None:
    """List this server's soundboard sounds."""
    if ctx.guild_id is not None:
        found = await bot.request_soundboard_sounds(ctx.guild_id)
        await ctx.respond(", ".join(sound.name for sound in found) or "No sounds.")

Bot(status=..., activities=[...], afk=False, idle_since=None) задаёт, что бот показывает при подключении. await bot.update_presence(...) меняет это на каждом шарде, а также на шардах, которые подключатся позже; непереданный параметр сохраняет своё значение.

status — это Status: ONLINE, IDLE, DND или INVISIBLE. Активности строит bostie.activity:

Конструктор Показывает
activity.playing(name) Играет в name
activity.listening(name) Слушает name
activity.watching(name) Смотрит name
activity.competing(name) Соревнуется в name
activity.streaming(name, url) Стримит name — для стриминговых сервисов, которые поддерживает Discord
activity.custom(text) пользовательский статус с текстом text

Каждый, кроме custom, принимает state= — вторую строку текста — и возвращает activity.ActivityDict.

Запросы

Некоторые данные приходят только через шлюз, как ответ на запрос:

  • await bot.request_members(guild_id, query="ab", limit=10) возвращает участников, чьё имя пользователя или никнейм начинается с query, — не больше limit (от 0 до 100). user_ids=[...] вместо этого запрашивает до 100 участников по ID. Без того и другого запрашиваются все участники, для чего нужен GUILD_MEMBERS. presences=True добавляет их присутствие и требует GUILD_PRESENCES. Ответ — RequestedMembers с members, presences и not_found — ID, которые Discord не нашёл. Discord ограничивает частоту этого запроса: отклонённый запрос вызывает MemberRequestRateLimited с retry_after.
  • await bot.request_soundboard_sounds(guild_id) возвращает звуки звуковой панели сервера.
  • await bot.request_channel_info(guild_id) возвращает голосовой статус и время начала голоса в каналах сервера; fields=["status"] запрашивает меньше.

Каждый запрос ждёт до 30 секунд каждую часть ответа (для участников — timeout=) и после этого вызывает TimeoutError. Запрос к шарду, который начинает новую сессию, вызывает StateError; повторите его, когда шард будет готов.

Сырые команды шлюза

bot.gateway.send(op, d) отправляет любую команду шлюза. Опкод 3 (присутствие) уходит на все шарды; полезная нагрузка с guild_id — на шард этого сервера, с guild_ids — на каждый шард с его долей ID, а всё остальное — на шард 0. Ответы Discord приходят как события, и только для тех имён событий, которые кто-то слушает, поэтому сочетайте сырую команду со слушателем listen_raw или с wait_for.

bot.gateway также даёт shard_ids — шарды, которые запускает этот процесс; shard_for(guild_id) — шард, на который Discord присылает события сервера; shard(shard_id) — со своими send, reconnect и latency; reconnect(), который обрывает все соединения и возобновляет сессии на новых; и latency — среднее время кругового пути heartbeat. Команда для шарда, который этот процесс не запускает, вызывает ShardNotRunning, а команда, вставшая в очередь за слишком большим числом других, — GatewayQueueFull.

Обратное давление

Ядро на Rust читает события так быстро, как их присылает Discord, и передаёт их в Python. Когда цикл событий отстаёт, шард перестаёт читать, пока Python не догонит, пишет предупреждение в лог и отправляет ShardBackpressure с числом ожидающих байтов. Причина — долгая блокирующая работа в обработчике или слушателе; перенесите её в поток через asyncio.to_thread.