Первоначальная версия VidConf
This commit is contained in:
83
backend/schemas/chat.py
Normal file
83
backend/schemas/chat.py
Normal 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
|
||||
Reference in New Issue
Block a user