mirror of
https://github.com/A-Minos/nonebot-plugin-tetris-stats.git
synced 2026-03-05 05:36:54 +08:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0bc3b86820 | |||
|
|
57d0b15242 | ||
|
|
56fe45efcf | ||
| 13f005179f | |||
| 243a8a286d | |||
| 8b7de6745b | |||
| d0fc82d88d | |||
| bb4da8accc | |||
| 56e06a7001 | |||
|
|
7c0b3cd240 | ||
| dca0619021 | |||
|
|
f56dce6918 | ||
|
|
ff3eb79967 | ||
|
|
0ac917f95e |
2
.github/workflows/Release.yml
vendored
2
.github/workflows/Release.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: astral-sh/setup-uv@v5
|
||||
- uses: astral-sh/setup-uv@v6
|
||||
name: Setup UV
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
2
.github/workflows/Test.yml
vendored
2
.github/workflows/Test.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
uses: astral-sh/setup-uv@v6
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-suffix: ${{ env.PYTHON_VERSION }}_${{ env.OS }}
|
||||
|
||||
2
.github/workflows/TypeCheck.yml
vendored
2
.github/workflows/TypeCheck.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: astral-sh/setup-uv@v5
|
||||
- uses: astral-sh/setup-uv@v6
|
||||
name: Setup UV
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
@@ -7,7 +7,7 @@ ci:
|
||||
autoupdate_commit_msg: ':arrow_up: auto update by pre-commit hooks'
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.11.0
|
||||
rev: v0.11.8
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
|
||||
@@ -18,6 +18,7 @@ class ScopedConfig(BaseModel):
|
||||
request_timeout: float = 30.0
|
||||
screenshot_quality: float = 2
|
||||
proxy: Proxy = Field(default_factory=Proxy)
|
||||
development: bool = False
|
||||
|
||||
|
||||
class Config(BaseModel):
|
||||
|
||||
@@ -1,25 +1,93 @@
|
||||
from typing import TypeAlias
|
||||
from datetime import datetime
|
||||
from enum import IntEnum
|
||||
from typing import Literal, TypeAlias
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from ..base import FailedModel, SuccessModel
|
||||
|
||||
|
||||
class RankType(IntEnum):
|
||||
PERCENTILE = 1
|
||||
ISSUE = 2
|
||||
ZENITH = 3
|
||||
PERCENTILELAX = 4
|
||||
PERCENTILEVLAX = 5
|
||||
PERCENTILEMLAX = 6
|
||||
|
||||
|
||||
class ValueType(IntEnum):
|
||||
NONE = 0
|
||||
NUMBER = 1
|
||||
TIME = 2
|
||||
TIME_INV = 3
|
||||
FLOOR = 4
|
||||
ISSUE = 5
|
||||
NUMBER_INV = 6
|
||||
|
||||
|
||||
class ArType(IntEnum):
|
||||
UNRANKED = 0
|
||||
RANKED = 1
|
||||
COMPETITIVE = 2
|
||||
|
||||
|
||||
class Rank(IntEnum):
|
||||
NONE = 0
|
||||
BRONZE = 1
|
||||
SILVER = 2
|
||||
GOLD = 3
|
||||
PLATINUM = 4
|
||||
DIAMOND = 5
|
||||
ISSUED = 100
|
||||
|
||||
|
||||
class Ally(BaseModel):
|
||||
id: str = Field(alias='_id')
|
||||
username: str
|
||||
role: Literal['anon', 'user', 'bot', 'halfmod', 'mod', 'admin', 'sysop', 'hidden', 'banned']
|
||||
country: str | None = None
|
||||
supporter: bool
|
||||
avatar_revision: int | None = None
|
||||
|
||||
|
||||
class X(BaseModel):
|
||||
ally: Ally | None = None
|
||||
|
||||
|
||||
class Achievement(BaseModel):
|
||||
# 这**都是些啥
|
||||
k: int
|
||||
o: int
|
||||
rt: int
|
||||
vt: int
|
||||
achievement_id: int = Field(alias='k')
|
||||
category: str
|
||||
primary_name: str = Field(alias='name')
|
||||
objective: str = Field(alias='object')
|
||||
flavor_text: str = Field(alias='desc')
|
||||
order: int = Field(alias='o')
|
||||
rank_type: RankType = Field(alias='rt')
|
||||
value_type: ValueType = Field(alias='vt')
|
||||
ar_type: ArType = Field(alias='art')
|
||||
min: int
|
||||
deci: int
|
||||
name: str
|
||||
object: str
|
||||
category: str
|
||||
hidden: bool
|
||||
desc: str
|
||||
nolb: bool
|
||||
event: str | None = None
|
||||
event_past: bool | None = None
|
||||
disabled: bool | None = None
|
||||
pair: bool | None = None
|
||||
achieved_score: float | None = Field(None, alias='v')
|
||||
a: float | None = None
|
||||
t: datetime | None = None
|
||||
pos: int | None = None
|
||||
total: int | None = None
|
||||
rank: Rank | None = None
|
||||
x: X | None = None
|
||||
n: str
|
||||
stub: bool
|
||||
|
||||
tiebreak: int
|
||||
notifypb: bool
|
||||
id: str | None = Field(None, alias='_id')
|
||||
progress: float | None = None
|
||||
stub: bool | None = None
|
||||
|
||||
|
||||
class AchievementsSuccessModel(SuccessModel):
|
||||
|
||||
@@ -86,7 +86,7 @@ class Record(BaseModel):
|
||||
pb: bool
|
||||
oncepb: bool
|
||||
ts: datetime
|
||||
revolution: None
|
||||
revolution: str | None
|
||||
user: User
|
||||
otherusers: list
|
||||
leaderboards: list[str]
|
||||
@@ -97,7 +97,7 @@ class Record(BaseModel):
|
||||
|
||||
|
||||
class Best(BaseModel):
|
||||
record: None # WTF
|
||||
record: Record | None
|
||||
rank: int
|
||||
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from yarl import URL
|
||||
from ...db import BindStatus, create_or_update_bind, trigger
|
||||
from ...utils.host import HostPage, get_self_netloc
|
||||
from ...utils.image import get_avatar
|
||||
from ...utils.lang import get_lang
|
||||
from ...utils.render import Bind, render
|
||||
from ...utils.render.schemas.base import Avatar, People
|
||||
from ...utils.screenshot import screenshot
|
||||
@@ -65,7 +66,7 @@ async def _(nb_user: User, account: Player, event_session: EventSession, bot_inf
|
||||
'v1/binding',
|
||||
Bind(
|
||||
platform='TETR.IO',
|
||||
status='unknown',
|
||||
type='unknown',
|
||||
user=People(
|
||||
avatar=str(
|
||||
URL(f'http://{netloc}/host/resource/tetrio/avatars/{user.ID}')
|
||||
@@ -79,7 +80,8 @@ async def _(nb_user: User, account: Player, event_session: EventSession, bot_inf
|
||||
avatar=await get_avatar(bot_info, 'Data URI', '../../static/logo/logo.svg'),
|
||||
name=bot_info.user_name,
|
||||
),
|
||||
command='io查我',
|
||||
prompt='io查我',
|
||||
lang=get_lang(),
|
||||
),
|
||||
)
|
||||
) as page_hash:
|
||||
|
||||
@@ -5,9 +5,10 @@ from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[im
|
||||
|
||||
from ...db import trigger
|
||||
from ...utils.host import HostPage, get_self_netloc
|
||||
from ...utils.lang import get_lang
|
||||
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.render.schemas.v2.tetrio.user.list import Data, List, TetraLeague, User
|
||||
from ...utils.screenshot import screenshot
|
||||
from .. import alc
|
||||
from . import command
|
||||
@@ -63,12 +64,15 @@ async def _(
|
||||
'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,
|
||||
data=[
|
||||
Data(
|
||||
user=User(
|
||||
id=i.id,
|
||||
name=i.username.upper(),
|
||||
avatar=f'https://tetr.io/user-content/avatars/{i.id}.jpg',
|
||||
country=i.country,
|
||||
xp=i.xp,
|
||||
),
|
||||
tetra_league=TetraLeague(
|
||||
rank=i.league.rank,
|
||||
tr=round(i.league.tr, 2),
|
||||
@@ -81,12 +85,11 @@ async def _(
|
||||
vs=metrics.vs,
|
||||
adpl=metrics.adpl,
|
||||
),
|
||||
xp=i.xp,
|
||||
join_at=None,
|
||||
)
|
||||
for i in league.data.entries
|
||||
if isinstance(i, Entry)
|
||||
],
|
||||
lang=get_lang(),
|
||||
),
|
||||
)
|
||||
) as page_hash:
|
||||
|
||||
@@ -4,22 +4,22 @@ from typing import TypeVar, overload
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from ....utils.exception import FallbackError
|
||||
from ....utils.render.schemas.tetrio.user.base import TetraLeagueHistoryData
|
||||
from ....utils.render.schemas.base import HistoryData
|
||||
from ..api.schemas.labs.leagueflow import Empty, LeagueFlowSuccess
|
||||
from ..api.schemas.summaries.league import InvalidData, LeagueSuccessModel, NeverPlayedData, NeverRatedData, RatedData
|
||||
|
||||
|
||||
def flow_to_history(
|
||||
leagueflow: LeagueFlowSuccess,
|
||||
handle: Callable[[list[TetraLeagueHistoryData]], list[TetraLeagueHistoryData]] | None = None,
|
||||
) -> list[TetraLeagueHistoryData]:
|
||||
handle: Callable[[list[HistoryData]], list[HistoryData]] | None = None,
|
||||
) -> list[HistoryData]:
|
||||
if isinstance(leagueflow.data, Empty):
|
||||
raise FallbackError
|
||||
start_time = leagueflow.data.start_time.astimezone(ZoneInfo('Asia/Shanghai'))
|
||||
ret = [
|
||||
TetraLeagueHistoryData(
|
||||
HistoryData(
|
||||
record_at=start_time + timedelta(milliseconds=i.timestamp_offset),
|
||||
tr=i.post_match_tr,
|
||||
score=i.post_match_tr,
|
||||
)
|
||||
for i in leagueflow.data.points
|
||||
if start_time + timedelta(milliseconds=i.timestamp_offset)
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
from asyncio import gather
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import timedelta
|
||||
from hashlib import md5
|
||||
from math import ceil, floor
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from yarl import URL
|
||||
|
||||
from ....utils.exception import FallbackError, WhatTheFuckError
|
||||
from ....utils.chart import get_split, get_value_bounds, handle_history_data
|
||||
from ....utils.exception import FallbackError
|
||||
from ....utils.host import HostPage, get_self_netloc
|
||||
from ....utils.lang import get_lang
|
||||
from ....utils.metrics import get_metrics
|
||||
from ....utils.render import render
|
||||
from ....utils.render.schemas.base import Avatar, Ranking
|
||||
from ....utils.render.schemas.tetrio.user.base import TetraLeagueHistoryData
|
||||
from ....utils.render.schemas.tetrio.user.info_v1 import Info, Radar, TetraLeague, TetraLeagueHistory, User
|
||||
from ....utils.render.schemas.base import Avatar, Trending
|
||||
from ....utils.render.schemas.v1.base import History
|
||||
from ....utils.render.schemas.v1.tetrio.user.info import Info, Multiplayer, Singleplayer, User
|
||||
from ....utils.screenshot import screenshot
|
||||
from ..api import Player
|
||||
from ..api.schemas.summaries.league import RatedData
|
||||
@@ -19,109 +20,6 @@ from ..constant import TR_MAX, TR_MIN
|
||||
from .tools import flow_to_history, get_league_data
|
||||
|
||||
|
||||
def get_value_bounds(values: list[int | float]) -> tuple[int, int]:
|
||||
value_max = 10 * ceil(max(values) / 10)
|
||||
value_min = 10 * floor(min(values) / 10)
|
||||
return value_max, value_min
|
||||
|
||||
|
||||
def get_split(value_max: int, value_min: int) -> tuple[int, int]:
|
||||
offset = 0
|
||||
overflow = 0
|
||||
|
||||
while True:
|
||||
if (new_max_value := value_max + offset + overflow) > TR_MAX:
|
||||
overflow -= 1
|
||||
continue
|
||||
if (new_min_value := value_min - offset + overflow) < TR_MIN:
|
||||
overflow += 1
|
||||
continue
|
||||
if ((new_max_value - new_min_value) / 40).is_integer():
|
||||
split_value = int((value_max + offset - (value_min - offset)) / 4)
|
||||
break
|
||||
offset += 1
|
||||
return split_value, offset + overflow
|
||||
|
||||
|
||||
def get_specified_point(
|
||||
previous_point: TetraLeagueHistoryData,
|
||||
behind_point: TetraLeagueHistoryData,
|
||||
point_time: datetime,
|
||||
) -> TetraLeagueHistoryData:
|
||||
"""根据给出的 previous_point 和 behind_point, 推算 point_time 点处的数据
|
||||
|
||||
Args:
|
||||
previous_point (Data): 前面的数据点
|
||||
behind_point (Data): 后面的数据点
|
||||
point_time (datetime): 要推算的点的位置
|
||||
|
||||
Returns:
|
||||
Data: 要推算的点的数据
|
||||
"""
|
||||
# 求两个点的斜率
|
||||
slope = (behind_point.tr - previous_point.tr) / (
|
||||
datetime.timestamp(behind_point.record_at) - datetime.timestamp(previous_point.record_at)
|
||||
)
|
||||
return TetraLeagueHistoryData(
|
||||
record_at=point_time,
|
||||
tr=previous_point.tr + slope * (datetime.timestamp(point_time) - datetime.timestamp(previous_point.record_at)),
|
||||
)
|
||||
|
||||
|
||||
def handle_history_data(data: list[TetraLeagueHistoryData]) -> list[TetraLeagueHistoryData]: # noqa: C901, PLR0912
|
||||
# 按照 记录时间 对数据进行排序
|
||||
data.sort(key=lambda x: x.record_at)
|
||||
|
||||
# 定义时间边界, 右边界为当前时间的当天零点, 左边界为右边界前推9天
|
||||
# 返回值的[0]和[-1]分别应满足left_border和right_border
|
||||
zero = datetime.now(ZoneInfo('Asia/Shanghai')).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
left_border = zero - timedelta(days=9)
|
||||
right_border = zero.replace(microsecond=1000)
|
||||
|
||||
lefts: list[TetraLeagueHistoryData] = []
|
||||
in_border: list[TetraLeagueHistoryData] = []
|
||||
rights: list[TetraLeagueHistoryData] = []
|
||||
|
||||
# 根据 记录时间 将数据分类到对应的列表中
|
||||
for i in data:
|
||||
if i.record_at < left_border:
|
||||
lefts.append(i)
|
||||
elif i.record_at < right_border:
|
||||
in_border.append(i)
|
||||
else:
|
||||
rights.append(i)
|
||||
|
||||
ret: list[TetraLeagueHistoryData] = []
|
||||
|
||||
# 处理左边界的点
|
||||
if lefts and in_border: # 如果边界左侧和边界内都有值则推算
|
||||
ret.append(get_specified_point(lefts[-1], in_border[0], left_border))
|
||||
elif lefts and not in_border: # 如果边界左侧有值但是边界内没有值则直接取左侧的最后一个值
|
||||
ret.append(TetraLeagueHistoryData(tr=lefts[-1].tr, record_at=left_border))
|
||||
elif not lefts and in_border: # 如果边界左侧没有值但是边界内有值则直接取边界内的第一个值
|
||||
ret.append(TetraLeagueHistoryData(tr=in_border[0].tr, record_at=left_border))
|
||||
elif not lefts and not in_border and rights: # 如果边界左侧和边界内都没有值但是边界右侧有值则直接取边界右侧的第一个值 # fmt: skip
|
||||
ret.append(TetraLeagueHistoryData(tr=rights[0].tr, record_at=left_border))
|
||||
else: # 暂时没想到其他情况
|
||||
raise WhatTheFuckError
|
||||
|
||||
# 添加边界内数据
|
||||
ret.extend(in_border)
|
||||
|
||||
# 处理右边界的点
|
||||
if in_border and rights: # 如果边界内和边界右侧都有值则推算
|
||||
ret.append(get_specified_point(in_border[-1], rights[0], right_border))
|
||||
elif not in_border and rights: # 如果边界内没有值但是边界右侧有值则直接取右侧的第一个值
|
||||
ret.append(TetraLeagueHistoryData(tr=rights[0].tr, record_at=right_border))
|
||||
elif in_border and not rights: # 如果边界内有值但是边界右侧没有值则直接取边界内的最后一个值
|
||||
ret.append(TetraLeagueHistoryData(tr=in_border[-1].tr, record_at=right_border))
|
||||
elif not in_border and not rights and lefts: # 如果边界内和边界右侧都没有值但是边界左侧有值则直接取边界左侧的最后一个值 # fmt: skip
|
||||
ret.append(TetraLeagueHistoryData(tr=lefts[-1].tr, record_at=right_border))
|
||||
else: # 暂时没想到其他情况
|
||||
raise WhatTheFuckError
|
||||
return ret
|
||||
|
||||
|
||||
async def make_query_image_v1(player: Player) -> bytes:
|
||||
(
|
||||
(user, user_info, league, sprint, blitz, leagueflow),
|
||||
@@ -134,8 +32,8 @@ async def make_query_image_v1(player: Player) -> bytes:
|
||||
if league_data.vs is None:
|
||||
raise FallbackError
|
||||
histories = flow_to_history(leagueflow, handle_history_data)
|
||||
value_max, value_min = get_value_bounds([i.tr for i in histories])
|
||||
split_value, offset = get_split(value_max, value_min)
|
||||
values = get_value_bounds([i.score for i in histories])
|
||||
split_value, offset = get_split(values, TR_MAX, TR_MIN)
|
||||
if sprint.data.record is not None:
|
||||
duration = timedelta(milliseconds=sprint.data.record.results.stats.finaltime).total_seconds()
|
||||
sprint_value = f'{duration:.3f}s' if duration < 60 else f'{duration // 60:.0f}m {duration % 60:.3f}s' # noqa: PLR2004
|
||||
@@ -143,6 +41,9 @@ async def make_query_image_v1(player: Player) -> bytes:
|
||||
sprint_value = 'N/A'
|
||||
blitz_value = f'{blitz.data.record.results.stats.score:,}' if blitz.data.record is not None else 'N/A'
|
||||
netloc = get_self_netloc()
|
||||
dsps: float
|
||||
dspp: float
|
||||
# make mypy happy
|
||||
async with HostPage(
|
||||
page=await render(
|
||||
'v1/tetrio/info',
|
||||
@@ -159,38 +60,40 @@ async def make_query_image_v1(player: Player) -> bytes:
|
||||
name=user.name.upper(),
|
||||
bio=user_info.data.bio,
|
||||
),
|
||||
ranking=Ranking(
|
||||
rating=round(league_data.glicko, 2),
|
||||
multiplayer=Multiplayer(
|
||||
glicko=f'{round(league_data.glicko, 2):,}',
|
||||
rd=round(league_data.rd, 2),
|
||||
),
|
||||
tetra_league=TetraLeague(
|
||||
rank=league_data.rank,
|
||||
tr=round(league_data.tr, 2),
|
||||
tr=f'{round(league_data.tr, 2):,}',
|
||||
global_rank=league_data.standing,
|
||||
pps=league_data.pps,
|
||||
lpm=round(lpm := (league_data.pps * 24), 2),
|
||||
apm=league_data.apm,
|
||||
apl=round(league_data.apm / lpm, 2),
|
||||
vs=league_data.vs,
|
||||
adpm=round(adpm := (league_data.vs * 0.6), 2),
|
||||
adpl=round(adpm / lpm, 2),
|
||||
),
|
||||
tetra_league_history=TetraLeagueHistory(
|
||||
data=histories,
|
||||
split_interval=split_value,
|
||||
min_tr=value_min,
|
||||
max_tr=value_max,
|
||||
offset=offset,
|
||||
),
|
||||
radar=Radar(
|
||||
history=History(
|
||||
data=histories,
|
||||
split_interval=split_value,
|
||||
min_value=values.value_min,
|
||||
max_value=values.value_max,
|
||||
offset=offset,
|
||||
),
|
||||
lpm=(metrics := get_metrics(pps=league_data.pps, apm=league_data.apm, vs=league_data.vs)).lpm,
|
||||
pps=metrics.pps,
|
||||
lpm_trending=Trending.KEEP,
|
||||
apm=metrics.apm,
|
||||
apl=metrics.apl,
|
||||
apm_trending=Trending.KEEP,
|
||||
adpm=metrics.adpm,
|
||||
vs=metrics.vs,
|
||||
adpl=metrics.adpl,
|
||||
adpm_trending=Trending.KEEP,
|
||||
app=(app := (league_data.apm / (60 * league_data.pps))),
|
||||
dsps=(dsps := ((league_data.vs / 100) - (league_data.apm / 60))),
|
||||
dspp=(dspp := (dsps / league_data.pps)),
|
||||
ci=150 * dspp - 125 * app + 50 * (league_data.vs / league_data.apm) - 25,
|
||||
ge=2 * ((app * dsps) / league_data.pps),
|
||||
),
|
||||
sprint=sprint_value,
|
||||
blitz=blitz_value,
|
||||
singleplayer=Singleplayer(
|
||||
sprint=sprint_value,
|
||||
blitz=blitz_value,
|
||||
),
|
||||
lang=get_lang(),
|
||||
),
|
||||
)
|
||||
) as page_hash:
|
||||
|
||||
@@ -6,11 +6,14 @@ from yarl import URL
|
||||
|
||||
from ....utils.exception import FallbackError
|
||||
from ....utils.host import HostPage, get_self_netloc
|
||||
from ....utils.lang import get_lang
|
||||
from ....utils.metrics import get_metrics
|
||||
from ....utils.render import render
|
||||
from ....utils.render.schemas.base import Avatar
|
||||
from ....utils.render.schemas.tetrio.user.info_v2 import (
|
||||
from ....utils.render.schemas.v2.tetrio.user.info import (
|
||||
Achievement,
|
||||
Badge,
|
||||
Best,
|
||||
Blitz,
|
||||
Info,
|
||||
Sprint,
|
||||
@@ -18,7 +21,9 @@ from ....utils.render.schemas.tetrio.user.info_v2 import (
|
||||
TetraLeague,
|
||||
TetraLeagueStatistic,
|
||||
User,
|
||||
Week,
|
||||
Zen,
|
||||
Zenith,
|
||||
)
|
||||
from ....utils.screenshot import screenshot
|
||||
from ..api import Player
|
||||
@@ -29,10 +34,24 @@ from .tools import flow_to_history, handling_special_value
|
||||
async def make_query_image_v2(player: Player) -> bytes:
|
||||
(
|
||||
(user, user_info, league, sprint, blitz, zen),
|
||||
(avatar_revision, banner_revision, leagueflow),
|
||||
(avatar_revision, banner_revision, leagueflow, zenith, zenithex, achievements),
|
||||
) = await gather(
|
||||
gather(player.user, player.get_info(), player.league, player.sprint, player.blitz, player.zen),
|
||||
gather(player.avatar_revision, player.banner_revision, player.get_leagueflow()),
|
||||
gather(
|
||||
player.user,
|
||||
player.get_info(),
|
||||
player.league,
|
||||
player.sprint,
|
||||
player.blitz,
|
||||
player.zen,
|
||||
),
|
||||
gather(
|
||||
player.avatar_revision,
|
||||
player.banner_revision,
|
||||
player.get_leagueflow(),
|
||||
player.get_summaries('zenith'),
|
||||
player.get_summaries('zenithex'),
|
||||
player.get_summaries('achievements'),
|
||||
),
|
||||
)
|
||||
if sprint.data.record is not None:
|
||||
duration = timedelta(milliseconds=sprint.data.record.results.stats.finaltime).total_seconds()
|
||||
@@ -62,12 +81,9 @@ async def make_query_image_v2(player: Player) -> bytes:
|
||||
user=User(
|
||||
id=user.ID,
|
||||
name=user.name.upper(),
|
||||
bio=user_info.data.bio,
|
||||
banner=str(
|
||||
URL(f'http://{netloc}/host/resource/tetrio/banners/{user.ID}') % {'revision': banner_revision}
|
||||
)
|
||||
if banner_revision is not None and banner_revision != 0
|
||||
else None,
|
||||
country=user_info.data.country,
|
||||
role=user_info.data.role,
|
||||
botmaster=user_info.data.botmaster,
|
||||
avatar=str(
|
||||
URL(f'http://{netloc}/host/resource/tetrio/avatars/{user.ID}') % {'revision': avatar_revision}
|
||||
)
|
||||
@@ -76,6 +92,15 @@ async def make_query_image_v2(player: Player) -> bytes:
|
||||
type='identicon',
|
||||
hash=md5(user.ID.encode()).hexdigest(), # noqa: S324
|
||||
),
|
||||
banner=str(
|
||||
URL(f'http://{netloc}/host/resource/tetrio/banners/{user.ID}') % {'revision': banner_revision}
|
||||
)
|
||||
if banner_revision is not None and banner_revision != 0
|
||||
else None,
|
||||
bio=user_info.data.bio,
|
||||
friend_count=user_info.data.friend_count,
|
||||
supporter_tier=user_info.data.supporter_tier,
|
||||
bad_standing=user_info.data.badstanding or False,
|
||||
badges=[
|
||||
Badge(
|
||||
id=i.id,
|
||||
@@ -85,12 +110,22 @@ async def make_query_image_v2(player: Player) -> bytes:
|
||||
)
|
||||
for i in user_info.data.badges
|
||||
],
|
||||
country=user_info.data.country,
|
||||
role=user_info.data.role,
|
||||
xp=user_info.data.xp,
|
||||
friend_count=user_info.data.friend_count,
|
||||
supporter_tier=user_info.data.supporter_tier,
|
||||
bad_standing=user_info.data.badstanding or False,
|
||||
ar=user_info.data.ar,
|
||||
achievements=[
|
||||
Achievement(
|
||||
key=i.achievement_id,
|
||||
rank_type=i.rank_type,
|
||||
ar_type=i.ar_type,
|
||||
stub=i.stub,
|
||||
rank=i.rank,
|
||||
achieved_score=i.achieved_score,
|
||||
pos=i.pos,
|
||||
progress=i.progress,
|
||||
total=i.total,
|
||||
)
|
||||
for i in achievements.data
|
||||
],
|
||||
playtime=play_time,
|
||||
join_at=user_info.data.ts,
|
||||
),
|
||||
@@ -113,6 +148,40 @@ async def make_query_image_v2(player: Player) -> bytes:
|
||||
)
|
||||
if not isinstance(league.data, NeverPlayedData | InvalidData)
|
||||
else None,
|
||||
zenith=Zenith(
|
||||
week=Week(
|
||||
altitude=zenith.data.record.results.stats.zenith.altitude,
|
||||
global_rank=zenith.data.rank,
|
||||
country_rank=zenith.data.rank_local,
|
||||
play_at=zenith.data.record.ts,
|
||||
)
|
||||
if zenith.data.record is not None
|
||||
else None,
|
||||
best=Best(
|
||||
altitude=zenith.data.best.record.results.stats.zenith.altitude,
|
||||
global_rank=zenith.data.best.rank,
|
||||
play_at=zenith.data.best.record.ts,
|
||||
)
|
||||
if zenith.data.best.record is not None
|
||||
else None,
|
||||
),
|
||||
zenithex=Zenith(
|
||||
week=Week(
|
||||
altitude=zenithex.data.record.results.stats.zenith.altitude,
|
||||
global_rank=zenithex.data.rank,
|
||||
country_rank=zenithex.data.rank_local,
|
||||
play_at=zenithex.data.record.ts,
|
||||
)
|
||||
if zenithex.data.record is not None
|
||||
else None,
|
||||
best=Best(
|
||||
altitude=zenithex.data.best.record.results.stats.zenith.altitude,
|
||||
global_rank=zenithex.data.best.rank,
|
||||
play_at=zenithex.data.best.record.ts,
|
||||
)
|
||||
if zenithex.data.best.record is not None
|
||||
else None,
|
||||
),
|
||||
statistic=Statistic(
|
||||
total=handling_special_value(user_info.data.gamesplayed),
|
||||
wins=handling_special_value(user_info.data.gameswon),
|
||||
@@ -120,6 +189,7 @@ async def make_query_image_v2(player: Player) -> bytes:
|
||||
sprint=Sprint(
|
||||
time=sprint_value,
|
||||
global_rank=sprint.data.rank,
|
||||
country_rank=sprint.data.rank_local,
|
||||
play_at=sprint.data.record.ts,
|
||||
)
|
||||
if sprint.data.record is not None
|
||||
@@ -127,11 +197,13 @@ async def make_query_image_v2(player: Player) -> bytes:
|
||||
blitz=Blitz(
|
||||
score=blitz.data.record.results.stats.score,
|
||||
global_rank=blitz.data.rank,
|
||||
country_rank=blitz.data.rank_local,
|
||||
play_at=blitz.data.record.ts,
|
||||
)
|
||||
if blitz.data.record is not None
|
||||
else None,
|
||||
zen=Zen(level=zen.data.level, score=zen.data.score),
|
||||
lang=get_lang(),
|
||||
),
|
||||
),
|
||||
) as page_hash:
|
||||
|
||||
@@ -12,6 +12,7 @@ from nonebot_plugin_apscheduler import scheduler
|
||||
from nonebot_plugin_orm import get_session
|
||||
from sqlalchemy import select
|
||||
|
||||
from ....config.config import config
|
||||
from ....utils.exception import RequestError
|
||||
from ....utils.retry import retry
|
||||
from .. import alc
|
||||
@@ -136,14 +137,16 @@ async def get_tetra_league_data() -> None:
|
||||
await session.commit()
|
||||
|
||||
|
||||
@driver.on_startup
|
||||
async def _() -> None:
|
||||
async with get_session() as session:
|
||||
latest_time = await session.scalar(
|
||||
select(TETRIOLeagueStats.update_time).order_by(TETRIOLeagueStats.id.desc()).limit(1)
|
||||
)
|
||||
if latest_time is None or datetime.now(tz=UTC) - latest_time.replace(tzinfo=UTC) > timedelta(hours=6):
|
||||
await get_tetra_league_data()
|
||||
if not config.tetris.development:
|
||||
|
||||
@driver.on_startup
|
||||
async def _() -> None:
|
||||
async with get_session() as session:
|
||||
latest_time = await session.scalar(
|
||||
select(TETRIOLeagueStats.update_time).order_by(TETRIOLeagueStats.id.desc()).limit(1)
|
||||
)
|
||||
if latest_time is None or datetime.now(tz=UTC) - latest_time.replace(tzinfo=UTC) > timedelta(hours=6):
|
||||
await get_tetra_league_data()
|
||||
|
||||
|
||||
from . import all, detail # noqa: A004, E402
|
||||
|
||||
@@ -10,13 +10,14 @@ from sqlalchemy.orm import selectinload
|
||||
|
||||
from ....db import trigger
|
||||
from ....utils.host import HostPage, get_self_netloc
|
||||
from ....utils.lang import get_lang
|
||||
from ....utils.metrics import get_metrics
|
||||
from ....utils.render import render
|
||||
from ....utils.render.schemas.tetrio.rank.v1 import Data as DataV1
|
||||
from ....utils.render.schemas.tetrio.rank.v1 import ItemData as ItemDataV1
|
||||
from ....utils.render.schemas.tetrio.rank.v2 import AverageData as AverageDataV2
|
||||
from ....utils.render.schemas.tetrio.rank.v2 import Data as DataV2
|
||||
from ....utils.render.schemas.tetrio.rank.v2 import ItemData as ItemDataV2
|
||||
from ....utils.render.schemas.v1.tetrio.rank import Data as DataV1
|
||||
from ....utils.render.schemas.v1.tetrio.rank import ItemData as ItemDataV1
|
||||
from ....utils.render.schemas.v2.tetrio.rank import AverageData as AverageDataV2
|
||||
from ....utils.render.schemas.v2.tetrio.rank import Data as DataV2
|
||||
from ....utils.render.schemas.v2.tetrio.rank import ItemData as ItemDataV2
|
||||
from ....utils.screenshot import screenshot
|
||||
from .. import alc
|
||||
from ..constant import GAME_TYPE
|
||||
@@ -82,6 +83,7 @@ async def make_image_v1(latest_data: TETRIOLeagueStats, compare_data: TETRIOLeag
|
||||
for i in zip(latest_data.fields, compare_data.fields, strict=True)
|
||||
},
|
||||
updated_at=latest_data.update_time,
|
||||
lang=get_lang(),
|
||||
),
|
||||
)
|
||||
) as page_hash:
|
||||
@@ -109,6 +111,7 @@ async def make_image_v2(latest_data: TETRIOLeagueStats, compare_data: TETRIOLeag
|
||||
for i in zip(latest_data.fields, compare_data.fields, strict=True)
|
||||
},
|
||||
updated_at=latest_data.update_time,
|
||||
lang=get_lang(),
|
||||
),
|
||||
)
|
||||
) as page_hash:
|
||||
|
||||
@@ -12,9 +12,10 @@ from sqlalchemy.orm import selectinload
|
||||
|
||||
from ....db import trigger
|
||||
from ....utils.host import HostPage, get_self_netloc
|
||||
from ....utils.lang import get_lang
|
||||
from ....utils.metrics import get_metrics
|
||||
from ....utils.render import render
|
||||
from ....utils.render.schemas.tetrio.rank.detail import Data, SpecialData
|
||||
from ....utils.render.schemas.v2.tetrio.rank.detail import Data, SpecialData
|
||||
from ....utils.screenshot import screenshot
|
||||
from .. import alc
|
||||
from ..api.typedefs import ValidRank
|
||||
@@ -122,6 +123,7 @@ async def make_image(rank: ValidRank, latest: TETRIOLeagueStats, compare: TETRIO
|
||||
vs_holder=latest_data.high_vs.username.upper(),
|
||||
),
|
||||
updated_at=latest.update_time.replace(tzinfo=UTC).astimezone(ZoneInfo('Asia/Shanghai')),
|
||||
lang=get_lang(),
|
||||
),
|
||||
)
|
||||
) as page_hash:
|
||||
|
||||
@@ -16,11 +16,12 @@ from ....db import query_bind_info, trigger
|
||||
from ....i18n import Lang
|
||||
from ....utils.exception import RecordNotFoundError
|
||||
from ....utils.host import HostPage, get_self_netloc
|
||||
from ....utils.lang import get_lang
|
||||
from ....utils.metrics import get_metrics
|
||||
from ....utils.render import render
|
||||
from ....utils.render.schemas.base import Avatar
|
||||
from ....utils.render.schemas.tetrio.record.base import Finesse, Max, Mini, Tspins, User
|
||||
from ....utils.render.schemas.tetrio.record.blitz import Record, Statistic
|
||||
from ....utils.render.schemas.v2.tetrio.record.base import Finesse, Max, Mini, Tspins, User
|
||||
from ....utils.render.schemas.v2.tetrio.record.blitz import Record, Statistic
|
||||
from ....utils.screenshot import screenshot
|
||||
from ....utils.typedefs import Me
|
||||
from .. import alc
|
||||
@@ -145,6 +146,7 @@ async def make_blitz_image(player: Player) -> bytes:
|
||||
level=stats.level,
|
||||
),
|
||||
play_at=blitz.data.record.ts,
|
||||
lang=get_lang(),
|
||||
),
|
||||
)
|
||||
) as page_hash:
|
||||
|
||||
@@ -16,11 +16,12 @@ from ....db import query_bind_info, trigger
|
||||
from ....i18n import Lang
|
||||
from ....utils.exception import RecordNotFoundError
|
||||
from ....utils.host import HostPage, get_self_netloc
|
||||
from ....utils.lang import get_lang
|
||||
from ....utils.metrics import get_metrics
|
||||
from ....utils.render import render
|
||||
from ....utils.render.schemas.base import Avatar
|
||||
from ....utils.render.schemas.tetrio.record.base import Finesse, Max, Mini, Statistic, Tspins, User
|
||||
from ....utils.render.schemas.tetrio.record.sprint import Record
|
||||
from ....utils.render.schemas.v2.tetrio.record.base import Finesse, Max, Mini, Statistic, Tspins, User
|
||||
from ....utils.render.schemas.v2.tetrio.record.sprint import Record
|
||||
from ....utils.screenshot import screenshot
|
||||
from ....utils.typedefs import Me
|
||||
from .. import alc
|
||||
@@ -90,7 +91,7 @@ async def make_sprint_image(player: Player) -> bytes:
|
||||
netloc = get_self_netloc()
|
||||
async with HostPage(
|
||||
page=await render(
|
||||
'v2/tetrio/record/40l',
|
||||
'v2/tetrio/record/sprint',
|
||||
Record(
|
||||
type='best',
|
||||
user=User(
|
||||
@@ -145,6 +146,7 @@ async def make_sprint_image(player: Player) -> bytes:
|
||||
),
|
||||
),
|
||||
play_at=sprint.data.record.ts,
|
||||
lang=get_lang(),
|
||||
),
|
||||
)
|
||||
) as page_hash:
|
||||
|
||||
@@ -13,6 +13,7 @@ from yarl import URL
|
||||
from ...db import query_bind_info, remove_bind, trigger
|
||||
from ...utils.host import HostPage, get_self_netloc
|
||||
from ...utils.image import get_avatar
|
||||
from ...utils.lang import get_lang
|
||||
from ...utils.render import Bind, render
|
||||
from ...utils.render.schemas.base import Avatar, People
|
||||
from ...utils.screenshot import screenshot
|
||||
@@ -53,7 +54,7 @@ async def _(nb_user: User, event_session: EventSession, bot_info: UserInfo = Bot
|
||||
'v1/binding',
|
||||
Bind(
|
||||
platform='TETR.IO',
|
||||
status='unlink',
|
||||
type='unlink',
|
||||
user=People(
|
||||
avatar=str(
|
||||
URL(f'http://{netloc}/host/resource/tetrio/avatars/{user.ID}')
|
||||
@@ -67,7 +68,8 @@ async def _(nb_user: User, event_session: EventSession, bot_info: UserInfo = Bot
|
||||
avatar=await get_avatar(bot_info, 'Data URI', '../../static/logo/logo.svg'),
|
||||
name=bot_info.user_name,
|
||||
),
|
||||
command='io绑定{游戏ID}',
|
||||
prompt='io绑定{游戏ID}',
|
||||
lang=get_lang(),
|
||||
),
|
||||
)
|
||||
) as page_hash:
|
||||
|
||||
@@ -8,6 +8,7 @@ from nonebot_plugin_userinfo import BotUserInfo, EventUserInfo, UserInfo
|
||||
from ...db import BindStatus, create_or_update_bind, trigger
|
||||
from ...utils.host import HostPage, get_self_netloc
|
||||
from ...utils.image import get_avatar
|
||||
from ...utils.lang import get_lang
|
||||
from ...utils.render import Bind, render
|
||||
from ...utils.render.schemas.base import People
|
||||
from ...utils.screenshot import screenshot
|
||||
@@ -44,7 +45,7 @@ async def _(
|
||||
'v1/binding',
|
||||
Bind(
|
||||
platform=GAME_TYPE,
|
||||
status='unknown',
|
||||
type='unknown',
|
||||
user=People(
|
||||
avatar=await get_avatar(event_user_info, 'Data URI', None),
|
||||
name=user.user_name,
|
||||
@@ -53,7 +54,8 @@ async def _(
|
||||
avatar=await get_avatar(bot_info, 'Data URI', '../../static/logo/logo.svg'),
|
||||
name=bot_info.user_name,
|
||||
),
|
||||
command='top查我',
|
||||
prompt='top查我',
|
||||
lang=get_lang(),
|
||||
),
|
||||
)
|
||||
) as page_hash:
|
||||
|
||||
@@ -11,12 +11,13 @@ from ...db import query_bind_info, trigger
|
||||
from ...i18n import Lang
|
||||
from ...utils.exception import FallbackError
|
||||
from ...utils.host import HostPage, get_self_netloc
|
||||
from ...utils.lang import get_lang
|
||||
from ...utils.metrics import TetrisMetricsBasicWithLPM, get_metrics
|
||||
from ...utils.render import render
|
||||
from ...utils.render.avatar import get_avatar
|
||||
from ...utils.render.schemas.base import People
|
||||
from ...utils.render.schemas.top_info import Data as InfoData
|
||||
from ...utils.render.schemas.top_info import Info
|
||||
from ...utils.render.schemas.base import People, Trending
|
||||
from ...utils.render.schemas.v1.top.info import Data as InfoData
|
||||
from ...utils.render.schemas.v1.top.info import Info
|
||||
from ...utils.screenshot import screenshot
|
||||
from ...utils.typedefs import Me
|
||||
from . import alc
|
||||
@@ -79,8 +80,23 @@ async def make_query_image(profile: UserProfile) -> bytes:
|
||||
'v1/top/info',
|
||||
Info(
|
||||
user=People(avatar=get_avatar(profile.user_name), name=profile.user_name),
|
||||
today=InfoData(pps=today.pps, lpm=today.lpm, apm=today.apm, apl=today.apl),
|
||||
history=InfoData(pps=history.pps, lpm=history.lpm, apm=history.apm, apl=history.apl),
|
||||
today=InfoData(
|
||||
pps=today.pps,
|
||||
lpm=today.lpm,
|
||||
lpm_trending=Trending.KEEP,
|
||||
apm=today.apm,
|
||||
apl=today.apl,
|
||||
apm_trending=Trending.KEEP,
|
||||
),
|
||||
historical=InfoData(
|
||||
pps=history.pps,
|
||||
lpm=history.lpm,
|
||||
lpm_trending=Trending.KEEP,
|
||||
apm=history.apm,
|
||||
apl=history.apl,
|
||||
apm_trending=Trending.KEEP,
|
||||
),
|
||||
lang=get_lang(),
|
||||
),
|
||||
)
|
||||
) as page_hash:
|
||||
|
||||
@@ -9,6 +9,7 @@ from nonebot_plugin_waiter import suggest # type: ignore[import-untyped]
|
||||
from ...db import query_bind_info, remove_bind, trigger
|
||||
from ...utils.host import HostPage, get_self_netloc
|
||||
from ...utils.image import get_avatar
|
||||
from ...utils.lang import get_lang
|
||||
from ...utils.render import Bind, render
|
||||
from ...utils.render.schemas.base import People
|
||||
from ...utils.screenshot import screenshot
|
||||
@@ -46,7 +47,7 @@ async def _(
|
||||
'v1/binding',
|
||||
Bind(
|
||||
platform='TOP',
|
||||
status='unlink',
|
||||
type='unlink',
|
||||
user=People(
|
||||
avatar=await get_avatar(event_user_info, 'Data URI', None),
|
||||
name=user.user_name,
|
||||
@@ -55,7 +56,8 @@ async def _(
|
||||
avatar=await get_avatar(bot_info, 'Data URI', '../../static/logo/logo.svg'),
|
||||
name=bot_info.user_name,
|
||||
),
|
||||
command='top绑定{游戏ID}',
|
||||
prompt='top绑定{游戏ID}',
|
||||
lang=get_lang(),
|
||||
),
|
||||
)
|
||||
) as page_hash:
|
||||
|
||||
@@ -8,6 +8,7 @@ from nonebot_plugin_userinfo import BotUserInfo, EventUserInfo, UserInfo
|
||||
from ...db import BindStatus, create_or_update_bind, trigger
|
||||
from ...utils.host import HostPage, get_self_netloc
|
||||
from ...utils.image import get_avatar
|
||||
from ...utils.lang import get_lang
|
||||
from ...utils.render import Bind, render
|
||||
from ...utils.render.schemas.base import People
|
||||
from ...utils.screenshot import screenshot
|
||||
@@ -45,7 +46,7 @@ async def _(
|
||||
'v1/binding',
|
||||
Bind(
|
||||
platform=GAME_TYPE,
|
||||
status='unknown',
|
||||
type='unknown',
|
||||
user=People(
|
||||
avatar=await get_avatar(event_user_info, 'Data URI', None), name=user_info.data.name
|
||||
),
|
||||
@@ -53,7 +54,8 @@ async def _(
|
||||
avatar=await get_avatar(bot_info, 'Data URI', '../../static/logo/logo.svg'),
|
||||
name=bot_info.user_remark or bot_info.user_displayname or bot_info.user_name,
|
||||
),
|
||||
command='茶服查我',
|
||||
prompt='茶服查我',
|
||||
lang=get_lang(),
|
||||
),
|
||||
)
|
||||
) as page_hash:
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from asyncio import gather
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from http import HTTPStatus
|
||||
from typing import Literal, NamedTuple
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from nonebot.adapters import Event
|
||||
from nonebot.matcher import Matcher
|
||||
@@ -12,21 +13,27 @@ from nonebot_plugin_session import EventSession
|
||||
from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[import-untyped]
|
||||
from nonebot_plugin_user import get_user
|
||||
from nonebot_plugin_userinfo import EventUserInfo, UserInfo
|
||||
from sqlalchemy import select
|
||||
|
||||
from ...db import query_bind_info, trigger
|
||||
from ...i18n import Lang
|
||||
from ...utils.chart import get_split, get_value_bounds, handle_history_data
|
||||
from ...utils.exception import RequestError
|
||||
from ...utils.host import HostPage, get_self_netloc
|
||||
from ...utils.image import get_avatar
|
||||
from ...utils.lang import get_lang
|
||||
from ...utils.metrics import TetrisMetricsProWithLPMADPM, get_metrics
|
||||
from ...utils.render import render
|
||||
from ...utils.render.avatar import get_avatar as get_random_avatar
|
||||
from ...utils.render.schemas.base import People, Ranking
|
||||
from ...utils.render.schemas.tos_info import Info, Multiplayer, Radar
|
||||
from ...utils.render.schemas.base import HistoryData, People, Trending
|
||||
from ...utils.render.schemas.v1.base import History
|
||||
from ...utils.render.schemas.v1.tos.info import Info, Multiplayer, Singleplayer
|
||||
from ...utils.screenshot import screenshot
|
||||
from ...utils.time_it import time_it
|
||||
from ...utils.typedefs import Me, Number
|
||||
from . import alc
|
||||
from .api import Player
|
||||
from .api.models import TOSHistoricalData
|
||||
from .api.schemas.user_info import UserInfoSuccess
|
||||
from .constant import GAME_TYPE
|
||||
|
||||
@@ -148,6 +155,7 @@ async def _(account: Player, event_session: EventSession):
|
||||
command_args=[],
|
||||
):
|
||||
user_info, game_data = await gather(account.get_info(), get_game_data(account))
|
||||
await get_historical_data(user_info.data.teaid)
|
||||
if game_data is not None:
|
||||
await UniMessage.image(raw=await make_query_image(user_info, game_data, None)).finish()
|
||||
await make_query_text(user_info, game_data).finish()
|
||||
@@ -156,7 +164,7 @@ async def _(account: Player, event_session: EventSession):
|
||||
class GameData(NamedTuple):
|
||||
game_num: int
|
||||
metrics: TetrisMetricsProWithLPMADPM
|
||||
OR: Number
|
||||
or_: Number
|
||||
dspp: Number
|
||||
ge: Number
|
||||
|
||||
@@ -199,12 +207,49 @@ async def get_game_data(player: Player, query_num: int = 50) -> GameData | None:
|
||||
return GameData(
|
||||
game_num=num,
|
||||
metrics=metrics,
|
||||
OR=total_offset / total_receive * 100,
|
||||
or_=total_offset / total_receive * 100,
|
||||
dspp=total_dig / total_pieses,
|
||||
ge=2 * ((total_attack * total_dig) / total_pieses**2),
|
||||
)
|
||||
|
||||
|
||||
@time_it
|
||||
async def get_historical_data(unique_identifier: str) -> list[HistoryData]:
|
||||
async with get_session() as session:
|
||||
user_infos = (
|
||||
await session.scalars(
|
||||
select(TOSHistoricalData)
|
||||
.where(TOSHistoricalData.user_unique_identifier == unique_identifier)
|
||||
.where(TOSHistoricalData.api_type == 'User Info')
|
||||
.where(
|
||||
TOSHistoricalData.update_time
|
||||
> (
|
||||
datetime.now(ZoneInfo('Asia/Shanghai')).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
- timedelta(days=9)
|
||||
).replace(tzinfo=timezone.utc)
|
||||
)
|
||||
.order_by(TOSHistoricalData.id.asc())
|
||||
)
|
||||
).all()
|
||||
if user_infos:
|
||||
extra_info = (
|
||||
await session.scalars(
|
||||
select(TOSHistoricalData)
|
||||
.where(TOSHistoricalData.id < user_infos[0].id)
|
||||
.where(TOSHistoricalData.user_unique_identifier == unique_identifier)
|
||||
.where(TOSHistoricalData.api_type == 'User Info')
|
||||
.limit(1)
|
||||
)
|
||||
).one_or_none()
|
||||
if extra_info is not None:
|
||||
user_infos = [extra_info, *user_infos]
|
||||
return [
|
||||
HistoryData(score=float(i.data.data.rating_now), record_at=i.update_time.astimezone(ZoneInfo('Asia/Shanghai')))
|
||||
for i in user_infos
|
||||
if isinstance(i.data, UserInfoSuccess)
|
||||
]
|
||||
|
||||
|
||||
async def make_query_image(user_info: UserInfoSuccess, game_data: GameData, event_user_info: UserInfo | None) -> bytes:
|
||||
metrics = game_data.metrics
|
||||
sprint_value = (
|
||||
@@ -216,6 +261,8 @@ async def make_query_image(user_info: UserInfoSuccess, game_data: GameData, even
|
||||
if user_info.data.pb_sprint != '2147483647'
|
||||
else 'N/A'
|
||||
)
|
||||
data = handle_history_data(await get_historical_data(user_info.data.teaid))
|
||||
values = get_value_bounds([i.score for i in data])
|
||||
async with HostPage(
|
||||
await render(
|
||||
'v1/tos/info',
|
||||
@@ -226,26 +273,38 @@ async def make_query_image(user_info: UserInfoSuccess, game_data: GameData, even
|
||||
else get_random_avatar(user_info.data.teaid),
|
||||
name=user_info.data.name,
|
||||
),
|
||||
ranking=Ranking(rating=float(user_info.data.ranking), rd=round(float(user_info.data.rd_now), 2)),
|
||||
multiplayer=Multiplayer(
|
||||
pps=metrics.pps,
|
||||
history=History(
|
||||
data=data,
|
||||
max_value=values.value_max,
|
||||
min_value=values.value_min,
|
||||
split_interval=(split := get_split(value_bound=values, min_value=0)).split_value,
|
||||
offset=split.offset,
|
||||
),
|
||||
rating=round(float(user_info.data.rating_now), 2),
|
||||
rd=round(float(user_info.data.rd_now), 2),
|
||||
lpm=metrics.lpm,
|
||||
pps=metrics.pps,
|
||||
lpm_trending=Trending.KEEP,
|
||||
apm=metrics.apm,
|
||||
apl=metrics.apl,
|
||||
vs=metrics.vs,
|
||||
apm_trending=Trending.KEEP,
|
||||
adpm=metrics.adpm,
|
||||
vs=metrics.vs,
|
||||
adpl=metrics.adpl,
|
||||
),
|
||||
radar=Radar(
|
||||
adpm_trending=Trending.KEEP,
|
||||
app=(app := (metrics.apm / (60 * metrics.pps))),
|
||||
OR=game_data.OR,
|
||||
or_=game_data.or_,
|
||||
dspp=game_data.dspp,
|
||||
ci=150 * game_data.dspp - 125 * app + 50 * (metrics.vs / metrics.apm) - 25,
|
||||
ge=game_data.ge,
|
||||
),
|
||||
sprint=sprint_value,
|
||||
challenge=f'{int(user_info.data.pb_challenge):,}' if user_info.data.pb_challenge != '0' else 'N/A',
|
||||
marathon=f'{int(user_info.data.pb_marathon):,}' if user_info.data.pb_marathon != '0' else 'N/A',
|
||||
singleplayer=Singleplayer(
|
||||
sprint=sprint_value,
|
||||
challenge=f'{int(user_info.data.pb_challenge):,}' if user_info.data.pb_challenge != '0' else 'N/A',
|
||||
marathon=f'{int(user_info.data.pb_marathon):,}' if user_info.data.pb_marathon != '0' else 'N/A',
|
||||
),
|
||||
lang=get_lang(),
|
||||
),
|
||||
)
|
||||
) as page_hash:
|
||||
|
||||
@@ -9,6 +9,7 @@ from nonebot_plugin_waiter import suggest # type: ignore[import-untyped]
|
||||
from ...db import query_bind_info, remove_bind, trigger
|
||||
from ...utils.host import HostPage, get_self_netloc
|
||||
from ...utils.image import get_avatar
|
||||
from ...utils.lang import get_lang
|
||||
from ...utils.render import Bind, render
|
||||
from ...utils.render.avatar import get_avatar as get_random_avatar
|
||||
from ...utils.render.schemas.base import People
|
||||
@@ -47,7 +48,7 @@ async def _(
|
||||
'v1/binding',
|
||||
Bind(
|
||||
platform='TOS',
|
||||
status='unlink',
|
||||
type='unlink',
|
||||
user=People(
|
||||
avatar=await get_avatar(event_user_info, 'Data URI', None)
|
||||
if event_user_info is not None
|
||||
@@ -58,7 +59,8 @@ async def _(
|
||||
avatar=await get_avatar(bot_info, 'Data URI', '../../static/logo/logo.svg'),
|
||||
name=bot_info.user_name,
|
||||
),
|
||||
command='茶服绑定{游戏ID}',
|
||||
prompt='茶服绑定{游戏ID}',
|
||||
lang=get_lang(),
|
||||
),
|
||||
)
|
||||
) as page_hash:
|
||||
|
||||
@@ -67,6 +67,19 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"title": "Template",
|
||||
"description": "Scope 'template' of lang item",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"template_language": {
|
||||
"title": "template_language",
|
||||
"description": "value of lang item type 'template_language'",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
{
|
||||
"scope": "error",
|
||||
"types": [{ "subtype": "MessageFormatError", "types": ["TETR.IO", "TOS", "TOP"] }]
|
||||
}
|
||||
},
|
||||
{ "scope": "template", "types": ["template_language"] }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -12,5 +12,8 @@
|
||||
"TOS": "Username/ID is invalid",
|
||||
"TOP": "Username is invalid"
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"template_language": "en-US"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# This file is @generated by tarina.lang CLI tool
|
||||
# It is not intended for manual editing.
|
||||
|
||||
from tarina.lang.model import LangItem, LangModel # type: ignore[import-untyped]
|
||||
from tarina.lang.model import LangItem, LangModel
|
||||
|
||||
|
||||
class InteractionWrong:
|
||||
@@ -27,6 +27,11 @@ class Error:
|
||||
MessageFormatError = ErrorMessageformaterror
|
||||
|
||||
|
||||
class Template:
|
||||
template_language: LangItem = LangItem('template', 'template_language')
|
||||
|
||||
|
||||
class Lang(LangModel):
|
||||
interaction = Interaction
|
||||
error = Error
|
||||
template = Template
|
||||
|
||||
@@ -10,5 +10,8 @@
|
||||
"TOS": "用户名/ID不合法",
|
||||
"TOP": "用户名不合法"
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"template_language": "zh-CN"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ from nonebot.log import logger
|
||||
from playwright.__main__ import main
|
||||
from playwright.async_api import Browser, BrowserContext, async_playwright
|
||||
|
||||
from ..config.config import config
|
||||
|
||||
driver = get_driver()
|
||||
|
||||
global_config = driver.config
|
||||
@@ -76,6 +78,7 @@ class BrowserManager:
|
||||
"""启动浏览器实例"""
|
||||
playwright = await async_playwright().start()
|
||||
cls._browser = await playwright.firefox.launch(
|
||||
headless=not config.tetris.development,
|
||||
firefox_user_prefs={
|
||||
'network.http.max-persistent-connections-per-server': 64,
|
||||
},
|
||||
|
||||
122
nonebot_plugin_tetris_stats/utils/chart.py
Normal file
122
nonebot_plugin_tetris_stats/utils/chart.py
Normal file
@@ -0,0 +1,122 @@
|
||||
from datetime import datetime, timedelta
|
||||
from math import ceil, floor, inf
|
||||
from typing import NamedTuple
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from .exception import WhatTheFuckError
|
||||
from .render.schemas.base import HistoryData
|
||||
from .typedefs import Number
|
||||
|
||||
|
||||
class ValueBound(NamedTuple):
|
||||
value_max: int
|
||||
value_min: int
|
||||
|
||||
|
||||
class Split(NamedTuple):
|
||||
split_value: int
|
||||
offset: int
|
||||
|
||||
|
||||
def get_value_bounds(values: list[int | float]) -> ValueBound:
|
||||
value_max = 10 * ceil(max(values) / 10)
|
||||
value_min = 10 * floor(min(values) / 10)
|
||||
return ValueBound(value_max, value_min)
|
||||
|
||||
|
||||
def get_split(value_bound: ValueBound, max_value: Number = inf, min_value: Number = -inf) -> Split:
|
||||
offset = 0
|
||||
overflow = 0
|
||||
|
||||
while True:
|
||||
if (new_max_value := value_bound.value_max + offset + overflow) > max_value:
|
||||
overflow -= 1
|
||||
continue
|
||||
if (new_min_value := value_bound.value_min - offset + overflow) < min_value:
|
||||
overflow += 1
|
||||
continue
|
||||
if ((new_max_value - new_min_value) / 40).is_integer():
|
||||
split_value = int((value_bound.value_max + offset - (value_bound.value_min - offset)) / 4)
|
||||
break
|
||||
offset += 1
|
||||
return Split(split_value, offset + overflow)
|
||||
|
||||
|
||||
def get_specified_point(
|
||||
previous_point: HistoryData,
|
||||
behind_point: HistoryData,
|
||||
point_time: datetime,
|
||||
) -> HistoryData:
|
||||
"""根据给出的 previous_point 和 behind_point, 推算 point_time 点处的数据
|
||||
|
||||
Args:
|
||||
previous_point (Data): 前面的数据点
|
||||
behind_point (Data): 后面的数据点
|
||||
point_time (datetime): 要推算的点的位置
|
||||
|
||||
Returns:
|
||||
Data: 要推算的点的数据
|
||||
"""
|
||||
# 求两个点的斜率
|
||||
slope = (behind_point.score - previous_point.score) / (
|
||||
datetime.timestamp(behind_point.record_at) - datetime.timestamp(previous_point.record_at)
|
||||
)
|
||||
return HistoryData(
|
||||
record_at=point_time,
|
||||
score=previous_point.score
|
||||
+ slope * (datetime.timestamp(point_time) - datetime.timestamp(previous_point.record_at)),
|
||||
)
|
||||
|
||||
|
||||
def handle_history_data(data: list[HistoryData]) -> list[HistoryData]: # noqa: C901, PLR0912
|
||||
# 按照 记录时间 对数据进行排序
|
||||
data.sort(key=lambda x: x.record_at)
|
||||
|
||||
# 定义时间边界, 右边界为当前时间的当天零点, 左边界为右边界前推9天
|
||||
# 返回值的[0]和[-1]分别应满足left_border和right_border
|
||||
zero = datetime.now(ZoneInfo('Asia/Shanghai')).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
left_border = zero - timedelta(days=9)
|
||||
right_border = zero.replace(microsecond=1000)
|
||||
|
||||
lefts: list[HistoryData] = []
|
||||
in_border: list[HistoryData] = []
|
||||
rights: list[HistoryData] = []
|
||||
|
||||
# 根据 记录时间 将数据分类到对应的列表中
|
||||
for i in data:
|
||||
if i.record_at < left_border:
|
||||
lefts.append(i)
|
||||
elif i.record_at < right_border:
|
||||
in_border.append(i)
|
||||
else:
|
||||
rights.append(i)
|
||||
|
||||
ret: list[HistoryData] = []
|
||||
|
||||
# 处理左边界的点
|
||||
if lefts and in_border: # 如果边界左侧和边界内都有值则推算
|
||||
ret.append(get_specified_point(lefts[-1], in_border[0], left_border))
|
||||
elif lefts and not in_border: # 如果边界左侧有值但是边界内没有值则直接取左侧的最后一个值
|
||||
ret.append(HistoryData(score=lefts[-1].score, record_at=left_border))
|
||||
elif not lefts and in_border: # 如果边界左侧没有值但是边界内有值则直接取边界内的第一个值
|
||||
ret.append(HistoryData(score=in_border[0].score, record_at=left_border))
|
||||
elif not lefts and not in_border and rights: # 如果边界左侧和边界内都没有值但是边界右侧有值则直接取边界右侧的第一个值 # fmt: skip
|
||||
ret.append(HistoryData(score=rights[0].score, record_at=left_border))
|
||||
else: # 暂时没想到其他情况
|
||||
raise WhatTheFuckError
|
||||
|
||||
# 添加边界内数据
|
||||
ret.extend(in_border)
|
||||
|
||||
# 处理右边界的点
|
||||
if in_border and rights: # 如果边界内和边界右侧都有值则推算
|
||||
ret.append(get_specified_point(in_border[-1], rights[0], right_border))
|
||||
elif not in_border and rights: # 如果边界内没有值但是边界右侧有值则直接取右侧的第一个值
|
||||
ret.append(HistoryData(score=rights[0].score, record_at=right_border))
|
||||
elif in_border and not rights: # 如果边界内有值但是边界右侧没有值则直接取边界内的最后一个值
|
||||
ret.append(HistoryData(score=in_border[-1].score, record_at=right_border))
|
||||
elif not in_border and not rights and lefts: # 如果边界内和边界右侧都没有值但是边界左侧有值则直接取边界左侧的最后一个值 # fmt: skip
|
||||
ret.append(HistoryData(score=lefts[-1].score, record_at=right_border))
|
||||
else: # 暂时没想到其他情况
|
||||
raise WhatTheFuckError
|
||||
return ret
|
||||
@@ -12,7 +12,7 @@ from nonebot import get_app, get_driver
|
||||
from nonebot.log import logger
|
||||
from yarl import URL
|
||||
|
||||
from ..config.config import CACHE_PATH
|
||||
from ..config.config import CACHE_PATH, config
|
||||
from ..games.tetrio.api.cache import request
|
||||
from .image import img_to_png
|
||||
from .templates import TEMPLATES_DIR
|
||||
@@ -45,15 +45,21 @@ class HostPage:
|
||||
async def __aenter__(self) -> str:
|
||||
return self.page_hash
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb) -> None: # noqa: ANN001
|
||||
self.pages.pop(self.page_hash, None)
|
||||
if not config.tetris.development:
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb) -> None: # noqa: ANN001
|
||||
self.pages.pop(self.page_hash, None)
|
||||
else:
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb) -> None: # noqa: ANN001
|
||||
pass
|
||||
|
||||
|
||||
@driver.on_startup
|
||||
def _():
|
||||
app.mount(
|
||||
'/host/assets',
|
||||
StaticFiles(directory=TEMPLATES_DIR / 'assets'),
|
||||
'/host/_nuxt',
|
||||
StaticFiles(directory=TEMPLATES_DIR / '_nuxt'),
|
||||
name='assets',
|
||||
)
|
||||
logger.success('assets mounted')
|
||||
|
||||
8
nonebot_plugin_tetris_stats/utils/lang.py
Normal file
8
nonebot_plugin_tetris_stats/utils/lang.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from typing import cast
|
||||
|
||||
from ..i18n.model import Lang
|
||||
from .typedefs import Lang as LangType
|
||||
|
||||
|
||||
def get_lang() -> LangType:
|
||||
return cast('LangType', Lang.template.template_language())
|
||||
@@ -4,20 +4,26 @@ from jinja2 import Environment, FileSystemLoader
|
||||
from nonebot.compat import PYDANTIC_V2
|
||||
|
||||
from ..templates import TEMPLATES_DIR
|
||||
from .schemas.base import Base
|
||||
from .schemas.bind import Bind
|
||||
from .schemas.tetrio.rank.detail import Data as TETRIORankDetailData
|
||||
from .schemas.tetrio.rank.v1 import Data as TETRIORankDataV1
|
||||
from .schemas.tetrio.rank.v2 import Data as TETRIORankDataV2
|
||||
from .schemas.tetrio.record.blitz import Record as TETRIORecordBlitz
|
||||
from .schemas.tetrio.record.sprint import Record as TETRIORecordSprint
|
||||
from .schemas.tetrio.user.info_v1 import Info as TETRIOUserInfoV1
|
||||
from .schemas.tetrio.user.info_v2 import Info as TETRIOUserInfoV2
|
||||
from .schemas.tetrio.user.list_v2 import List as TETRIOUserListV2
|
||||
from .schemas.top_info import Info as TOPInfo
|
||||
from .schemas.tos_info import Info as TOSInfo
|
||||
from .schemas.v1.tetrio.rank import Data as TETRIORankDataV1
|
||||
from .schemas.v1.tetrio.user.info import Info as TETRIOUserInfoV1
|
||||
from .schemas.v1.top.info import Info as TOPInfoV1
|
||||
from .schemas.v1.tos.info import Info as TOSInfoV1
|
||||
from .schemas.v2.tetrio.rank import Data as TETRIORankDataV2
|
||||
from .schemas.v2.tetrio.rank.detail import Data as TETRIORankDetailDataV2
|
||||
from .schemas.v2.tetrio.record.blitz import Record as TETRIORecordBlitzV2
|
||||
from .schemas.v2.tetrio.record.sprint import Record as TETRIORecordSprintV2
|
||||
from .schemas.v2.tetrio.tetra_league import Data as TETRIOTetraLeagueDataV2
|
||||
from .schemas.v2.tetrio.user.info import Info as TETRIOUserInfoV2
|
||||
from .schemas.v2.tetrio.user.list import List as TETRIOUserListV2
|
||||
|
||||
env = Environment(
|
||||
loader=FileSystemLoader(TEMPLATES_DIR), autoescape=True, trim_blocks=True, lstrip_blocks=True, enable_async=True
|
||||
loader=FileSystemLoader(TEMPLATES_DIR),
|
||||
autoescape=False, # noqa: S701
|
||||
trim_blocks=True,
|
||||
lstrip_blocks=True,
|
||||
enable_async=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -28,48 +34,26 @@ async def render(render_type: Literal['v1/tetrio/info'], data: TETRIOUserInfoV1)
|
||||
@overload
|
||||
async def render(render_type: Literal['v1/tetrio/rank'], data: TETRIORankDataV1) -> str: ...
|
||||
@overload
|
||||
async def render(render_type: Literal['v1/top/info'], data: TOPInfo) -> str: ...
|
||||
async def render(render_type: Literal['v1/top/info'], data: TOPInfoV1) -> str: ...
|
||||
@overload
|
||||
async def render(render_type: Literal['v1/tos/info'], data: TOSInfo) -> str: ...
|
||||
async def render(render_type: Literal['v1/tos/info'], data: TOSInfoV1) -> str: ...
|
||||
@overload
|
||||
async def render(render_type: Literal['v2/tetrio/rank'], data: TETRIORankDataV2) -> str: ...
|
||||
@overload
|
||||
async def render(render_type: Literal['v2/tetrio/rank/detail'], data: TETRIORankDetailDataV2) -> str: ...
|
||||
@overload
|
||||
async def render(render_type: Literal['v2/tetrio/record/blitz'], data: TETRIORecordBlitzV2) -> str: ...
|
||||
@overload
|
||||
async def render(render_type: Literal['v2/tetrio/record/sprint'], data: TETRIORecordSprintV2) -> str: ...
|
||||
@overload
|
||||
async def render(render_type: Literal['v2/tetrio/tetra-league'], data: TETRIOTetraLeagueDataV2) -> str: ...
|
||||
@overload
|
||||
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
|
||||
async def render(render_type: Literal['v2/tetrio/record/40l'], data: TETRIORecordSprint) -> str: ...
|
||||
@overload
|
||||
async def render(render_type: Literal['v2/tetrio/record/blitz'], data: TETRIORecordBlitz) -> str: ...
|
||||
@overload
|
||||
async def render(render_type: Literal['v2/tetrio/rank'], data: TETRIORankDataV2) -> str: ...
|
||||
@overload
|
||||
async def render(render_type: Literal['v2/tetrio/rank/detail'], data: TETRIORankDetailData) -> str: ...
|
||||
|
||||
|
||||
async def render(
|
||||
render_type: Literal[
|
||||
'v1/binding',
|
||||
'v1/tetrio/info',
|
||||
'v1/tetrio/rank',
|
||||
'v1/top/info',
|
||||
'v1/tos/info',
|
||||
'v2/tetrio/user/info',
|
||||
'v2/tetrio/user/list',
|
||||
'v2/tetrio/record/40l',
|
||||
'v2/tetrio/record/blitz',
|
||||
'v2/tetrio/rank',
|
||||
'v2/tetrio/rank/detail',
|
||||
],
|
||||
data: Bind
|
||||
| TETRIOUserInfoV1
|
||||
| TETRIORankDataV1
|
||||
| TOPInfo
|
||||
| TOSInfo
|
||||
| TETRIOUserInfoV2
|
||||
| TETRIOUserListV2
|
||||
| TETRIORecordSprint
|
||||
| TETRIORecordBlitz
|
||||
| TETRIORankDataV2
|
||||
| TETRIORankDetailData,
|
||||
render_type: str,
|
||||
data: Base,
|
||||
) -> str:
|
||||
if PYDANTIC_V2:
|
||||
return await env.get_template('index.html').render_async(
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel
|
||||
from strenum import StrEnum
|
||||
|
||||
from ...typedefs import Number
|
||||
from ...typedefs import Lang, Number
|
||||
|
||||
|
||||
class Base(BaseModel):
|
||||
lang: Lang
|
||||
|
||||
|
||||
class Avatar(BaseModel):
|
||||
@@ -18,3 +24,14 @@ class People(BaseModel):
|
||||
class Ranking(BaseModel):
|
||||
rating: Number
|
||||
rd: Number
|
||||
|
||||
|
||||
class HistoryData(BaseModel):
|
||||
score: Number
|
||||
record_at: datetime
|
||||
|
||||
|
||||
class Trending(StrEnum):
|
||||
UP = 'up'
|
||||
KEEP = 'keep'
|
||||
DOWN = 'down'
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .base import People
|
||||
from .base import Base, People
|
||||
|
||||
|
||||
class Bind(BaseModel):
|
||||
class Bind(Base):
|
||||
platform: Literal['TETR.IO', 'TOP', 'TOS']
|
||||
status: Literal['error', 'success', 'unknown', 'unlink', 'unverified']
|
||||
type: Literal['success', 'unknown', 'unlink', 'unverified', 'error']
|
||||
user: People
|
||||
bot: People
|
||||
command: str
|
||||
prompt: str
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ......games.tetrio.api.typedefs import ValidRank
|
||||
|
||||
|
||||
class SpecialData(BaseModel):
|
||||
apm: float
|
||||
pps: float
|
||||
lpm: float
|
||||
vs: float
|
||||
adpm: float
|
||||
apl: float | None = None
|
||||
adpl: float | None = None
|
||||
apm_holder: str | None = None
|
||||
pps_holder: str | None = None
|
||||
vs_holder: str | None = None
|
||||
|
||||
|
||||
class Data(BaseModel):
|
||||
name: ValidRank
|
||||
trending: float
|
||||
require_tr: float
|
||||
players: int
|
||||
|
||||
minimum_data: SpecialData
|
||||
average_data: SpecialData
|
||||
maximum_data: SpecialData
|
||||
|
||||
updated_at: datetime
|
||||
@@ -1,25 +0,0 @@
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ......games.tetrio.api.typedefs import ValidRank
|
||||
|
||||
|
||||
class AverageData(BaseModel):
|
||||
pps: float
|
||||
apm: float
|
||||
apl: float
|
||||
vs: float
|
||||
adpl: float
|
||||
|
||||
|
||||
class ItemData(BaseModel):
|
||||
require_tr: float
|
||||
trending: float
|
||||
average_data: AverageData
|
||||
players: int
|
||||
|
||||
|
||||
class Data(BaseModel):
|
||||
items: dict[ValidRank, ItemData]
|
||||
updated_at: datetime
|
||||
@@ -1,10 +0,0 @@
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .....typedefs import Number
|
||||
|
||||
|
||||
class TetraLeagueHistoryData(BaseModel):
|
||||
record_at: datetime
|
||||
tr: Number
|
||||
@@ -1,49 +0,0 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ......games.tetrio.api.typedefs import Rank
|
||||
from .....typedefs import Number
|
||||
from ...base import People, Ranking
|
||||
from .base import TetraLeagueHistoryData
|
||||
|
||||
|
||||
class User(People):
|
||||
bio: str | None
|
||||
|
||||
|
||||
class TetraLeague(BaseModel):
|
||||
rank: Rank
|
||||
tr: Number
|
||||
global_rank: Number
|
||||
pps: Number
|
||||
lpm: Number
|
||||
apm: Number
|
||||
apl: Number
|
||||
vs: Number
|
||||
adpm: Number
|
||||
adpl: Number
|
||||
|
||||
|
||||
class TetraLeagueHistory(BaseModel):
|
||||
data: list[TetraLeagueHistoryData]
|
||||
split_interval: Number
|
||||
min_tr: Number
|
||||
max_tr: Number
|
||||
offset: Number
|
||||
|
||||
|
||||
class Radar(BaseModel):
|
||||
app: Number
|
||||
dsps: Number
|
||||
dspp: Number
|
||||
ci: Number
|
||||
ge: Number
|
||||
|
||||
|
||||
class Info(BaseModel):
|
||||
user: User
|
||||
ranking: Ranking
|
||||
tetra_league: TetraLeague
|
||||
tetra_league_history: TetraLeagueHistory
|
||||
radar: Radar
|
||||
sprint: str
|
||||
blitz: str
|
||||
@@ -1,17 +0,0 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ...typedefs import Number
|
||||
from .base import People
|
||||
|
||||
|
||||
class Data(BaseModel):
|
||||
pps: Number
|
||||
lpm: Number
|
||||
apm: Number
|
||||
apl: Number
|
||||
|
||||
|
||||
class Info(BaseModel):
|
||||
user: People
|
||||
today: Data
|
||||
history: Data
|
||||
@@ -1,32 +0,0 @@
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from ...typedefs import Number
|
||||
from .base import People, Ranking
|
||||
|
||||
|
||||
class Multiplayer(BaseModel):
|
||||
pps: Number
|
||||
lpm: Number
|
||||
apm: Number
|
||||
apl: Number
|
||||
vs: Number
|
||||
adpm: Number
|
||||
adpl: Number
|
||||
|
||||
|
||||
class Radar(BaseModel):
|
||||
app: Number
|
||||
OR: Number = Field(serialization_alias='or')
|
||||
dspp: Number
|
||||
ci: Number
|
||||
ge: Number
|
||||
|
||||
|
||||
class Info(BaseModel):
|
||||
user: People
|
||||
ranking: Ranking
|
||||
multiplayer: Multiplayer
|
||||
radar: Radar
|
||||
sprint: str
|
||||
challenge: str
|
||||
marathon: str
|
||||
12
nonebot_plugin_tetris_stats/utils/render/schemas/v1/base.py
Normal file
12
nonebot_plugin_tetris_stats/utils/render/schemas/v1/base.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ....typedefs import Number
|
||||
from ..base import HistoryData
|
||||
|
||||
|
||||
class History(BaseModel):
|
||||
data: list[HistoryData]
|
||||
split_interval: Number
|
||||
min_value: Number
|
||||
max_value: Number
|
||||
offset: Number
|
||||
@@ -3,6 +3,7 @@ from datetime import datetime
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ......games.tetrio.api.typedefs import ValidRank
|
||||
from ...base import Base
|
||||
|
||||
|
||||
class ItemData(BaseModel):
|
||||
@@ -11,6 +12,6 @@ class ItemData(BaseModel):
|
||||
players: int
|
||||
|
||||
|
||||
class Data(BaseModel):
|
||||
class Data(Base):
|
||||
items: dict[ValidRank, ItemData]
|
||||
updated_at: datetime
|
||||
@@ -0,0 +1,50 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .......games.tetrio.api.typedefs import Rank
|
||||
from ......typedefs import Number
|
||||
from ....base import Base, People, Trending
|
||||
from ...base import History
|
||||
|
||||
|
||||
class User(People):
|
||||
bio: str | None
|
||||
|
||||
|
||||
class Multiplayer(BaseModel):
|
||||
glicko: str
|
||||
rd: Number
|
||||
rank: Rank
|
||||
tr: str
|
||||
global_rank: Number
|
||||
|
||||
history: History
|
||||
|
||||
lpm: Number
|
||||
pps: Number
|
||||
lpm_trending: Trending
|
||||
|
||||
apm: Number
|
||||
apl: Number
|
||||
apm_trending: Trending
|
||||
|
||||
adpm: Number
|
||||
vs: Number
|
||||
adpl: Number
|
||||
adpm_trending: Trending
|
||||
|
||||
app: Number
|
||||
ci: Number
|
||||
dspp: Number
|
||||
dsps: Number
|
||||
ge: Number
|
||||
|
||||
|
||||
class Singleplayer(BaseModel):
|
||||
sprint: str
|
||||
blitz: str
|
||||
|
||||
|
||||
class Info(Base):
|
||||
user: User
|
||||
multiplayer: Multiplayer
|
||||
singleplayer: Singleplayer
|
||||
@@ -0,0 +1,19 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .....typedefs import Number
|
||||
from ...base import Base, People, Trending
|
||||
|
||||
|
||||
class Data(BaseModel):
|
||||
pps: Number
|
||||
lpm: Number
|
||||
lpm_trending: Trending
|
||||
apm: Number
|
||||
apl: Number
|
||||
apm_trending: Trending
|
||||
|
||||
|
||||
class Info(Base):
|
||||
user: People
|
||||
today: Data
|
||||
historical: Data
|
||||
@@ -0,0 +1,42 @@
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from .....typedefs import Number
|
||||
from ...base import Base, People, Trending
|
||||
from ..base import History
|
||||
|
||||
|
||||
class Multiplayer(BaseModel):
|
||||
history: History
|
||||
rating: Number
|
||||
rd: Number
|
||||
|
||||
lpm: Number
|
||||
pps: Number
|
||||
lpm_trending: Trending
|
||||
|
||||
apm: Number
|
||||
apl: Number
|
||||
apm_trending: Trending
|
||||
|
||||
adpm: Number
|
||||
vs: Number
|
||||
adpl: Number
|
||||
adpm_trending: Trending
|
||||
|
||||
app: Number
|
||||
ci: Number
|
||||
dspp: Number
|
||||
or_: Number = Field(serialization_alias='or')
|
||||
ge: Number
|
||||
|
||||
|
||||
class Singleplayer(BaseModel):
|
||||
sprint: str
|
||||
challenge: str
|
||||
marathon: str
|
||||
|
||||
|
||||
class Info(Base):
|
||||
user: People
|
||||
multiplayer: Multiplayer
|
||||
singleplayer: Singleplayer
|
||||
@@ -0,0 +1,27 @@
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .......games.tetrio.api.typedefs import ValidRank
|
||||
from ......typedefs import Number
|
||||
from ....base import Base
|
||||
|
||||
|
||||
class AverageData(BaseModel):
|
||||
pps: Number
|
||||
apm: Number
|
||||
apl: Number
|
||||
vs: Number
|
||||
adpl: Number
|
||||
|
||||
|
||||
class ItemData(BaseModel):
|
||||
require_tr: Number
|
||||
trending: Number
|
||||
average_data: AverageData
|
||||
players: Number
|
||||
|
||||
|
||||
class Data(Base):
|
||||
items: dict[ValidRank, ItemData]
|
||||
updated_at: datetime
|
||||
@@ -0,0 +1,33 @@
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .......games.tetrio.api.typedefs import ValidRank
|
||||
from ......typedefs import Number
|
||||
from ....base import Base
|
||||
|
||||
|
||||
class SpecialData(BaseModel):
|
||||
apm: Number
|
||||
pps: Number
|
||||
lpm: Number
|
||||
vs: Number
|
||||
adpm: Number
|
||||
apl: Number | None = None
|
||||
adpl: Number | None = None
|
||||
apm_holder: str | None = None
|
||||
pps_holder: str | None = None
|
||||
vs_holder: str | None = None
|
||||
|
||||
|
||||
class Data(Base):
|
||||
name: ValidRank
|
||||
trending: Number
|
||||
require_tr: Number
|
||||
players: Number
|
||||
|
||||
minimum_data: SpecialData
|
||||
average_data: SpecialData
|
||||
maximum_data: SpecialData
|
||||
|
||||
updated_at: datetime
|
||||
@@ -3,7 +3,7 @@ from typing import Literal
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ...base import People
|
||||
from ....base import Base, People
|
||||
|
||||
|
||||
class User(People):
|
||||
@@ -61,7 +61,7 @@ class Statistic(BaseModel):
|
||||
finesse: Finesse
|
||||
|
||||
|
||||
class Record(BaseModel):
|
||||
class Record(Base):
|
||||
type: Literal['best', 'personal_best', 'recent', 'disputed']
|
||||
|
||||
user: User
|
||||
@@ -0,0 +1,39 @@
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .....typedefs import Number
|
||||
from ...base import Base
|
||||
|
||||
|
||||
class StatisticalData(BaseModel):
|
||||
pps: Number
|
||||
apm: Number
|
||||
apl: Number
|
||||
vs: Number
|
||||
adpl: Number
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
|
||||
|
||||
class Handling(BaseModel):
|
||||
arr: Number
|
||||
das: Number
|
||||
sdf: Number
|
||||
|
||||
|
||||
class Game(BaseModel):
|
||||
user: User
|
||||
points: Number
|
||||
average_data: StatisticalData
|
||||
data: list[StatisticalData]
|
||||
handling: Handling
|
||||
|
||||
|
||||
class Data(Base):
|
||||
replay_id: str
|
||||
games: list[Game]
|
||||
play_at: datetime
|
||||
@@ -3,10 +3,11 @@ from typing import Literal
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ......games.tetrio.api.typedefs import Rank
|
||||
from .....typedefs import Number
|
||||
from ...base import Avatar
|
||||
from .base import TetraLeagueHistoryData
|
||||
from .......games.tetrio.api.schemas.summaries.achievements import ArType, RankType
|
||||
from .......games.tetrio.api.schemas.summaries.achievements import Rank as AchievementRank
|
||||
from .......games.tetrio.api.typedefs import Rank
|
||||
from ......typedefs import Number
|
||||
from ....base import Avatar, Base, HistoryData
|
||||
|
||||
|
||||
class Badge(BaseModel):
|
||||
@@ -16,12 +17,25 @@ class Badge(BaseModel):
|
||||
receive_at: datetime | None
|
||||
|
||||
|
||||
class Achievement(BaseModel):
|
||||
key: int
|
||||
rank_type: RankType
|
||||
ar_type: ArType
|
||||
stub: bool | None
|
||||
rank: AchievementRank | None
|
||||
achieved_score: float | None
|
||||
pos: int | None
|
||||
progress: float | None
|
||||
total: int | None
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
country: str | None
|
||||
|
||||
role: Literal['anon', 'user', 'bot', 'halfmod', 'mod', 'admin', 'sysop', 'hidden', 'banned']
|
||||
botmaster: str | None
|
||||
|
||||
avatar: str | Avatar
|
||||
banner: str | None
|
||||
@@ -36,6 +50,9 @@ class User(BaseModel):
|
||||
badges: list[Badge]
|
||||
xp: Number
|
||||
|
||||
ar: Number
|
||||
achievements: list[Achievement]
|
||||
|
||||
playtime: str | None
|
||||
join_at: datetime | None
|
||||
|
||||
@@ -74,18 +91,20 @@ class TetraLeague(BaseModel):
|
||||
|
||||
decaying: bool
|
||||
|
||||
history: list[TetraLeagueHistoryData] | None
|
||||
history: list[HistoryData] | None
|
||||
|
||||
|
||||
class Sprint(BaseModel):
|
||||
time: str
|
||||
global_rank: int | None
|
||||
global_rank: Number | None
|
||||
country_rank: Number | None
|
||||
play_at: datetime
|
||||
|
||||
|
||||
class Blitz(BaseModel):
|
||||
score: int
|
||||
score: Number
|
||||
global_rank: int | None
|
||||
country_rank: int | None
|
||||
play_at: datetime
|
||||
|
||||
|
||||
@@ -94,9 +113,29 @@ class Zen(BaseModel):
|
||||
score: int
|
||||
|
||||
|
||||
class Info(BaseModel):
|
||||
class Week(BaseModel):
|
||||
altitude: Number
|
||||
global_rank: int | None
|
||||
country_rank: int | None
|
||||
play_at: datetime
|
||||
|
||||
|
||||
class Best(BaseModel):
|
||||
altitude: Number
|
||||
global_rank: int | None
|
||||
play_at: datetime
|
||||
|
||||
|
||||
class Zenith(BaseModel):
|
||||
week: Week | None
|
||||
best: Best | None
|
||||
|
||||
|
||||
class Info(Base):
|
||||
user: User
|
||||
tetra_league: TetraLeague | None
|
||||
zenith: Zenith | None
|
||||
zenithex: Zenith | None
|
||||
statistic: Statistic | None
|
||||
sprint: Sprint | None
|
||||
blitz: Blitz | None
|
||||
@@ -1,24 +1,23 @@
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ......games.tetrio.api.typedefs import Rank
|
||||
from .....typedefs import Number
|
||||
from ...base import Avatar
|
||||
from .......games.tetrio.api.typedefs import Rank
|
||||
from ......typedefs import Number
|
||||
from ....base import Avatar, Base
|
||||
|
||||
|
||||
class TetraLeague(BaseModel):
|
||||
pps: Number
|
||||
apm: Number
|
||||
apl: Number
|
||||
vs: Number | None
|
||||
adpl: Number | None
|
||||
|
||||
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):
|
||||
@@ -26,11 +25,14 @@ class User(BaseModel):
|
||||
name: str
|
||||
avatar: str | Avatar
|
||||
country: str | None
|
||||
tetra_league: TetraLeague
|
||||
xp: Number
|
||||
join_at: datetime | None
|
||||
|
||||
|
||||
class List(BaseModel):
|
||||
class Data(BaseModel):
|
||||
user: User
|
||||
tetra_league: TetraLeague
|
||||
|
||||
|
||||
class List(Base):
|
||||
show_index: bool
|
||||
users: list[User]
|
||||
data: list[Data]
|
||||
@@ -16,6 +16,7 @@ async def screenshot(url: str) -> bytes:
|
||||
context = await BrowserManager.get_context('screenshot', factory=context_factory)
|
||||
async with await context.new_page() as page:
|
||||
await page.goto(url)
|
||||
await page.wait_for_selector('#content')
|
||||
size: ViewportSize = await page.evaluate("""
|
||||
() => {
|
||||
const element = document.querySelector('#content');
|
||||
@@ -26,5 +27,4 @@ async def screenshot(url: str) -> bytes:
|
||||
};
|
||||
""")
|
||||
await page.set_viewport_size(size)
|
||||
await page.wait_for_load_state('networkidle')
|
||||
return await page.locator('id=content').screenshot(animations='disabled', timeout=5000, type='png')
|
||||
|
||||
@@ -30,7 +30,7 @@ async def download_templates(tag: str) -> Path:
|
||||
tag = (
|
||||
(
|
||||
await client.get(
|
||||
'https://github.com/A-Minos/tetris-stats-templates/releases/latest', follow_redirects=True
|
||||
'https://github.com/A-Minos/tetris-stats-templates-new/releases/latest', follow_redirects=True
|
||||
)
|
||||
)
|
||||
.url.path.strip('/')
|
||||
@@ -43,7 +43,7 @@ async def download_templates(tag: str) -> Path:
|
||||
async with (
|
||||
client.stream(
|
||||
'GET',
|
||||
f'https://github.com/A-Minos/tetris-stats-templates/releases/download/{tag}/dist.zip',
|
||||
f'https://github.com/A-Minos/tetris-stats-templates-new/releases/download/{tag}/dist.zip',
|
||||
follow_redirects=True,
|
||||
) as response,
|
||||
aopen(path, 'wb') as file,
|
||||
@@ -107,7 +107,7 @@ async def init_templates(tag: str) -> bool:
|
||||
async def check_tag(tag: str) -> bool:
|
||||
async with AsyncClient(proxy=config.tetris.proxy.github or config.tetris.proxy.main) as client:
|
||||
return (
|
||||
await client.get(f'https://github.com/A-Minos/tetris-stats-templates/releases/tag/{tag}')
|
||||
await client.get(f'https://github.com/A-Minos/tetris-stats-templates-new/releases/tag/{tag}')
|
||||
).status_code != HTTPStatus.NOT_FOUND
|
||||
|
||||
|
||||
|
||||
@@ -59,3 +59,5 @@ Me: TypeAlias = Literal[
|
||||
'self',
|
||||
'oneself',
|
||||
]
|
||||
|
||||
Lang: TypeAlias = Literal['zh-CN', 'en-US']
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#:schema https://json.schemastore.org/uv.json
|
||||
|
||||
[project]
|
||||
name = "nonebot-plugin-tetris-stats"
|
||||
version = "1.7.2"
|
||||
version = "1.8.3"
|
||||
description = "一款基于 NoneBot2 的用于查询 Tetris 相关游戏数据的插件"
|
||||
readme = "README.md"
|
||||
authors = [{ name = "shoucandanghehe", email = "wallfjjd@gmail.com" }]
|
||||
@@ -29,6 +31,7 @@ dependencies = [
|
||||
"pillow>=11.0.0",
|
||||
"playwright>=1.48.0",
|
||||
"rich>=13.9.3",
|
||||
"strenum>=0.4.15",
|
||||
"yarl>=1.16.0",
|
||||
]
|
||||
classifiers = [
|
||||
@@ -57,6 +60,7 @@ dev = [
|
||||
"nonebot-adapter-kaiheila>=0.3.4",
|
||||
"nonebot-adapter-onebot>=2.4.6",
|
||||
"nonebot-adapter-qq>=1.5.3",
|
||||
"nonebot-plugin-tarina-lang-turbo>=0.1.1",
|
||||
"ruff>=0.7.1",
|
||||
]
|
||||
typecheck = [
|
||||
@@ -158,7 +162,7 @@ defineConstant = { PYDANTIC_V2 = true }
|
||||
typeCheckingMode = "standard"
|
||||
|
||||
[tool.bumpversion]
|
||||
current_version = "1.7.2"
|
||||
current_version = "1.8.3"
|
||||
tag = true
|
||||
sign_tags = true
|
||||
tag_name = "{new_version}"
|
||||
@@ -175,4 +179,4 @@ asyncio_mode = "auto"
|
||||
asyncio_default_fixture_loop_scope = "session"
|
||||
|
||||
[tool.nonebot]
|
||||
plugins = ["nonebot_plugin_tetris_stats"]
|
||||
plugins = ["nonebot_plugin_tetris_stats", "nonebot_plugin_tarina_lang_turbo"]
|
||||
|
||||
Reference in New Issue
Block a user