Files
vidconf/docs/api/chat.md

273 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# API Чата
Текстовый чат в реальном времени для участников конференции. Реализован через WebSocket с аутентификацией по LiveKit-токену и broadcast через Redis pub/sub.
## WS-эндпоинт
```
WS /api/v1/conferences/{conference_id}/chat
```
Подключение требует явной аутентификации на уровне протокола (первое сообщение).
## Жизненный цикл соединения
### 1. Accept и ожидание auth-сообщения (таймаут 10 с)
Сервер принимает WebSocket-соединение и ждёт первого (и только первого) сообщения типа `auth`:
```json
{
"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 сообщений** открытой сессии конференции:
```json
{
"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. Двусторонний обмен
Клиент отправляет сообщения:
```json
{
"type": "message",
"text": "Мое сообщение"
}
```
Сервер пишет сообщение в БД, затем публикует его в Redis pub/sub. **Отправитель получает своё сообщение обратно через pub/sub** (echo). Порядок доставки единый для всех подписчиков канала.
Полученное сообщение (для всех участников, в т.ч. отправителя):
```json
{
"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`, отсутствующие поля), сервер отправляет:
```json
{
"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):
1. Сервер перечитывает статус конференции свежим SELECT (не полагаясь на закэшированный объект)
2. Если статус = `ended`, отправляет close 4404; сообщение **не записывается** в БД
3. Если статус ≠ `ended` и открытой сессии нет, автоматически создаёт её
Это гарантирует, что "фантомные" сообщения (отправленные после `room_finished`, когда сеанс уже завершается) не создаются.
## Схема сообщения чата
### ChatMessageOut (выход)
```typescript
{
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):
```json
{
"enabled": true
}
```
**Доступ клиенту:** флаг приходит в ответе `join` / `guest-join` (`JoinOut.chat_enabled`), по нему фронт показывает/скрывает UI панели чата.
**Изменение:** через админ-панель (`PATCH /api/v1/admin/settings`); без рестарта backend.
### Бутстрап из config/plugins.yaml
При первом запуске backend загружает `config/plugins.yaml` и создаёт запись в `instance_settings` (однократный идемпотентный импорт):
```yaml
chat:
enabled: true
```
Если `instance_settings['chat']` уже существует, бутстрап не перезаписывает.
## Примеры использования
### JavaScript/TypeScript (frontend)
```typescript
// Подключение
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`:
```bash
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](../db/schema.md#chat_messages).
Строка: `(id, session_id, user_id, guest_access_id, author_name, text, created_at)`.
### Тоггл в JoinOut
При входе в конференцию (`POST /api/v1/conferences/{id}/join` или `/guest-join`) ответ включает:
```json
{
"livekit_url": "...",
"token": "...",
"room_name": "...",
"conference_id": "550e8400-e29b-41d4-a716-446655440000",
"chat_enabled": true
}
```
Фронт проверяет `chat_enabled` перед рендерингом UI панели чата.
## Ссылки
- [Database Schema — chat_messages](../db/schema.md#chat_messages)
- [Instance Settings](../db/schema.md#instance_settings)
- [Backend Code — api/chat.py](../../backend/api/chat.py)
- [Backend Code — services/chat.py](../../backend/services/chat.py)
- [Frontend Component — useChat.ts](../../frontend/src/hooks/useChat.ts)
- [Frontend Component — ChatPanel.tsx](../../frontend/src/components/room/ChatPanel.tsx)