Gateway: события в реальном времени
Чтобы бот реагировал на сообщения и другие события, ему не нужно ничего опрашивать — Voice сам push'ит события боту через постоянное соединение, как только они происходят. Это соединение называется Gateway.
Протокол
Gateway реализован поверх SignalR — WebSocket-протокол ASP.NET Core с автоматическим фолбэком на Server-Sent Events/long polling, если WebSocket недоступен. Если вы пишете библиотеку не на .NET, ищите клиент протокола SignalR для своего языка (реализации есть для JavaScript/TypeScript, Java, Python, Go, Rust и т.д.) — реализовывать сам протокол SignalR с нуля обычно не требуется.
Подключение
wss://<ваш gateway-хост>/hubs/bots?access_token=<токен бота>
Токен передаётся параметром строки запроса access_token (см.
«Аутентификация ботов») — это единственный способ аутентификации
для этого соединения, отдельного «рукопожатия» после подключения не требуется.
Сразу после успешного подключения сервер сам определяет, в какие «комнаты» (группы рассылки) включить это соединение, основываясь на:
- группах, в которых состоит бот (сервер запрашивает их на подключении);
- Gateway Intents, выданных боту.
Клиенту не нужно ничего «подписывать» вручную — после подключения события просто начинают приходить сами, в соответствии с правами бота.
Если владелец меняет боту intents уже после того, как бот подключён, соединение продолжит получать события по старому набору intents до переподключения (комнаты рассылки определяются один раз, в момент установки соединения). Переподключитесь, чтобы применить новые intents.
Топики
После подключения на соединении можно слушать четыре именованных «топика» (в терминах SignalR — это имена методов, которые сервер вызывает на клиенте):
| Топик | Требует intent | Что в нём приходит |
|---|---|---|
chat-events | GuildMessages | Новые/изменённые/удалённые сообщения, реакции, индикаторы набора текста |
channel-events | GuildVoiceStates | Подключение/отключение участников голосовых каналов |
group-events | Guilds | Вход/выход участников, изменения структуры группы (категории, каналы, роли), смена статуса присутствия |
interaction-events | — (не гейтится intent'ами) | Входящие interactions — слэш-команды, клики по компонентам, сабмиты модальных окон, запросы автодополнения. Это личная, адресная доставка ровно этому боту, а не рассылка по группе. |
Формат события (конверт)
Каждый вызов топика передаёт один аргумент — объект-конверт. Общая форма (набор полей зависит
от топика — например, channel_id/message_id есть не везде):
{
"GroupId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"ChatId": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"MessageId": "b3fc2c96-3f66-afa6-9b1d-eb4d3b7d4bad",
"UserId": "2c963f66-afa6-9b1d-eb4d-3b7d4bad9bdd",
"Type": "MessageCreated",
"PayloadJson": "{...сериализованный полезный объект в виде JSON-строки...}",
"Timestamp": 1730000000000
}
PascalCaseВ отличие от gRPC-методов BotsApi/InteractionsApi (там JSON — camelCase, как в примерах
«Основные методы»), конверт Gateway-события — это сырой
System.Text.Json без camelCase-политики, поэтому его поля — Type, PayloadJson, Timestamp,
GroupId, ChatId, UserId, MessageId — именно в PascalCase, как показано выше.
Важно: Payload приходит не как вложенный объект, а как строка, содержащая JSON — поле
PayloadJson. Библиотеке нужно распарсить эту строку отдельным шагом после получения конверта.
Такая двухуровневая схема — намеренное решение платформы: она позволяет добавлять новые виды
событий (Type), не меняя форму самого конверта и не требуя обновления клиентов, которые эти
новые события ещё не умеют обрабатывать.
Как это использовать
on(topic, envelope) => {
payload = json_parse(envelope.PayloadJson)
switch (envelope.Type) {
case "MessageCreated":
handle_new_message(payload) // payload — это объект сообщения
case "MessageReactionAdded":
handle_reaction(payload)
// ... остальные известные типы
default:
// неизвестный тип события — просто игнорируем, а не падаем.
// Сервер может добавлять новые типы событий со временем.
}
}
Список возможных значений Type может со временем расширяться — платформа явно спроектирована
так, чтобы добавление нового типа события не требовало обновления уже написанных клиентов.
Хорошая библиотека должна тихо пропускать (а не падать с ошибкой на) любой незнакомый Type.
Известные значения Type по топикам
Список ниже — ориентировочный (актуальный на момент написания документации), а не закрытый
перечень. У каждой группы — отдельная страница на каждое значение Type с описанием payload'а и
JSON-примером:
- «Структура группы» (топик
group-events) —GroupUpdated,CategoryCreated/CategoryUpdated/CategoryDeleted/CategoryMoved,ChannelCreated/ChannelDeleted/ChannelMoved,RoleCreated/RoleUpdated/RoleMoved/RoleDeleted/RoleAssigned/RoleRemoved— payload у всех один и тот же: полный текущий снимок группы, а не diff; - «Участники группы» (топик
group-events) —UserJoined,UserLeft,UserKicked; - «Статус и профиль» (топик
group-events) —UserProfileUpdated,UserStatusChanged; - «Голосовые каналы» (топик
channel-events) —UserConnected,UserDisconnected; - «Сообщения» (топик
chat-events) —MessageCreated,MessageUpdated,MessageDeleted,MessageReactionAdded,MessageReactionRemoved,UserTyping,BotTyping; - «Interactions» (топик
interaction-events) —InteractionCreated, единственный тип на этом топике, личная адресная доставка боту.
Переподключение
Реализуйте автоматическое переподключение с задержкой (большинство SignalR-клиентов умеют это из коробки) — соединение может обрываться по сетевым причинам, при развёртывании новой версии сервера или при отзыве токена бота (в последнем случае переподключение с тем же токеном будет раз за разом завершаться ошибкой аутентификации — такую ситуацию стоит обрабатывать отдельно, не как временный сбой).