Протокол шлюза (WebSocket)
Спецификация, раздел 6.5 и 6.7. Реализация — backend/internal/gateway, клиент — web/src/lib/gateway.ts.
Подключение
wss://gateway.klichat.ru/?v=1&encoding=json (в dev — ws://localhost:8081/; Vite проксирует /gateway). v принимает 1, 9, 10 (псевдонимы для библиотек ботов); другое значение → закрытие 4012. compress=zlib-stream поддерживается: кадры уходят двоичными, сжатые одним потоком zlib на соединение, каждый завершается Z_SYNC_FLUSH (00 00 ff ff) — это формат, который ждут zlib-sync в discord.js и zlib в discord.py. Без параметра работает обычное сжатие WebSocket (permessage-deflate). encoding=msgpack — с 1.0.
Пакет: {"op": <int>, "d": <any>, "s": <int|null>, "t": <string|null>}. s и t заполнены только у DISPATCH. Лимит входящих пакетов: 120 за 60 секунд на соединение, иначе 4008. Максимальный размер пакета — 64 КБ.
Жизненный цикл
клиент сервер
│ ── WebSocket connect ───────────────────▶ │
│ ◀── op 10 HELLO {heartbeat_interval} ─── │
│ ── op 2 IDENTIFY {token, properties} ───▶ │
│ ◀── op 0 READY (s=1) ─────────────────── │
│ ── op 1 HEARTBEAT <last s> (каждые 41,25 с)
│ ◀── op 11 HEARTBEAT_ACK
│ ◀── op 0 DISPATCH … (s растёт)
│ … обрыв …
│ ── connect, HELLO, op 6 RESUME {token, session_id, seq}
│ ◀── пропущенные DISPATCH, затем RESUMED — или op 9 INVALID_SESSION (false) → IDENTIFY
- Первый HEARTBEAT клиент шлёт через случайную долю интервала, далее — строго по интервалу.
- Нет HEARTBEAT полтора интервала → сервер закрывает соединение
4009. Нет ACK у клиента → клиент переподключается с RESUME. op 7 RECONNECT— сервер просит переподключиться (деплой, ребаланс); клиент делает RESUME.- Буфер RESUME на сервере — 500 последних событий на сессию или 3 минуты (Redis).
Опкоды
| op | Имя | Направление | Назначение |
|---|---|---|---|
| 0 | DISPATCH | ← | Событие t с номером s |
| 1 | HEARTBEAT | ↔ | Клиент шлёт последний s; сервер может запросить |
| 2 | IDENTIFY | → | {token, properties:{os,browser,device}, intents?, shard?, presence?} |
| 3 | PRESENCE_UPDATE | → | {status, activities, afk, since}: бот — статус и активность; человек — игра (type 0) и «отошёл» (status: "idle" или afk: true), см. «Присутствие» |
| 4 | VOICE_STATE_UPDATE | → | {guild_id, channel_id|null, self_mute, self_deaf}: вход (ответ — VOICE_SERVER_UPDATE с токеном), выход (channel_id: null), свои mute/deaf в текущем канале; guild_id: null с channel_id ЛС — звонок; веб-клиент входит через POST /channels/{id}/voice/join |
| 6 | RESUME | → | {token, session_id, seq} |
| 7 | RECONNECT | ← | Переподключиться |
| 8 | REQUEST_GUILD_MEMBERS | → | {guild_id, query?, limit, user_ids?, presences?, nonce?} → GUILD_MEMBERS_CHUNK |
| 9 | INVALID_SESSION | ← | d: false — нужен новый IDENTIFY; d: true — можно RESUME позже |
| 10 | HELLO | ← | {heartbeat_interval: 41250} |
| 11 | HEARTBEAT_ACK | ← | Подтверждение |
| 14 | SUBSCRIBE_MEMBER_LIST | → | {guild_id, channels:{<channel_id>:[[0,99],[100,199]]}} → GUILD_MEMBER_LIST_UPDATE; одна подписка на сессию (последний канал), до 3 диапазонов по 100 строк |
Коды закрытия
| Код | Значение | Переподключаться |
|---|---|---|
| 4000 | Неизвестная ошибка | да |
| 4001 | Неизвестный опкод | да |
| 4002 | Ошибка разбора пакета | да |
| 4003 | Пакет до IDENTIFY | да |
| 4004 | Токен неверен или отозван | нет |
| 4005 | Повторный IDENTIFY | да |
| 4007 | Неверный seq в RESUME | да, с IDENTIFY |
| 4008 | Превышен лимит пакетов | да, с задержкой |
| 4009 | Сессия истекла (нет heartbeat) | да |
| 4010 | Неверный шард | нет |
| 4011 | Нужно шардирование | нет |
| 4012 | Неверная версия API | нет |
| 4013 | Неверные intents | нет |
| 4014 | Привилегированные intents не включены | нет |
READY
{"op":0,"s":1,"t":"READY","d":{
"v":1,
"user":{"id":"…","username":"nikita","global_name":"Никита","avatar":null,"bot":false},
"session_id":"3f9c…","resume_gateway_url":"wss://gateway.klichat.ru",
"guilds":[{"id":"…","unavailable":true}],
"private_channels":[…],"relationships":[…],"read_state":[…],"calls":[…],"user_settings":{…}
}}
calls — идущие звонки в ЛС пользователя (объекты Call с voice_states).
Для пользователей серверы лежат прямо в READY (guilds[] в форме GUILD_CREATE). Для ботов, как у Discord, READY содержит guilds: [{id, unavailable: true}], а затем приходит GUILD_CREATE на каждый сервер.
READY бота
{"op":0,"s":1,"t":"READY","d":{
"v":1,
"user":{"id":"…","username":"dezhurnyy","global_name":"Дежурный","discriminator":"0","avatar":null,"bot":true,"public_flags":0,"mfa_enabled":true,"verified":true},
"session_id":"3f9c…","resume_gateway_url":"ws://localhost:8081",
"application":{"id":"…","flags":262144},
"shard":[0,1],
"guilds":[{"id":"…","unavailable":true}],
"private_channels":[],"relationships":[],"read_state":[],"presences":[]
}}
application.flags — флаги приложения в битах Discord: GATEWAY_PRESENCE 1<<12, GATEWAY_GUILD_MEMBERS 1<<14, GATEWAY_MESSAGE_CONTENT 1<<18 по включённым в «Приложениях» привилегированным intents.
GUILD_CREATE боту — объект Guild с channels, threads, roles, emojis, voice_states, member_count, joined_at и members: без intent GUILD_MEMBERS в members только сам бот, с ним — до 250 первых участников; presences заполняются только с GUILD_PRESENCES.
Присутствие
Статусы как у Discord: online, idle, dnd, invisible; чужой невидимый — offline. Человек выбирает статус и свой статус в PATCH /users/@me/settings: они хранятся на сервере, приходят в READY (user_settings.status, user_settings.custom_status: {text} | null) и с каждой новой сессией, так что невидимый остаётся невидимым после любого переподключения. Смена — USER_SETTINGS_UPDATE своим сессиям (тело как ответ PATCH) и PRESENCE_UPDATE друзьям и серверам.
{"t":"PRESENCE_UPDATE","d":{"user":{"id":"…"},"guild_id":"…","status":"dnd","client_status":{"web":"dnd"},
"activities":[{"id":"custom","name":"Custom Status","type":4,"state":"на паре до шести"},
{"name":"Dota 2","type":0,"steam_app_id":"570"}]}}
- Свой статус — активность type 4, текст в
state, первой в списке; игра — type 0 (steam_app_id— наше поле для обложки). Уofflineактивностей нет. - Невидимого остальные видят
offline(в READY, GUILD_CREATE, списке участников, PRESENCE_UPDATE); его собственные сессии получаютPRESENCE_UPDATEо себе безguild_idсоstatus: "invisible". Серверные копии присутствия о самом себе шлюз людям не пересылает — только ботам. - op 3 человека статус не меняет:
status: "idle"илиafk: trueотмечает сессию как «отошедшую», и когда отошли все сессии, выбравшийonlineстановитсяidle. Ручныеidle,dnd,invisibleважнее. Клиент может прислать то же вpresenceIDENTIFY.activitiesкаждого op 3 заменяют игру целиком (пустой список — не играет). - Бот задаёт всё через op 3 и
presenceв IDENTIFY, как в Discord (client.user.setPresence): статус (offline— невидимый), одна активность типов 0, 1 (сurlhttp/https), 2, 3, 5 и свой статус type 4. IDENTIFY безpresence(или с{}) возвращает статус, заданный до переподключения, — если бот вернулся в течение 10 минут. - «Не беспокоить» — push-уведомления не отправляются, пока статус выбран (даже с закрытым клиентом); счётчики упоминаний растут как обычно.
REQUEST_GUILD_MEMBERS (op 8)
{"op":8,"d":{"guild_id":"…","query":"","limit":0,"presences":false,"user_ids":["…"],"nonce":"…"}} — query (начало имени пользователя, отображаемого имени или ника, до 100 результатов), user_ids (строка или список, до 100) или всё (query: "", limit 0 — без ограничения). Ответ — GUILD_MEMBERS_CHUNK частями по 1000 участников:
{"t":"GUILD_MEMBERS_CHUNK","d":{"guild_id":"…","members":[{"user":{…},"roles":[…],"joined_at":"…"}],
"chunk_index":0,"chunk_count":1,"nonce":"…","presences":[…],"not_found":["…"]}}
nonce возвращается как прислан, presences только при presences: true (боту — при intent GUILD_PRESENCES), not_found — при запросе по user_ids. Боту без GUILD_MEMBERS полный список не отдаётся: приходит пустая часть, чтобы библиотека не ждала таймаута; поиск по имени и по id работает и без него.
События DISPATCH
READY, RESUMED, GUILD_CREATE/UPDATE/DELETE, GUILD_MEMBER_ADD/UPDATE/REMOVE, GUILD_MEMBERS_CHUNK, GUILD_MEMBER_LIST_UPDATE, GUILD_ROLE_CREATE/UPDATE/DELETE, GUILD_BAN_ADD/REMOVE, GUILD_EMOJIS_UPDATE, GUILD_STICKERS_UPDATE, GUILD_AUDIT_LOG_ENTRY_CREATE, GUILD_IMPORT_UPDATE, CHANNEL_CREATE/UPDATE/DELETE, CHANNEL_PINS_UPDATE, THREAD_CREATE/UPDATE/DELETE, MESSAGE_CREATE/UPDATE/DELETE/DELETE_BULK, MESSAGE_REACTION_ADD/REMOVE/REMOVE_ALL/REMOVE_EMOJI, MESSAGE_ACK, TYPING_START, PRESENCE_UPDATE, VOICE_STATE_UPDATE, VOICE_SERVER_UPDATE, CALL_CREATE/UPDATE/DELETE, AUTO_MODERATION_RULE_CREATE/UPDATE/DELETE, AUTO_MODERATION_ACTION_EXECUTION, RELATIONSHIP_ADD/REMOVE, USER_UPDATE, USER_SETTINGS_UPDATE, INVITE_CREATE/DELETE, WEBHOOKS_UPDATE, INTERACTION_CREATE (2.0).
USER_UPDATE приходит не только о себе: при смене имени или аватара друга либо собеседника в ЛС клиент получает его публичный объект user; участникам общих серверов то же изменение приходит как GUILD_MEMBER_UPDATE (полный member).
Формы объектов — как в openapi.yaml (Message, Channel, Member, Role…). Событие MESSAGE_CREATE возвращает nonce из запроса, чтобы клиент заменил оптимистичное сообщение.
Intents (боты)
Биты как у Discord: GUILDS 1<<0, GUILD_MEMBERS 1<<1 (привилегированный), GUILD_MODERATION 1<<2, GUILD_EXPRESSIONS 1<<3, GUILD_INVITES 1<<6, GUILD_VOICE_STATES 1<<7, GUILD_PRESENCES 1<<8 (привилегированный), GUILD_MESSAGES 1<<9, GUILD_MESSAGE_REACTIONS 1<<10, GUILD_MESSAGE_TYPING 1<<11, DIRECT_MESSAGES 1<<12, DIRECT_MESSAGE_REACTIONS 1<<13, DIRECT_MESSAGE_TYPING 1<<14, MESSAGE_CONTENT 1<<15 (привилегированный), AUTO_MODERATION_CONFIGURATION 1<<20, AUTO_MODERATION_EXECUTION 1<<21. Пользовательские сессии intents не передают и получают всё.
IDENTIFY бота: {"token": "klb_…", "intents": 33281, "shard": [0, 1], "properties": {…}} — токен без префикса Bot (с ним тоже принимается). Неизвестные биты intents игнорируются. Привилегированные intents (GUILD_MEMBERS, GUILD_PRESENCES, MESSAGE_CONTENT) должны быть включены владельцем в разделе «Приложения», иначе соединение закрывается кодом 4014 (PrivilegedIntentsRequired в discord.py). shard[0] != 0 → 4010 (пока один шард).
Фильтр событий бота по intents:
| Intent | События |
|---|---|
GUILDS | GUILD_CREATE/UPDATE/DELETE, GUILD_ROLE_*, CHANNEL_CREATE/UPDATE/DELETE серверов, CHANNEL_PINS_UPDATE, THREAD_* |
GUILD_MEMBERS | GUILD_MEMBER_ADD/UPDATE/REMOVE; открывает GET /guilds/{id}/members |
GUILD_MODERATION | GUILD_BAN_ADD/REMOVE, GUILD_AUDIT_LOG_ENTRY_CREATE |
GUILD_EXPRESSIONS | GUILD_EMOJIS_UPDATE, GUILD_STICKERS_UPDATE {guild_id, stickers} |
GUILD_WEBHOOKS | WEBHOOKS_UPDATE {guild_id, channel_id} |
| — (только пользователям) | GUILD_IMPORT_UPDATE {guild_id, import: {id, status, progress?, error}} — импорт из Discord |
GUILD_INVITES | INVITE_CREATE/DELETE |
GUILD_VOICE_STATES | VOICE_STATE_UPDATE серверов |
GUILD_PRESENCES | PRESENCE_UPDATE серверов |
GUILD_MESSAGES / DIRECT_MESSAGES | MESSAGE_CREATE/UPDATE/DELETE, CHANNEL_PINS_UPDATE в ЛС |
GUILD_MESSAGE_REACTIONS / DIRECT_MESSAGE_REACTIONS | MESSAGE_REACTION_ADD/REMOVE/REMOVE_ALL/REMOVE_EMOJI |
GUILD_MESSAGE_TYPING / DIRECT_MESSAGE_TYPING | TYPING_START |
AUTO_MODERATION_EXECUTION | AUTO_MODERATION_ACTION_EXECUTION |
| всегда | READY, USER_UPDATE, VOICE_SERVER_UPDATE |
Без MESSAGE_CONTENT в MESSAGE_CREATE/UPDATE серверов поля content, embeds, attachments приходят пустыми, если бот не автор и не упомянут. Пользовательские события (MESSAGE_ACK, RELATIONSHIP_*, CALL_*, GUILD_MEMBER_LIST_UPDATE, USER_GUILD_SETTINGS_UPDATE, CHANNEL_RECIPIENT_*, ЛС-каналы в CHANNEL_CREATE) ботам не отправляются. Перевыпуск токена или удаление приложения закрывает сессии бота кодом 4004.
Статус реализации
| Возможность | Состояние |
|---|---|
| HELLO, HEARTBEAT/ACK, лимит пакетов, коды 4001–4009, 4012 | готово (фаза 0) |
IDENTIFY с dev-токеном dev:<username> (только KLICHAT_ENV=dev) | готово (фаза 0) |
IDENTIFY с access-токеном сессии (Bearer необязателен) | готово (фаза 1) |
IDENTIFY ботов токеном klb_… с intents, код 4014 для привилегированных intents, READY бота и GUILD_CREATE на каждый сервер, фильтр DISPATCH по intents, вырезание содержимого без MESSAGE_CONTENT, закрытие 4004 при перевыпуске токена | готово (фаза 5) |
| op 8 REQUEST_GUILD_MEMBERS → GUILD_MEMBERS_CHUNK (query, user_ids, presences, nonce, части по 1000); WEBHOOKS_UPDATE | готово (фаза 5) |
| READY с ЛС, друзьями, read_state и присутствием друзей | готово (фаза 1) |
Статусы online/idle/dnd/invisible и свой статус (PATCH /users/@me/settings, USER_SETTINGS_UPDATE, активность type 4), «отошёл» по сессиям через op 3, статус и активность ботов через op 3 и presence в IDENTIFY | готово |
| MESSAGE_CREATE/UPDATE/DELETE, MESSAGE_ACK, TYPING_START, CHANNEL_CREATE/UPDATE/DELETE, RELATIONSHIP_ADD/REMOVE, PRESENCE_UPDATE для ЛС через NATS | готово (фаза 1) |
Группы ЛС: CHANNEL_RECIPIENT_ADD/REMOVE {channel_id, user}, системные сообщения типов 1, 2, 4 | готово (фаза 1) |
READY с серверами (guilds[] в форме GUILD_CREATE: каналы с перекрытиями, роли, свой участник, member_count, presences тех, кто в сети); подписка сессии на guild.{id}.> всех серверов, GUILD_CREATE/DELETE подписывают и отписывают на лету | готово (фаза 2) |
GUILD_CREATE (вступление), GUILD_UPDATE/DELETE, GUILD_MEMBER_ADD/UPDATE/REMOVE ({guild_id, user}), GUILD_BAN_ADD/REMOVE, GUILD_ROLE_CREATE/UPDATE/DELETE, INVITE_CREATE/DELETE, CHANNEL_* серверов, MESSAGE_*/TYPING_START в каналах серверов (с guild_id; в приватных каналах — только тем, у кого VIEW_CHANNEL), PRESENCE_UPDATE участникам серверов с guild_id | готово (фаза 2) |
USER_GUILD_SETTINGS_UPDATE (свои настройки уведомлений сервера), GUILD_AUDIT_LOG_ENTRY_CREATE ({guild_id, …} всем участникам сервера); READY содержит user_guild_settings[] | готово (фаза 2) |
Превью ссылок: worker строит embeds после MESSAGE_CREATE и присылает то же сообщение событием MESSAGE_UPDATE (без edited_timestamp); скрытие превью — PATCH …/messages/{id} с flags: 4, тоже MESSAGE_UPDATE | готово (фаза 2) |
Ветки: THREAD_CREATE (объект ветки + newly_created), THREAD_UPDATE (архив, закрытие, название), THREAD_DELETE ({id, guild_id, parent_id, type}), THREAD_MEMBERS_UPDATE ({id, guild_id, member_count, added_members[], removed_member_ids[]}); в приватных каналах — только тем, кто видит родителя; READY и GUILD_CREATE содержат threads[] (активные ветки, с member у своих) | готово (фаза 2) |
Ленивый список участников: op 14 → GUILD_MEMBER_LIST_UPDATE {guild_id, id ("everyone" или id канала с перекрытиями), member_count, online_count, groups:[{id: роль|online|offline, count}], ops:[{op:"SYNC", range:[a,b], items:[{group}|{member:{…, presence}}]}]}; строки плоского списка — заголовки групп и участники, группы — hoist-роли по позиции для тех, кто в сети, затем «online» и «offline», внутри по имени; при PRESENCE_UPDATE, GUILD_MEMBER_*, GUILD_ROLE_*, CHANNEL_UPDATE этого сервера подписанные диапазоны пересылаются заново через 750 мс | готово (фаза 2) |
| RESUME через Redis | фаза 2 |
Реакции: MESSAGE_REACTION_ADD/REMOVE {user_id, channel_id, message_id, guild_id?, member?, emoji:{id, name, animated?}}, MESSAGE_REACTION_REMOVE_ALL {channel_id, message_id, guild_id?}, MESSAGE_REACTION_REMOVE_EMOJI {…, emoji} тем, кто видит канал; сводка reactions[] в сообщении (me — для запросившего) | готово (фаза 5) |
Закрепы: CHANNEL_PINS_UPDATE {channel_id, guild_id?, last_pin_timestamp}, системное сообщение типа 6 с message_reference, MESSAGE_UPDATE с pinned | готово (фаза 5) |
Эмодзи сервера: GUILD_EMOJIS_UPDATE {guild_id, emojis[]}; emojis[] в объекте сервера (READY, GUILD_CREATE) | готово (фаза 5) |
Автомод: AUTO_MODERATION_RULE_CREATE/UPDATE/DELETE (объект правила) и AUTO_MODERATION_ACTION_EXECUTION {guild_id, action, rule_id, rule_trigger_type, user_id, channel_id, message_id?, alert_system_message_id?, matched_keyword} участникам сервера — текст сообщения в событие не входит, он в оповещении (сообщение типа 24 с embed в канале модераторов) | готово (фаза 5) |
Звонки в ЛС: CALL_CREATE {channel_id, message_id, region, ringing, voice_states} получателям, когда первый входит (плюс MESSAGE_CREATE типа 3); CALL_UPDATE {…, ringing} при входе, отказе, повторном звонке и по истечении 60 с; CALL_DELETE {channel_id} и MESSAGE_UPDATE с call.ended_timestamp, когда выходит последний; VOICE_STATE_UPDATE без guild_id получателям канала | готово (фаза 3) |
Голос: VOICE_STATE_UPDATE (объект voice_state с member; channel_id: null — вышел) тем, кто видит канал; VOICE_SERVER_UPDATE {token, endpoint, region, expires_at, guild_id, channel_id, voice_state} пользователю при входе через op 4 и при переводе модератором; voice_states[] в READY и GUILD_CREATE; отключение сессии, из которой входили (session_id), снимает состояние; вебхуки LiveKit и сверка с SFU раз в 15 с снимают тех, кто не подключился за 15 с или пропал | готово (фаза 3) |
Intents, шарды, GET /gateway/bot, коды 4010–4014 | фаза 5 |
Полное описание REST — OpenAPI 3.1. Разделы: вся документация.