This reverts commit 68701dbb1d.
This commit is contained in:
jimmy-sketch
2025-07-24 20:23:26 +08:00
parent 68701dbb1d
commit 7b6f92593e
28 changed files with 1394 additions and 644 deletions

1
app/models/__init__.py Normal file
View File

@@ -0,0 +1 @@

29
app/models/oauth.py Normal file
View File

@@ -0,0 +1,29 @@
# OAuth 相关模型
from __future__ import annotations
from pydantic import BaseModel
class TokenRequest(BaseModel):
grant_type: str
username: str | None = None
password: str | None = None
refresh_token: str | None = None
client_id: str
client_secret: str
scope: str = "*"
class TokenResponse(BaseModel):
access_token: str
token_type: str = "Bearer"
expires_in: int
refresh_token: str
scope: str = "*"
class UserCreate(BaseModel):
username: str
password: str
email: str
country_code: str = "CN"

40
app/models/score.py Normal file
View File

@@ -0,0 +1,40 @@
from enum import Enum, IntEnum
from typing import Any
from pydantic import BaseModel
class GameMode(str, Enum):
OSU = "osu"
TAIKO = "taiko"
FRUITS = "fruits"
MANIA = "mania"
class APIMod(BaseModel):
acronym: str
settings: dict[str, Any] = {}
# https://github.com/ppy/osu/blob/master/osu.Game/Rulesets/Scoring/HitResult.cs
class HitResult(IntEnum):
PERFECT = 0 # [Order(0)]
GREAT = 1 # [Order(1)]
GOOD = 2 # [Order(2)]
OK = 3 # [Order(3)]
MEH = 4 # [Order(4)]
MISS = 5 # [Order(5)]
LARGE_TICK_HIT = 6 # [Order(6)]
SMALL_TICK_HIT = 7 # [Order(7)]
SLIDER_TAIL_HIT = 8 # [Order(8)]
LARGE_BONUS = 9 # [Order(9)]
SMALL_BONUS = 10 # [Order(10)]
LARGE_TICK_MISS = 11 # [Order(11)]
SMALL_TICK_MISS = 12 # [Order(12)]
IGNORE_HIT = 13 # [Order(13)]
IGNORE_MISS = 14 # [Order(14)]
NONE = 15 # [Order(15)]
COMBO_BREAK = 16 # [Order(16)]
LEGACY_COMBO_INCREASE = 99 # [Order(99)] @deprecated

31
app/models/signalr.py Normal file
View File

@@ -0,0 +1,31 @@
from __future__ import annotations
from typing import Any
from pydantic import BaseModel, Field, model_validator
class MessagePackArrayModel(BaseModel):
@model_validator(mode="before")
@classmethod
def unpack(cls, v: Any) -> Any:
if isinstance(v, list):
fields = list(cls.model_fields.keys())
if len(v) != len(fields):
raise ValueError(f"Expected list of length {len(fields)}, got {len(v)}")
return dict(zip(fields, v))
return v
class Transport(BaseModel):
transport: str
transfer_formats: list[str] = Field(
default_factory=lambda: ["Binary"], alias="transferFormats"
)
class NegotiateResponse(BaseModel):
connectionId: str
connectionToken: str
negotiateVersion: int = 1
availableTransports: list[Transport]

View File

@@ -0,0 +1,99 @@
from __future__ import annotations
import datetime
from enum import IntEnum
from typing import Any
import msgpack
from pydantic import Field, field_validator
from .signalr import MessagePackArrayModel
from .score import (
APIMod as APIModBase,
HitResult,
)
class APIMod(APIModBase, MessagePackArrayModel): ...
class SpectatedUserState(IntEnum):
Idle = 0
Playing = 1
Paused = 2
Passed = 3
Failed = 4
Quit = 5
class SpectatorState(MessagePackArrayModel):
beatmap_id: int | None = None
ruleset_id: int | None = None # 0,1,2,3
mods: list[APIMod] = Field(default_factory=list)
state: SpectatedUserState
maximum_statistics: dict[HitResult, int] = Field(default_factory=dict)
def __eq__(self, other: object) -> bool:
if not isinstance(other, SpectatorState):
return False
return (
self.beatmap_id == other.beatmap_id
and self.ruleset_id == other.ruleset_id
and self.mods == other.mods
and self.state == other.state
)
class ScoreProcessorStatistics(MessagePackArrayModel):
base_score: int
maximum_base_score: int
accuracy_judgement_count: int
combo_portion: float
bouns_portion: float
class FrameHeader(MessagePackArrayModel):
total_score: int
acc: float
combo: int
max_combo: int
statistics: dict[HitResult, int] = Field(default_factory=dict)
score_processor_statistics: ScoreProcessorStatistics
received_time: datetime.datetime
mods: list[APIMod] = Field(default_factory=list)
@field_validator("received_time", mode="before")
@classmethod
def validate_timestamp(cls, v: Any) -> datetime.datetime:
if isinstance(v, msgpack.ext.Timestamp):
return v.to_datetime()
if isinstance(v, list):
return v[0].to_datetime()
if isinstance(v, datetime.datetime):
return v
if isinstance(v, int | float):
return datetime.datetime.fromtimestamp(v, tz=datetime.UTC)
if isinstance(v, str):
return datetime.datetime.fromisoformat(v)
raise ValueError(f"Cannot convert {type(v)} to datetime")
class ReplayButtonState(IntEnum):
NONE = 0
LEFT1 = 1
RIGHT1 = 2
LEFT2 = 4
RIGHT2 = 8
SMOKE = 16
class LegacyReplayFrame(MessagePackArrayModel):
time: int # from ReplayFrame,the parent of LegacyReplayFrame
x: float | None = None
y: float | None = None
button_state: ReplayButtonState
class FrameDataBundle(MessagePackArrayModel):
header: FrameHeader
frames: list[LegacyReplayFrame]

214
app/models/user.py Normal file
View File

@@ -0,0 +1,214 @@
from __future__ import annotations
from datetime import datetime
from enum import Enum
from .score import GameMode
from pydantic import BaseModel
from app.database import LazerUserAchievement # 添加数据库模型导入
class PlayStyle(str, Enum):
MOUSE = "mouse"
KEYBOARD = "keyboard"
TABLET = "tablet"
TOUCH = "touch"
class Country(BaseModel):
code: str
name: str
class Cover(BaseModel):
custom_url: str | None = None
url: str
id: int | None = None
class Level(BaseModel):
current: int
progress: int
class GradeCounts(BaseModel):
ss: int = 0
ssh: int = 0
s: int = 0
sh: int = 0
a: int = 0
class Statistics(BaseModel):
count_100: int = 0
count_300: int = 0
count_50: int = 0
count_miss: int = 0
level: Level
global_rank: int | None = None
global_rank_exp: int | None = None
pp: float = 0.0
pp_exp: float = 0.0
ranked_score: int = 0
hit_accuracy: float = 0.0
play_count: int = 0
play_time: int = 0
total_score: int = 0
total_hits: int = 0
maximum_combo: int = 0
replays_watched_by_others: int = 0
is_ranked: bool = False
grade_counts: GradeCounts
country_rank: int | None = None
rank: dict | None = None
class Kudosu(BaseModel):
available: int = 0
total: int = 0
class MonthlyPlaycount(BaseModel):
start_date: str
count: int
class UserAchievement(BaseModel):
achieved_at: datetime
achievement_id: int
# 添加数据库模型转换方法
def to_db_model(self, user_id: int) -> LazerUserAchievement:
return LazerUserAchievement(
user_id=user_id,
achievement_id=self.achievement_id,
achieved_at=self.achieved_at
)
class RankHighest(BaseModel):
rank: int
updated_at: datetime
class RankHistory(BaseModel):
mode: str
data: list[int]
class DailyChallengeStats(BaseModel):
daily_streak_best: int = 0
daily_streak_current: int = 0
last_update: datetime | None = None
last_weekly_streak: datetime | None = None
playcount: int = 0
top_10p_placements: int = 0
top_50p_placements: int = 0
user_id: int
weekly_streak_best: int = 0
weekly_streak_current: int = 0
class Team(BaseModel):
flag_url: str
id: int
name: str
short_name: str
class Page(BaseModel):
html: str = ""
raw: str = ""
class User(BaseModel):
# 基本信息
id: int
username: str
avatar_url: str
country_code: str
default_group: str = "default"
is_active: bool = True
is_bot: bool = False
is_deleted: bool = False
is_online: bool = True
is_supporter: bool = False
is_restricted: bool = False
last_visit: datetime | None = None
pm_friends_only: bool = False
profile_colour: str | None = None
# 个人资料
cover_url: str | None = None
discord: str | None = None
has_supported: bool = False
interests: str | None = None
join_date: datetime
location: str | None = None
max_blocks: int = 100
max_friends: int = 500
occupation: str | None = None
playmode: GameMode = GameMode.OSU
playstyle: list[PlayStyle] = []
post_count: int = 0
profile_hue: int | None = None
profile_order: list[str] = [
"me",
"recent_activity",
"top_ranks",
"medals",
"historical",
"beatmaps",
"kudosu",
]
title: str | None = None
title_url: str | None = None
twitter: str | None = None
website: str | None = None
session_verified: bool = False
support_level: int = 0
# 关联对象
country: Country
cover: Cover
kudosu: Kudosu
statistics: Statistics
statistics_rulesets: dict[str, Statistics]
# 计数信息
beatmap_playcounts_count: int = 0
comments_count: int = 0
favourite_beatmapset_count: int = 0
follower_count: int = 0
graveyard_beatmapset_count: int = 0
guest_beatmapset_count: int = 0
loved_beatmapset_count: int = 0
mapping_follower_count: int = 0
nominated_beatmapset_count: int = 0
pending_beatmapset_count: int = 0
ranked_beatmapset_count: int = 0
ranked_and_approved_beatmapset_count: int = 0
unranked_beatmapset_count: int = 0
scores_best_count: int = 0
scores_first_count: int = 0
scores_pinned_count: int = 0
scores_recent_count: int = 0
# 历史数据
account_history: list[dict] = []
active_tournament_banner: dict | None = None
active_tournament_banners: list[dict] = []
badges: list[dict] = []
current_season_stats: dict | None = None
daily_challenge_user_stats: DailyChallengeStats | None = None
groups: list[dict] = []
monthly_playcounts: list[MonthlyPlaycount] = []
page: Page = Page()
previous_usernames: list[str] = []
rank_highest: RankHighest | None = None
rank_history: RankHistory | None = None
rankHistory: RankHistory | None = None # 兼容性别名
replays_watched_counts: list[dict] = []
team: Team | None = None
user_achievements: list[UserAchievement] = []