Перейти к основному содержимому

POST /api/messenger/calls — Начать звонок

Инициатор создаёт сессию 1:1-звонка контакту, получает дескриптор входа в медиа-комнату и в реальном времени доставляет callee приглашение. Создаётся CallSessionEntity в статусе Ringing (SessionKind = Direct) с медиа-комнатой (Room, owned VO MediaRoom); звонок разрешён только между Accepted-контактами вне чёрного списка (гейт CommunicationPolicy.CanCall, INV-CALL-1). Дом полей, статусы и инварианты — call-session.md.

Media-стек — внешний SFU (ADR-0062). Медиа (звук/видео) идёт клиент ↔ SFU; бэкенд не в медиа-плоскости и не релеит SDP/ICE. Вход в комнату раздаётся дескриптором MediaJoinModel (комната + короткоживущий токен доступа); токены/SDP/ICE не персистятся (INV-CALL-4/6). Медиа-звонки за флагом CALLS_MEDIA_ENABLED; иначе — MSG_CALLS_DISABLED.

Назначение

Дать пользователю (инвестор / агент / менеджер) позвонить подтверждённому контакту: завести журнал-запись звонка, зарезервировать медиа-комнату и вернуть инициатору токен входа, а callee — доставить приглашение в реальном времени. При звонке в контексте сделки запись стартует автоматически по политике. Owning-сервис — messenger, feature group calls, сценарий «звонок поверх контактов».

Metadata

ПолеЗначение
Сервис/контекстmessenger
API areacalls
Feature groupcalls
Статусdraft
Документdocs/06-services/messenger/api/calls/start-call.md

Актор и доступ

ПроверкаЗначение
Actor typeUser — инициатор звонка (CallerUserId = actor.UserId)
Auth policy / Permissionspermission messenger.calls.use
Scope (RBAC)платформа (звонок person-level, вне организаций)
Record-levelдоступ по членству пары: CallerUserId = actor; CalleeUserId — существующий Accepted-контакт
Tenant isolationне применяется — мессенджер платформенный (ADR-0052); изоляция — по Caller/Callee + гейт контакта
Auditcreate: CallSessionEntity (Ringing), метка StartedAt; иммутабельный факт лога

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/messenger/calls
Success status201 (Created)
Idempotency headerне применяется — повтор ловит guard активного звонка (INV-CALL-5): второй Ringing/Answered на пару → MSG_CALL_ALREADY_ACTIVE
Correlationсквозной trace через traceparent

Входные данные / параметры запроса

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
CallerUserIdidentity claimsGuidДаиз JWT; = CallSession.CallerUserId
CalleeUserIdbodyGuidДа!= Empty, != CallerUserId; существующий Accepted-контакт (шаги 3–4)
MediaTypebodyCallMediaTypeДаenum строкой Audio/Video (call-session.md); unknown → ошибка валидации

Тело запроса — только { CalleeUserId, MediaType }. SDP/ICE в контракте нет: медиа-негоциация идёт клиент ↔ SFU вне REST и вне БД (INV-CALL-4); вход в комнату инициатор получает дескриптором MediaJoinModel в ответе.

Модель ответа CallSessionModel

Проекция CallSessionEntity (дом полей — сущность; здесь состав строки). Дельта к журналу: несёт Room — дескриптор входа инициатора в медиа-комнату (только в start/answer/media-token, никогда в списке/логе).

ПолеТипОбязательностьИсточникMaskingОписание
CallIdGuidДаCallSessionEntity.IdИдентификатор сессии (адресация answer/decline/end/media-token).
SessionKindSessionKindДаcomputedВсегда Direct для 1:1-звонка.
CallerUserIdGuidДаCallSessionEntity.CallerUserIdИнициатор.
CalleeUserIdGuidДаCallSessionEntity.CalleeUserIdВызываемый.
ContactIdGuidДаCallSessionEntity.ContactIdКонтакт-разрешение пары (contact.md).
MediaTypeCallMediaTypeДаCallSessionEntity.MediaTypeAudio/Video.
StatusCallStatusДаCallSessionEntity.StatusВсегда Ringing на успехе создания.
StartedAtDateTimeOffsetДаCallSessionEntity.StartedAtМомент StartCall.
RecordingStatusRecordingStatus?НетCallRecordingEntity.StatusPending при авто-записи; null если запись не стартовала.
RecordingIdGuid?НетCallRecordingEntity.IdСсылка на запись; null без записи.
RoomMediaJoinModel?НетcomputedAccessToken/Credential — SensitiveДескриптор входа инициатора в комнату SFU (messenger-hub.md). Только в start/answer/media-token.

AnsweredAt/EndedAt/EndReason/DurationSecondsnull на этом шаге. Room.AccessToken (и TURN Credential) — Sensitive: короткоживущий токен, минтится при формировании ответа, в БД не хранится, в логи не пишется (INV-CALL-6).

Алгоритм

  1. Контекст и доступ. Операцию выполняет инициатор звонка (User), CallerUserId — из JWT; требуется право messenger.calls.use (иначе — отказ доступа). Медиа-звонки должны быть включены флагом CALLS_MEDIA_ENABLED; иначе — MSG_CALLS_DISABLED.
  2. Проверка входа. CalleeUserId непустой и != CallerUserId (иначе MSG_CALL_SELF); MediaType — известное значение enum (Audio/Video), иначе ошибка валидации.
  3. Проверки предусловий. Вызываемый существует и активен — иначе MSG_CALL_USER_NOT_FOUND. Между инициатором и вызываемым есть контакт-разрешение (contact.md) со статусом Accepted — иначе MSG_CALL_NOT_CONTACT; ни одна сторона не в чёрном списке другой — иначе MSG_CALL_BLOCKED (гейт CommunicationPolicy.CanCall, INV-CALL-1). Активного звонка пары в статусе Ringing/Answered нет — иначе MSG_CALL_ALREADY_ACTIVE (INV-CALL-5).
  4. Резерв комнаты. Резервируется медиа-комната (Room, owned VO MediaRoom): детерминированное RoomName из id сессии + nonce (уникально среди активных, INV-CALL-6) и Domain медиа-развёртывания. Если медиа-стек не смог зарезервировать комнату — MSG_RECORDING_PROVISION_FAILED.
  5. Оценка записи. Типизированная политика RecordingPolicy.ShouldAutoRecord(chatKind, SessionKind.Direct) решает, писать ли звонок. Если да и запись включена (RECORDINGS_ENABLED) — заводится CallRecordingEntity (Status = Pending, Trigger = AutoPolicy, InitiatedByUserId = null, INV-REC-3) в контексте сделки; иначе запись не создаётся. Сбой провизии рекордера — MSG_RECORDING_PROVISION_FAILED.
  6. Создание сессии. Заводится CallSessionEntity: Status = Ringing, SessionKind = Direct, зафиксированы CallerUserId, CalleeUserId, ContactId, MediaType, Room, StartedAt. Гонку двух параллельных StartCall на пару закрывает партиал-индекс активных звонков ix_call_active + guard INV-CALL-5.
  7. Намерение доставки. В той же транзакции, что и создание сессии, пишется строка realtime_outbox CallInvited (адресат user:{CalleeUserId}) — persist-then-push. Токен входа callee (MediaJoinModel) минтится диспетчером realtime-dispatch в момент доставки, в outbox/БД не хранится. Интеграционных Kafka-событий 1:1-звонок не публикует.
  8. Результат. Карточка CallSessionModel (Status = Ringing, Room — токен инициатора, RecordingStatus/RecordingId при авто-записи) → HTTP 201 Created, Location: /api/messenger/calls/{CallId}.

Диаграмма

Побочные эффекты

ТипДетали
Integration eventsnone — 1:1-звонок Kafka-события не публикует (call-session.md)
Realtime push (hub)CallInviteuser:{CalleeUserId} (комната + токен доступа, токен минтится при доставке, не хранится)
External callsрезерв медиа-комнаты в SFU (ADR-0062); при авто-записи — провизия рекордера
Cache / projections / searchсессия попадает в partial ix_call_active (guard) и журнал ix_call_caller/ix_call_callee; при записи — ix_recording_active
Notificationsoffline/пропущенный-звонок push — вне MVP

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400неизвестный MediaType, CalleeUserId = Empty«Проверьте параметры звонка»no retry
MSG_CALL_SELF400CalleeUserId == CallerUserId«Нельзя позвонить самому себе»no retry
MSG_CALL_USER_NOT_FOUND404callee не найден или неактивен«Пользователь недоступен»no retry
MSG_CALL_NOT_CONTACT403пара не в Accepted-контакте (INV-CALL-1)«Звонок доступен только подтверждённым контактам»no retry
MSG_CALL_BLOCKED403чёрный список любой стороны (INV-CALL-1)«Звонок недоступен»no retry
MSG_CALL_ALREADY_ACTIVE409активный Ringing/Answered звонок пары (INV-CALL-5)«Звонок с этим пользователем уже идёт»retry после завершения
MSG_RECORDING_PROVISION_FAILED503медиа-стек не зарезервировал комнату или рекордер недоступен при авто-записи«Не удалось подготовить звонок, попробуйте позже»retry позже
MSG_CALLS_DISABLED503CALLS_MEDIA_ENABLED = false«Звонки временно недоступны»retry позже

Совместимость и наблюдаемость

  • Новые optional поля ответа (Room, RecordingStatus, RecordingId, SessionKind) — additive; смена маршрута/семантики статуса — breaking. Kafka-контракта у 1:1-звонков нет, версионируется только REST.
  • Logs: call start requested/created/blocked (CallId, CallerUserId, CalleeUserId, MediaType; токен доступа/Credential — никогда в логи); metrics: messenger_call_start_total{result}, latency p95; traces — сквозной trace (traceparent); dashboard pin-messenger.

Acceptance Criteria

  • Happy path. Given Accepted-контакт вне чёрного списка, CALLS_MEDIA_ENABLED, нет активного звонка пары; When POST {calleeUserId, mediaType}; Then 201, сессия Ringing/Direct, Room — токен инициатора, строка CallInvited в realtime_outbox адресована callee, StartedAt проставлен.
  • Авто-запись в сделке. Given RecordingPolicy.ShouldAutoRecord = true и RECORDINGS_ENABLED; When POST; Then заводится CallRecordingEntity Pending (Trigger = AutoPolicy, InitiatedByUserId = null, INV-REC-3), RecordingStatus = Pending в ответе; вне сделки/политики — записи нет, поля null.
  • Не контакт. Given пара не в Accepted-контакте; When POST; Then 403 MSG_CALL_NOT_CONTACT (INV-CALL-1).
  • Чёрный список. Given любая сторона в ЧС другой; When POST; Then 403 MSG_CALL_BLOCKED (INV-CALL-1).
  • Нельзя себе. Given CalleeUserId == CallerUserId; When POST; Then 400 MSG_CALL_SELF.
  • Callee недоступен. Given callee не найден/неактивен; When POST; Then 404 MSG_CALL_USER_NOT_FOUND.
  • Активный звонок / гонка. Given уже есть Ringing/Answered звонок пары; When POST; Then 409 MSG_CALL_ALREADY_ACTIVE (INV-CALL-5); две параллельные StartCall → ровно один Ringing (ix_call_active), второй → тот же код.
  • Медиа/рекордер недоступны. Given SFU не зарезервировал комнату или рекордер недоступен при авто-записи; When POST; Then 503 MSG_RECORDING_PROVISION_FAILED (retry позже).
  • Флаг выключен. Given CALLS_MEDIA_ENABLED = false; When POST; Then 503 MSG_CALLS_DISABLED.
  • Sensitive. На любом исходе Room.AccessToken/TURN Credential не персистятся и не пишутся в логи (INV-CALL-6); токен callee минтится в момент доставки, а не при создании.

Обратные ссылки

Автоссылки: где используется этот документ.

Связанные документы