Files
vidconf/docs/api/chat.md

12 KiB
Raw Blame History

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. Проверка допуска

Сервер выполняет последовательность проверок (порядок важен для единообразной обработки ошибок):

  1. Тоггл чата: флаг chat.enabled из настроек инстанса в БД (instance_settings). Проверяется на каждом подключении, без рестарта backend.
  2. LiveKit-токен: верификация подписи и целостности (livekit.api.TokenVerifier)
  3. Идентичность:
    • Зарегистрированный пользователь: identity = строка UUID пользователя; grant video.room должен совпадать с conference.slug
    • Гость: identity = guest:{guest_access_id} (UUID гостевой записи); grant video.room должен совпадать с conference.slug
  4. Конференция: существует и статус ≠ 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.roomconference.slug).
4404 Чат недоступен Чат выключен в настройках инстанса (chat.enabled = false), или конференция не найдена, или конференция завершена (status = ended). Единый код для всех случаев — чтобы по коду закрытия нельзя было перебором отличить существующую конференцию от несуществующей.

Штатное закрытие: когда клиент сам закрывает соединение — это не ошибка, обработчик корректно завершается.

Защита от фантомных сессий

Если клиент пытается отправить сообщение уже после завершения конференции (room_finished от LiveKit):

  1. Сервер перечитывает статус конференции свежим SELECT (не полагаясь на закэшированный объект)
  2. Если статус = ended, отправляет close 4404; сообщение не записывается в БД
  3. Если статус ≠ 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 (проверка на уровне БД: CHECK user_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 панели чата.

Ссылки