# 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) — реализация развёртки