Compare commits

...

2 Commits

Author SHA1 Message Date
20dcc2bc3d 🔖 1.5.0 2024-08-25 23:17:32 +08:00
606dddbca2 适配新赛季 list 2024-08-25 23:16:33 +08:00
14 changed files with 147 additions and 43 deletions

View File

@@ -23,7 +23,7 @@ command = Subcommand(
) )
from . import bind, config, query, rank, record # noqa: E402 from . import bind, config, list, query, rank, record # noqa: E402
main_command.add(command) main_command.add(command)
@@ -31,6 +31,7 @@ __all__ = [
'alc', 'alc',
'bind', 'bind',
'config', 'config',
'list',
'query', 'query',
'rank', 'rank',
'record', 'record',

View File

@@ -1,7 +1,6 @@
from typing import Literal, overload from typing import Literal, overload
from uuid import UUID from uuid import UUID
from msgspec import to_builtins
from nonebot.compat import type_validate_json from nonebot.compat import type_validate_json
from yarl import URL from yarl import URL
@@ -87,4 +86,4 @@ async def records(
async def get(url: URL, parameter: Parameter, extra_headers: dict | None = None) -> bytes: async def get(url: URL, parameter: Parameter, extra_headers: dict | None = None) -> bytes:
return await Cache.get(url % to_builtins(parameter), extra_headers) return await Cache.get(url % parameter.to_params(), extra_headers)

View File

@@ -1,7 +1,9 @@
from datetime import datetime from datetime import datetime
from typing import Literal from typing import Literal
from pydantic import BaseModel from pydantic import BaseModel, Field
from ...typing import Prisecter
class AggregateStats(BaseModel): class AggregateStats(BaseModel):
@@ -39,11 +41,29 @@ class Garbage(BaseModel):
cleared: int cleared: int
class P(BaseModel): # what is P class P(BaseModel):
pri: float pri: float
sec: float sec: float
ter: float ter: float
def to_prisecter(self) -> Prisecter:
return Prisecter(f'{self.pri}:{self.sec}:{self.ter}')
class ArCounts(BaseModel):
bronze: int | None = Field(default=None, alias='1')
silver: int | None = Field(default=None, alias='2')
gold: int | None = Field(default=None, alias='3')
platinum: int | None = Field(default=None, alias='4')
diamond: int | None = Field(default=None, alias='5')
issued: int | None = Field(default=None, alias='100')
top3: int | None = Field(default=None, alias='t3')
top5: int | None = Field(default=None, alias='t5')
top10: int | None = Field(default=None, alias='t10')
top25: int | None = Field(default=None, alias='t25')
top50: int | None = Field(default=None, alias='t50')
top100: int | None = Field(default=None, alias='t100')
class Cache(BaseModel): class Cache(BaseModel):
status: str status: str

View File

@@ -1,10 +1,15 @@
from typing import Annotated from typing import Any
from msgspec import Meta, Struct from pydantic import BaseModel, Field
from ...typing import Prisecter
class Parameter(Struct, omit_defaults=True): class Parameter(BaseModel):
after: str | None = None after: Prisecter | None = None
before: str | None = None before: Prisecter | None = None
limit: Annotated[int, Meta(ge=1, le=100)] = 25 limit: int = Field(default=25, ge=1, le=100)
country: str | None = None country: str | None = None
def to_params(self) -> dict[str, Any]:
return self.model_dump(exclude_defaults=True)

View File

@@ -4,22 +4,7 @@ from typing import Literal
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from ...typing import Rank, ValidRank from ...typing import Rank, ValidRank
from ..base import FailedModel, P, SuccessModel from ..base import ArCounts, FailedModel, P, SuccessModel
class ArCounts(BaseModel):
bronze: int | None = Field(default=None, alias='1')
silver: int | None = Field(default=None, alias='2')
gold: int | None = Field(default=None, alias='3')
platinum: int | None = Field(default=None, alias='4')
diamond: int | None = Field(default=None, alias='5')
issued: int | None = Field(default=None, alias='100')
top3: int | None = Field(default=None, alias='t3')
top5: int | None = Field(default=None, alias='t5')
top10: int | None = Field(default=None, alias='t10')
top25: int | None = Field(default=None, alias='t25')
top50: int | None = Field(default=None, alias='t50')
top100: int | None = Field(default=None, alias='t100')
class League(BaseModel): class League(BaseModel):

View File

@@ -7,5 +7,4 @@ class User(BaseModel):
avatar_revision: int | None avatar_revision: int | None
banner_revision: int | None banner_revision: int | None
country: str | None country: str | None
verified: int | None = None
supporter: int supporter: int

View File

@@ -3,7 +3,7 @@ from typing import Literal
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from .base import FailedModel from .base import ArCounts, FailedModel
from .base import SuccessModel as BaseSuccessModel from .base import SuccessModel as BaseSuccessModel
@@ -14,13 +14,19 @@ class Badge(BaseModel):
ts: datetime | Literal[False] | None = None ts: datetime | Literal[False] | None = None
class Discord(BaseModel): class Connection(BaseModel):
id: str id: str
username: str username: str
display_username: str
class Connections(BaseModel): class Connections(BaseModel):
discord: Discord | None = None discord: Connection | None = None
twitch: Connection | None = None
twitter: Connection | None = None
reddit: Connection | None = None
youtube: Connection | None = None
steam: Connection | None = None
class Distinguishment(BaseModel): class Distinguishment(BaseModel):
@@ -28,9 +34,9 @@ class Distinguishment(BaseModel):
class Data(BaseModel): class Data(BaseModel):
id: str = Field(..., alias='_id') id: str = Field(default=..., alias='_id')
username: str username: str
role: Literal['anon', 'user', 'bot', 'halfmod', 'mod', 'admin', 'sysop', 'banned'] role: Literal['anon', 'user', 'bot', 'halfmod', 'mod', 'admin', 'sysop', 'hidden', 'banned']
ts: datetime | None = None ts: datetime | None = None
botmaster: str | None = None botmaster: str | None = None
badges: list[Badge] badges: list[Badge]
@@ -42,7 +48,6 @@ class Data(BaseModel):
badstanding: bool | None = None badstanding: bool | None = None
supporter: bool | None = None # osk说是必有, 但实际上不是 fkosk supporter: bool | None = None # osk说是必有, 但实际上不是 fkosk
supporter_tier: int supporter_tier: int
verified: bool | None = None
avatar_revision: int | None = None avatar_revision: int | None = None
"""This user's avatar ID. Get their avatar at """This user's avatar ID. Get their avatar at
@@ -57,6 +62,9 @@ class Data(BaseModel):
connections: Connections connections: Connections
friend_count: int | None = None friend_count: int | None = None
distinguishment: Distinguishment | None = None distinguishment: Distinguishment | None = None
achievements: list[int]
ar: int
ar_counts: ArCounts
class UserInfoSuccess(BaseSuccessModel): class UserInfoSuccess(BaseSuccessModel):

View File

@@ -1,4 +1,4 @@
from typing import Literal from typing import Literal, NewType
S1ValidRank = Literal[ S1ValidRank = Literal[
'x+', 'x+',
@@ -43,3 +43,5 @@ Records = Literal[
'blitz_recent', 'blitz_recent',
'blitz_progression', 'blitz_progression',
] ]
Prisecter = NewType('Prisecter', str)

View File

@@ -0,0 +1,90 @@
from nonebot_plugin_alconna import Args, Option, Subcommand
from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_session import EventSession
from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[import-untyped]
from ...db import trigger
from ...utils.host import HostPage, get_self_netloc
from ...utils.metrics import get_metrics
from ...utils.render import render
from ...utils.render.schemas.tetrio.user.list_v2 import List, TetraLeague, User
from ...utils.screenshot import screenshot
from .. import alc
from . import command
from .api.leaderboards import by
from .api.schemas.base import P
from .api.schemas.leaderboards import Parameter
from .constant import GAME_TYPE
command.add(
Subcommand(
'list',
Option('--max-tr', Args['max_tr', float], help_text='TR的上限'),
Option('--min-tr', Args['min_tr', float], help_text='TR的下限'),
Option('--limit', Args['limit', int], help_text='查询数量'),
Option('--country', Args['country', str], help_text='国家代码'),
help_text='查询 TETR.IO 段位排行榜',
)
)
@alc.assign('TETRIO.list')
async def _(
event_session: EventSession,
max_tr: float | None = None,
min_tr: float | None = None,
limit: int | None = None,
country: str | None = None,
):
async with trigger(
session_persist_id=await get_session_persist_id(event_session),
game_platform=GAME_TYPE,
command_type='list',
command_args=[
f'{key} {value}'
for key, value in zip(
('--max-tr', '--min-tr', '--limit', '--country'), (max_tr, min_tr, limit, country), strict=True
)
if value is not None
],
):
parameter = Parameter(
# ?: 似乎是只需要 pri 至少 league 榜的返回值只有 pri
after=P(pri=max_tr, sec=0, ter=0).to_prisecter() if max_tr is not None else None,
before=P(pri=min_tr, sec=0, ter=0).to_prisecter() if min_tr is not None else None,
limit=limit or 25,
country=country,
)
league = await by('league', parameter)
async with HostPage(
await render(
'v2/tetrio/user/list',
List(
show_index=True,
users=[
User(
id=i.id,
name=i.username.upper(),
avatar=f'https://tetr.io/user-content/avatars/{i.id}.jpg',
country=i.country,
tetra_league=TetraLeague(
rank=i.league.rank,
tr=round(i.league.tr, 2),
glicko=round(i.league.glicko, 2),
rd=round(i.league.rd, 2),
decaying=i.league.decaying,
pps=(metrics := get_metrics(pps=i.league.pps, apm=i.league.apm, vs=i.league.vs)).pps,
apm=metrics.apm,
apl=metrics.apl,
vs=metrics.vs,
adpl=metrics.adpl,
),
xp=i.xp,
join_at=None,
)
for i in league.data.entries
],
),
)
) as page_hash:
await UniMessage.image(raw=await screenshot(f'http://{get_self_netloc()}/host/{page_hash}.html')).finish()

View File

@@ -209,7 +209,6 @@ async def make_query_image_v2(player: Player) -> bytes:
friend_count=user_info.data.friend_count, friend_count=user_info.data.friend_count,
supporter_tier=user_info.data.supporter_tier, supporter_tier=user_info.data.supporter_tier,
bad_standing=user_info.data.badstanding or False, bad_standing=user_info.data.badstanding or False,
verified=user_info.data.verified or False,
playtime=play_time, playtime=play_time,
join_at=user_info.data.ts, join_at=user_info.data.ts,
), ),

View File

@@ -88,9 +88,7 @@ async def get_tetra_league_data() -> None:
prisecter = P(pri=9007199254740991, sec=9007199254740991, ter=9007199254740991) # * from ch.tetr.io prisecter = P(pri=9007199254740991, sec=9007199254740991, ter=9007199254740991) # * from ch.tetr.io
results: list[BySuccessModel] = [] results: list[BySuccessModel] = []
while True: while True:
model = await limit_by( model = await limit_by('league', Parameter(after=prisecter.to_prisecter(), limit=100), x_session_id)
'league', Parameter(after=f'{prisecter.pri}:{prisecter.sec}:{prisecter.ter}', limit=100), x_session_id
)
prisecter = model.data.entries[-1].p prisecter = model.data.entries[-1].p
results.append(model) results.append(model)
if len(model.data.entries) < 100: # 分页值 # noqa: PLR2004 if len(model.data.entries) < 100: # 分页值 # noqa: PLR2004

View File

@@ -21,7 +21,7 @@ class User(BaseModel):
name: str name: str
country: str | None country: str | None
role: Literal['anon', 'user', 'bot', 'halfmod', 'mod', 'admin', 'sysop', 'banned'] role: Literal['anon', 'user', 'bot', 'halfmod', 'mod', 'admin', 'sysop', 'hidden', 'banned']
avatar: str | Avatar avatar: str | Avatar
banner: str | None banner: str | None
@@ -31,7 +31,6 @@ class User(BaseModel):
friend_count: int | None friend_count: int | None
supporter_tier: int supporter_tier: int
verified: bool
bad_standing: bool bad_standing: bool
badges: list[Badge] badges: list[Badge]

View File

@@ -26,7 +26,6 @@ class User(BaseModel):
name: str name: str
avatar: str | Avatar avatar: str | Avatar
country: str | None country: str | None
verified: bool
tetra_league: TetraLeague tetra_league: TetraLeague
xp: Number xp: Number
join_at: datetime | None join_at: datetime | None

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = 'nonebot-plugin-tetris-stats' name = 'nonebot-plugin-tetris-stats'
version = '1.4.18' version = '1.5.0'
description = '一款基于 NoneBot2 的用于查询 Tetris 相关游戏数据的插件' description = '一款基于 NoneBot2 的用于查询 Tetris 相关游戏数据的插件'
authors = ['scdhh <wallfjjd@gmail.com>'] authors = ['scdhh <wallfjjd@gmail.com>']
readme = 'README.md' readme = 'README.md'