POST /api/messenger/chats/{chatId}/conferences — Начать конференцию в чате сделки
Участник Deal-чата поднимает групповой звонок. Создаётся ConferenceSessionEntity
(Provisioning → Active), инициатор становится модератором в ростере
(call-participant.md), остальным участникам чата уходит приглашение
ConferenceInvite с дескриптором входа в медиа-комнату. Дом полей, статусы и инварианты (INV-CONF-*) —
conference-session.md.
Назначение
Дать участнику чата сделки (инвестор / агент / менеджер застройщика) собрать видеовстречу по сделке: завести
сессию конференции, зарезервировать медиа-комнату, при необходимости включить запись по политике и позвать в
неё активных участников Deal-чата. На выходе — карточка конференции с дескриптором входа Room. Owning-сервис —
messenger, feature group conferences, сценарий «групповой звонок по сделке».
| Поле | Значение |
|---|
| Сервис/контекст | messenger |
| API area | conferences |
| Feature group | conferences |
| Статус | draft |
| Документ | docs/06-services/messenger/api/conferences/start-conference.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User — активный участник Deal-чата (стартовый модератор) |
| Auth policy / Permissions | permission messenger.conferences.use |
| Scope (RBAC) | не применяется — доступ по членству в чате, не по scope |
| Record-level | активный ParticipantEntity чата (RemovedAt is null, INV-CHAT-4); гейт группы chat:{ChatId} |
| Tenant isolation | не применяется (кросс-tenant, conference-session.md); срез — по членству в Deal-чате |
| Audit | create: ConferenceSessionEntity (Provisioning/Active), метка StartedAt; строка ростера инициатора (Moderator, Joined) |
HTTP-контракт
| Поле | Значение |
|---|
| Method | POST |
| Route | /api/messenger/chats/{chatId}/conferences |
| Success status | 201 (Created) |
| Idempotency header | не применяется — повтор ловит guard одной активной конференции на чат (INV-CONF-2): второй Provisioning/Active → MSG_CONFERENCE_ALREADY_ACTIVE |
| Correlation | сквозной trace через traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
ChatId | route | Guid | Да | != Empty; существующий Deal-чат, актор — активный участник (шаги 1, 3) | — |
ActorUserId | identity claims | Guid | Да | из JWT; активный Participant чата | — |
MediaType | body | CallMediaType | Да | enum строкой Audio/Video (call-session.md); unknown → ошибка валидации | — |
Title | body | string? | Нет | ≤ 120; по умолчанию — из контекста сделки | — |
Модель ответа ConferenceModel
Проекция ConferenceSessionEntity с ростером и дескриптором входа. Поле
Room (тип MediaJoinModel, дом — messenger-hub.md) присутствует только в
ответах start / join / media-token и никогда — в get / list.
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
ConferenceId | Guid | Да | ConferenceSessionEntity.Id | — | Идентификатор конференции (адресация join/leave/end). |
ChatId | Guid | Да | .ChatId | — | Deal-чат конференции. |
DealId | Guid? | Нет | .DealId | — | Сделка (денорм из чата). |
MediaType | CallMediaType | Да | .MediaType | — | Audio/Video. |
Status | ConferenceStatus | Да | .Status | — | Provisioning/Active на успехе старта. |
Title | string? | Нет | .Title | — | Название встречи. |
StartedByUserId | Guid | Да | .StartedByUserId | — | Инициатор (стартовый модератор). |
StartedAt | DateTimeOffset | Да | .StartedAt | — | Момент старта. |
EndedAt | DateTimeOffset? | Нет | .EndedAt | — | null для активной. |
DurationSeconds | int? | Нет | .DurationSeconds | — | null до завершения. |
RecordingStatus | RecordingStatus? | Нет | CallRecordingEntity.Status | — | Статус записи, если ведётся (call-recording.md); null — запись не заведена. |
RecordingId | Guid? | Нет | CallRecordingEntity.Id | — | Идентификатор записи (если заведена). |
ActiveParticipantCount | int | Да | .ActiveParticipantCount | — | Активных участников на момент ответа. |
Participants | ConferenceParticipantEntry[] | Да | ростер (call-participant.md) | — | На старте — только инициатор (Moderator, Joined). |
Room | MediaJoinModel? | Нет | вычисляется | Sensitive: AccessToken/TURN-креды не логируются | Дескриптор входа в комнату; здесь — присутствует. |
Вложенная модель ConferenceParticipantEntry
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
UserId | Guid | Да | CallParticipantEntity.UserId | — | Участник. |
Role | ParticipantRole? | Нет | .Role | — | Орг-роль на момент входа (снимок). |
ConferenceRole | ConferenceRole | Да | .ConferenceRole | — | Moderator/Speaker. |
JoinedAt | DateTimeOffset? | Нет | .JoinedAt | — | Момент входа. |
LeftAt | DateTimeOffset? | Нет | .LeftAt | — | Момент выхода; null для активного. |
DurationSeconds | int? | Нет | .DurationSeconds | — | Время в конференции; null до выхода. |
ScreenShareState | ScreenShareState | Да | .ScreenShareState | — | None/Sharing. |
IsActive | bool | Да | computed (State == Joined) | — | true — участник сейчас в комнате. |
Предусловия и постусловия
Предусловия. Актор авторизован и имеет messenger.conferences.use; флаг CONFERENCE_CALLS_ENABLED включён.
Чат существует, типа Deal и не Closed (INV-CONF-1); актор — его активный участник (INV-CHAT-4). На чат нет
другой конференции в статусе Provisioning/Active (INV-CONF-2). Media-стек доступен для резервирования комнаты.
Постусловия. Создана ConferenceSessionEntity (Provisioning → Active, StartedByUserId = actor,
StartedAt, MediaType, Room), инициатор — единственная строка ростера (ConferenceRole = Moderator,
State = Joined, INV-CONF-6); ActiveParticipantCount = 1. При включённой политике и RECORDINGS_ENABLED
заведена CallRecordingEntity (Status = Pending, Trigger = AutoPolicy, SessionKind = Conference). В группу
chat:{ChatId} доставлен ConferenceInvite (комната + токен минтится при доставке). Kafka-события старт не
публикует. При отказе резервирования комнаты сессия не создаётся (MSG_RECORDING_PROVISION_FAILED), состояние
чата не меняется.
Алгоритм
- Контекст и доступ. Операцию выполняет активный участник Deal-чата (
User); ActorUserId — из JWT;
требуется право messenger.conferences.use. Конференции включены флагом CONFERENCE_CALLS_ENABLED, иначе —
MSG_CONFERENCES_DISABLED.
- Проверка входа.
ChatId непустой; MediaType — известное значение enum (Audio/Video), иначе ошибка
валидации; Title (если задан) — ≤ 120.
- Загрузка чата и доступ. Чат по
ChatId; нет → MSG_CHAT_NOT_FOUND. Актор — активный участник чата
(INV-CHAT-4), иначе MSG_CHAT_ACCESS_DENIED. Чат должен быть типа Deal и не Closed (INV-CONF-1), иначе
MSG_CONFERENCE_CHAT_INVALID (в Direct-переписке используется 1:1-звонок).
- Guard одной активной. На чат допустима максимум одна конференция в статусе
Provisioning/Active
(INV-CONF-2, partial unique ix_conf_active); иначе MSG_CONFERENCE_ALREADY_ACTIVE.
- Резервирование комнаты. Формируется
MediaRoom (детерминированное RoomName из Id + nonce, уникальное
среди активных, INV-CONF-5); токены доступа/ICE/SDP не персистятся (INV-CONF-7). Если медиа-стек не
поднял комнату — MSG_RECORDING_PROVISION_FAILED (сессия не создаётся).
- Решение о записи. Политика записи для конференций сделки (как правило — с записью) определяет
авто-запись; при включении заводится
CallRecordingEntity (Status = Pending, Trigger = AutoPolicy,
SessionKind = Conference, call-recording.md). Запись гейтится флагом
RECORDINGS_ENABLED; при выключенном флаге конференция стартует без записи.
- Создание сессии. Заводится
ConferenceSessionEntity (Provisioning → Active, StartedByUserId =
актор, StartedAt, MediaType, Room, Title) и строка ростера инициатора
(call-participant.md): State = Joined, ConferenceRole = Moderator
(INV-CONF-6); пересчитывается ActiveParticipantCount = 1.
- Приглашение. В том же согласованном изменении фиксируется намерение доставки
ConferenceInvite
(persist-then-push): после коммита realtime-dispatch пушит приглашение в группу chat:{ChatId} и минтит
Room.AccessToken в момент доставки. Интеграционных Kafka-событий старт не публикует.
- Результат. Карточка
ConferenceModel (Status = Active, Room присутствует) → HTTP 201 Created,
Location: /api/messenger/conferences/{ConferenceId}.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|
| Integration events | none — старт конференции Kafka не публикует (conference-session.md) |
| Realtime push (hub) | ConferenceInvite → chat:{ChatId} (комната + токен, messenger-hub.md) |
| Токены доступа | Room.AccessToken/TURN-креды минтятся при доставке приглашения, не хранятся в realtime_outbox/БД, не логируются |
| External calls | резервирование медиа-комнаты в media-стеке (SFU); при отказе — MSG_RECORDING_PROVISION_FAILED |
| Cache / projections / search | сессия попадает в partial ix_conf_active (guard одной активной) |
| Notifications | offline push «вас зовут на встречу» — вне конференс-операции (потребитель hub/notifications) |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
VALIDATION_FAILED | 400 | ChatId = Empty, неизвестный MediaType, Title > 120 | «Проверьте параметры конференции» | no retry |
MSG_CHAT_NOT_FOUND | 404 | чата нет | «Чат не найден» | no retry |
MSG_CHAT_ACCESS_DENIED | 403 | актор не активный участник чата | «Нет доступа к чату» | no retry |
MSG_CONFERENCE_CHAT_INVALID | 409 | чат не Deal либо Closed (INV-CONF-1) | «Конференция доступна только в чате сделки» | no retry |
MSG_CONFERENCE_ALREADY_ACTIVE | 409 | уже есть активная/Provisioning конференция на чат (INV-CONF-2) | «Конференция в этом чате уже идёт» | retry после завершения |
MSG_CONFERENCES_DISABLED | 503 | CONFERENCE_CALLS_ENABLED = false | «Конференции временно недоступны» | retry позже |
MSG_RECORDING_PROVISION_FAILED | 503 | media-стек не поднял комнату/рекордер | «Не удалось запустить конференцию» | retry позже |
Acceptance Criteria
- Старт с записью (happy). Дано: актор — активный участник Deal-чата, активной конференции на чат нет, флаги
включены. Когда стартует видео-конференцию. Тогда
201 с ConferenceModel (Status = Active, Room
присутствует, инициатор — модератор в ростере); участники чата получают ConferenceInvite; заведена запись
Pending.
- Guard одной активной. Дано: на чат уже есть конференция
Provisioning/Active. Когда актор стартует
вторую. Тогда 409 MSG_CONFERENCE_ALREADY_ACTIVE (INV-CONF-2); дубля не создаётся.
- Не Deal-чат / закрыт. Дано: чат
Direct либо Closed. Когда актор стартует конференцию. Тогда
409 MSG_CONFERENCE_CHAT_INVALID (в Direct используется 1:1-звонок).
- Не участник чата. Дано: актор не активный участник чата. Когда стартует. Тогда
403 MSG_CHAT_ACCESS_DENIED.
- Media-стек не поднял комнату. Дано: SFU недоступен/не подтвердил комнату. Когда актор стартует. Тогда
503 MSG_RECORDING_PROVISION_FAILED, сессия не создаётся, повтор допустим позже.
- Запись выключена флагом. Дано:
RECORDINGS_ENABLED = false, политика требует запись. Когда актор стартует.
Тогда конференция стартует без записи (RecordingStatus = null), а не отказывает.
- Невалидный вход. Дано: неизвестный
MediaType или Title > 120. Когда актор стартует. Тогда
400 VALIDATION_FAILED.
- Гонка двух стартов. Дано: два параллельных старта на одном чате. Тогда партиал-уникальность
ix_conf_active
пропускает ровно один; второй — 409 MSG_CONFERENCE_ALREADY_ACTIVE.
Совместимость и наблюдаемость
- Новые optional поля ответа — additive; смена маршрута/семантики статуса — breaking. Kafka-контракта у старта нет.
- Logs:
conference start requested/created/room-provision-failed (ConferenceId, ChatId, MediaType,
WasRecorded; токены — никогда в логи); metrics: messenger_conference_start_total{result}, latency p95;
traces — сквозной trace (traceparent); dashboard pin-messenger.
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы