TETR.IO 添加 list 命令

This commit is contained in:
2024-06-28 15:07:48 +08:00
parent 34c857387e
commit aacf518004
9 changed files with 166 additions and 12 deletions

View File

@@ -11,7 +11,7 @@ from nonebot_plugin_orm import AsyncSession, get_session
from nonebot_plugin_user import User # type: ignore[import-untyped] from nonebot_plugin_user import User # type: ignore[import-untyped]
from sqlalchemy import select from sqlalchemy import select
from ..utils.typing import CommandType, GameType from ..utils.typing import AllCommandType, BaseCommandType, GameType, TETRIOCommandType
from .models import Bind, TriggerHistoricalData from .models import Bind, TriggerHistoricalData
UTC = timezone.utc UTC = timezone.utc
@@ -92,7 +92,7 @@ async def anti_duplicate_add(cls: type[T], model: T) -> None:
async def trigger( async def trigger(
session_persist_id: int, session_persist_id: int,
game_platform: Literal['IO'], game_platform: Literal['IO'],
command_type: CommandType | Literal['rank', 'config', 'record'], command_type: TETRIOCommandType,
command_args: list[str], command_args: list[str],
) -> AsyncGenerator: ) -> AsyncGenerator:
yield yield
@@ -103,7 +103,7 @@ async def trigger(
async def trigger( async def trigger(
session_persist_id: int, session_persist_id: int,
game_platform: GameType, game_platform: GameType,
command_type: CommandType, command_type: BaseCommandType,
command_args: list[str], command_args: list[str],
) -> AsyncGenerator: ) -> AsyncGenerator:
yield yield
@@ -113,7 +113,7 @@ async def trigger(
async def trigger( async def trigger(
session_persist_id: int, session_persist_id: int,
game_platform: GameType, game_platform: GameType,
command_type: CommandType | Literal['rank', 'config', 'record'], command_type: AllCommandType,
command_args: list[str], command_args: list[str],
) -> AsyncGenerator: ) -> AsyncGenerator:
trigger_time = datetime.now(UTC) trigger_time = datetime.now(UTC)

View File

@@ -1,6 +1,6 @@
from collections.abc import Callable, Sequence from collections.abc import Callable, Sequence
from datetime import datetime from datetime import datetime
from typing import Any, Literal from typing import Any
from nonebot.compat import PYDANTIC_V2, type_validate_json from nonebot.compat import PYDANTIC_V2, type_validate_json
from nonebot_plugin_orm import Model from nonebot_plugin_orm import Model
@@ -9,7 +9,7 @@ from sqlalchemy import JSON, DateTime, Dialect, String, TypeDecorator
from sqlalchemy.orm import Mapped, MappedAsDataclass, mapped_column from sqlalchemy.orm import Mapped, MappedAsDataclass, mapped_column
from typing_extensions import override from typing_extensions import override
from ..utils.typing import CommandType, GameType from ..utils.typing import AllCommandType, GameType
class PydanticType(TypeDecorator): class PydanticType(TypeDecorator):
@@ -76,6 +76,6 @@ class TriggerHistoricalData(MappedAsDataclass, Model):
trigger_time: Mapped[datetime] = mapped_column(DateTime) trigger_time: Mapped[datetime] = mapped_column(DateTime)
session_persist_id: Mapped[int] session_persist_id: Mapped[int]
game_platform: Mapped[GameType] = mapped_column(String(32), index=True) game_platform: Mapped[GameType] = mapped_column(String(32), index=True)
command_type: Mapped[CommandType | Literal['rank', 'config', 'record']] = mapped_column(String(16), index=True) command_type: Mapped[AllCommandType] = mapped_column(String(16), index=True)
command_args: Mapped[list[str]] = mapped_column(JSON) command_args: Mapped[list[str]] = mapped_column(JSON)
finish_time: Mapped[datetime] = mapped_column(DateTime) finish_time: Mapped[datetime] = mapped_column(DateTime)

View File

@@ -82,6 +82,14 @@ command.add(
), ),
), ),
), ),
Subcommand(
'list',
Option('--max-tr', Arg('max_tr', float), help_text='TR的上限'),
Option('--min-tr', Arg('min_tr', float), help_text='TR的下限'),
Option('--limit', Arg('limit', int), help_text='查询数量'),
Option('--country', Arg('country', str), help_text='国家代码'),
help_text='查询 TETR.IO 段位排行榜',
),
Subcommand( Subcommand(
'rank', 'rank',
Args(Arg('rank', ValidRank, notice='TETR.IO 段位')), Args(Arg('rank', ValidRank, notice='TETR.IO 段位')),
@@ -155,11 +163,12 @@ alc.shortcut(
add_block_handlers(alc.assign('TETRIO.query')) add_block_handlers(alc.assign('TETRIO.query'))
from . import bind, config, query, rank, record # noqa: E402 from . import bind, config, list, query, rank, record # noqa: E402
__all__ = [ __all__ = [
'bind', 'bind',
'config', 'config',
'list',
'query', 'query',
'rank', 'rank',
'record', 'record',

View File

@@ -10,7 +10,7 @@ class _User(BaseModel):
username: str username: str
role: str role: str
xp: float xp: float
supporter: bool supporter: bool | None = None
verified: bool verified: bool
country: str | None = None country: str | None = None

View File

@@ -1,4 +1,5 @@
from typing import Literal, NamedTuple, overload from typing import Literal, NamedTuple, TypedDict, overload
from urllib.parse import urlencode
from nonebot.compat import type_validate_json from nonebot.compat import type_validate_json
@@ -10,6 +11,24 @@ from .schemas.base import FailedModel
from .schemas.tetra_league import TetraLeague, TetraLeagueSuccess from .schemas.tetra_league import TetraLeague, TetraLeagueSuccess
class Parameter(TypedDict, total=False):
after: float
before: float
limit: int
country: str
async def leaderboard(parameter: Parameter | None = None) -> TetraLeagueSuccess:
league: TetraLeague = type_validate_json(
TetraLeague, # type: ignore[arg-type]
(await Cache.get(splice_url([BASE_URL, 'users/lists/league', f'?{urlencode(parameter or {})}']))),
)
if isinstance(league, FailedModel):
msg = f'排行榜数据请求错误:\n{league.error}'
raise RequestError(msg)
return league
class FullExport(NamedTuple): class FullExport(NamedTuple):
model: TetraLeagueSuccess model: TetraLeagueSuccess
original: bytes original: bytes

View File

@@ -0,0 +1,74 @@
from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_session import EventSession # type: ignore[import-untyped]
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.tetrio_user_list_v2 import List, TetraLeague, User
from ...utils.screenshot import screenshot
from .. import alc
from .api.schemas.tetra_league import ValidLeague
from .api.tetra_league import Parameter, leaderboard
from .constant import GAME_TYPE
@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=[],
):
parameter: Parameter = {}
if max_tr is not None:
parameter['after'] = max_tr
if min_tr is not None:
parameter['before'] = min_tr
if limit is not None:
parameter['limit'] = limit
if country is not None:
parameter['country'] = country
league = await leaderboard(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,
verified=i.verified,
tetra_league=TetraLeague(
rank=i.league.rank,
tr=round(i.league.rating, 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.users
if isinstance(i.league, ValidLeague)
],
),
)
) as page_hash:
await UniMessage.image(raw=await screenshot(f'http://{get_self_netloc()}/host/{page_hash}.html')).finish()

View File

@@ -9,6 +9,7 @@ from .schemas.tetrio.tetrio_info import Info as TETRIOInfo
from .schemas.tetrio.tetrio_record_blitz import Record as TETRIORecordBlitz from .schemas.tetrio.tetrio_record_blitz import Record as TETRIORecordBlitz
from .schemas.tetrio.tetrio_record_sprint import Record as TETRIORecordSprint from .schemas.tetrio.tetrio_record_sprint import Record as TETRIORecordSprint
from .schemas.tetrio.tetrio_user_info_v2 import Info as TETRIOUserInfoV2 from .schemas.tetrio.tetrio_user_info_v2 import Info as TETRIOUserInfoV2
from .schemas.tetrio.tetrio_user_list_v2 import List as TETRIOUserListV2
from .schemas.top_info import Info as TOPInfo from .schemas.top_info import Info as TOPInfo
from .schemas.tos_info import Info as TOSInfo from .schemas.tos_info import Info as TOSInfo
@@ -37,6 +38,10 @@ async def render(render_type: Literal['v1/tos/info'], data: TOSInfo) -> str: ...
async def render(render_type: Literal['v2/tetrio/user/info'], data: TETRIOUserInfoV2) -> str: ... async def render(render_type: Literal['v2/tetrio/user/info'], data: TETRIOUserInfoV2) -> str: ...
@overload
async def render(render_type: Literal['v2/tetrio/user/list'], data: TETRIOUserListV2) -> str: ...
@overload @overload
async def render(render_type: Literal['v2/tetrio/record/40l'], data: TETRIORecordSprint) -> str: ... async def render(render_type: Literal['v2/tetrio/record/40l'], data: TETRIORecordSprint) -> str: ...
@@ -52,10 +57,18 @@ async def render(
'v1/top/info', 'v1/top/info',
'v1/tos/info', 'v1/tos/info',
'v2/tetrio/user/info', 'v2/tetrio/user/info',
'v2/tetrio/user/list',
'v2/tetrio/record/40l', 'v2/tetrio/record/40l',
'v2/tetrio/record/blitz', 'v2/tetrio/record/blitz',
], ],
data: Bind | TETRIOInfo | TOPInfo | TOSInfo | TETRIOUserInfoV2 | TETRIORecordSprint | TETRIORecordBlitz, data: Bind
| TETRIOInfo
| TOPInfo
| TOSInfo
| TETRIOUserInfoV2
| TETRIOUserListV2
| TETRIORecordSprint
| TETRIORecordBlitz,
) -> str: ) -> str:
if PYDANTIC_V2: if PYDANTIC_V2:
return await env.get_template('index.html').render_async( return await env.get_template('index.html').render_async(

View File

@@ -0,0 +1,37 @@
from datetime import datetime
from pydantic import BaseModel
from .....games.tetrio.api.typing import Rank
from ....typing import Number
from ..base import Avatar
class TetraLeague(BaseModel):
rank: Rank
tr: Number
glicko: Number | None
rd: Number | None
decaying: bool
pps: Number
apm: Number
apl: Number
vs: Number | None
adpl: Number | None
class User(BaseModel):
id: str
name: str
avatar: str | Avatar
country: str | None
verified: bool
tetra_league: TetraLeague
xp: Number
join_at: datetime | None
class List(BaseModel):
show_index: bool
users: list[User]

View File

@@ -2,7 +2,9 @@ from typing import Literal
Number = float | int Number = float | int
GameType = Literal['IO', 'TOP', 'TOS'] GameType = Literal['IO', 'TOP', 'TOS']
CommandType = Literal['bind', 'query'] BaseCommandType = Literal['bind', 'query']
TETRIOCommandType = BaseCommandType | Literal['rank', 'config', 'list', 'record']
AllCommandType = BaseCommandType | TETRIOCommandType
Me = Literal[ Me = Literal[
'', '',
'自己', '自己',