GET /api/messenger/recordings/{recordingId}/download — Ссылка на скачивание записи
Выдаёт участнику сессии короткоживущую ссылку на бинарь готовой записи
(call-recording.md). Бинарь хранится во внешнем file storage (S3), в
messenger — только FileId + метаданные (INV-REC-6); ссылка — пресайн-URL с ограниченным TTL, выдаётся
по запросу и не сохраняется. Это чувствительное чтение (Audit = ReadSensitive).
Назначение
Дать участнику ссылку, по которой он скачает файл записи напрямую из file storage. Метод существует
отдельно от get-recording.md, потому что выдача ссылки — чувствительное действие
(аудит) и ссылка короткоживущая. file storage не публикует событий — этот метод и есть то, как «запись
готова» становится действием: событие call-recording-available
уведомляет, а ссылку клиент берёт здесь. Owning-сервис — messenger, feature group recordings.
| Поле | Значение |
|---|
| Сервис/контекст | messenger |
| API area | recordings |
| Feature group | recordings |
| Статус | draft |
| Документ | docs/06-services/messenger/api/recordings/get-recording-download.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User — участник сессии-владельца записи |
| Auth policy / Permissions | permission messenger.recordings.view |
| Scope (RBAC) | не применяется (доступ — по участию в сессии) |
| Record-level | Direct: actor.UserId ∈ {CallerUserId, CalleeUserId} звонка; Conference: активное членство в ChatId конференции (INV-CHAT-4) |
| Tenant isolation | нет (кросс-tenant); изоляция — по участию/членству |
| Audit | ReadSensitive — выдача ссылки на бинарь записи (кто, какую запись, когда); URL в аудит/лог не пишется |
HTTP-контракт
| Поле | Значение |
|---|
| Method | GET |
| Route | /api/messenger/recordings/{recordingId}/download |
| Success status | 200 (OK) |
| Idempotency header | не применяется (чтение; каждый вызов выдаёт свежий пресайн) |
| Correlation | сквозной trace через traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
RecordingId | route | Guid | Да | != Empty; существующая запись в Available, актор — участник сессии (шаги 3–5) | — |
ActorUserId | identity claims | Guid | Да | из JWT; участник сессии-владельца | — |
Модель ответа RecordingDownloadModel
Короткоживущий дескриптор скачивания. Url — Sensitive: пресайн-URL с ограниченным TTL; исключён из
логов/трейсов; клиент использует его сразу.
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
RecordingId | Guid | Да | CallRecordingEntity.Id | — | Идентификатор записи. |
FileId | Guid | Да | .FileId | — | Стабильная ссылка на бинарь в file storage. |
Url | string | Да | file storage (пресайн) | exclude из логов | Короткоживущий пресайн-URL скачивания (TTL ~1 ч). |
ExpiresAt | DateTimeOffset | Да | computed (пресайн TTL) | — | Истечение ссылки; после — запросить заново. |
Format | RecordingFormat | Да | .Format | — | Контейнер (Mp4/Webm/Mkv/Mp3). |
SizeBytes | long | Да | .SizeBytes | — | Размер файла (для прогресса/проверки). |
Алгоритм
- Контекст и доступ. Операцию выполняет участник сессии-владельца (
User); ActorUserId — из JWT;
требуется право messenger.recordings.view. Запись включена флагом RECORDINGS_ENABLED, иначе
MSG_RECORDINGS_DISABLED.
- Проверка входа.
RecordingId непустой; иначе ошибка валидации.
- Загрузка записи. Запись по
RecordingId; нет → MSG_RECORDING_NOT_FOUND. Читаем SessionKind,
ссылку на сессию, Status, FileId.
- Право (по типу сессии).
Direct — актор ∈ {Caller, Callee}; Conference — актор активный член
ChatId (INV-CHAT-4). Иначе → MSG_RECORDING_FORBIDDEN (либо MSG_RECORDING_NOT_FOUND для сокрытия).
- Готовность. Требуется
Status == Available (INV-REC-2): иначе MSG_RECORDING_NOT_READY (ещё
Pending/Recording/Uploading) — клиент повторит после события call-recording-available. При
Deleted/Failed (FileId пуст) → MSG_RECORDING_FILE_MISSING.
- Пресайн. На
FileId у file storage запрашивается короткоживущий пресайн-GET-URL (TTL ~1 ч); сбой/
недоступность file storage → MSG_STORAGE_UNAVAILABLE (клиент повторит).
- Результат.
RecordingDownloadModel (Url Sensitive) → HTTP 200 OK; факт выдачи ссылки пишется в
аудит ReadSensitive (без URL). Записи в БД метод не меняет.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|
| Integration events | none — метод чтения, Kafka не публикует |
| External calls | file storage (S3): пресайн-GET на FileId, TTL ~1 ч; сбой → MSG_STORAGE_UNAVAILABLE (retry) |
| Cache / projections / search | none — состояние записи не меняется |
| Notifications | none |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
VALIDATION_FAILED | 400 | RecordingId = Empty | «Проверьте параметры запроса» | no retry |
MSG_RECORDING_FILE_MISSING | 400 | FileId пуст (запись Deleted/без файла) | «Файл записи недоступен» | no retry |
MSG_RECORDING_NOT_FOUND | 404 | записи нет или актор не участник (сокрытие) | «Запись не найдена» | no retry |
MSG_RECORDING_FORBIDDEN | 403 | актор не участник сессии-владельца | «Недостаточно прав» | no retry |
MSG_RECORDING_NOT_READY | 409 | Status != Available (ещё Pending/Recording/Uploading) | «Запись ещё готовится» | retry после события готовности |
MSG_STORAGE_UNAVAILABLE | 503 | file storage не выдал пресайн | «Хранилище временно недоступно» | retry позже |
MSG_RECORDINGS_DISABLED | 503 | RECORDINGS_ENABLED = false | «Записи временно недоступны» | retry позже |
Acceptance Criteria
- Скачивание готовой записи. Дано запись в
Available и актор — участник сессии-владельца
(Direct: ∈ {Caller, Callee}; Conference: активный член ChatId). Когда GET. Тогда file storage
выдаёт короткоживущий presign-GET-URL (TTL ~1 ч), факт выдачи пишется в аудит ReadSensitive (без URL),
ответ 200 c RecordingDownloadModel; состояние записи в БД не меняется.
- Ещё не готова. Дано запись в
Pending/Recording/Uploading. Когда GET. Тогда
MSG_RECORDING_NOT_READY (409) — клиент повторит после события call-recording-available.
- Не участник. Дано актор не участник сессии-владельца. Когда
GET. Тогда MSG_RECORDING_FORBIDDEN
(403) либо MSG_RECORDING_NOT_FOUND (404) для сокрытия существования записи.
- Файл отсутствует. Дано запись
Deleted/Failed (пустой FileId). Когда GET. Тогда
MSG_RECORDING_FILE_MISSING (400).
- Хранилище недоступно. Дано file storage не выдал presign. Когда
GET. Тогда MSG_STORAGE_UNAVAILABLE
(503) с retry.
- Записи выключены. Дано
RECORDINGS_ENABLED = false. Когда GET. Тогда MSG_RECORDINGS_DISABLED
(503).
- Свежесть ссылки. Дано истёкший presign. Когда клиент повторяет запрос. Тогда выдаётся новая ссылка —
каждый вызов генерирует свежий presign, ответ не кэшируется и не идемпотентен.
- Приватность.
Url/presign/бинарь не пишутся в логи/трейсы/аудит — фиксируется только факт выдачи
(RecordingId, участник, ExpiresAt).
Совместимость и наблюдаемость
- Новые optional-поля — additive; смена семантики
Url/TTL — breaking. Kafka-контракта нет.
- Logs:
recording download issued (RecordingId, участник, ExpiresAt) — без Url; metrics:
messenger_recording_download_total{result}, messenger_storage_presign_latency_seconds; correlation
через traceparent. Url/пресайн/бинарь в логи/трейсы/аудит не пишутся (только факт выдачи).
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы