feat(room): поднятие руки и очередь для организатора
Транспорт — существующий аутентифицированный WS чата (api/chat.py), а не отдельный эндпоинт: сервер уже держит это соединение на каждого участника (обоснование — докстринг chat_websocket и useChat.ts). Состояние очереди — Redis (services/hand_queue.py), не Postgres: это эфемерное состояние звонка, а не история, и два процесса uvicorn делают наивную память одного процесса недостаточной. HSETNX даёт идемпотентное «поднять» (повторный клик не переставляет в конец очереди), снапшот шлётся всем участникам при любом изменении — организатор, зашедший позже, сразу видит актуальную картину. Опустить чужую руку может организатор (решение оператора) — проверка через conference.owner_id, не через identity клиента. Участник, вышедший из комнаты LiveKit (webhook participant_left), теряет место в очереди автоматически; переподключение WS чата место не сбрасывает (Redis не привязан к жизни соединения). room_finished чистит очередь целиком — она не должна пережить завершение звонка. Побочный эффект транспортного решения: поднять руку нельзя, если чат выключен настройкой инстанса (WS вообще не открывается) — принятый компромисс ради переиспользования уже готового канала. UI: кнопка «Рука» в тулбаре (у всех, бейдж — общий счётчик), бейдж на плитке говорящего (видно всем), панель «Очередь» организатору (HandQueuePanel). Кнопка «Рука» и панель «Очередь» намеренно НЕ прячутся в мобильную шторку настроек, в отличие от «Вида», — поднятие руки посреди разговора требует кнопки под рукой, а не в два клика вглубь настроек. Этим же коммитом (файлы разделяемые с задачей B2, RoomParticipantTile.tsx/ useChat.ts/RoomStage.tsx/RoomPage.tsx/room.css) — проброс conferenceId и каркас forced_mute-обработки, без которых кнопки принудительного мьюта не скомпилировались бы; сама реализация мьюта — следующим коммитом.
This commit is contained in:
@@ -27,6 +27,7 @@ from repositories.conferences import (
|
||||
ConferenceRepository,
|
||||
ConferenceSessionRepository,
|
||||
)
|
||||
from services import hand_queue
|
||||
from services.egress import run_track_egress
|
||||
from services.instance_settings import InstanceSettingsService
|
||||
from services.pipeline_producer import enqueue_pipeline
|
||||
@@ -154,6 +155,17 @@ class WebhookDispatcher:
|
||||
return
|
||||
user_id, guest_id = identity
|
||||
|
||||
# Очередь поднятых рук живёт в Redis по `conference.id`, независимо
|
||||
# от `ConferenceSession` (задача B1) — снимаем руку СРАЗУ, до guard'а
|
||||
# на отсутствующий открытый сеанс ниже: пропущенный/задержанный
|
||||
# `room_started` не должен оставлять фантомную запись в очереди у
|
||||
# реально вышедшего участника. Не путать с обрывом WS-соединения
|
||||
# самой очереди рук — то живёт своей жизнью и переживается без
|
||||
# потери места (см. `services/hand_queue.py`).
|
||||
removed = await hand_queue.lower_hand(conference.id, identity=event.participant.identity)
|
||||
if removed:
|
||||
await hand_queue.publish_snapshot(conference.id)
|
||||
|
||||
session_record = await self._sessions.get_open_by_conference(conference.id)
|
||||
if session_record is None:
|
||||
logger.warning(
|
||||
@@ -308,6 +320,10 @@ class WebhookDispatcher:
|
||||
now = datetime.now(UTC)
|
||||
await self._sessions.close(session_record, t_end=now)
|
||||
await self._sessions.close_all_open_participants(session_id=session_record.id, left_at=now)
|
||||
# Очередь поднятых рук — состояние звонка, не история; следующий
|
||||
# заход (в т.ч. у закреплённой конференции) должен начинать с чистой
|
||||
# очереди, а не наследовать поднятые руки из прошлого раза.
|
||||
await hand_queue.clear(conference.id)
|
||||
|
||||
# Незакреплённая умирает по завершении (история/саммари остаются);
|
||||
# закреплённая возвращается в ожидание следующего вхождения (ADR-001, п.2).
|
||||
|
||||
Reference in New Issue
Block a user