Первоначальная версия VidConf
This commit is contained in:
540
docs/api/conferences.md
Normal file
540
docs/api/conferences.md
Normal file
@@ -0,0 +1,540 @@
|
||||
# API конференций
|
||||
|
||||
Полная справка по эндпоинтам динамических конференций (см. ADR-001). Конференции создаются мгновенно или планируются; вместо предустановленных комнат и бронирований.
|
||||
|
||||
## Обзор
|
||||
|
||||
**Конференции** — это постоянные сущности (номер, ссылка, владелец). Каждый запуск создаёт сеанс (`ConferenceSession`) — единицу AI-пайплайна.
|
||||
|
||||
- **Номер:** 9 десятичных цифр, уникален, генерируется с retry
|
||||
- **Ссылка:** `slug = secrets.token_urlsafe(8)` (11 base64url-символов), URL: `/j/{slug}`
|
||||
- **Доступ:** По номеру или ссылке (публичные эндпоинты без auth, rate limit 10/мин)
|
||||
- **Гости:** Представляются при входе (имя обязательно, email факультативен для саммари)
|
||||
|
||||
## Эндпоинты
|
||||
|
||||
### POST /api/v1/conferences
|
||||
|
||||
**Создать конференцию.**
|
||||
|
||||
Без `scheduled_at` → мгновенная (создатель входит сразу); с `scheduled_at` → плановая.
|
||||
|
||||
**Требует auth:** JWT Bearer token
|
||||
|
||||
**Request body:**
|
||||
```json
|
||||
{
|
||||
"title": "Встреча Q3",
|
||||
"scheduled_at": "2026-07-20T14:00:00Z",
|
||||
"duration_minutes": 60,
|
||||
"is_pinned": true,
|
||||
"recurrence": {
|
||||
"type": "weekly",
|
||||
"weekdays": [0, 2, 4],
|
||||
"time_local": "14:00",
|
||||
"timezone": "Europe/Moscow",
|
||||
"anchor_date": "2026-07-20",
|
||||
"duration_minutes": 60
|
||||
},
|
||||
"is_closed": false,
|
||||
"password": null,
|
||||
"summary_recipients": null
|
||||
}
|
||||
```
|
||||
|
||||
**Поля:**
|
||||
- `title` (str, опционально, ≤255 символов) — название конференции
|
||||
- `scheduled_at` (ISO 8601 UTC, опционально) — время запуска; если пусто → мгновенная (status=active)
|
||||
- `duration_minutes` (int, опционально, >0) — ожидаемая длительность (не обязывает)
|
||||
- `is_pinned` (bool, опционально, default=false) — закреплённая ли (видна в "Моих конференциях")
|
||||
- `recurrence` (RecurrenceRule, опционально) — только если is_pinned=true; см. ниже
|
||||
- `is_closed` (bool, опционально, default=false) — требуется пароль для входа
|
||||
- `password` (str, опционально, ≥4 символа) — обязателен если is_closed=true
|
||||
- `summary_recipients` (str, опционально: `'all'`, `'owner'`, или null) — переопределение рассылки саммари конкретной конференции; null = использовать дефолт инстанса
|
||||
- `participants` (array, опционально) — список приглашённых; каждый элемент: `{"user_id": "uuid" или null, "email": "string" или null}`. Организатор добавляется автоматически (не требуется в массиве, но если передан — дедуплицируется). Внешние email'ы приглашаются отдельно
|
||||
|
||||
**RecurrenceRule (JSONB в БД):**
|
||||
```json
|
||||
{
|
||||
"type": "weekly|biweekly|monthly|every_n_days",
|
||||
"weekdays": [0, 1, 2, 3, 4, 5, 6],
|
||||
"day_of_month": null,
|
||||
"interval_days": null,
|
||||
"anchor_date": "2026-07-20",
|
||||
"time_local": "HH:MM",
|
||||
"timezone": "IANA (e.g. Europe/Moscow)",
|
||||
"duration_minutes": 60
|
||||
}
|
||||
```
|
||||
|
||||
- **weekly:** `weekdays` обязателен (0=пн, 6=вс), список непустой
|
||||
- **biweekly:** `weekdays` обязателен, повтор через 2 недели от anchor_date
|
||||
- **monthly:** `day_of_month` обязателен (1..31; 31 → последний день короткого месяца)
|
||||
- **every_n_days:** `interval_days` обязателен (≥1)
|
||||
|
||||
**Response (201 Created):**
|
||||
```json
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"number": "123456789",
|
||||
"slug": "abc123456",
|
||||
"title": "Встреча Q3",
|
||||
"status": "active|scheduled|ended",
|
||||
"is_pinned": true,
|
||||
"is_closed": false,
|
||||
"scheduled_at": "2026-07-20T14:00:00Z",
|
||||
"duration_minutes": 60,
|
||||
"recurrence": { /* RecurrenceRule */ },
|
||||
"summary_recipients": null,
|
||||
"next_occurrence": "2026-07-22T14:00:00Z",
|
||||
"created_at": "2026-07-16T12:00:00Z",
|
||||
"join": {
|
||||
"livekit_url": "wss://livekit.example.com",
|
||||
"token": "eyJh...",
|
||||
"room_name": "abc123456",
|
||||
"conference_id": "550e8400-e29b-41d4-a716-446655440000"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Мгновенная конференция: `status=active`, `join` заполнено. Плановая: `status=scheduled`, `join=null`.
|
||||
|
||||
**Коды ошибок:**
|
||||
- `422` — `closed_conference_requires_password` (is_closed но нет password)
|
||||
- `422` — `recurrence_requires_pinned` (recurrence но is_pinned=false)
|
||||
- `422` — `scheduled_at_in_the_past` (дата в прошлом)
|
||||
|
||||
---
|
||||
|
||||
### GET /api/v1/conferences/my
|
||||
|
||||
**Мои конференции: закреплённые + предстоящие разовые владельца ИЛИ приглашённого.**
|
||||
|
||||
С 2026-07-20 (решение поверх ADR-003) в выдачу попадает конференция, если
|
||||
текущий пользователь:
|
||||
- владелец (`owner_id == user.id`), **ИЛИ**
|
||||
- приглашён по `user_id` (строка `conference_invitees` с `user_id == user.id`), **ИЛИ**
|
||||
- приглашён по email (строка `conference_invitees` с `lower(email) == lower(email пользователя)`
|
||||
— внешнее приглашение на адрес, под которым человек впоследствии зарегистрировался).
|
||||
|
||||
Критерии показа не меняются (закреплённая — безусловно; разовая — `status=scheduled`
|
||||
и `scheduled_at` в будущем), меняется только круг «чья» конференция. Для
|
||||
приглашённого `is_owner=false`, `organizer_name` — имя фактического владельца;
|
||||
`participants` не заполняется (как и для владельца — список не раздувает состав).
|
||||
|
||||
**Требует auth:** JWT Bearer token
|
||||
|
||||
**Query parameters:** нет
|
||||
|
||||
**Response (200 OK):**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"number": "123456789",
|
||||
"slug": "abc123456",
|
||||
"title": "Встреча Q3",
|
||||
"status": "scheduled",
|
||||
"is_pinned": true,
|
||||
"is_closed": false,
|
||||
"scheduled_at": "2026-07-20T14:00:00Z",
|
||||
"duration_minutes": 60,
|
||||
"recurrence": { /* RecurrenceRule */ },
|
||||
"summary_recipients": null,
|
||||
"next_occurrence": "2026-07-22T14:00:00Z",
|
||||
"created_at": "2026-07-16T12:00:00Z",
|
||||
"join": null
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### GET /api/v1/conferences/calendar
|
||||
|
||||
**Развёртка вхождений (occurrences) конференций в диапазоне дат — владельца ИЛИ приглашённого.**
|
||||
|
||||
Тот же принцип видимости, что у `GET /conferences/my` (см. выше, решение от
|
||||
2026-07-20): вхождения строятся по конференциям, где пользователь владелец
|
||||
или приглашён (по `user_id` или по `lower(email)`).
|
||||
|
||||
**Требует auth:** JWT Bearer token
|
||||
|
||||
**Query parameters:**
|
||||
- `from` (ISO 8601 UTC) — начало диапазона
|
||||
- `to` (ISO 8601 UTC) — конец диапазона
|
||||
- Максимальная ширина: 62 дня; иначе 422
|
||||
|
||||
**Response (200 OK):**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"conference_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"title": "Встреча Q3",
|
||||
"starts_at": "2026-07-20T14:00:00Z",
|
||||
"ends_at": "2026-07-20T15:00:00Z",
|
||||
"number": "123456789",
|
||||
"slug": "abc123456",
|
||||
"is_pinned": true,
|
||||
"is_closed": false
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### GET /api/v1/conferences/resolve
|
||||
|
||||
**Найти конференцию по номеру или ссылке — публичный эндпоинт без auth.**
|
||||
|
||||
**Rate limit:** 10 запросов в минуту на IP
|
||||
|
||||
**Query parameters:**
|
||||
- `q` (str, ≥1 символ) — номер (9 цифр) или slug
|
||||
|
||||
**Response (200 OK):**
|
||||
```json
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"title": "Встреча Q3",
|
||||
"status": "active|scheduled|ended",
|
||||
"is_closed": false,
|
||||
"requires_password": false
|
||||
}
|
||||
```
|
||||
|
||||
**Коды ошибок:**
|
||||
- `404` — не найдена (живая, мёртвая или несуществующая — единообразный ответ для безопасности)
|
||||
- `429` — exceeded rate limit
|
||||
|
||||
---
|
||||
|
||||
### POST /api/v1/conferences/{conference_id}/join
|
||||
|
||||
**Вход зарегистрированного пользователя в конференцию.**
|
||||
|
||||
**Требует auth:** JWT Bearer token
|
||||
|
||||
**Path parameters:**
|
||||
- `conference_id` (UUID)
|
||||
|
||||
**Request body:**
|
||||
```json
|
||||
{
|
||||
"password": "optional_password"
|
||||
}
|
||||
```
|
||||
|
||||
**Response (200 OK):**
|
||||
```json
|
||||
{
|
||||
"livekit_url": "wss://livekit.example.com",
|
||||
"token": "eyJh...",
|
||||
"room_name": "abc123456",
|
||||
"conference_id": "550e8400-e29b-41d4-a716-446655440000"
|
||||
}
|
||||
```
|
||||
|
||||
**Коды ошибок:**
|
||||
- `404` — conference_not_found
|
||||
- `403` — password_required (закрытая конференция без пароля в теле запроса)
|
||||
- `403` — invalid_password
|
||||
- `410` — conference_ended (статус=ended)
|
||||
|
||||
---
|
||||
|
||||
### POST /api/v1/conferences/{conference_id}/guest-join
|
||||
|
||||
**Вход гостем: представиться (имя обязательно) — публичный, без auth.**
|
||||
|
||||
**Rate limit:** 10 запросов в минуту на IP
|
||||
|
||||
**Path parameters:**
|
||||
- `conference_id` (UUID)
|
||||
|
||||
**Request body:**
|
||||
```json
|
||||
{
|
||||
"display_name": "Иван Иванов",
|
||||
"email": "ivan@example.com",
|
||||
"password": "optional_password"
|
||||
}
|
||||
```
|
||||
|
||||
**Поля:**
|
||||
- `display_name` (str, 1..255) — обязателен
|
||||
- `email` (EmailStr, опционально) — для рассылки саммари
|
||||
- `password` (str, опционально) — для закрытых конференций
|
||||
|
||||
**Response (200 OK):**
|
||||
```json
|
||||
{
|
||||
"livekit_url": "wss://livekit.example.com",
|
||||
"token": "eyJh...",
|
||||
"room_name": "abc123456",
|
||||
"conference_id": "550e8400-e29b-41d4-a716-446655440000"
|
||||
}
|
||||
```
|
||||
|
||||
**Коды ошибок:**
|
||||
- `404` — conference_not_found
|
||||
- `403` — password_required
|
||||
- `403` — invalid_password
|
||||
- `410` — conference_ended
|
||||
- `429` — exceeded rate limit
|
||||
- `422` — validation_error (display_name пуст, email некорректен)
|
||||
|
||||
---
|
||||
|
||||
### PATCH /api/v1/conferences/{conference_id}
|
||||
|
||||
**Изменить конференцию — только владелец или администратор.**
|
||||
|
||||
**Требует auth:** JWT Bearer token
|
||||
|
||||
**Path parameters:**
|
||||
- `conference_id` (UUID)
|
||||
|
||||
**Request body:** все поля опциональны (как ConferenceCreateIn, но PATCH)
|
||||
```json
|
||||
{
|
||||
"title": "Новое название",
|
||||
"scheduled_at": "2026-07-25T15:00:00Z",
|
||||
"duration_minutes": 90,
|
||||
"is_pinned": true,
|
||||
"recurrence": null,
|
||||
"is_closed": false,
|
||||
"password": null
|
||||
}
|
||||
```
|
||||
|
||||
**Response (200 OK):** обновленная ConferenceOut
|
||||
|
||||
**Коды ошибок:**
|
||||
- `404` — conference_not_found
|
||||
- `403` — not_owner
|
||||
- `422` — invalid_conference_state (например, попытка добавить recurrence к неживой конференции)
|
||||
|
||||
---
|
||||
|
||||
### DELETE /api/v1/conferences/{conference_id}
|
||||
|
||||
**Удалить конференцию — только владелец или администратор.**
|
||||
|
||||
**Требует auth:** JWT Bearer token
|
||||
|
||||
**Path parameters:**
|
||||
- `conference_id` (UUID)
|
||||
|
||||
**Response (204 No Content)** — без тела
|
||||
|
||||
**Коды ошибок:**
|
||||
- `404` — conference_not_found
|
||||
- `403` — not_owner
|
||||
- `409` — conference_active (активную конференцию нельзя удалить, дождитесь завершения)
|
||||
|
||||
---
|
||||
|
||||
### GET /api/v1/conferences/{conference_id}
|
||||
|
||||
**Получить детальную информацию о конференции (с полным составом участников) — владелец, администратор ИЛИ приглашённый.**
|
||||
|
||||
Приглашённый определяется так же, как в `GET /my`/`GET /calendar`: строка
|
||||
`conference_invitees` с `user_id == user.id` либо с `lower(email) == lower(email
|
||||
пользователя)`. Это расширение касается ТОЛЬКО чтения — `PATCH`/`DELETE`
|
||||
по-прежнему разрешены только владельцу или администратору (403 приглашённому).
|
||||
|
||||
**Требует auth:** JWT Bearer token
|
||||
|
||||
**Path parameters:**
|
||||
- `conference_id` (UUID)
|
||||
|
||||
**Response (200 OK):**
|
||||
```json
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"number": "123456789",
|
||||
"slug": "abc123456",
|
||||
"title": "Встреча Q3",
|
||||
"status": "scheduled",
|
||||
"is_pinned": true,
|
||||
"is_closed": false,
|
||||
"scheduled_at": "2026-07-20T14:00:00Z",
|
||||
"duration_minutes": 60,
|
||||
"recurrence": { "type": "weekly", "weekdays": [0, 2, 4] },
|
||||
"summary_recipients": "all",
|
||||
"owner_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"is_owner": true,
|
||||
"organizer_name": "Иван Иванов",
|
||||
"participants": [
|
||||
{
|
||||
"user_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"email": "ivan@example.com",
|
||||
"name_user": "Иван Иванов",
|
||||
"avatar_url": "/media/avatars/550e8400-e29b-41d4-a716-446655440000.jpg"
|
||||
},
|
||||
{
|
||||
"user_id": null,
|
||||
"email": "external@example.com",
|
||||
"name_user": null,
|
||||
"avatar_url": null
|
||||
}
|
||||
],
|
||||
"created_at": "2026-07-16T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Поля:**
|
||||
- `owner_id` — UUID владельца конференции
|
||||
- `is_owner` — true если текущий пользователь владелец
|
||||
- `organizer_name` — имя организатора (берётся из `users.name_user`)
|
||||
- `participants` — массив приглашённых (зарегистрированные и внешние). **Заполняется только в этом эндпоинте**; в GET /my и /calendar пустой массив.
|
||||
|
||||
**Коды ошибок:**
|
||||
- `404` — conference_not_found
|
||||
- `403` — not_owner (пользователь не владелец, не администратор и не приглашён)
|
||||
|
||||
---
|
||||
|
||||
## Статусы конференции
|
||||
|
||||
| Статус | Описание |
|
||||
|--------|---------|
|
||||
| `scheduled` | Плановая, время ещё не наступило или никто не вошёл |
|
||||
| `active` | В настоящий момент идёт (кто-то вошёл или прошло scheduled_at) |
|
||||
| `ended` | Завершена; истории, фразы, саммари остаются; вход возвращает 410 |
|
||||
|
||||
Переходы:
|
||||
- Мгновенная создание → `active` сразу
|
||||
- Плановая создание → `scheduled`
|
||||
- Webhook `room_started` → `active`
|
||||
- Webhook `room_finished` → `ended` (если не pinned) или `scheduled` (если pinned)
|
||||
- Beat-задача → `ended` если истекло `scheduled_at + duration_minutes` и никто не входил
|
||||
|
||||
---
|
||||
|
||||
## LiveKit identity
|
||||
|
||||
Интеграция с LiveKit на уровне webhook'ов и токенов:
|
||||
|
||||
- **Зарегистрированный:** `identity = str(user_id)`, `name = user.name_user`
|
||||
- **Гость:** `identity = "guest:{guest_access.id}"`, `name = guest_access.display_name`
|
||||
|
||||
Email гостя в LiveKit не передаётся (PII не утекает участникам).
|
||||
|
||||
---
|
||||
|
||||
## Ошибки и коды HTTP
|
||||
|
||||
| Код | Описание |
|
||||
|-----|---------|
|
||||
| `200` | OK |
|
||||
| `201` | Created (POST конференции) |
|
||||
| `204` | No Content (DELETE) |
|
||||
| `400` | Bad Request (некорректный JSON) |
|
||||
| `401` | Unauthorized (JWT missing или invalid) |
|
||||
| `403` | Forbidden (password_required, invalid_password, not_owner) |
|
||||
| `404` | Not Found (конференция не существует или не найдена) |
|
||||
| `409` | Conflict (conference_active — нельзя удалить активную) |
|
||||
| `410` | Gone (conference_ended — вход в завершённую) |
|
||||
| `422` | Unprocessable Entity (валидация, invalid range на /calendar) |
|
||||
| `429` | Too Many Requests (rate limit на /resolve, /guest-join) |
|
||||
|
||||
---
|
||||
|
||||
## Примеры запросов
|
||||
|
||||
### Создать мгновенную конференцию
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/api/v1/conferences \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"title":"Quick call"}'
|
||||
```
|
||||
|
||||
### Создать плановую с повторением (еженедельно по пн/ср/пт)
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/api/v1/conferences \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"title": "Q3 standup",
|
||||
"scheduled_at": "2026-07-21T09:00:00Z",
|
||||
"duration_minutes": 30,
|
||||
"is_pinned": true,
|
||||
"is_closed": true,
|
||||
"password": "secret123",
|
||||
"recurrence": {
|
||||
"type": "weekly",
|
||||
"weekdays": [0, 2, 4],
|
||||
"time_local": "09:00",
|
||||
"timezone": "Europe/Moscow",
|
||||
"anchor_date": "2026-07-21",
|
||||
"duration_minutes": 30
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### Вход по номеру (публичный, без auth)
|
||||
```bash
|
||||
curl http://localhost:8000/api/v1/conferences/resolve?q=123456789
|
||||
```
|
||||
|
||||
### Вход гостем
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/api/v1/conferences/550e8400-e29b-41d4-a716-446655440000/guest-join \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"display_name": "Иван",
|
||||
"email": "ivan@example.com",
|
||||
"password": "secret123"
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Рассылка приглашений
|
||||
|
||||
При создании (POST) или изменении (PATCH) конференции отправляются .ics-приглашения:
|
||||
|
||||
**Получатели:**
|
||||
- Организатор (владелец конференции, автоматически)
|
||||
- Все приглашённые в `participants` (зарегистрированные пользователи и внешние email'ы)
|
||||
|
||||
**Содержимое письма:**
|
||||
- Тема: `"Приглашение: <название конференции>"`
|
||||
- Вложение `.ics` (iCalendar формат)
|
||||
- `METHOD:REQUEST` — приглашение
|
||||
- `VTIMEZONE` — часовой пояс инстанса (для корректного отображения)
|
||||
- `RRULE` — правило повтора (для закреплённых конференций с `recurrence`)
|
||||
- `SEQUENCE` — номер версии события
|
||||
|
||||
**Логика рассылки:**
|
||||
- **Создание (POST):** Рассылка всем в `participants` и организатору. `SEQUENCE=0`.
|
||||
- **Изменение расписания (PATCH `scheduled_at`, `duration_minutes`, `recurrence`, `title`):** Инкрементируется `SEQUENCE`, рассылка повторяется.
|
||||
- **Изменение только состава участников (PATCH `participants`):** Рассылка отправляется, но `SEQUENCE` **не меняется** (календарные клиенты игнорируют обновления с неизменённым SEQUENCE).
|
||||
- **Прочие изменения (пароль, `is_closed`, `summary_recipients`):** Рассылка **не отправляется**.
|
||||
|
||||
**Внешние приглашённые:**
|
||||
- Письма отправляются на email'ы без требования аутентификации.
|
||||
- При входе по ссылке гост вводит имя и опционально email.
|
||||
- Если email гостя совпадает с приглашённым — он может получить саммари (зависит от `summary_recipients`).
|
||||
|
||||
---
|
||||
|
||||
## Примечания
|
||||
|
||||
- Все `datetime` сохраняются и возвращаются в UTC (ISO 8601, суффикс `Z`)
|
||||
- Преобразование в локальное время — на клиенте по таймзоне браузера
|
||||
- `.ics` экспорт включает `VTIMEZONE` для правильной конвертации (`services/ics.py`)
|
||||
- Номер и slug неизменны всю жизнь конференции и не переиспользуются
|
||||
- `recurrence` хранится как JSONB (можно запросить всё, развёртка только в памяти)
|
||||
|
||||
---
|
||||
|
||||
## Ссылки
|
||||
|
||||
- [Architecture Overview](../architecture/README.md) — система компонентов
|
||||
- [Database Schema](../db/schema.md) — таблицы `conferences`, `conference_sessions`, `guest_access`
|
||||
- [ADR-001: Динамические конференции вместо бронирований](../architecture/adr/001-dynamic-conferences-pivot.md) — обоснование решения
|
||||
- [Recurrence Service](../../backend/services/recurrence.py) — реализация развёртки
|
||||
Reference in New Issue
Block a user