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:
@@ -34,6 +34,7 @@ from models.instance_setting import InstanceSetting
|
||||
from models.participant import ConferenceParticipant
|
||||
from models.session import ConferenceSession
|
||||
from models.user import User
|
||||
from services import hand_queue
|
||||
from services.conference_ids import generate_number, generate_slug
|
||||
from services.egress import EgressStartResult
|
||||
|
||||
@@ -217,6 +218,53 @@ async def test_full_cycle_joined_left_finished(
|
||||
assert conference.ended_at is not None
|
||||
|
||||
|
||||
async def test_participant_left_removes_raised_hand_from_queue(
|
||||
client: httpx.AsyncClient, db_session: AsyncSession
|
||||
) -> None:
|
||||
"""Задача B1: участник с поднятой рукой вышел из конференции — рука исчезает из очереди."""
|
||||
conference = await _make_conference(db_session, generate_slug())
|
||||
user = await _make_user(db_session, "webhook-hand-1@example.com")
|
||||
await db_session.commit()
|
||||
|
||||
identity = str(user.id)
|
||||
await hand_queue.raise_hand(conference.id, identity=identity, name=user.name_user)
|
||||
assert [e.identity for e in await hand_queue.snapshot(conference.id)] == [identity]
|
||||
|
||||
left = _load_fixture(
|
||||
"participant_left.json",
|
||||
event_id=f"evt-{uuid.uuid4()}",
|
||||
room_name=conference.slug,
|
||||
identity=identity,
|
||||
)
|
||||
resp = await _post_webhook(client, left)
|
||||
assert resp.status_code == 200
|
||||
|
||||
assert await hand_queue.snapshot(conference.id) == []
|
||||
|
||||
|
||||
async def test_room_finished_clears_hand_queue(
|
||||
client: httpx.AsyncClient, db_session: AsyncSession
|
||||
) -> None:
|
||||
"""Задача B1: очередь поднятых рук — состояние звонка, не переживает его завершение."""
|
||||
conference = await _make_conference(db_session, generate_slug())
|
||||
await db_session.commit()
|
||||
|
||||
started = _load_fixture(
|
||||
"room_started.json", event_id=f"evt-{uuid.uuid4()}", room_name=conference.slug
|
||||
)
|
||||
assert (await _post_webhook(client, started)).status_code == 200
|
||||
|
||||
await hand_queue.raise_hand(conference.id, identity="guest:leftover", name="Leftover Guest")
|
||||
assert len(await hand_queue.snapshot(conference.id)) == 1
|
||||
|
||||
finished = _load_fixture(
|
||||
"room_finished.json", event_id=f"evt-{uuid.uuid4()}", room_name=conference.slug
|
||||
)
|
||||
assert (await _post_webhook(client, finished)).status_code == 200
|
||||
|
||||
assert await hand_queue.snapshot(conference.id) == []
|
||||
|
||||
|
||||
async def test_pinned_conference_returns_to_scheduled_on_finish(
|
||||
client: httpx.AsyncClient, db_session: AsyncSession
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user