"""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