Интенты, присутствие и шлюз¶
Шлюз — это 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.