12 KiB
API Чата
Текстовый чат в реальном времени для участников конференции. Реализован через WebSocket с аутентификацией по LiveKit-токену и broadcast через Redis pub/sub.
WS-эндпоинт
WS /api/v1/conferences/{conference_id}/chat
Подключение требует явной аутентификации на уровне протокола (первое сообщение).
Жизненный цикл соединения
1. Accept и ожидание auth-сообщения (таймаут 10 с)
Сервер принимает WebSocket-соединение и ждёт первого (и только первого) сообщения типа auth:
{
"type": "auth",
"token": "<LiveKit access-token>"
}
Внимание: токен не передаётся в query-параметре и не логируется nginx'ом (требование безопасности — токены не должны попадать в логи доступа).
2. Проверка допуска
Сервер выполняет последовательность проверок (порядок важен для единообразной обработки ошибок):
- Тоггл чата: флаг
chat.enabledиз настроек инстанса в БД (instance_settings). Проверяется на каждом подключении, без рестарта backend. - LiveKit-токен: верификация подписи и целостности (
livekit.api.TokenVerifier) - Идентичность:
- Зарегистрированный пользователь:
identity= строка UUID пользователя; grantvideo.roomдолжен совпадать сconference.slug - Гость:
identity=guest:{guest_access_id}(UUID гостевой записи); grantvideo.roomдолжен совпадать сconference.slug
- Зарегистрированный пользователь:
- Конференция: существует и статус ≠
ended
3. Отправка истории
При успешной аутентификации сервер отправляет последние 50 сообщений открытой сессии конференции:
{
"type": "history",
"messages": [
{
"id": 1,
"author_id": "550e8400-e29b-41d4-a716-446655440000",
"author_name": "Иван Петров",
"is_guest": false,
"text": "Привет, все слышат?",
"created_at": "2026-07-18T14:30:00Z"
},
...
]
}
Порядок: Подписка на Redis pub/sub канал происходит перед отправкой истории. Это гарантирует, что сообщения, пришедшие от других клиентов в окне между SELECT истории и subscribe, не будут потеряны. На стыке возможен дубликат (одно сообщение и в history, и в первом pub/sub); дедуплицирование по id.
4. Двусторонний обмен
Клиент отправляет сообщения:
{
"type": "message",
"text": "Мое сообщение"
}
Сервер пишет сообщение в БД, затем публикует его в Redis pub/sub. Отправитель получает своё сообщение обратно через pub/sub (echo). Порядок доставки единый для всех подписчиков канала.
Полученное сообщение (для всех участников, в т.ч. отправителя):
{
"type": "message",
"message": {
"id": 2,
"author_id": "550e8400-e29b-41d4-a716-446655440001",
"author_name": "Анна Смирнова",
"is_guest": true,
"text": "Отлично, видно хорошо!",
"created_at": "2026-07-18T14:30:15Z"
}
}
5. Обработка ошибок протокола
Если клиент отправит невалидное JSON или нарушит протокол (невалидный type, отсутствующие поля), сервер отправляет:
{
"type": "error",
"code": "invalid_message"
}
Соединение остаётся открытым; клиент может отправить следующее сообщение. Полный разрыв соединения происходит только по close-кодам ниже.
Close-коды
Сервер закрывает WebSocket соединение со следующими кодами:
| Код | Причина | Детали |
|---|---|---|
| 4401 | Невалидный auth-токен | Таймаут ожидания auth-сообщения, невалидный JSON, невалидная подпись LiveKit-токена, отсутствие identity/room в token claims или невалидный UUID. Детали причины не раскрываются клиенту. |
| 4403 | Неправильная комната | Токен валиден, но выдан не для этой конференции (grant video.room ≠ conference.slug). |
| 4404 | Чат недоступен | Чат выключен в настройках инстанса (chat.enabled = false), или конференция не найдена, или конференция завершена (status = ended). Единый код для всех случаев — чтобы по коду закрытия нельзя было перебором отличить существующую конференцию от несуществующей. |
Штатное закрытие: когда клиент сам закрывает соединение — это не ошибка, обработчик корректно завершается.
Защита от фантомных сессий
Если клиент пытается отправить сообщение уже после завершения конференции (room_finished от LiveKit):
- Сервер перечитывает статус конференции свежим SELECT (не полагаясь на закэшированный объект)
- Если статус =
ended, отправляет close 4404; сообщение не записывается в БД - Если статус ≠
endedи открытой сессии нет, автоматически создаёт её
Это гарантирует, что "фантомные" сообщения (отправленные после room_finished, когда сеанс уже завершается) не создаются.
Схема сообщения чата
ChatMessageOut (выход)
{
id: number, // BIGINT Identity(always=True), уникален в пределах инстанса
author_id: string | null, // UUID пользователя ИЛИ UUID гостевой записи; null не возможно (см. schema.md)
author_name: string, // Снапшот отображаемого имени из LiveKit-токена на момент отправки (макс 255 символов)
is_guest: boolean, // true если guest_access_id заполнен
text: string, // Текст сообщения (1..2000 символов)
created_at: string // UTC ISO-8601 с суффиксом Z
}
Особенности:
author_name— снапшот, не ссылка на текущее имя пользователя (переживает переименование и удаление гостевой записи)author_idникогда не NULL (проверка на уровне БД: CHECKuser_id IS NOT NULL OR guest_access_id IS NOT NULL)
Конфигурация
Настройка чата в инстансе
Флаг chat.enabled хранится в таблице instance_settings (key = "chat", value = JSONB):
{
"enabled": true
}
Доступ клиенту: флаг приходит в ответе join / guest-join (JoinOut.chat_enabled), по нему фронт показывает/скрывает UI панели чата.
Изменение: через админ-панель (PATCH /api/v1/admin/settings); без рестарта backend.
Бутстрап из config/plugins.yaml
При первом запуске backend загружает config/plugins.yaml и создаёт запись в instance_settings (однократный идемпотентный импорт):
chat:
enabled: true
Если instance_settings['chat'] уже существует, бутстрап не перезаписывает.
Примеры использования
JavaScript/TypeScript (frontend)
// Подключение
const ws = new WebSocket('ws://localhost:8000/api/v1/conferences/550e8400-e29b-41d4-a716-446655440000/chat')
// Отправка auth-токена (должен быть получен с backend при join)
ws.onopen = () => {
ws.send(JSON.stringify({
type: 'auth',
token: livekit_token
}))
}
// Получение истории и новых сообщений
ws.onmessage = (event) => {
const msg = JSON.parse(event.data)
if (msg.type === 'history') {
// Отрисовать историю из msg.messages
} else if (msg.type === 'message') {
// Добавить новое сообщение msg.message
} else if (msg.type === 'error') {
console.error('Chat error:', msg.code)
}
}
// Отправка сообщения
function sendMessage(text: string) {
ws.send(JSON.stringify({
type: 'message',
text: text
}))
}
// Закрытие
ws.onclose = (event) => {
if (event.code === 4403) {
console.error('Wrong room')
} else if (event.code === 4404) {
console.error('Chat unavailable or conference ended')
}
}
curl (тестирование)
WebSocket из curl поддерживается ограниченно; рекомендуется использовать утилиту wscat:
npm install -g wscat
# Подключение и отправка auth
wscat -c ws://localhost:8000/api/v1/conferences/550e8400-e29b-41d4-a716-446655440000/chat
# Затем вручную:
{"type":"auth","token":"eyJ..."}
{"type":"message","text":"Hello"}
Интеграция с остальной системой
Redis pub/sub
Канал: chat:{conference_id} (UUID конференции).
Publisher: services/chat.py:ChatService.persist_and_publish() после INSERT в БД.
Subscribers: все подключённые WebSocket-клиенты конференции (несколько инстансов backend в балансировке).
Таблица chat_messages
См. docs/db/schema.md.
Строка: (id, session_id, user_id, guest_access_id, author_name, text, created_at).
Тоггл в JoinOut
При входе в конференцию (POST /api/v1/conferences/{id}/join или /guest-join) ответ включает:
{
"livekit_url": "...",
"token": "...",
"room_name": "...",
"conference_id": "550e8400-e29b-41d4-a716-446655440000",
"chat_enabled": true
}
Фронт проверяет chat_enabled перед рендерингом UI панели чата.