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:
61
backend/schemas/room_events.py
Normal file
61
backend/schemas/room_events.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""Pydantic-схемы событий комнаты, мультиплексируемых поверх WS-чата (`api/chat.py`).
|
||||
|
||||
Отдельный протокол от собственно чата (`schemas/chat.py`): очередь поднятых
|
||||
рук и уведомления о принудительном мьюте — эфемерное состояние звонка
|
||||
(Redis, не БД, см. `services/hand_queue.py`) и не должны попадать в
|
||||
персистентную историю сообщений чата, хотя и едут по тому же соединению.
|
||||
"""
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, Field, field_serializer
|
||||
|
||||
|
||||
class RaiseHandIn(BaseModel):
|
||||
"""Клиент поднимает свою руку."""
|
||||
|
||||
type: Literal["raise_hand"]
|
||||
|
||||
|
||||
class LowerHandIn(BaseModel):
|
||||
"""Клиент опускает руку — свою (без `identity`) либо, только для организатора, чужую."""
|
||||
|
||||
type: Literal["lower_hand"]
|
||||
identity: str | None = None
|
||||
|
||||
|
||||
class HandQueueEntryOut(BaseModel):
|
||||
"""Один участник в очереди поднятых рук."""
|
||||
|
||||
identity: str
|
||||
name: str
|
||||
raised_at: datetime
|
||||
|
||||
@field_serializer("raised_at")
|
||||
def _serialize_raised_at(self, value: datetime) -> str:
|
||||
return value.astimezone(UTC).isoformat().replace("+00:00", "Z")
|
||||
|
||||
|
||||
class HandQueueOut(BaseModel):
|
||||
"""Снапшот очереди поднятых рук — рассылается всем участникам при любом изменении."""
|
||||
|
||||
type: Literal["hand_queue"] = "hand_queue"
|
||||
queue: list[HandQueueEntryOut] = Field(default_factory=list)
|
||||
|
||||
|
||||
ForcedMuteSource = Literal["microphone", "camera"]
|
||||
|
||||
|
||||
class ForcedMuteOut(BaseModel):
|
||||
"""Организатор принудительно выключил трек участника (задача B2) — уведомление всем.
|
||||
|
||||
Рассылается всем (не только затронутому), как и `HandQueueOut`: канал —
|
||||
общий broadcast, а не адресная доставка одному соединению; получатели,
|
||||
для которых `identity` не совпадает с их собственной, событие
|
||||
игнорируют.
|
||||
"""
|
||||
|
||||
type: Literal["forced_mute"] = "forced_mute"
|
||||
identity: str
|
||||
source: ForcedMuteSource
|
||||
Reference in New Issue
Block a user