Files
vidconf/docs/api/conferences.md
Max Ronzhin 8757bec8ac
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
first commit
2026-07-23 02:38:05 +03:00

21 KiB
Raw Blame History

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:

{
  "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 в БД):

{
  "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):

{
  "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.

Коды ошибок:

  • 422closed_conference_requires_password (is_closed но нет password)
  • 422recurrence_requires_pinned (recurrence но is_pinned=false)
  • 422scheduled_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):

[
  {
    "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):

[
  {
    "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):

{
  "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:

{
  "password": "optional_password"
}

Response (200 OK):

{
  "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:

{
  "display_name": "Иван Иванов",
  "email": "ivan@example.com",
  "password": "optional_password"
}

Поля:

  • display_name (str, 1..255) — обязателен
  • email (EmailStr, опционально) — для рассылки саммари
  • password (str, опционально) — для закрытых конференций

Response (200 OK):

{
  "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)

{
  "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):

{
  "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_startedactive
  • Webhook room_finishedended (если не 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)

Примеры запросов

Создать мгновенную конференцию

curl -X POST http://localhost:8000/api/v1/conferences \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title":"Quick call"}'

Создать плановую с повторением (еженедельно по пн/ср/пт)

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)

curl http://localhost:8000/api/v1/conferences/resolve?q=123456789

Вход гостем

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 (можно запросить всё, развёртка только в памяти)

Ссылки