Event Contract: pin.messenger.call-recording-available (CallRecordingAvailableEvent)
Версионируемый контракт интеграционного Kafka-события «запись звонка готова». Consumer-документы ссылаются сюда, не переописывая payload.
Назначение
Фиксирует переход записи звонка/конференции в Available (call-recording.md):
бинарь сохранён в file storage, известен стабильный FileId, метаданные заполнены. Публичное, потому что
file storage событий не публикует, а «доступность записи» нужна внешним потребителям синхронно недостижимым
образом: crm прикрепляет запись к сделке/встрече, notifications уведомляет участников. Это же событие —
триггер будущего пайплайна транскрипции/конспектирования (../calls/transcription-roadmap.md).
Producer — messenger (ingest-эндпоинт финализации записи, ../api/internal/jibri-recording-ingest.md).
Metadata
| Поле | Значение |
|---|---|
| Event type | pin.messenger.call-recording-available |
| Topic | pin.messenger.call-recording-available.v1 |
| Partitions / key | 3 / ключ = SessionId (порядок сегментов записи в рамках сессии) |
| ContractVersion | 1 |
| Producer | messenger |
| Статус | draft |
Событие публикуется атомарно с переходом записи
Uploading → Available(транзакционный outbox): пока изменение не зафиксировано, событие не отправляется; после фиксации — доставляется at-least-once.
Payload CallRecordingAvailableEvent
Публичные бизнес-поля готовой записи. Запись бинаря — по FileId (file storage); ссылок/URL в событии нет
(потребитель получает пресайн сам). Snapshot ссылается на дом сущности
(call-recording.md).
| Поле | Тип | Обязательность | Источник (поле агрегата) | Compatibility | Описание |
|---|---|---|---|---|---|
RecordingId | Guid | Да | CallRecordingEntity.Id | Immutable | Идентификатор записи; бизнес-ключ идемпотентности. |
SessionKind | SessionKind | Да | .SessionKind | Additive | Direct / Conference. |
SessionId | Guid | Да | .CallSessionId / .ConferenceSessionId | Immutable | Сессия-владелец; Kafka-ключ. |
ChatId | Guid? | Нет | .ChatId | Immutable | Deal-чат (для конференций); null для 1:1. |
DealId | Guid? | Нет | .DealId | Immutable | Сделка; маршрутизация в crm; null для 1:1. |
TenantId | Guid? | Нет | .DealTenantId | Immutable | DeveloperCompanyId сделки; null для 1:1. |
MediaType | CallMediaType | Да | .MediaType | Additive | Аудио/видео. |
FileId | Guid | Да | .FileId | Immutable | Ссылка на бинарь в file storage (без URL). |
Format | RecordingFormat | Да | .Format | Additive | Формат контейнера (Mp4/Webm/Mkv/Mp3). |
DurationSeconds | int | Да | .DurationSeconds | — | Длительность записи (≥ 0). |
SizeBytes | long | Да | .SizeBytes | — | Размер файла (> 0). |
Trigger | RecordingTrigger | Да | .Trigger | Additive | AutoPolicy / Manual. |
StartedAt | DateTimeOffset | Да | .StartedAt | — | Начало захвата. |
EndedAt | DateTimeOffset | Да | .StoppedAt | — | Конец захвата. |
AvailableAt | DateTimeOffset | Да | .AvailableAt | — | Момент готовности. |
Enum'ы (SessionKind, CallMediaType, RecordingFormat, RecordingTrigger) — из call-recording.md/call-session.md, не дублируются.
Когда публикуется
| Условие | Переход состояния | Публикация |
|---|---|---|
Бинарь выгружен в file storage, задан FileId + метаданные | Uploading → Available (call-recording.md INV-REC-2) | атомарно со сменой состояния (outbox) |
Потребители (consumers)
| Consumer-контекст | Consumer | Side effects | Идемпотентность | Задержка (SLA) |
|---|---|---|---|---|
crm | attach-call-recording | При DealId != null — прикрепить FileId к таймлайну/встрече сделки | RecordingId (first-seen) | < 1 мин |
notifications | recording-ready-notify | Уведомить участников сессии «запись готова» (in-app/offline) | RecordingId | < 1 мин |
Потребители, которым нужен доступ к файлу, запрашивают пресайн у file storage сами (или ведут пользователя к ../api/recordings/get-recording-download.md) — в событии нет URL/секретов.
Гарантии доставки, порядок и обработка отказов
- At-least-once + идемпотентность. Доставка хотя бы раз; оба потребителя дедуплицируют по
RecordingId(first-seen), повтор не плодит вложение сделки и не шлёт повторное уведомление. Частичный отказ fan-out независим: падениеnotificationsне откатывает attach вcrm— редоставка повторяет обоих, дубли гасятся. - Порядок относительно
conference-ended. Для конференций это событие приходит после conference-ended.md (разные топики/ключи — межтопикового порядка нет).crm, прикрепляя запись, находит уже созданную карточку встречи поRecordingId/ConferenceId; если событие встречи ещё не обработано — attach ретраится (backoff), затем DLQ + алерт.conference-ended.WasRecorded = true— сигнал потребителю ждать это событие;RecordingIdиз встречи совпадает сRecordingIdздесь. - Запись не стала доступной. Если выгрузка бинаря в file storage не финализировалась, запись остаётся
Uploading/Failed(call-recording.md), фоновыйrecording-finalize-retryдобирает; при окончательномFailedсобытие не публикуется — потребители не ждут его бессрочно (у встречиWasRecorded = true, ноcall-recording-availableне придёт; расхождение — предмет алерта эксплуатации, не бизнес-ошибка потребителя). - Retry/DLQ. Транзиентный сбой потребителя — ретрай по offset; poison после исчерпания попыток → DLQ топика + алерт, offset продвигается (поток других записей не блокируется).
Версионирование и совместимость
- Версия — один
ContractVersion = 1; consumer приVersion != ContractVersionлогирует предупреждение и продолжает. Версия кодируется суффиксом.v1. - Additive-only для minor: новые поля — optional с default, без bump.
- Breaking — новый
ContractVersion+ топик.v2+ dual-publish на период миграции. - Unknown enum (
SessionKind/RecordingFormat/RecordingTrigger) — fallback с предупреждением.
Безопасность
| Поле | Класс данных | Правило |
|---|---|---|
FileId | Internal | include (ссылка, не бинарь; доступ к файлу — через пресайн file storage) |
RecordingId/SessionId/ChatId/DealId/TenantId | Internal | include |
прочие (MediaType/Format/DurationSeconds/SizeBytes/Trigger/timestamps) | Public | include |
Событие НЕ содержит бинарь, ссылки/URL, токены, ФИО/телефон, транскрипт — только id/enum/bounded-скаляры.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Aggregate: CallRecordingEntity — запись звонка (хранение в file storage)
- API (6): GET /api/messenger/recordings/{recordingId} — Карточка записи, GET /api/messenger/recordings/{recordingId}/download — Ссылка на скачивание записи, POST /api/messenger/conferences/{conferenceId}/end — Завершить конференцию для всех, POST /api/messenger/conferences/{conferenceId}/recording/stop — Остановить запись конференции, POST /internal/call-recordings/{sessionId}/ingest — Финализация записи (jibri → file storage), SignalR Hub /hubs/messenger — MessengerHub (realtime-контракт)
- Use Cases (1): UC-MSG-007. Авто-запись звонка по сделке и её выдача участнику
- События (2): События сервиса messenger: реестр топиков, Event Contract: pin.messenger.conference-ended (ConferenceEndedEvent)
- Фоновые задачи (2): Background Job: ConferenceRoomReaperJob, Background Job: RecordingFinalizeRetryJob
- adr (1): ADR-0062: Медиа-стек звонков и запись на Jitsi (JVB SFU + coturn + jibri → file storage; JWT из messenger; нативный lib-jitsi-meet)
- calls (6): Жизненный цикл сессии звонка — 1:1 и конференция (messenger), Медиа-звонки и запись на Jitsi — интеграционный гид (messenger), Медиа-звонки на Jitsi: план поставки (playbook аналитик → разработчик), Backend-реализация медиа-звонков: спека → код (messenger), Jitsi: роадмап транскрипции и конспектов (будущая фаза), Jitsi: сквозной пайплайн авто-записи
- registries (1): Реестр событий PIN
Связанные документы
- Источник (операция): ../api/internal/jibri-recording-ingest.md; сущность: ../domain/call-recording.md.
- Реестр событий: README.md; пайплайн записи: ../calls/recording-pipeline.md; роадмап транскрипции: ../calls/transcription-roadmap.md.
- Решения: ADR-0062, ADR-0006 Kafka.