Войти

Протокол шлюза (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

Опкоды

opИмяНаправлениеНазначение
0DISPATCHСобытие t с номером s
1HEARTBEATКлиент шлёт последний s; сервер может запросить
2IDENTIFY{token, properties:{os,browser,device}, intents?, shard?, presence?}
3PRESENCE_UPDATE{status, activities, afk, since}: бот — статус и активность; человек — игра (type 0) и «отошёл» (status: "idle" или afk: true), см. «Присутствие»
4VOICE_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
6RESUME{token, session_id, seq}
7RECONNECTПереподключиться
8REQUEST_GUILD_MEMBERS{guild_id, query?, limit, user_ids?, presences?, nonce?}GUILD_MEMBERS_CHUNK
9INVALID_SESSIONd: false — нужен новый IDENTIFY; d: true — можно RESUME позже
10HELLO{heartbeat_interval: 41250}
11HEARTBEAT_ACKПодтверждение
14SUBSCRIBE_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"}]}}

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] != 04010 (пока один шард).

Фильтр событий бота по intents:

IntentСобытия
GUILDSGUILD_CREATE/UPDATE/DELETE, GUILD_ROLE_*, CHANNEL_CREATE/UPDATE/DELETE серверов, CHANNEL_PINS_UPDATE, THREAD_*
GUILD_MEMBERSGUILD_MEMBER_ADD/UPDATE/REMOVE; открывает GET /guilds/{id}/members
GUILD_MODERATIONGUILD_BAN_ADD/REMOVE, GUILD_AUDIT_LOG_ENTRY_CREATE
GUILD_EXPRESSIONSGUILD_EMOJIS_UPDATE, GUILD_STICKERS_UPDATE {guild_id, stickers}
GUILD_WEBHOOKSWEBHOOKS_UPDATE {guild_id, channel_id}
— (только пользователям)GUILD_IMPORT_UPDATE {guild_id, import: {id, status, progress?, error}} — импорт из Discord
GUILD_INVITESINVITE_CREATE/DELETE
GUILD_VOICE_STATESVOICE_STATE_UPDATE серверов
GUILD_PRESENCESPRESENCE_UPDATE серверов
GUILD_MESSAGES / DIRECT_MESSAGESMESSAGE_CREATE/UPDATE/DELETE, CHANNEL_PINS_UPDATE в ЛС
GUILD_MESSAGE_REACTIONS / DIRECT_MESSAGE_REACTIONSMESSAGE_REACTION_ADD/REMOVE/REMOVE_ALL/REMOVE_EMOJI
GUILD_MESSAGE_TYPING / DIRECT_MESSAGE_TYPINGTYPING_START
AUTO_MODERATION_EXECUTIONAUTO_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. Разделы: вся документация.