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

POST /api/messenger/calls/{callId}/answer — Принять звонок

Вызываемый (Callee) принимает входящий звонок и получает дескриптор входа в медиа-комнату. Переход CallSessionEntity Ringing → Answered (метка AnsweredAt, INV-CALL-3). Дом полей, статусы и инварианты — call-session.md.

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

Назначение

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

Metadata

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

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

ПроверкаЗначение
Actor typeUserтолько вызываемый (actor.UserId == CallSession.CalleeUserId)
Auth policy / Permissionspermission messenger.calls.use
Scope (RBAC)платформа (person-level)
Record-levelдоступ по membership: только CalleeUserId может принять; иначе MSG_CALL_FORBIDDEN
Tenant isolationне применяется (ADR-0052); изоляция — по Caller/Callee
Auditupdate: переход → Answered, метка AnsweredAt (иммутабельная фаза лога)

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/messenger/calls/{callId}/answer
Success status200 (OK)
Idempotency headerне применяется — повтор ловит guard состояния (шаг 4): второй Answer уже не RingingMSG_CALL_INVALID_STATE
Correlationсквозной trace через traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
CallIdrouteGuidДа!= Empty; существующая сессия в Ringing, где актор — Callee (шаги 3–4)
ActorUserIdidentity claimsGuidДаиз JWT; должен == CalleeUserId

Тела запроса нет. SDP/ICE не передаются: медиа-негоциация идёт клиент ↔ SFU (INV-CALL-4); токен входа callee возвращается дескриптором MediaJoinModel в ответе.

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

Проекция CallSessionEntity (дом — сущность; здесь состав строки). Дельта к start-call.md: Status = Answered, заполнен AnsweredAt, Room — токен входа callee.

ПолеТипОбязательностьИсточникMaskingОписание
CallIdGuidДаCallSessionEntity.IdИдентификатор сессии.
SessionKindSessionKindДаcomputedВсегда Direct.
CallerUserIdGuidДаCallSessionEntity.CallerUserIdИнициатор.
CalleeUserIdGuidДаCallSessionEntity.CalleeUserIdВызываемый (актор).
MediaTypeCallMediaTypeДаCallSessionEntity.MediaTypeAudio/Video.
StatusCallStatusДаCallSessionEntity.StatusAnswered на успехе.
StartedAtDateTimeOffsetДаCallSessionEntity.StartedAtМомент StartCall.
AnsweredAtDateTimeOffsetДаCallSessionEntity.AnsweredAtМомент принятия (старт длительности).
RecordingStatusRecordingStatus?НетCallRecordingEntity.StatusСостояние записи, если стартовала; иначе null.
RecordingIdGuid?НетCallRecordingEntity.IdСсылка на запись; null без записи.
RoomMediaJoinModel?НетcomputedAccessToken/Credential — SensitiveДескриптор входа callee в комнату SFU. Только в start/answer/media-token.

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

Алгоритм

  1. Контекст и доступ. Операцию выполняет вызываемый (User); ActorUserId — из JWT; требуется право messenger.calls.use. Медиа-звонки должны быть включены флагом CALLS_MEDIA_ENABLED, иначе MSG_CALLS_DISABLED.
  2. Проверка входа. CallId непустой; иначе ошибка валидации. Тела запроса нет.
  3. Загрузка сессии. Сессия звонка по CallId; нет → MSG_CALL_NOT_FOUND. Контакт и чёрный список повторно не проверяются — гейт общения отработал при StartCall (INV-CALL-1); ответ на уже идущий звонок его не требует.
  4. Право и переход. Принять звонок может только вызываемый: если ActorUserId != CalleeUserIdMSG_CALL_FORBIDDEN. Переход Ringing → Answered (INV-CALL-3): если статус не Ringing (уже Answered/Declined/Missed/Ended) → MSG_CALL_INVALID_STATE. При успехе — Status = Answered, AnsweredAt (старт отсчёта длительности).
  5. Намерение доставки. В той же транзакции пишется строка realtime_outbox CallAnswered (адресаты user:{Caller}, user:{Callee}) — persist-then-push. Kafka-событий 1:1-звонок не публикует.
  6. Согласованность. Переход → Answered фиксируется атомарно; если между загрузкой и фиксацией фоновый reaper таймаутов (call-timeout-reaper) перевёл сессию в Missed, guard Status == Ringing не выполнится на перечитанном состоянии → MSG_CALL_INVALID_STATE.
  7. Результат. Карточка CallSessionModel (Status = Answered, AnsweredAt, Room — токен callee в ту же комнату) → HTTP 200 OK.

Диаграмма

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

ТипДетали
Integration eventsnone — 1:1-звонок Kafka-события не публикует
Realtime push (hub)CallSignal(Answered) → обеим сторонам (non-PII)
External callsnone на этом шаге (комната зарезервирована при StartCall)
Cache / projections / searchсессия остаётся в partial ix_call_active (Answered) до завершения
Notificationsnone (in-app realtime)

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400CallId = Empty«Проверьте параметры ответа на звонок»no retry
MSG_CALL_NOT_FOUND404сессии нет или актор не участник (скрытие)«Звонок не найден»no retry
MSG_CALL_FORBIDDEN403актор не Callee (принять может только вызываемый)«Недостаточно прав для ответа на звонок»no retry
MSG_CALL_INVALID_STATE409Status != Ringing (уже Answered/Declined/Missed/Ended, INV-CALL-3)«Звонок уже завершён или недоступен»no retry (перечитать)
MSG_CALLS_DISABLED503CALLS_MEDIA_ENABLED = false«Звонки временно недоступны»retry позже

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

  • Новые optional поля ответа (Room, RecordingStatus, RecordingId, SessionKind) — additive; смена маршрута/семантики статуса — breaking. Kafka-контракта нет.
  • Logs: call answer requested/answered/invalid-state (CallId, участники; токен доступа — никогда в логи); metrics: messenger_call_answer_total{result}, latency p95; traces — сквозной trace (traceparent); dashboard pin-messenger.

Acceptance Criteria

  • Happy path. Given сессия в Ringing, актор — Callee, CALLS_MEDIA_ENABLED; When POST /answer; Then 200, переход Ringing → Answered (INV-CALL-3), AnsweredAt (старт длительности), Room — токен callee в ту же комнату, CallAnswered обеим сторонам.
  • Принять может только Callee. Given актор — Caller или третье лицо; When POST; Then 403 MSG_CALL_FORBIDDEN.
  • Уже не Ringing. Given статус Answered/Declined/Missed/Ended; When POST; Then 409 MSG_CALL_INVALID_STATE (INV-CALL-3); повторный Answer → тот же код.
  • Гонка с reaper. Given call-timeout-reaper перевёл Ringing → Missed между загрузкой и фиксацией; When POST; Then 409 MSG_CALL_INVALID_STATE (guard Status == Ringing на перечитанном состоянии).
  • Нет сессии / скрытие. Given несуществующий CallId или актор не участник; When POST; Then 404 MSG_CALL_NOT_FOUND.
  • Флаг выключен. Given CALLS_MEDIA_ENABLED = false; When POST; Then 503 MSG_CALLS_DISABLED.
  • Sensitive. Room.AccessToken/TURN Credential не персистятся и не в логи (INV-CALL-6).

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

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

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