Reapply "Merge branch 'main' of https://github.com/GooGuTeam/osu_lazer_api"
This reverts commit 68701dbb1d.
This commit is contained in:
1
app/models/__init__.py
Normal file
1
app/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
29
app/models/oauth.py
Normal file
29
app/models/oauth.py
Normal 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
40
app/models/score.py
Normal 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
31
app/models/signalr.py
Normal 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]
|
||||
99
app/models/spectator_hub.py
Normal file
99
app/models/spectator_hub.py
Normal 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
214
app/models/user.py
Normal 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] = []
|
||||
Reference in New Issue
Block a user