Jitsi: именование комнат, admission и presence
Как медиа-комната (MediaRoom) отображается на MUC-комнату Jitsi, кто авторитет её жизненного цикла и как
messenger узнаёт о входе/выходе участников. Комнаты создаются prosody/jicofo при входе клиента; авторитет
admission и состава — серверные hook-и prosody, а не клиентские события.
Именование комнат
| Сессия | Имя комнаты (MediaRoom.RoomName) |
|---|---|
| 1:1-звонок (call-session.md) | call-{CallSessionId} |
| Конференция (conference-session.md) | conf-{ConferenceSessionId} |
Имя детерминировано из id сессии (плюс nonce), уникальность среди активных — INV-CALL-6/INV-CONF-5.
Причина: reservation-вебхук prosody несёт в теле только имя комнаты (см. ниже), поэтому по нему
messenger должен однозначно найти сессию.
Деривация и fail-closed сверка
- Формат:
{prefix}-{SessionId:N}— префиксcall/conf,SessionIdв форматеN(32 hex без дефисов), опциональный nonce-суффикс-{n}при пересоздании комнаты послеFailed(уникальность среди активных). Пример:conf-3fa85f6457174562b3fc2c963f66afa6. - Разбор: по префиксу
call-/conf-→SessionKind; по хвосту →Guid→ id сессии. Невалидный формат или неизвестный id → admission-вебхук отвечает4xx(MSG_JITSI_ROOM_UNKNOWN), комната не поднимается. - Fail-closed match: имя из вебхука сверяется с сохранённым
MediaRoom.RoomNameнайденной сессии байт-в-байт. Расхождение (даже при валидном формате и живой сессии) → отказ. Комната поднимается, только если имя из prosody точно равно тому, чтоmessengerзаписал при старте сессии. Это второй гейт поверхroom-claim JWT (jitsi-jwt.md), ограничивающего токен одной комнатой.
Authoritative lifecycle: mod_reservations
Комната PIN не создаётся императивно бэкендом — она поднимается, когда клиент с валидным JWT
(jitsi-jwt.md) входит по имени. Перед созданием prosody-модуль mod_reservations
перехватывает conference-IQ к jicofo (pre-iq/host, дочерний conference в namespace
http://jitsi.org/protocol/focus) и буферизует его, пока messenger не подтвердит бронь HTTP-вызовом
(fail-closed: без 2xx комната не создаётся — prosody шлёт reservation-error инициатору).
Wire-формат reservation-вебхука
mod_reservations штатно бьёт в {prefix}/conference и DELETE {prefix}/conference/{conflict_id}. PIN
переименовывает сегмент пути conference → reservations (двухстрочная правка констант url в
call_api_create_conference/room_destroyed/handle_conference_already_exist, инвентарь —
jitsi-modifications.md), чтобы попадать в замороженный роут
POST /api/messenger/internal/jitsi/reservations. Возвращаемый id = RoomName, поэтому DELETE/GET
приходят по ключу имени комнаты (/reservations/{roomName}).
Admission — POST /internal/jitsi/reservations (тело application/x-www-form-urlencoded,
http.formencode; NB — форма, не JSON, унаследовано от Jicofo):
| Поле формы | Источник в prosody | Пример |
|---|---|---|
name | node комнаты (RoomReservation:get_room_name) | conf-3fa85f64...afa6 |
start_time | Java-datetime yyyy-MM-dd'T'HH:mm:ss.SSSX (to_java_date_string) | 2026-07-15T10:03:22.000Z |
mail_owner | jid.bare(creator_jid) — bare-JID инициатора | b1a7c9@meet.pin.internal |
Аутентификация вебхука — общая для internal/* (HMAC X-Pin-Signature + timestamp с replay-окном, либо
mTLS; fail-closed). Заголовок несётся через reservations_api_headers (см. конфиг ниже).
ALLOW — 200/201 + JSON. messenger по name находит активную сессию (call-/conf-), выполняет
fail-closed сверку имени и проверяет статус (Ringing/Provisioning/Active); при успехе
ConferenceSession.Status: Provisioning → Active и отдаёт:
{
"name": "conf-3fa85f6457174562b3fc2c963f66afa6",
"id": "conf-3fa85f6457174562b3fc2c963f66afa6",
"mail_owner": "b1a7c9@meet.pin.internal",
"start_time": "2026-07-15T10:03:22.000Z",
"duration": 14400,
"max_occupants": 20,
"lobby": false
}
name— prosody сверяет с именем комнаты (lowercased); несовпадение → бронь отклоняется на его стороне.id— корреляционный идентификатор брони;messengerвозвращаетRoomName, prosody запоминает его какconflict_idи шлёт с нимDELETE/повторныйGET.duration(секунды) — верхняя граница жизни комнаты:expires_at = start_time + duration, prosody сам разрушит комнату по истечении (evict_expired_reservations). Выставляем с запасом над политикой сессии.max_occupants(опц.) — лимит участников конференции (требуетreservations_enable_max_occupants);lobby(опц.) — не используем в MVP (false).
DENY — non-2xx. Любой не-2xx → prosody блокирует создание комнаты (reservation-error, occupant не
входит). Тело ошибки — JSON { "message": "..." } (используется как текст ошибки). Коды:
Код от messenger | Смысл | Реакция prosody |
|---|---|---|
404 MSG_JITSI_ROOM_UNKNOWN | имя не разобрано / сессии нет / имя не совпало | комната не поднимается |
409 (Conflict) | бронь уже существует | prosody дозапрашивает GET /reservations/{id} — идемпотентно |
4xx/5xx прочие | политика/ошибка | комната не поднимается; клиент видит отказ входа |
таймаут (reservations_api_timeout) | messenger не ответил | set_status_failed(500) — не поднимается |
Финализация — DELETE /internal/jitsi/reservations/{roomName} на muc-room-destroyed: messenger
освобождает бронь. Лайфсайкл-финализацию сессии (→ Ended) ведёт presence-канал (RoomDestroyed, ниже) и
reaper (../background/conference-room-reaper.md); оба идемпотентны.
Конфиг vhost prosody (reservations)
Ключи — из mod_reservations.lua (module:get_option(...)):
VirtualHost "meet.pin.internal"
modules_enabled = { "reservations"; "pin_muc_presence"; /* + token-модули */ }
reservations_api_prefix = "https://messenger.pin.internal/api/messenger/internal/jitsi"
reservations_api_headers = {
["Authorization"] = "Bearer <svc>"; -- либо HMAC-заголовок X-Pin-Signature (см. internal/*)
}
reservations_api_timeout = 20 -- сек; таймаут не ретраится
reservations_api_retry_count = 3 -- ретраи на 5xx/сетевой сбой
reservations_api_retry_delay = 3 -- сек между ретраями
reservations_enable_max_occupants = true -- разрешить поле max_occupants из ответа
-- lobby/password support не включаем (MVP)
Так admission — авторитетный серверный гейт: даже с валидным JWT комната не поднимется, если messenger
не подтвердил бронь (сессия отсутствует/завершена/имя не совпало).
Presence: кастомный модуль mod_pin_muc_presence
Ростер конференции (call-participant.md) — авторитетно серверный:
кто реально в комнате, знает prosody, а не клиент. Небольшой кастомный модуль mod_pin_muc_presence.lua
подписывается на MUC-события и шлёт их в messenger единым эндпоинтом
POST /api/messenger/internal/jitsi/events (JSON, наша схема, та же HMAC-аутентификация internal/*).
Wire-формат presence-вебхука
{
"room": "conf-3fa85f6457174562b3fc2c963f66afa6",
"occupantJid": "conf-3fa8...afa6@conference.meet.pin.internal/9a1f",
"occupantUserId": "b1a7c9de-0000-4000-8000-000000000001",
"nick": "Анна Риелтор",
"role": "moderator",
"event": "OccupantJoined",
"occurredAt": "2026-07-15T10:03:25.118Z"
}
| Поле | Источник в prosody | Заметка |
|---|---|---|
room | node комнаты | ключ для разбора сессии (как в admission) |
occupantJid | occupant.nick/occupant.jid | полный MUC-JID (с ресурсом) |
occupantUserId | session.jitsi_meet_context_user.id | из context.user.id JWT (token/util.lib.lua → jitsi_meet_context_user) → маппится на PIN UserId |
nick | display-name occupant-а | best-effort |
role | occupant.role (moderator/participant) | роль присвоена mod_token_affiliation из context.user.moderator |
event | тип hook-а | OccupantJoined / OccupantLeft / RecordingStatusChanged / RoomDestroyed |
occurredAt | серверное время prosody (UTC) | для упорядочивания и LastActivityAt |
Хук → событие:
muc-occupant-joined→OccupantJoined;muc-occupant-left→OccupantLeft(+ причина, если доступна);- статус jibri (
RecordingStatusChanged) — метаданные записи (recording-pipeline.md); muc-room-destroyed→RoomDestroyed— лайфсайкл-канал финализации (ConferenceSession → Ended); параллельныйDELETE /reservations/{roomName}чистит бронь. Оба идемпотентны.
messenger сверяет presence-события с командами join/leave; при расхождении побеждает серверное
событие (INV-CPART-5) — клиент мог не долететь. Screen-share prosody видит best-effort; живой mute/камера
не персистятся (принадлежат media-плоскости).
Почему кастомный модуль, а не готовые механизмы
prosody-egress(thirdparty/jitsi/infra-provisioning/nomad/prosody-egress.hcl) — проприетарный egress-канал 8x8, не часть открытой поставки и завязан на их инфраструктуру; тянуть его ради двух хук-ов избыточно.- Готового
muc_webhooks-модуля в открытой поставкеjitsi-meet/resources/prosody-pluginsнет — presence нужно эмитить самим. - Серверные hook-и (
muc-occupant-joined/left) — авторитетный источник состава (надёжнее клиентскихUSER_JOINED/USER_LEFT, которые могут теряться при разрыве). Поэтому — минимальный собственный модуль по образцуmod_muc_census/mod_measure_message_count.
Поток admission + presence (sequence)
Маппинг на сущности
| Prosody-факт | Сущность/поле |
|---|---|
admission POST /reservations подтверждён | ConferenceSession.Status: Provisioning → Active |
OccupantJoined | CallParticipant.State → Joined, JoinedAt; пересчёт ActiveParticipantCount/PeakParticipantCount |
OccupantLeft | CallParticipant.State → Left, LeftAt, LeaveReason, DurationSeconds |
RoomDestroyed / комната опустела | ConferenceSession.Status → Ended (LastParticipantLeft/EmptyTimeout) |
RecordingStatusChanged | статус записи (jibri) — recording-pipeline.md |
Обратные ссылки
Автоссылки: где используется этот документ.
- calls (8): Жизненный цикл сессии звонка — 1:1 и конференция (messenger), Медиа-звонки и запись на Jitsi — интеграционный гид (messenger), Медиа-звонки на Jitsi: план поставки (playbook аналитик → разработчик), Jitsi-звонки: план верификации и приёмки, Jitsi: инвентарь изменений в thirdparty/jitsi, Jitsi: минтинг JWT входа в .NET, Jitsi: нативный клиент lib-jitsi-meet на Next.js 16, Jitsi: k8s-топология и взаимодействие с бэкендом
Связанные документы
- Вебхуки (контракты): ../api/internal/; сущности: conference-session.md, call-participant.md; reaper: ../background/conference-room-reaper.md.
- JWT (
room-claim и roles): jitsi-jwt.md; минтинг в .NET —JitsiTokenService(backend-implementation.md); клиент: jitsi-client.md; изменения prosody: jitsi-modifications.md. - Источник:
thirdparty/jitsi/jitsi-meet/resources/prosody-plugins/mod_reservations.lua(POST/DELETE, form-encode, коды 200/201/409,to_java_date_string,muc-room-destroyed);.../token/util.lib.lua(jitsi_meet_context_user);.../mod_token_affiliation.lua(роль из claim-ов). - Решение: ADR-0062.