mirror of
https://github.com/A-Minos/nonebot-plugin-tetris-stats.git
synced 2026-03-05 05:36:54 +08:00
✨ 适配 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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -21,3 +21,13 @@ ValidRank = Literal[
|
||||
]
|
||||
|
||||
Rank = ValidRank | Literal['z'] # 未定级
|
||||
|
||||
Summaries = Literal[
|
||||
'40l',
|
||||
'blitz',
|
||||
'zenith',
|
||||
'zenithex',
|
||||
# 'league', # 等待正式赛季开始
|
||||
'zen',
|
||||
'achievements',
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user