Files
vidconf/backend/schemas/chat.py

84 lines
3.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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