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

POST /internal/jitsi/events — Presence-вебхук media-стека (авторитет ростера)

Граница интеграции media-стека: небольшой кастомный prosody-модуль шлёт messenger server-side события присутствия (создание/уничтожение комнаты, вход/выход участника, смена состояния записи). Это авторитетный источник активного ростера (call-participant.md, INV-CPART-5): серверные presence-события надёжнее клиентских и разрешают расхождения с командами join/leave.

Граница именования. Внутренний вебхук от media-стека — имена Jitsi/prosody/jibri предметны. Наружу media-стек нейтрален (SFU, ADR-0062).

Назначение

messenger принимает поток presence-событий MUC-комнаты и приводит к ним свою модель: обновляет ростер конференции (вход/выход/демонстрация экрана), переводит запись Pending → Recording при подтверждённом старте захвата и обрабатывает уничтожение комнаты так же, как DELETE резервирования. Каждое принятое событие рождает соответствующий realtime-push участникам. События идемпотентны и упорядочиваются по монотонному OccurredAt.

Metadata

ПолеЗначение
Сервис/контекстmessenger
API areainternal (вебхук media-стека)
Feature groupmedia (jitsi-интеграция)
Статусdraft
Документdocs/06-services/messenger/api/internal/jitsi-events.md

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

ПроверкаЗначение
Actor typeExternalSystem — кастомный prosody presence-модуль
Auth policy / Permissionsподписанный вебхук: X-Pin-Signature = HMAC над сырым телом + X-Pin-Timestamp, окно replay ≤ 5 мин, общий секрет; либо mTLS
Scope (RBAC)не применяется (доверенная граница инфраструктуры)
Record-levelсессия резолвится по RoomName = Room.RoomName; несовпадение → deny (fail-closed)
Tenant isolationне применяется; резолв по имени комнаты, не по TenantId
Auditupdate: ростер (Joined/Left), запись (→ Recording) — иммутабельные фазовые метки/soft-leave
Trusted boundaryвход подписан/mTLS; полная валидация подписи → совпадения имени → формы события

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/messenger/internal/jitsi/events
Success status200 (тело JitsiEventAckModel)
Idempotencyключ (RoomName, EventType, OccupantJid, OccurredAt); повтор — no-op ack; порядок — по монотонному OccurredAt
Correlationсквозной trace через traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
X-Pin-SignatureheaderstringДаHMAC над сырым телом; несовпадение → MSG_JITSI_SIGNATURE_INVALIDсекрет не логируется
X-Pin-TimestampheaderDateTimeOffsetДаокно replay ≤ 5 мин
EventTypebodyJitsiMucEventTypeДаRoomCreated/RoomDestroyed/OccupantJoined/OccupantLeft/RecordingStatusChanged
RoomNamebodystringДасверка с Room.RoomName сессии; несовпадение → MSG_JITSI_ROOM_UNKNOWNмаршрутный id
OccupantUserIdbodyGuid?Нетиз token-контекста context.user.id; обязателен для OccupantJoined/OccupantLeftлогическая ссылка identity.users
OccupantJidbodystring?НетJID участника; часть ключа идемпотентностине PII (эфемерный jid)
Rolebodystring?Нетроль участника в MUC (moderator/participant)
OccurredAtbodyDateTimeOffsetДамомент события у media-стека; порядок применения
RecordingStatebodystring?Нетдля RecordingStatusChanged (on/off)

Enum JitsiMucEventType

ЗначениеОписание
RoomCreatedКомната поднята (после allow резервирования).
RoomDestroyedКомната уничтожена (эквивалент DELETE резервирования).
OccupantJoinedУчастник вошёл в комнату.
OccupantLeftУчастник вышел/отключился.
RecordingStatusChangedИзменилось состояние захвата (RecordingState = on/off).

Модель ответа JitsiEventAckModel

ПолеТипОбязательностьИсточникMaskingОписание
AcceptedboolДаcomputedСобытие принято (или идемпотентно проигнорировано как повтор/устаревшее).

Алгоритм

  1. Контекст и доступ. Вызывает prosody presence-модуль (ExternalSystem). Проверяется подпись X-Pin-Signature над сырым телом + свежесть X-Pin-Timestamp; невалидно → MSG_JITSI_SIGNATURE_INVALID.
  2. Резолв и сверка комнаты. По RoomName резолвится сессия-владелец (конференция — conference-session.md; 1:1 — call-session.md); RoomName должен точно совпасть с Room.RoomName, иначе → MSG_JITSI_ROOM_UNKNOWN (fail-closed).
  3. Идемпотентность и порядок. Ключ дедупликации — (RoomName, EventType, OccupantJid, OccurredAt); уже виденное или более старое по OccurredAt, чем текущее состояние строки, — принимается как no-op (Accepted = true), состояние не откатывается.
  4. OccupantJoined/OccupantLeft → ростер. Для конференции upsert-ится CallParticipantEntity (авторитетный ростер, INV-CPART-1/5): Joined — реактивация/создание строки с JoinedAt; Left — soft-leave (LeftAt, LeaveReason, DurationSeconds). Пересчитываются ActiveParticipantCount/ PeakParticipantCount, обновляется LastActivityAt. Каждый переход рождает push MediaParticipant (Joined/Left) в hub-группу chat:{ChatId} (конференция) либо user:{peer} (1:1). Если событие несёт videoType = desktop — обновляется ScreenShareState (Sharing/None) с push ScreenShareState.
  5. RecordingStatusChanged(on) → запись. Активная запись сессии переводится Pending → Recording (call-recording.md, INV-REC-2/4: NoticeState уже Announced), StartedAt фиксируется; push RecordingState(Started). off — фиксируется как остановка захвата, финал записи приходит из jibri-recording-ingest.md.
  6. RoomDestroyed → опустошение. Обрабатывается как DELETE резервирования (jitsi-reservation.md, шаг 7): активный ростер помечается вышедшим, LastActivityAt обновляется, финальное завершение сессии отдаётся conference-room-reaper (без форс-terminate). RoomCreated — отметка presence-активности (сверка с admission), состав не меняет.
  7. Согласованность и результат. Обновление ростера/записи и запись намерения push фиксируются в одном согласованном изменении (persist-then-push); диспетчер realtime-dispatch доставляет push после фиксации. Ответ — JitsiEventAckModel { Accepted }200 OK.

Диаграмма

Побочные эффекты

ТипДетали
Realtime push (hub)MediaParticipant(Joined/Left)chat:{ChatId} (конференция) / user:{peer} (1:1); ScreenShareState при videoType=desktop; RecordingState(Started) при on
Integration eventsnone напрямую; завершение сессии (через reaper) публикует pin.messenger.conference-ended.v1 из своего пути
External callsnone (входящий вебхук)
Cache / projectionsпартиал ix_call_participants_active, ix_conf_active, ix_conf_last_activity

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
MSG_JITSI_SIGNATURE_INVALID401подпись не сошлась / timestamp вне окна replay«Подпись запроса недействительна»no retry (переподписать)
MSG_JITSI_ROOM_UNKNOWN403RoomName не резолвится / не совпадает с Room.RoomName«Комната неизвестна»no retry
VALIDATION_FAILED400неизвестный EventType, пустой RoomName, отсутствует OccupantUserId для join/leave«Проверьте параметры запроса»no retry

Устаревшее/повторное событие (OccurredAt не новее состояния) — не ошибка: 200 {Accepted:true} как no-op.

Acceptance Criteria

  • Вход участника. Дано провизированная конференция и валидная подпись. Когда OccupantJoined. Тогда CallParticipantEntity upsert-ится Joined (JoinedAt), пересчитываются ActiveParticipantCount/ PeakParticipantCount и LastActivityAt, push MediaParticipant(Joined) в chat:{ChatId}, 200 {Accepted:true}.
  • Выход участника. Дано участник в ростере. Когда OccupantLeft. Тогда soft-leave (LeftAt, LeaveReason, DurationSeconds), ActiveParticipantCount--, push MediaParticipant(Left).
  • Демонстрация экрана. Дано событие с videoType = desktop. Когда обработано. Тогда ScreenShareState (Sharing/None) обновлён, push ScreenShareState.
  • Старт записи. Дано активная запись сессии в Pending (NoticeState = Announced, INV-REC-4). Когда RecordingStatusChanged(on). Тогда запись Pending → Recording (StartedAt), push RecordingState(Started); финал записи придёт отдельным вебхуком финализации.
  • Устаревшее/повторное событие. Дано OccurredAt не новее текущего состояния строки (или ключ уже виден). Когда событие приходит. Тогда no-op, 200 {Accepted:true}, состояние не откатывается (авторитет ростера, INV-CPART-5).
  • Уничтожение комнаты. Дано RoomDestroyed. Когда обработано. Тогда активный ростер помечается вышедшим, LastActivityAt обновляется, финальное завершение сессии отдаётся conference-room-reaper (без форс-terminate).
  • Неверная подпись. Дано подпись не сошлась / timestamp вне окна. Когда событие. Тогда MSG_JITSI_SIGNATURE_INVALID (401).
  • Неизвестная комната. Дано RoomName не совпал с Room.RoomName сессии. Когда событие. Тогда MSG_JITSI_ROOM_UNKNOWN (403, fail-closed).
  • Неполный payload. Дано OccupantJoined/OccupantLeft без OccupantUserId (или неизвестный EventType). Когда событие. Тогда VALIDATION_FAILED (400).

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

  • Новые значения JitsiMucEventType и optional-поля payload — additive (неизвестный EventTypeVALIDATION_FAILED, presence-модуль не шлёт неподдержанное); смена ключа идемпотентности/семантики — breaking (rollout модуля).
  • Logs: presence event (RoomName, EventType, OccurredAt, result), дедуп-пропуски — без токенов/PII; metrics: messenger_jitsi_event_total{type,result}, roster drift (расхождение presence vs команды); traces — сквозной traceparent; alert на всплеск room-unknown.

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

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

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