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

GET /api/messenger/calls — Журнал звонков (входящие / исходящие / пропущенные)

Постраничный журнал звонков пользователя: все сессии, где актор — Caller или Callee, с направлением, статусом, длительностью и причиной завершения. Read-only проекция CallSessionEntity. Дом полей и статусы — call-session.md.

Media-стек — внешний SFU (ADR-0062). Журнал доступен независимо от media-фазы: он читает факты лога. SDP/ICE/токены и дескриптор входа MediaJoinModel в журнале отсутствуют (INV-CALL-4/6) — историческая лента комнату не выдаёт. Медиа-звонки за флагом CALLS_MEDIA_ENABLED.

Назначение

Отдать пользователю ленту его звонков для экрана «Звонки» (история набора, пропущенные, длительность). Owning- сервис — messenger, feature group calls, сценарий «звонок поверх контактов».

Metadata

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

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

ПроверкаЗначение
Actor typeUser — владелец журнала (actor.UserId)
Auth policy / Permissionspermission messenger.calls.use
Scope (RBAC)платформа (person-level)
Record-levelпредикат «актор — участник»: CallerUserId = actor OR CalleeUserId = actor (чужие звонки не видны)
Tenant isolationне применяется (ADR-0052); срез — по Caller/Callee
Auditread (не sensitive: журнал собственных звонков; SDP/медиа отсутствуют)

HTTP-контракт

ПолеЗначение
MethodGET
Route/api/messenger/calls
Success status200
Idempotency headerне применяется (безопасный GET)
Correlationсквозной trace через traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ActorUserIdidentity claimsGuidДаиз JWT; предикат membership
PagingqueryPagingQueryНетTake 1..100 (default 20), Offset >= 0 (default 0)защита от unbounded
DirectionqueryCallDirection?Нетenum строкой Incoming/Outgoing; фильтр по стороне актора (вычисляемо)
MissedOnlyquerybool?Нетtrue → только Status = Missed (экран пропущенных)
RecordedOnlyquerybool?Нетtrue → только сессии с записью (HasRecording = true)
MediaTypequeryCallMediaType?Нетenum Audio/Video; unknown → ошибка валидации
CounterpartUserIdqueryGuid?Нет!= Empty; журнал с конкретным собеседником
From / ToqueryDateTimeOffset?Нетокно по StartedAt; From <= To

Все фильтры комбинируются AND. Сортировка фиксирована StartedAt desc, tiebreak Id desc (монотонность лога).

Модель ответа — постраничный список CallLogListModel

Постраничный ответ CallLogListModel { Items: CallLogItemEntry[], Total, Take, Offset }. Элемент — проекция CallSessionEntity с вычисляемыми полями (Direction, CounterpartUserId, HasRecording) от лица актора.

CallLogItemEntry

ПолеТипОбязательностьИсточникMaskingОписание
CallIdGuidДаCallSessionEntity.IdИдентификатор сессии.
SessionKindSessionKindДаcomputedВсегда Direct для 1:1-звонка.
DirectionCallDirectionДаcomputedIncoming при actor == CalleeUserId, иначе Outgoing.
CounterpartUserIdGuidДаcomputedВторая сторона (actor == Caller ? Callee : Caller); display-профиль — messenger-user.md.
MediaTypeCallMediaTypeДаCallSessionEntity.MediaTypeAudio/Video.
StatusCallStatusДаCallSessionEntity.StatusRinging/Answered/Declined/Missed/Ended/Failed.
StartedAtDateTimeOffsetДаCallSessionEntity.StartedAtМомент вызова (ключ сортировки).
AnsweredAtDateTimeOffset?НетCallSessionEntity.AnsweredAtnull, если не отвечен.
EndedAtDateTimeOffset?НетCallSessionEntity.EndedAtnull для активной сессии.
EndReasonCallEndReason?НетCallSessionEntity.EndReasonПричина завершения; null для активной/Missed.
DurationSecondsint?НетCallSessionEntity.DurationSecondsДлительность разговора; null — соединение не состоялось.
HasRecordingboolДаcomputedtrue, если у сессии есть запись (в любом статусе).
RecordingStatusRecordingStatus?НетCallRecordingEntity.StatusСостояние записи; null, если записи нет.
RecordingIdGuid?НетCallRecordingEntity.IdСсылка на запись (для скачивания через recordings-API); null без записи.

Direction/CounterpartUserId/HasRecording не хранятся — вычисляются проекцией от ActorUserId. SDP/ICE/ медиа и дескриптор входа MediaJoinModel в журнал не входят (INV-CALL-4/6) — токен/комната в истории не раздаются (только в start/answer/media-token).

Алгоритм

  1. Контекст и доступ. Операцию выполняет владелец журнала (User); ActorUserId — из JWT; требуется право messenger.calls.use (иначе FORBIDDEN). Журнал читается независимо от CALLS_MEDIA_ENABLED.
  2. Проверка входа. Paging.Take ∈ 1..100, Offset >= 0; Direction/MediaType — известные enum (unknown → ошибка валидации); CounterpartUserId (если задан) непустой; From <= To. Существование CounterpartUserId не проверяется — несуществующий даёт пустую страницу, не 404.
  3. Выборка. Сессии, где актор — участник (CallerUserId = actor OR CalleeUserId = actor), с фильтрами (комбинируются AND): MissedOnlyStatus = Missed; RecordedOnly → только с записью; MediaType; окно StartedAt ∈ [From, To]; собеседник CounterpartUserId; направление (Incoming ⇒ CalleeUserId = actor, Outgoing ⇒ CallerUserId = actor). Сортировка фиксирована StartedAt desc, tiebreak Id desc; индексы ix_call_caller/ix_call_callee.
  4. Маппинг. Direction, CounterpartUserId, HasRecording вычисляются от лица актора; RecordingStatus/ RecordingId берутся из связанной записи (если есть); enum передаются строкой; активные сессии (Ringing/Answered) попадают в журнал наравне с терминальными (UI показывает «идёт вызов»). SDP/ICE/медиа и дескриптор входа MediaJoinModel в модель не входят (INV-CALL-4/6).
  5. Результат. Постраничный список CallLogListModel (Items + Total/Take/Offset) → HTTP 200; пустой журнал — 200 с Items = [] (не 404).

Диаграмма

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400Take вне 1..100, неизвестный Direction/MediaType, From > To, CounterpartUserId = Empty«Проверьте параметры запроса»no retry
UNAUTHORIZED401нет/просрочен JWT«Требуется авторизация»no retry
FORBIDDEN403у актора нет permission messenger.calls.use«Недостаточно прав»no retry

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

  • Новые optional поля ответа/фильтры — additive (без breaking); удаление/переименование/смена типа поля или значения enum — breaking (migration + rollout). Unknown enum во входе — ошибка валидации (не игнор).
  • Logs: call log listed (набор фильтров, Total; id собеседников — не в логи как PII-контекст); metrics: messenger_call_list_total{result}, latency p95 (< 500 мс, SLO списков); traces — сквозной trace (traceparent); dashboard pin-messenger.

Acceptance Criteria

  • Happy path. Given у актора есть звонки; When GET с фильтрами; Then 200, только сессии, где актор — участник, StartedAt desc (tiebreak Id desc); Direction/CounterpartUserId/HasRecording вычислены от лица актора.
  • Пропущенные. Given MissedOnly = true; When GET; Then только Status = Missed.
  • С записью. Given RecordedOnly = true; When GET; Then только сессии с записью, RecordingId заполнен.
  • Комбинированный фильтр. Given From/To, CounterpartUserId, Direction; When GET; Then условия комбинируются AND; несуществующий CounterpartUserId → пустая страница (не 404).
  • Независимость от флага. Given CALLS_MEDIA_ENABLED = false; When GET; Then 200 (журнал читает факты лога).
  • Без токена комнаты. Given любой элемент; Then MediaJoinModel/SDP/ICE отсутствуют (INV-CALL-4/6).
  • Пустой журнал. Given у актора нет звонков; When GET; Then 200, Items = [] (не 404).
  • Валидация. Given Take вне 1..100 / неизвестный Direction|MediaType / From > To / CounterpartUserId = Empty; When GET; Then 400 VALIDATION_FAILED.
  • Доступ. Given нет messenger.calls.use; When GET; Then 403 FORBIDDEN; чужие звонки не видны (предикат membership).

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

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

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