Первоначальная версия VidConf

This commit is contained in:
2026-07-23 01:04:01 +03:00
commit 896455381a
335 changed files with 61527 additions and 0 deletions

83
backend/schemas/chat.py Normal file
View File

@@ -0,0 +1,83 @@
"""Pydantic-схемы протокола WS-чата конференции (`api/chat.py`).
Входящий протокол — дискриминированное объединение по полю `type`: первым
сообщением клиент обязан прислать `auth` (LiveKit access-токен, не
query-параметр — не палим токен в логах nginx), далее — произвольное число
`message`. Исходящий протокол — `history` (один раз, сразу после успешной
аутентификации), `message` (broadcast через Redis pub/sub) и `error`.
"""
from datetime import UTC, datetime
from typing import Annotated, Any, Literal
from pydantic import BaseModel, Field, TypeAdapter, field_serializer, field_validator
# Ограничение длины текста сообщения.
MAX_MESSAGE_LENGTH = 2000
def _to_iso_z(value: datetime) -> str:
"""Отформатировать aware-datetime как UTC ISO-строку с суффиксом `Z`."""
return value.astimezone(UTC).isoformat().replace("+00:00", "Z")
class ChatAuthIn(BaseModel):
"""Первое сообщение клиента — аутентификация LiveKit access-токеном."""
type: Literal["auth"]
token: str
class ChatMessageIn(BaseModel):
"""Сообщение клиента с текстом чата — text обрезается по пробелам и не должен быть пустым."""
type: Literal["message"]
text: str = Field(min_length=1, max_length=MAX_MESSAGE_LENGTH)
@field_validator("text", mode="before")
@classmethod
def _strip(cls, value: Any) -> Any:
return value.strip() if isinstance(value, str) else value
# Дискриминированное объединение входящих сообщений клиента по полю `type`.
ChatClientEnvelope = Annotated[ChatAuthIn | ChatMessageIn, Field(discriminator="type")]
chat_client_envelope_adapter: TypeAdapter[ChatAuthIn | ChatMessageIn] = TypeAdapter(
ChatClientEnvelope
)
class ChatMessageOut(BaseModel):
"""Одно сообщение чата в исходящем протоколе (history/broadcast)."""
id: int
author_id: str | None
author_name: str
is_guest: bool
text: str
created_at: datetime
@field_serializer("created_at")
def _serialize_created_at(self, value: datetime) -> str:
return _to_iso_z(value)
class ChatHistoryOut(BaseModel):
"""История последних сообщений открытой сессии — отправляется один раз после auth."""
type: Literal["history"] = "history"
messages: list[ChatMessageOut]
class ChatMessageEventOut(BaseModel):
"""Одно новое сообщение чата — broadcast через Redis pub/sub (в т.ч. отправителю)."""
type: Literal["message"] = "message"
message: ChatMessageOut
class ChatErrorOut(BaseModel):
"""Сообщение об ошибке протокола (например, невалидный текст) без разрыва соединения."""
type: Literal["error"] = "error"
code: str