413 lines
9.9 KiB
Python
413 lines
9.9 KiB
Python
"""Pydantic-схемы (граница HTTP). Из них генерируется OpenAPI → типы фронта."""
|
|
from __future__ import annotations
|
|
|
|
from datetime import date
|
|
from typing import Any, Literal
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
|
|
WinReason = Literal["objectives", "worlds", "plastic", "resources"]
|
|
|
|
|
|
# ─── Auth ────────────────────────────────────────────────────────────────────
|
|
|
|
class AuthConfig(BaseModel):
|
|
# Доступные методы входа: ["telegram"] в проде, ["telegram","stub"] в деве.
|
|
methods: list[str] = []
|
|
telegram_bot_username: str | None = None
|
|
|
|
|
|
class TelegramAuthPayload(BaseModel):
|
|
# Полезная нагрузка Telegram Login Widget (проверяется по HMAC).
|
|
model_config = ConfigDict(extra="allow")
|
|
id: int
|
|
auth_date: int
|
|
hash: str
|
|
first_name: str | None = None
|
|
last_name: str | None = None
|
|
username: str | None = None
|
|
photo_url: str | None = None
|
|
|
|
|
|
class TelegramRegister(TelegramAuthPayload):
|
|
# Регистрация через Telegram с явно выбранным ником (когда тег занят/некорректен).
|
|
# Подпись виджета проверяется по тем же полям; nickname в HMAC не входит.
|
|
nickname: str
|
|
|
|
|
|
class DevLogin(BaseModel):
|
|
nickname: str
|
|
|
|
|
|
class DevUserCreate(BaseModel):
|
|
nickname: str
|
|
|
|
|
|
class DevUserRead(BaseModel):
|
|
id: int
|
|
nickname: str
|
|
is_active: bool = True
|
|
|
|
|
|
class OkResponse(BaseModel):
|
|
ok: bool = True
|
|
|
|
|
|
# ─── Справочники ─────────────────────────────────────────────────────────────
|
|
|
|
class ExpansionRead(BaseModel):
|
|
id: int
|
|
code: str
|
|
name_ru: str
|
|
is_base: bool
|
|
|
|
|
|
class FactionRead(BaseModel):
|
|
id: int
|
|
code: str
|
|
name_ru: str
|
|
expansion_id: int
|
|
|
|
|
|
class FactionRename(BaseModel):
|
|
name_ru: str
|
|
|
|
|
|
# ─── Пользователь ────────────────────────────────────────────────────────────
|
|
|
|
class GroupBrief(BaseModel):
|
|
id: int
|
|
name: str
|
|
role: str
|
|
|
|
|
|
class UserRead(BaseModel):
|
|
id: int
|
|
nickname: str
|
|
role: str
|
|
auth_provider: str
|
|
telegram_id: int | None = None
|
|
active_group_id: int | None = None
|
|
|
|
|
|
class MeRead(UserRead):
|
|
groups: list[GroupBrief] = []
|
|
|
|
|
|
class NicknameUpdate(BaseModel):
|
|
nickname: str
|
|
|
|
|
|
class ActiveGroupUpdate(BaseModel):
|
|
group_id: int | None = None
|
|
|
|
|
|
class NicknameAvailable(BaseModel):
|
|
available: bool
|
|
|
|
|
|
# ─── Группы и членство ───────────────────────────────────────────────────────
|
|
|
|
class GroupCreate(BaseModel):
|
|
name: str
|
|
expansion_ids: list[int] = []
|
|
|
|
|
|
class GroupRename(BaseModel):
|
|
name: str
|
|
|
|
|
|
class GroupExpansionsUpdate(BaseModel):
|
|
expansion_ids: list[int] = []
|
|
|
|
|
|
class GroupDetail(BaseModel):
|
|
id: int
|
|
name: str
|
|
owner_id: int
|
|
my_role: str
|
|
expansion_ids: list[int] = []
|
|
|
|
|
|
class MemberRead(BaseModel):
|
|
user_id: int
|
|
nickname: str
|
|
role: str
|
|
|
|
|
|
class MemberAdd(BaseModel):
|
|
nickname: str
|
|
|
|
|
|
class MemberRoleUpdate(BaseModel):
|
|
role: str
|
|
|
|
|
|
# ─── Партии ──────────────────────────────────────────────────────────────────
|
|
|
|
class RandomizeRequest(BaseModel):
|
|
group_id: int
|
|
exclude_faction_ids: list[int] = []
|
|
|
|
|
|
class RandomizeResponse(BaseModel):
|
|
faction: FactionRead
|
|
|
|
|
|
# Этап 1 (старт): только ростер, без мест.
|
|
class RosterParticipant(BaseModel):
|
|
user_id: int
|
|
faction_id: int
|
|
was_random: bool = False
|
|
|
|
|
|
class MatchCreate(BaseModel):
|
|
group_id: int
|
|
participants: list[RosterParticipant]
|
|
|
|
|
|
# Этап 2 (завершение): места, комментарии, причина победы.
|
|
class MatchFinishParticipant(BaseModel):
|
|
user_id: int
|
|
place: int = Field(ge=1)
|
|
comment: str | None = None
|
|
faction_id: int | None = None # опц. смена фракции при завершении
|
|
|
|
|
|
class MatchFinish(BaseModel):
|
|
participants: list[MatchFinishParticipant]
|
|
win_reason: WinReason
|
|
overall_comment: str | None = None
|
|
|
|
|
|
# Полный участник (правка завершённой партии админом).
|
|
class ParticipantInput(BaseModel):
|
|
user_id: int
|
|
faction_id: int
|
|
place: int = Field(ge=1)
|
|
was_random: bool = False
|
|
comment: str | None = None
|
|
|
|
|
|
class MatchUpdate(BaseModel):
|
|
played_at: date | None = None
|
|
overall_comment: str | None = None
|
|
win_reason: WinReason | None = None
|
|
participants: list[ParticipantInput] | None = None
|
|
|
|
|
|
class MatchParticipantRead(BaseModel):
|
|
user_id: int
|
|
nickname: str
|
|
faction_id: int
|
|
faction_name: str
|
|
place: int | None = None
|
|
was_random: bool
|
|
comment: str | None = None
|
|
|
|
|
|
class MatchRead(BaseModel):
|
|
id: int
|
|
group_id: int
|
|
status: str
|
|
played_at: date
|
|
started_at: str | None = None
|
|
finished_at: str | None = None
|
|
duration_minutes: int | None = None
|
|
win_reason: WinReason | None = None
|
|
player_count: int
|
|
overall_comment: str | None = None
|
|
created_by: int
|
|
can_modify: bool = False # может ли текущий зритель править/завершать партию
|
|
participants: list[MatchParticipantRead] = []
|
|
attachments: list[Any] = [] # задел под вложения (всегда пусто в v1)
|
|
|
|
|
|
# ─── Статистика ──────────────────────────────────────────────────────────────
|
|
|
|
class OverallStats(BaseModel):
|
|
games: int
|
|
wins: int
|
|
win_rate: float
|
|
avg_place: float | None = None
|
|
score: float | None = None
|
|
|
|
|
|
class LeaderboardEntry(OverallStats):
|
|
user_id: int
|
|
nickname: str
|
|
rank: int | None = None
|
|
|
|
|
|
class Leaderboard(BaseModel):
|
|
entries: list[LeaderboardEntry] = []
|
|
provisional: list[LeaderboardEntry] = []
|
|
min_games: int
|
|
|
|
|
|
class FactionStat(BaseModel):
|
|
faction_id: int
|
|
code: str
|
|
name_ru: str
|
|
expansion_code: str
|
|
games: int
|
|
wins: int
|
|
win_rate: float
|
|
avg_place: float | None = None
|
|
score: float | None = None
|
|
|
|
|
|
class RecentFormItem(BaseModel):
|
|
place: int
|
|
player_count: int
|
|
played_at: str
|
|
|
|
|
|
class ProfileStats(BaseModel):
|
|
user_id: int
|
|
overall: OverallStats
|
|
factions: list[FactionStat] = []
|
|
best_faction: FactionStat | None = None
|
|
worst_faction: FactionStat | None = None
|
|
most_played_faction: FactionStat | None = None
|
|
recent_form: list[RecentFormItem] = []
|
|
|
|
|
|
class FactionMeta(BaseModel):
|
|
faction_id: int
|
|
code: str
|
|
name_ru: str
|
|
games: int
|
|
wins: int
|
|
available: bool
|
|
|
|
|
|
class GroupStats(BaseModel):
|
|
group_id: int
|
|
total_matches: int
|
|
last_match_at: str | None = None
|
|
leaderboard: list[LeaderboardEntry] = []
|
|
provisional: list[LeaderboardEntry] = []
|
|
faction_meta: list[FactionMeta] = []
|
|
min_games: int
|
|
|
|
|
|
class MatchListParticipant(BaseModel):
|
|
user_id: int
|
|
nickname: str
|
|
faction_id: int
|
|
faction_name: str
|
|
place: int | None = None
|
|
was_random: bool
|
|
comment: str | None = None
|
|
|
|
|
|
class MatchListItem(BaseModel):
|
|
id: int
|
|
status: str
|
|
played_at: str
|
|
started_at: str | None = None
|
|
finished_at: str | None = None
|
|
duration_minutes: int | None = None
|
|
win_reason: WinReason | None = None
|
|
player_count: int
|
|
overall_comment: str | None = None
|
|
created_by: int
|
|
participants: list[MatchListParticipant] = []
|
|
|
|
|
|
class MatchList(BaseModel):
|
|
items: list[MatchListItem] = []
|
|
total: int
|
|
limit: int
|
|
offset: int
|
|
|
|
|
|
class GroupBriefStats(OverallStats):
|
|
id: int
|
|
name: str
|
|
|
|
|
|
class HomeInProgressMatch(BaseModel):
|
|
id: int
|
|
group_id: int
|
|
group_name: str
|
|
started_at: str | None = None
|
|
player_count: int
|
|
participants: list[MatchListParticipant] = []
|
|
|
|
|
|
class HomeResponse(BaseModel):
|
|
leaderboard: list[LeaderboardEntry] = []
|
|
provisional: list[LeaderboardEntry] = []
|
|
profile: ProfileStats
|
|
active_group: GroupBriefStats | None = None
|
|
in_progress: list[HomeInProgressMatch] = []
|
|
min_games: int
|
|
|
|
|
|
# ─── Админ ───────────────────────────────────────────────────────────────────
|
|
|
|
class AdminLogin(BaseModel):
|
|
username: str
|
|
password: str
|
|
|
|
|
|
class AdminMe(BaseModel):
|
|
id: int
|
|
nickname: str
|
|
role: str
|
|
|
|
|
|
class AdminUserRead(BaseModel):
|
|
id: int
|
|
nickname: str
|
|
role: str
|
|
is_active: bool
|
|
auth_provider: str
|
|
telegram_id: int | None = None
|
|
created_at: str
|
|
|
|
|
|
class AdminUserUpdate(BaseModel):
|
|
nickname: str | None = None
|
|
is_active: bool | None = None
|
|
|
|
|
|
class AdminGroupRead(BaseModel):
|
|
id: int
|
|
name: str
|
|
owner_id: int
|
|
created_at: str
|
|
|
|
|
|
class AdminMatchRead(BaseModel):
|
|
id: int
|
|
group_id: int
|
|
group_name: str | None = None
|
|
status: str
|
|
played_at: str
|
|
duration_minutes: int | None = None
|
|
win_reason: WinReason | None = None
|
|
player_count: int
|
|
created_by: int
|
|
created_at: str
|
|
|
|
|
|
class AuditLogItem(BaseModel):
|
|
id: int
|
|
actor_id: int | None = None
|
|
action: str
|
|
entity_type: str
|
|
entity_id: int | None = None
|
|
payload: dict | None = None
|
|
ip: str | None = None
|
|
user_agent: str | None = None
|
|
created_at: str
|
|
|
|
|
|
class AuditLogList(BaseModel):
|
|
items: list[AuditLogItem] = []
|
|
limit: int
|
|
offset: int
|