Files
ForbiddenStarsApp/backend/app/schemas/api.py
T
NotBigGhostandClaude Fable 5 4a681430af UI: рейтинг новичков в карточке профиля — серебристый
ProfileStats теперь отдаёт min_games (порог «Новичков» из API, без
дублирования константы на фронте): пока сыграно меньше min_games партий,
«Очки (рейтинг)» в профиле показываются серебристым, как в топах;
золотой — только подтверждённый рейтинг.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 03:10:53 +03:00

508 lines
13 KiB
Python
Raw 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-схемы (граница HTTP). Из них генерируется OpenAPI → типы фронта."""
from __future__ import annotations
from datetime import date
from typing import 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
bio: str | None = None
avatar_url: str | None = None
class MeRead(UserRead):
groups: list[GroupBrief] = []
class NicknameUpdate(BaseModel):
nickname: str
class ProfileUpdate(BaseModel):
bio: str | None = None
class ActiveGroupUpdate(BaseModel):
group_id: int | None = None
class NicknameAvailable(BaseModel):
available: bool
class UserSuggestion(BaseModel):
"""Подсказка автокомплита по нику (приглашение в группу и т.п.)."""
user_id: int
nickname: str
avatar_url: str | None = None
# ─── Группы и членство ───────────────────────────────────────────────────────
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 InvitationRead(BaseModel):
id: int
group_id: int
group_name: str
invited_by_nickname: str | None = None
created_at: str
# ─── Уведомления ───────────────────────────────────────────────────────────────
class NotificationRead(BaseModel):
id: int
type: str
title: str
body: str | None = None
link: str | None = None
read_at: str | None = None
created_at: str
class NotificationList(BaseModel):
items: list[NotificationRead]
unread_count: int
class NotificationMarkRead(BaseModel):
ids: list[int] | None = None
# ─── Партии ──────────────────────────────────────────────────────────────────
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 | None = Field(default=None, ge=1) # None у выбывшего (eliminated)
eliminated: bool = False # выбыл из партии → авто-проставится последнее место
comment: str | None = None
faction_id: int | None = None # опц. смена фракции при завершении
class MatchFinish(BaseModel):
participants: list[MatchFinishParticipant]
win_reason: WinReason
overall_comment: str | None = None
# Оптимистичная блокировка: версия партии, которую видел клиент (см. MatchRead.version).
expected_version: str | None = None
# Полный участник (правка завершённой партии админом).
class ParticipantInput(BaseModel):
user_id: int
faction_id: int
place: int | None = Field(default=None, ge=1) # None у выбывшего (eliminated)
eliminated: bool = False
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
expected_version: str | None = None # оптимистичная блокировка
class MatchParticipantRead(BaseModel):
user_id: int
nickname: str
faction_id: int
faction_name: str
place: int | None = None
eliminated: bool = False
was_random: bool
comment: str | None = None
class AttachmentRead(BaseModel):
id: int
kind: str # 'photo' (задел под видео)
url: str
mime_type: str
size_bytes: int
created_at: str
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 # может ли текущий зритель править/завершать партию
version: str # для оптимистичной блокировки (iso updated_at); клиент шлёт обратно
participants: list[MatchParticipantRead] = []
attachments: list[AttachmentRead] = []
# ─── Статистика ──────────────────────────────────────────────────────────────
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
avatar_url: str | 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] = []
# Порог «Новичков» (MIN_GAMES) — чтобы UI единообразно подсвечивал
# неподтверждённый рейтинг, не дублируя константу на фронте.
min_games: int = 0
class PublicProfile(BaseModel):
# Профиль другого игрока (read-only): шапка + глобальная статистика.
user_id: int
nickname: str
bio: str | None = None
avatar_url: str | None = None
stats: ProfileStats
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] = []
inactive: 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
eliminated: bool = False
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 AchievementRead(BaseModel):
slug: str
name: str
description: str = ""
condition: str = "" # текст condition.py (задел; не исполняется)
has_condition: bool = False
icon_url: str | None = None
class AchievementCreate(BaseModel):
name: str
description: str | None = None
condition: str | None = None
class AchievementUpdate(BaseModel):
name: str | None = None
description: str | None = None
condition: str | None = None
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