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 area | calls |
| Feature group | calls |
| Статус | draft |
| Документ | docs/06-services/messenger/api/calls/answer-call.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User — только вызываемый (actor.UserId == CallSession.CalleeUserId) |
| Auth policy / Permissions | permission messenger.calls.use |
| Scope (RBAC) | платформа (person-level) |
| Record-level | доступ по membership: только CalleeUserId может принять; иначе MSG_CALL_FORBIDDEN |
| Tenant isolation | не применяется (ADR-0052); изоляция — по Caller/Callee |
| Audit | update: переход → Answered, метка AnsweredAt (иммутабельная фаза лога) |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/messenger/calls/{callId}/answer |
| Success status | 200 (OK) |
| Idempotency header | не применяется — повтор ловит guard состояния (шаг 4): второй Answer уже не Ringing → MSG_CALL_INVALID_STATE |
| Correlation | сквозной trace через traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
CallId | route | Guid | Да | != Empty; существующая сессия в Ringing, где актор — Callee (шаги 3–4) | — |
ActorUserId | identity claims | Guid | Да | из JWT; должен == CalleeUserId | — |
Тела запроса нет. SDP/ICE не передаются: медиа-негоциация идёт клиент ↔ SFU (INV-CALL-4); токен входа callee
возвращается дескриптором MediaJoinModel в ответе.
Модель ответа CallSessionModel
Проекция CallSessionEntity (дом — сущность; здесь состав строки). Дельта к
start-call.md: Status = Answered, заполнен AnsweredAt, Room — токен входа callee.
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
CallId | Guid | Да | CallSessionEntity.Id | — | Идентификатор сессии. |
SessionKind | SessionKind | Да | computed | — | Всегда Direct. |
CallerUserId | Guid | Да | CallSessionEntity.CallerUserId | — | Инициатор. |
CalleeUserId | Guid | Да | CallSessionEntity.CalleeUserId | — | Вызываемый (актор). |
MediaType | CallMediaType | Да | CallSessionEntity.MediaType | — | Audio/Video. |
Status | CallStatus | Да | CallSessionEntity.Status | — | Answered на успехе. |
StartedAt | DateTimeOffset | Да | CallSessionEntity.StartedAt | — | Момент StartCall. |
AnsweredAt | DateTimeOffset | Да | CallSessionEntity.AnsweredAt | — | Момент принятия (старт длительности). |
RecordingStatus | RecordingStatus? | Нет | CallRecordingEntity.Status | — | Состояние записи, если стартовала; иначе null. |
RecordingId | Guid? | Нет | CallRecordingEntity.Id | — | Ссылка на запись; null без записи. |
Room | MediaJoinModel? | Нет | computed | AccessToken/Credential — Sensitive | Дескриптор входа callee в комнату SFU. Только в start/answer/media-token. |
Room.AccessToken (и TURN Credential) — Sensitive: короткоживущий токен, минтится при формировании
ответа, в БД не хранится, в логи не пишется (INV-CALL-6).
Алгоритм
- Контекст и доступ. Операцию выполняет вызываемый (
User);ActorUserId— из JWT; требуется правоmessenger.calls.use. Медиа-звонки должны быть включены флагомCALLS_MEDIA_ENABLED, иначеMSG_CALLS_DISABLED. - Проверка входа.
CallIdнепустой; иначе ошибка валидации. Тела запроса нет. - Загрузка сессии. Сессия звонка по
CallId; нет →MSG_CALL_NOT_FOUND. Контакт и чёрный список повторно не проверяются — гейт общения отработал приStartCall(INV-CALL-1); ответ на уже идущий звонок его не требует. - Право и переход. Принять звонок может только вызываемый: если
ActorUserId != CalleeUserId→MSG_CALL_FORBIDDEN. ПереходRinging → Answered(INV-CALL-3): если статус неRinging(ужеAnswered/Declined/Missed/Ended) →MSG_CALL_INVALID_STATE. При успехе —Status = Answered,AnsweredAt(старт отсчёта длительности). - Намерение доставки. В той же транзакции пишется строка
realtime_outboxCallAnswered(адресатыuser:{Caller},user:{Callee}) — persist-then-push. Kafka-событий 1:1-звонок не публикует. - Согласованность. Переход
→ Answeredфиксируется атомарно; если между загрузкой и фиксацией фоновый reaper таймаутов (call-timeout-reaper) перевёл сессию вMissed, guardStatus == Ringingне выполнится на перечитанном состоянии →MSG_CALL_INVALID_STATE. - Результат. Карточка
CallSessionModel(Status = Answered,AnsweredAt,Room— токен callee в ту же комнату) → HTTP200 OK.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Integration events | none — 1:1-звонок Kafka-события не публикует |
| Realtime push (hub) | CallSignal(Answered) → обеим сторонам (non-PII) |
| External calls | none на этом шаге (комната зарезервирована при StartCall) |
| Cache / projections / search | сессия остаётся в partial ix_call_active (Answered) до завершения |
| Notifications | none (in-app realtime) |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | CallId = Empty | «Проверьте параметры ответа на звонок» | no retry |
MSG_CALL_NOT_FOUND | 404 | сессии нет или актор не участник (скрытие) | «Звонок не найден» | no retry |
MSG_CALL_FORBIDDEN | 403 | актор не Callee (принять может только вызываемый) | «Недостаточно прав для ответа на звонок» | no retry |
MSG_CALL_INVALID_STATE | 409 | Status != Ringing (уже Answered/Declined/Missed/Ended, INV-CALL-3) | «Звонок уже завершён или недоступен» | no retry (перечитать) |
MSG_CALLS_DISABLED | 503 | CALLS_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); dashboardpin-messenger.
Acceptance Criteria
- Happy path. Given сессия в
Ringing, актор —Callee,CALLS_MEDIA_ENABLED; WhenPOST /answer; Then200, переходRinging → Answered(INV-CALL-3),AnsweredAt(старт длительности),Room— токен callee в ту же комнату,CallAnsweredобеим сторонам. - Принять может только Callee. Given актор —
Callerили третье лицо; WhenPOST; Then403 MSG_CALL_FORBIDDEN. - Уже не Ringing. Given статус
Answered/Declined/Missed/Ended; WhenPOST; Then409 MSG_CALL_INVALID_STATE(INV-CALL-3); повторныйAnswer→ тот же код. - Гонка с reaper. Given
call-timeout-reaperперевёлRinging → Missedмежду загрузкой и фиксацией; WhenPOST; Then409 MSG_CALL_INVALID_STATE(guardStatus == Ringingна перечитанном состоянии). - Нет сессии / скрытие. Given несуществующий
CallIdили актор не участник; WhenPOST; Then404 MSG_CALL_NOT_FOUND. - Флаг выключен. Given
CALLS_MEDIA_ENABLED = false; WhenPOST; Then503 MSG_CALLS_DISABLED. - Sensitive.
Room.AccessToken/TURNCredentialне персистятся и не в логи (INV-CALL-6).
Обратные ссылки
Автоссылки: где используется этот документ.
- API (5): GET /api/messenger/calls — Журнал звонков (входящие / исходящие / пропущенные), POST /api/messenger/calls — Начать звонок, POST /api/messenger/calls/{callId}/decline — Отклонить звонок, POST /api/messenger/calls/{callId}/end — Завершить звонок (hangup, call-лог), POST /api/messenger/calls/{callId}/media-token — Обновить токен входа в звонок
- Use Cases (1): UC-MSG-005. Сигналинг аудио/видео-звонка
- calls (1): Жизненный цикл сессии звонка — 1:1 и конференция (messenger)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность (дом полей, статусы,
INV-CALL-3/6,MediaRoom, методAnswer): call-session.md. - Дескриптор входа
MediaJoinModel: messenger-hub.md. - Соседние операции: start-call.md, decline-call.md, end-call.md, list-calls.md, media-token.md; контекст: README.
- ADR: 0055 свой мини-мессенджер, 0062 медиа-стек и запись; SDK realtime: 10-signalr-realtime.md.