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 area | calls (сигналинг) |
| Feature group | calls |
| Статус | draft |
| Документ | docs/06-services/messenger/api/calls/list-calls.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User — владелец журнала (actor.UserId) |
| Auth policy / Permissions | permission messenger.calls.use |
| Scope (RBAC) | платформа (person-level) |
| Record-level | предикат «актор — участник»: CallerUserId = actor OR CalleeUserId = actor (чужие звонки не видны) |
| Tenant isolation | не применяется (ADR-0052); срез — по Caller/Callee |
| Audit | read (не sensitive: журнал собственных звонков; SDP/медиа отсутствуют) |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | GET |
| Route | /api/messenger/calls |
| Success status | 200 |
| Idempotency header | не применяется (безопасный GET) |
| Correlation | сквозной trace через traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
ActorUserId | identity claims | Guid | Да | из JWT; предикат membership | — |
Paging | query | PagingQuery | Нет | Take 1..100 (default 20), Offset >= 0 (default 0) | защита от unbounded |
Direction | query | CallDirection? | Нет | enum строкой Incoming/Outgoing; фильтр по стороне актора (вычисляемо) | — |
MissedOnly | query | bool? | Нет | true → только Status = Missed (экран пропущенных) | — |
RecordedOnly | query | bool? | Нет | true → только сессии с записью (HasRecording = true) | — |
MediaType | query | CallMediaType? | Нет | enum Audio/Video; unknown → ошибка валидации | — |
CounterpartUserId | query | Guid? | Нет | != Empty; журнал с конкретным собеседником | — |
From / To | query | DateTimeOffset? | Нет | окно по StartedAt; From <= To | — |
Все фильтры комбинируются AND. Сортировка фиксирована StartedAt desc, tiebreak Id desc (монотонность лога).
Модель ответа — постраничный список CallLogListModel
Постраничный ответ CallLogListModel { Items: CallLogItemEntry[], Total, Take, Offset }. Элемент —
проекция CallSessionEntity с вычисляемыми полями (Direction,
CounterpartUserId, HasRecording) от лица актора.
CallLogItemEntry
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
CallId | Guid | Да | CallSessionEntity.Id | — | Идентификатор сессии. |
SessionKind | SessionKind | Да | computed | — | Всегда Direct для 1:1-звонка. |
Direction | CallDirection | Да | computed | — | Incoming при actor == CalleeUserId, иначе Outgoing. |
CounterpartUserId | Guid | Да | computed | — | Вторая сторона (actor == Caller ? Callee : Caller); display-профиль — messenger-user.md. |
MediaType | CallMediaType | Да | CallSessionEntity.MediaType | — | Audio/Video. |
Status | CallStatus | Да | CallSessionEntity.Status | — | Ringing/Answered/Declined/Missed/Ended/Failed. |
StartedAt | DateTimeOffset | Да | CallSessionEntity.StartedAt | — | Момент вызова (ключ сортировки). |
AnsweredAt | DateTimeOffset? | Нет | CallSessionEntity.AnsweredAt | — | null, если не отвечен. |
EndedAt | DateTimeOffset? | Нет | CallSessionEntity.EndedAt | — | null для активной сессии. |
EndReason | CallEndReason? | Нет | CallSessionEntity.EndReason | — | Причина завершения; null для активной/Missed. |
DurationSeconds | int? | Нет | CallSessionEntity.DurationSeconds | — | Длительность разговора; null — соединение не состоялось. |
HasRecording | bool | Да | computed | — | true, если у сессии есть запись (в любом статусе). |
RecordingStatus | RecordingStatus? | Нет | CallRecordingEntity.Status | — | Состояние записи; null, если записи нет. |
RecordingId | Guid? | Нет | CallRecordingEntity.Id | — | Ссылка на запись (для скачивания через recordings-API); null без записи. |
Direction/CounterpartUserId/HasRecording не хранятся — вычисляются проекцией от ActorUserId. SDP/ICE/
медиа и дескриптор входа MediaJoinModel в журнал не входят (INV-CALL-4/6) — токен/комната в истории не
раздаются (только в start/answer/media-token).
Алгоритм
- Контекст и доступ. Операцию выполняет владелец журнала (
User);ActorUserId— из JWT; требуется правоmessenger.calls.use(иначеFORBIDDEN). Журнал читается независимо отCALLS_MEDIA_ENABLED. - Проверка входа.
Paging.Take ∈ 1..100,Offset >= 0;Direction/MediaType— известные enum (unknown → ошибка валидации);CounterpartUserId(если задан) непустой;From <= To. СуществованиеCounterpartUserIdне проверяется — несуществующий даёт пустую страницу, не 404. - Выборка. Сессии, где актор — участник (
CallerUserId = actor OR CalleeUserId = actor), с фильтрами (комбинируются AND):MissedOnly→Status = Missed;RecordedOnly→ только с записью;MediaType; окноStartedAt ∈ [From, To]; собеседникCounterpartUserId; направление (Incoming ⇒ CalleeUserId = actor,Outgoing ⇒ CallerUserId = actor). Сортировка фиксированаStartedAt desc, tiebreakId desc; индексыix_call_caller/ix_call_callee. - Маппинг.
Direction,CounterpartUserId,HasRecordingвычисляются от лица актора;RecordingStatus/RecordingIdберутся из связанной записи (если есть); enum передаются строкой; активные сессии (Ringing/Answered) попадают в журнал наравне с терминальными (UI показывает «идёт вызов»). SDP/ICE/медиа и дескриптор входаMediaJoinModelв модель не входят (INV-CALL-4/6). - Результат. Постраничный список
CallLogListModel(Items+Total/Take/Offset) → HTTP200; пустой журнал —200сItems = [](не 404).
Диаграмма
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | Take вне 1..100, неизвестный Direction/MediaType, From > To, CounterpartUserId = Empty | «Проверьте параметры запроса» | no retry |
UNAUTHORIZED | 401 | нет/просрочен JWT | «Требуется авторизация» | no retry |
FORBIDDEN | 403 | у актора нет 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); dashboardpin-messenger.
Acceptance Criteria
- Happy path. Given у актора есть звонки; When
GETс фильтрами; Then200, только сессии, где актор — участник,StartedAt desc(tiebreakId desc);Direction/CounterpartUserId/HasRecordingвычислены от лица актора. - Пропущенные. Given
MissedOnly = true; WhenGET; Then толькоStatus = Missed. - С записью. Given
RecordedOnly = true; WhenGET; Then только сессии с записью,RecordingIdзаполнен. - Комбинированный фильтр. Given
From/To,CounterpartUserId,Direction; WhenGET; Then условия комбинируются AND; несуществующийCounterpartUserId→ пустая страница (не404). - Независимость от флага. Given
CALLS_MEDIA_ENABLED = false; WhenGET; Then200(журнал читает факты лога). - Без токена комнаты. Given любой элемент; Then
MediaJoinModel/SDP/ICE отсутствуют (INV-CALL-4/6). - Пустой журнал. Given у актора нет звонков; When
GET; Then200,Items = [](не404). - Валидация. Given
Takeвне1..100/ неизвестныйDirection|MediaType/From > To/CounterpartUserId = Empty; WhenGET; Then400 VALIDATION_FAILED. - Доступ. Given нет
messenger.calls.use; WhenGET; Then403 FORBIDDEN; чужие звонки не видны (предикат membership).
Обратные ссылки
Автоссылки: где используется этот документ.
- API (6): GET /api/messenger/calls/{callId}/recordings — Записи 1:1-звонка, POST /api/messenger/calls — Начать звонок, POST /api/messenger/calls/{callId}/answer — Принять звонок, 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. Сигналинг аудио/видео-звонка
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность (дом полей, статусы,
CallEndReason,DurationSeconds, индексыix_call_caller/ix_call_callee): call-session.md; запись: call-recording.md; display-профиль собеседника: messenger-user.md. - Соседние операции: start-call.md, answer-call.md, decline-call.md, end-call.md, media-token.md; запись — get-call-recordings.md; контекст: README.
- ADR: 0055 свой мини-мессенджер, 0062 медиа-стек и запись, 0056 simplicity-first.