适配 TETR.IO 新赛季 (#380)

*  新 api 的 schemas

* 👽️ 更新新赛季 api 的 schemas

*  添加依赖 async-lru

* 👽️ 更新新赛季 api 封装

* 👽️ 适配新赛季 api 40l

* 🐛 api_type 忘记更新了

* 👽️ 适配新赛季 api blitz

* 👽️ 适配新赛季 api bind

* 🔥 暂时删除一些指令
等待新赛季开始

* 🚨 auto fix by pre-commit hooks

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
呵呵です
2024-07-29 17:03:05 +08:00
committed by GitHub
parent d8d56b44db
commit 256d13d1df
21 changed files with 600 additions and 1194 deletions

View File

@@ -7,11 +7,12 @@ from sqlalchemy.orm import Mapped, MappedAsDataclass, mapped_column
from ....db.models import PydanticType
from .schemas.base import SuccessModel
from .typing import Summaries
class TETRIOHistoricalData(MappedAsDataclass, Model):
id: Mapped[int] = mapped_column(init=False, primary_key=True)
user_unique_identifier: Mapped[str] = mapped_column(String(24), index=True)
api_type: Mapped[Literal['User Info', 'User Records']] = mapped_column(String(16), index=True)
api_type: Mapped[Literal['User Info', Summaries]] = mapped_column(String(16), index=True)
data: Mapped[SuccessModel] = mapped_column(PydanticType(get_model=[SuccessModel.__subclasses__], models=set()))
update_time: Mapped[datetime] = mapped_column(DateTime, index=True)

View File

@@ -1,5 +1,7 @@
from typing import overload
from types import MappingProxyType
from typing import Literal, overload
from async_lru import alru_cache
from nonebot.compat import type_validate_json
from ....db import anti_duplicate_add
@@ -9,12 +11,31 @@ from ..constant import BASE_URL, USER_ID, USER_NAME
from .cache import Cache
from .models import TETRIOHistoricalData
from .schemas.base import FailedModel
from .schemas.summaries import (
AchievementsSuccessModel,
SoloSuccessModel,
SummariesModel,
ZenithSuccessModel,
ZenSuccessModel,
)
from .schemas.summaries.base import User as SummariesUser
from .schemas.user import User
from .schemas.user_info import UserInfo, UserInfoSuccess
from .schemas.user_records import SoloModeRecord, UserRecords, UserRecordsSuccess, Zen
from .typing import Summaries
class Player:
__SUMMARIES_MAPPING: MappingProxyType[Summaries, type[SummariesModel]] = MappingProxyType(
{
'40l': SoloSuccessModel,
'blitz': SoloSuccessModel,
'zenith': ZenithSuccessModel,
'zenithex': ZenithSuccessModel,
'zen': ZenSuccessModel,
'achievements': AchievementsSuccessModel,
}
)
@overload
def __init__(self, *, user_id: str, trust: bool = False): ...
@overload
@@ -36,7 +57,7 @@ class Player:
raise ValueError(msg)
self.__user: User | None = None
self._user_info: UserInfoSuccess | None = None
self._user_records: UserRecordsSuccess | None = None
self._summaries: dict[Summaries, SummariesModel] = {}
@property
def _request_user_parameter(self) -> str:
@@ -49,14 +70,21 @@ class Player:
@property
async def user(self) -> User:
if self.__user is None:
if self.__user is not None:
return self.__user
if (user := (await self._get_local_summaries_user())) is not None:
self.__user = User(
ID=user.id,
name=user.username,
)
else:
user_info = await self.get_info()
self.__user = User(
ID=user_info.data.user.id,
name=user_info.data.user.username,
ID=user_info.data.id,
name=user_info.data.username,
)
self.user_id = user_info.data.user.id
self.user_name = user_info.data.user.username
self.user_id = user_info.data.id
self.user_name = user_info.data.username
return self.__user
async def get_info(self) -> UserInfoSuccess:
@@ -79,36 +107,85 @@ class Player:
)
return self._user_info
async def get_records(self) -> UserRecordsSuccess:
"""Get User Records"""
if self._user_records is None:
raw_user_records = await Cache.get(
splice_url([BASE_URL, 'users/', f'{self._request_user_parameter}/', 'records'])
@overload
async def get_summaries(self, summaries_type: Literal['40l']) -> SoloSuccessModel: ...
@overload
async def get_summaries(self, summaries_type: Literal['blitz']) -> SoloSuccessModel: ...
@overload
async def get_summaries(self, summaries_type: Literal['zenith']) -> ZenithSuccessModel: ...
@overload
async def get_summaries(self, summaries_type: Literal['zenithex']) -> ZenithSuccessModel: ...
@overload
async def get_summaries(self, summaries_type: Literal['zen']) -> ZenSuccessModel: ...
@overload
async def get_summaries(self, summaries_type: Literal['achievements']) -> AchievementsSuccessModel: ...
async def get_summaries(self, summaries_type: Summaries) -> SummariesModel:
if summaries_type not in self._summaries:
raw_summaries = await Cache.get(
splice_url([BASE_URL, 'users/', f'{self._request_user_parameter}/', 'summaries/', summaries_type])
)
user_records: UserRecords = type_validate_json(UserRecords, raw_user_records) # type: ignore[arg-type]
if isinstance(user_records, FailedModel):
msg = f'用户Solo数据请求错误:\n{user_records.error}'
summaries: SummariesModel | FailedModel = type_validate_json(
self.__SUMMARIES_MAPPING[summaries_type] | FailedModel, # type: ignore[arg-type]
raw_summaries,
)
if isinstance(summaries, FailedModel):
msg = f'用户Summaries数据请求错误:\n{summaries.error}'
raise RequestError(msg)
self._user_records = user_records
self._summaries[summaries_type] = summaries
await anti_duplicate_add(
TETRIOHistoricalData,
TETRIOHistoricalData(
user_unique_identifier=(await self.user).unique_identifier,
api_type='User Records',
data=user_records,
update_time=user_records.cache.cached_at,
api_type=summaries_type,
data=summaries,
update_time=summaries.cache.cached_at,
),
)
return self._user_records
return self._summaries[summaries_type]
@property
async def sprint(self) -> SoloModeRecord:
return (await self.get_records()).data.records.sprint
@alru_cache
async def sprint(self) -> SoloSuccessModel:
return await self.get_summaries('40l')
@property
async def blitz(self) -> SoloModeRecord:
return (await self.get_records()).data.records.blitz
@alru_cache
async def blitz(self) -> SoloSuccessModel:
return await self.get_summaries('blitz')
@property
async def zen(self) -> Zen:
return (await self.get_records()).data.zen
@alru_cache
async def zen(self) -> ZenSuccessModel:
return await self.get_summaries('zen')
async def _get_local_summaries_user(self) -> SummariesUser | None:
allow_summaries: set[Literal['40l', 'blitz', 'zenith', 'zenithex']] = {
'40l',
'blitz',
'zenith',
'zenithex',
}
if has_summaries := (allow_summaries & self._summaries.keys()):
for i in has_summaries:
if (record := (await self.get_summaries(i)).data.record) is not None:
return record.user
return None
@property
@alru_cache
async def avatar_revision(self) -> int | None:
if self._user_info is not None:
return self._user_info.data.avatar_revision
if (user := (await self._get_local_summaries_user())) is not None:
return user.avatar_revision
return (await self.get_info()).data.avatar_revision
@property
@alru_cache
async def banner_revision(self) -> int | None:
if self._user_info is not None:
return self._user_info.data.banner_revision
if (user := (await self._get_local_summaries_user())) is not None:
return user.banner_revision
return (await self.get_info()).data.banner_revision

View File

@@ -0,0 +1,20 @@
from .achievements import Achievements, AchievementsSuccessModel
from .solo import Blitz, SoloSuccessModel, Sprint
from .zen import Zen, ZenSuccessModel
from .zenith import Zenith, ZenithEx, ZenithSuccessModel
SummariesModel = AchievementsSuccessModel | SoloSuccessModel | ZenSuccessModel | ZenithSuccessModel
__all__ = [
'Achievements',
'AchievementsSuccessModel',
'Blitz',
'Sprint',
'SoloSuccessModel',
'Zen',
'ZenSuccessModel',
'Zenith',
'ZenithEx',
'ZenithSuccessModel',
'SummariesModel',
]

View File

@@ -0,0 +1,29 @@
from typing import TypeAlias
from pydantic import BaseModel
from ..base import FailedModel, SuccessModel
class Achievement(BaseModel):
# 这**都是些啥
k: int
o: int
rt: int
vt: int
min: int
deci: int
name: str
object: str
category: str
hidden: bool
desc: str
n: str
stub: bool
class AchievementsSuccessModel(SuccessModel):
data: list[Achievement]
Achievements: TypeAlias = AchievementsSuccessModel | FailedModel

View File

@@ -0,0 +1,29 @@
from pydantic import BaseModel
class User(BaseModel):
id: str
username: str
avatar_revision: int
banner_revision: int
country: str
verified: int
supporter: int
class AggregateStats(BaseModel):
apm: float
pps: float
vsscore: float
class Finesse(BaseModel):
combo: int
faults: int
perfectpieces: int
class P(BaseModel): # what is P
pri: float
sec: float
ter: float

View File

@@ -0,0 +1,103 @@
from datetime import datetime
from typing import Literal, TypeAlias
from pydantic import BaseModel, Field
from ..base import FailedModel, SuccessModel
from .base import AggregateStats, Finesse, P, User
class Time(BaseModel):
start: int
zero: bool
locked: bool
prev: int
frameoffset: int
class Clears(BaseModel):
singles: int
doubles: int
triples: int
quads: int
realtspins: int
minitspins: int
minitspinsingles: int
tspinsingles: int
minitspindoubles: int
tspindoubles: int
tspintriples: int
tspinquads: int
allclear: int
class Garbage(BaseModel):
sent: int
received: int
attack: int
cleared: int
class Stats(BaseModel):
seed: int
lines: int
level_lines: int
level_lines_needed: int
inputs: int
holds: int
time: Time
score: int
zenlevel: int
zenprogress: int
level: int
combo: int
currentcombopower: int | None = None
topcombo: int
btb: int
topbtb: int
currentbtbchainpower: int | None = None
tspins: int
piecesplaced: int
clears: Clears
garbage: Garbage
kills: int
finesse: Finesse
finaltime: float
class Results(BaseModel):
aggregatestats: AggregateStats
stats: Stats
gameoverreason: str
class Record(BaseModel):
id: str = Field(..., alias='_id')
replayid: str
stub: bool
gamemode: Literal['40l', 'blitz']
pb: bool
oncepb: bool
ts: datetime
revolution: None
user: User
otherusers: list
leaderboards: list[str]
results: Results
extras: dict
disputed: bool
p: P
class Data(BaseModel):
record: Record | None
rank: int
rank_local: int
class SoloSuccessModel(SuccessModel):
data: Data
Sprint: TypeAlias = SoloSuccessModel | FailedModel
Blitz: TypeAlias = SoloSuccessModel | FailedModel

View File

@@ -0,0 +1,17 @@
from typing import TypeAlias
from pydantic import BaseModel
from ..base import FailedModel, SuccessModel
class Data(BaseModel):
level: int
score: int
class ZenSuccessModel(SuccessModel):
data: Data
Zen: TypeAlias = ZenSuccessModel | FailedModel

View File

@@ -0,0 +1,131 @@
from datetime import datetime
from typing import Literal, TypeAlias
from pydantic import BaseModel, Field
from ..base import FailedModel, SuccessModel
from .base import AggregateStats, Finesse, P, User
class Clears(BaseModel):
singles: int
doubles: int
triples: int
quads: int
pentas: int
realtspins: int
minitspins: int
minitspinsingles: int
tspinsingles: int
minitspindoubles: int
tspindoubles: int
minitspintriples: int
tspintriples: int
minitspinquads: int
tspinquads: int
tspinpentas: int
allclear: int
class Garbage(BaseModel):
sent: int
sent_nomult: int
maxspike: int
maxspike_nomult: int
received: int
attack: int
cleared: int
class _Zenith(BaseModel):
altitude: float
rank: float
peakrank: float
avgrankpts: float
floor: int
targetingfactor: float
targetinggrace: float
totalbonus: float
revives: int
revives_total: int = Field(..., alias='revivesTotal')
speedrun: bool
speedrun_seen: bool
splits: list[int]
class Stats(BaseModel):
lines: int
level_lines: int
level_lines_needed: int
inputs: int
holds: int
score: int
zenlevel: int
zenprogress: int
level: int
combo: int
topcombo: int
combopower: int
btb: int
topbtb: int
btbpower: int
tspins: int
piecesplaced: int
clears: Clears
garbage: Garbage
kills: int
finesse: Finesse
zenith: _Zenith
finaltime: int
class Results(BaseModel):
aggregatestats: AggregateStats
stats: Stats
gameoverreason: str
class ExtrasZenith(BaseModel):
mods: list[str]
class Extras(BaseModel):
zenith: ExtrasZenith
class Record(BaseModel):
id: str = Field(..., alias='_id')
replayid: str
stub: bool
gamemode: Literal['zenith', 'zenithex']
pb: bool
oncepb: bool
ts: datetime
revolution: None
user: User
otherusers: list
leaderboards: list[str]
results: Results
extras: Extras
disputed: bool
p: P
class Best(BaseModel):
record: None # WTF
rank: int
class Data(BaseModel):
record: Record | None
rank: int
rank_local: int
best: Best
class ZenithSuccessModel(SuccessModel):
data: Data
Zenith: TypeAlias = ZenithSuccessModel | FailedModel
ZenithEx: TypeAlias = ZenithSuccessModel | FailedModel

View File

@@ -3,7 +3,6 @@ from typing import Literal
from pydantic import BaseModel, Field
from ..typing import Rank
from .base import FailedModel
from .base import SuccessModel as BaseSuccessModel
@@ -15,67 +14,6 @@ class Badge(BaseModel):
ts: datetime | Literal[False] | None = None
class MetaLeague(BaseModel):
decaying: bool
class NeverPlayedLeague(MetaLeague):
gamesplayed: Literal[0]
gameswon: Literal[0]
rating: Literal[-1]
rank: Literal['z']
standing: Literal[-1]
standing_local: Literal[-1]
next_rank: None
prev_rank: None
next_at: Literal[-1]
prev_at: Literal[-1]
percentile: Literal[-1]
percentile_rank: Literal['z']
apm: None = None
pps: None = None
vs: None = None
class NeverRatedLeague(MetaLeague):
gamesplayed: Literal[1, 2, 3, 4, 5, 6, 7, 8, 9]
gameswon: int
rating: Literal[-1]
rank: Literal['z']
standing: Literal[-1]
standing_local: Literal[-1]
next_rank: None
prev_rank: None
next_at: Literal[-1]
prev_at: Literal[-1]
percentile: Literal[-1]
percentile_rank: Literal['z']
apm: float
pps: float
vs: float | None = None
class RatedLeague(MetaLeague):
gamesplayed: int
gameswon: int
rating: float
rank: Rank
bestrank: Rank
standing: int
standing_local: int
next_rank: Rank | None = None
prev_rank: Rank | None = None
next_at: int
prev_at: int
percentile: float
percentile_rank: str
glicko: float
rd: float
apm: float
pps: float
vs: float | None = None
class Discord(BaseModel):
id: str
username: str
@@ -89,7 +27,7 @@ class Distinguishment(BaseModel):
type: str
class User(BaseModel):
class Data(BaseModel):
id: str = Field(..., alias='_id')
username: str
role: Literal['anon', 'user', 'bot', 'halfmod', 'mod', 'admin', 'sysop', 'banned']
@@ -105,7 +43,6 @@ class User(BaseModel):
supporter: bool | None = None # osk说是必有, 但实际上不是 fkosk
supporter_tier: int
verified: bool
league: NeverPlayedLeague | NeverRatedLeague | RatedLeague
avatar_revision: int | None = None
"""This user's avatar ID. Get their avatar at
@@ -122,10 +59,6 @@ class User(BaseModel):
distinguishment: Distinguishment | None = None
class Data(BaseModel):
user: User
class UserInfoSuccess(BaseSuccessModel):
data: Data

View File

@@ -21,3 +21,13 @@ ValidRank = Literal[
]
Rank = ValidRank | Literal['z'] # 未定级
Summaries = Literal[
'40l',
'blitz',
'zenith',
'zenithex',
# 'league', # 等待正式赛季开始
'zen',
'achievements',
]