Compare commits

...

10 Commits
1.7.2 ... 1.8.2

Author SHA1 Message Date
243a8a286d 🔖 1.8.2 2025-04-28 04:09:04 +08:00
8b7de6745b 添加 development 模式 2025-04-28 04:08:55 +08:00
d0fc82d88d 添加开发依赖 nonebot-plugin-tarina-lang-turbo 2025-04-28 04:06:48 +08:00
bb4da8accc 🔖 1.8.1
Some checks are pending
Code Coverage / Test (macos-latest, 3.10) (push) Waiting to run
Code Coverage / Test (macos-latest, 3.11) (push) Waiting to run
Code Coverage / Test (macos-latest, 3.12) (push) Waiting to run
Code Coverage / Test (ubuntu-latest, 3.10) (push) Waiting to run
Code Coverage / Test (ubuntu-latest, 3.11) (push) Waiting to run
Code Coverage / Test (ubuntu-latest, 3.12) (push) Waiting to run
Code Coverage / Test (windows-latest, 3.10) (push) Waiting to run
Code Coverage / Test (windows-latest, 3.11) (push) Waiting to run
Code Coverage / Test (windows-latest, 3.12) (push) Waiting to run
Code Coverage / check (push) Blocked by required conditions
TypeCheck / TypeCheck (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
2025-04-27 15:18:25 +08:00
56e06a7001 🐛 修复 _lang 为私有变量不会默认序列化的bug 2025-04-27 15:17:12 +08:00
renovate[bot]
7c0b3cd240 ⬆️ Upgrade astral-sh/setup-uv action to v6 (#537)
Some checks failed
Code Coverage / Test (macos-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (macos-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (macos-latest, 3.12) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.12) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.12) (push) Has been cancelled
TypeCheck / TypeCheck (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Code Coverage / check (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-24 22:50:26 +08:00
dca0619021 🔖 1.8.0
Some checks are pending
Code Coverage / Test (macos-latest, 3.10) (push) Waiting to run
Code Coverage / Test (macos-latest, 3.11) (push) Waiting to run
Code Coverage / Test (macos-latest, 3.12) (push) Waiting to run
Code Coverage / Test (ubuntu-latest, 3.10) (push) Waiting to run
Code Coverage / Test (ubuntu-latest, 3.11) (push) Waiting to run
Code Coverage / Test (ubuntu-latest, 3.12) (push) Waiting to run
Code Coverage / Test (windows-latest, 3.10) (push) Waiting to run
Code Coverage / Test (windows-latest, 3.11) (push) Waiting to run
Code Coverage / Test (windows-latest, 3.12) (push) Waiting to run
Code Coverage / check (push) Blocked by required conditions
TypeCheck / TypeCheck (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
2025-04-24 03:07:04 +08:00
pre-commit-ci[bot]
f56dce6918 ⬆️ auto update by pre-commit hooks (#535)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.2 → v0.11.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.2...v0.11.6)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-04-24 03:01:28 +08:00
呵呵です
ff3eb79967 迁移到新模板 (#536)
Some checks are pending
Code Coverage / Test (macos-latest, 3.10) (push) Waiting to run
Code Coverage / Test (macos-latest, 3.11) (push) Waiting to run
Code Coverage / Test (macos-latest, 3.12) (push) Waiting to run
Code Coverage / Test (ubuntu-latest, 3.10) (push) Waiting to run
Code Coverage / Test (ubuntu-latest, 3.11) (push) Waiting to run
Code Coverage / Test (ubuntu-latest, 3.12) (push) Waiting to run
Code Coverage / Test (windows-latest, 3.10) (push) Waiting to run
Code Coverage / Test (windows-latest, 3.11) (push) Waiting to run
Code Coverage / Test (windows-latest, 3.12) (push) Waiting to run
Code Coverage / check (push) Blocked by required conditions
TypeCheck / TypeCheck (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
*  添加依赖 strenum

* 🐛 优化等待逻辑,修复截图爆炸

*  使用新模板

* ️ 关闭自动转译

*  同步新模板 schemas

* 🌐 添加模板语言的映射

*  适配 bind

*  更新模板

*  全部适配

* 🚨 make mypy happy

* Update nonebot_plugin_tetris_stats/games/tos/query.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

*  使用用户设置语言

* 🚨 auto fix by pre-commit hooks

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-04-23 19:25:50 +08:00
pre-commit-ci[bot]
0ac917f95e ⬆️ auto update by pre-commit hooks (#534)
Some checks failed
Code Coverage / Test (macos-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (macos-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (macos-latest, 3.12) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.12) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.12) (push) Has been cancelled
TypeCheck / TypeCheck (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Code Coverage / check (push) Has been cancelled
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.0 → v0.11.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.0...v0.11.2)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-03-28 16:35:48 +08:00
59 changed files with 2466 additions and 1906 deletions

View File

@@ -14,7 +14,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5 - uses: astral-sh/setup-uv@v6
name: Setup UV name: Setup UV
with: with:
enable-cache: true enable-cache: true

View File

@@ -28,7 +28,7 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Setup uv - name: Setup uv
uses: astral-sh/setup-uv@v5 uses: astral-sh/setup-uv@v6
with: with:
enable-cache: true enable-cache: true
cache-suffix: ${{ env.PYTHON_VERSION }}_${{ env.OS }} cache-suffix: ${{ env.PYTHON_VERSION }}_${{ env.OS }}

View File

@@ -9,7 +9,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5 - uses: astral-sh/setup-uv@v6
name: Setup UV name: Setup UV
with: with:
enable-cache: true enable-cache: true

View File

@@ -7,7 +7,7 @@ ci:
autoupdate_commit_msg: ':arrow_up: auto update by pre-commit hooks' autoupdate_commit_msg: ':arrow_up: auto update by pre-commit hooks'
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.0 rev: v0.11.6
hooks: hooks:
- id: ruff - id: ruff
args: [--fix, --exit-non-zero-on-fix] args: [--fix, --exit-non-zero-on-fix]

View File

@@ -18,6 +18,7 @@ class ScopedConfig(BaseModel):
request_timeout: float = 30.0 request_timeout: float = 30.0
screenshot_quality: float = 2 screenshot_quality: float = 2
proxy: Proxy = Field(default_factory=Proxy) proxy: Proxy = Field(default_factory=Proxy)
development: bool = False
class Config(BaseModel): class Config(BaseModel):

View File

@@ -86,7 +86,7 @@ class Record(BaseModel):
pb: bool pb: bool
oncepb: bool oncepb: bool
ts: datetime ts: datetime
revolution: None revolution: str | None
user: User user: User
otherusers: list otherusers: list
leaderboards: list[str] leaderboards: list[str]
@@ -97,7 +97,7 @@ class Record(BaseModel):
class Best(BaseModel): class Best(BaseModel):
record: None # WTF record: Record | None
rank: int rank: int

View File

@@ -13,6 +13,7 @@ from yarl import URL
from ...db import BindStatus, create_or_update_bind, trigger from ...db import BindStatus, create_or_update_bind, trigger
from ...utils.host import HostPage, get_self_netloc from ...utils.host import HostPage, get_self_netloc
from ...utils.image import get_avatar from ...utils.image import get_avatar
from ...utils.lang import get_lang
from ...utils.render import Bind, render from ...utils.render import Bind, render
from ...utils.render.schemas.base import Avatar, People from ...utils.render.schemas.base import Avatar, People
from ...utils.screenshot import screenshot from ...utils.screenshot import screenshot
@@ -65,7 +66,7 @@ async def _(nb_user: User, account: Player, event_session: EventSession, bot_inf
'v1/binding', 'v1/binding',
Bind( Bind(
platform='TETR.IO', platform='TETR.IO',
status='unknown', type='unknown',
user=People( user=People(
avatar=str( avatar=str(
URL(f'http://{netloc}/host/resource/tetrio/avatars/{user.ID}') 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'), avatar=await get_avatar(bot_info, 'Data URI', '../../static/logo/logo.svg'),
name=bot_info.user_name, name=bot_info.user_name,
), ),
command='io查我', prompt='io查我',
lang=get_lang(),
), ),
) )
) as page_hash: ) as page_hash:

View File

@@ -5,9 +5,10 @@ from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[im
from ...db import trigger from ...db import trigger
from ...utils.host import HostPage, get_self_netloc from ...utils.host import HostPage, get_self_netloc
from ...utils.lang import get_lang
from ...utils.metrics import get_metrics from ...utils.metrics import get_metrics
from ...utils.render import render 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 ...utils.screenshot import screenshot
from .. import alc from .. import alc
from . import command from . import command
@@ -63,12 +64,15 @@ async def _(
'v2/tetrio/user/list', 'v2/tetrio/user/list',
List( List(
show_index=True, show_index=True,
users=[ data=[
User( Data(
id=i.id, user=User(
name=i.username.upper(), id=i.id,
avatar=f'https://tetr.io/user-content/avatars/{i.id}.jpg', name=i.username.upper(),
country=i.country, avatar=f'https://tetr.io/user-content/avatars/{i.id}.jpg',
country=i.country,
xp=i.xp,
),
tetra_league=TetraLeague( tetra_league=TetraLeague(
rank=i.league.rank, rank=i.league.rank,
tr=round(i.league.tr, 2), tr=round(i.league.tr, 2),
@@ -81,12 +85,11 @@ async def _(
vs=metrics.vs, vs=metrics.vs,
adpl=metrics.adpl, adpl=metrics.adpl,
), ),
xp=i.xp,
join_at=None,
) )
for i in league.data.entries for i in league.data.entries
if isinstance(i, Entry) if isinstance(i, Entry)
], ],
lang=get_lang(),
), ),
) )
) as page_hash: ) as page_hash:

View File

@@ -4,22 +4,22 @@ from typing import TypeVar, overload
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
from ....utils.exception import FallbackError 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.labs.leagueflow import Empty, LeagueFlowSuccess
from ..api.schemas.summaries.league import InvalidData, LeagueSuccessModel, NeverPlayedData, NeverRatedData, RatedData from ..api.schemas.summaries.league import InvalidData, LeagueSuccessModel, NeverPlayedData, NeverRatedData, RatedData
def flow_to_history( def flow_to_history(
leagueflow: LeagueFlowSuccess, leagueflow: LeagueFlowSuccess,
handle: Callable[[list[TetraLeagueHistoryData]], list[TetraLeagueHistoryData]] | None = None, handle: Callable[[list[HistoryData]], list[HistoryData]] | None = None,
) -> list[TetraLeagueHistoryData]: ) -> list[HistoryData]:
if isinstance(leagueflow.data, Empty): if isinstance(leagueflow.data, Empty):
raise FallbackError raise FallbackError
start_time = leagueflow.data.start_time.astimezone(ZoneInfo('Asia/Shanghai')) start_time = leagueflow.data.start_time.astimezone(ZoneInfo('Asia/Shanghai'))
ret = [ ret = [
TetraLeagueHistoryData( HistoryData(
record_at=start_time + timedelta(milliseconds=i.timestamp_offset), 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 for i in leagueflow.data.points
if start_time + timedelta(milliseconds=i.timestamp_offset) if start_time + timedelta(milliseconds=i.timestamp_offset)

View File

@@ -1,17 +1,18 @@
from asyncio import gather from asyncio import gather
from datetime import datetime, timedelta from datetime import timedelta
from hashlib import md5 from hashlib import md5
from math import ceil, floor
from zoneinfo import ZoneInfo
from yarl import URL 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.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 import render
from ....utils.render.schemas.base import Avatar, Ranking from ....utils.render.schemas.base import Avatar, Trending
from ....utils.render.schemas.tetrio.user.base import TetraLeagueHistoryData from ....utils.render.schemas.v1.base import History
from ....utils.render.schemas.tetrio.user.info_v1 import Info, Radar, TetraLeague, TetraLeagueHistory, User from ....utils.render.schemas.v1.tetrio.user.info import Info, Multiplayer, Singleplayer, User
from ....utils.screenshot import screenshot from ....utils.screenshot import screenshot
from ..api import Player from ..api import Player
from ..api.schemas.summaries.league import RatedData 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 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: async def make_query_image_v1(player: Player) -> bytes:
( (
(user, user_info, league, sprint, blitz, leagueflow), (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: if league_data.vs is None:
raise FallbackError raise FallbackError
histories = flow_to_history(leagueflow, handle_history_data) histories = flow_to_history(leagueflow, handle_history_data)
value_max, value_min = get_value_bounds([i.tr for i in histories]) values = get_value_bounds([i.score for i in histories])
split_value, offset = get_split(value_max, value_min) split_value, offset = get_split(values, TR_MAX, TR_MIN)
if sprint.data.record is not None: if sprint.data.record is not None:
duration = timedelta(milliseconds=sprint.data.record.results.stats.finaltime).total_seconds() 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 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' sprint_value = 'N/A'
blitz_value = f'{blitz.data.record.results.stats.score:,}' if blitz.data.record is not None else '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() netloc = get_self_netloc()
dsps: float
dspp: float
# make mypy happy
async with HostPage( async with HostPage(
page=await render( page=await render(
'v1/tetrio/info', 'v1/tetrio/info',
@@ -159,38 +60,40 @@ async def make_query_image_v1(player: Player) -> bytes:
name=user.name.upper(), name=user.name.upper(),
bio=user_info.data.bio, bio=user_info.data.bio,
), ),
ranking=Ranking( multiplayer=Multiplayer(
rating=round(league_data.glicko, 2), glicko=f'{round(league_data.glicko, 2):,}',
rd=round(league_data.rd, 2), rd=round(league_data.rd, 2),
),
tetra_league=TetraLeague(
rank=league_data.rank, rank=league_data.rank,
tr=round(league_data.tr, 2), tr=f'{round(league_data.tr, 2):,}',
global_rank=league_data.standing, global_rank=league_data.standing,
pps=league_data.pps, history=History(
lpm=round(lpm := (league_data.pps * 24), 2), data=histories,
apm=league_data.apm, split_interval=split_value,
apl=round(league_data.apm / lpm, 2), min_value=values.value_min,
vs=league_data.vs, max_value=values.value_max,
adpm=round(adpm := (league_data.vs * 0.6), 2), offset=offset,
adpl=round(adpm / lpm, 2), ),
), lpm=(metrics := get_metrics(pps=league_data.pps, apm=league_data.apm, vs=league_data.vs)).lpm,
tetra_league_history=TetraLeagueHistory( pps=metrics.pps,
data=histories, lpm_trending=Trending.KEEP,
split_interval=split_value, apm=metrics.apm,
min_tr=value_min, apl=metrics.apl,
max_tr=value_max, apm_trending=Trending.KEEP,
offset=offset, adpm=metrics.adpm,
), vs=metrics.vs,
radar=Radar( adpl=metrics.adpl,
adpm_trending=Trending.KEEP,
app=(app := (league_data.apm / (60 * league_data.pps))), app=(app := (league_data.apm / (60 * league_data.pps))),
dsps=(dsps := ((league_data.vs / 100) - (league_data.apm / 60))), dsps=(dsps := ((league_data.vs / 100) - (league_data.apm / 60))),
dspp=(dspp := (dsps / league_data.pps)), dspp=(dspp := (dsps / league_data.pps)),
ci=150 * dspp - 125 * app + 50 * (league_data.vs / league_data.apm) - 25, ci=150 * dspp - 125 * app + 50 * (league_data.vs / league_data.apm) - 25,
ge=2 * ((app * dsps) / league_data.pps), ge=2 * ((app * dsps) / league_data.pps),
), ),
sprint=sprint_value, singleplayer=Singleplayer(
blitz=blitz_value, sprint=sprint_value,
blitz=blitz_value,
),
lang=get_lang(),
), ),
) )
) as page_hash: ) as page_hash:

View File

@@ -6,11 +6,13 @@ from yarl import URL
from ....utils.exception import FallbackError from ....utils.exception import FallbackError
from ....utils.host import HostPage, get_self_netloc from ....utils.host import HostPage, get_self_netloc
from ....utils.lang import get_lang
from ....utils.metrics import get_metrics from ....utils.metrics import get_metrics
from ....utils.render import render from ....utils.render import render
from ....utils.render.schemas.base import Avatar 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 (
Badge, Badge,
Best,
Blitz, Blitz,
Info, Info,
Sprint, Sprint,
@@ -18,7 +20,9 @@ from ....utils.render.schemas.tetrio.user.info_v2 import (
TetraLeague, TetraLeague,
TetraLeagueStatistic, TetraLeagueStatistic,
User, User,
Week,
Zen, Zen,
Zenith,
) )
from ....utils.screenshot import screenshot from ....utils.screenshot import screenshot
from ..api import Player from ..api import Player
@@ -29,10 +33,23 @@ from .tools import flow_to_history, handling_special_value
async def make_query_image_v2(player: Player) -> bytes: async def make_query_image_v2(player: Player) -> bytes:
( (
(user, user_info, league, sprint, blitz, zen), (user, user_info, league, sprint, blitz, zen),
(avatar_revision, banner_revision, leagueflow), (avatar_revision, banner_revision, leagueflow, zenith, zenithex),
) = await gather( ) = await gather(
gather(player.user, player.get_info(), player.league, player.sprint, player.blitz, player.zen), gather(
gather(player.avatar_revision, player.banner_revision, player.get_leagueflow()), 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'),
),
) )
if sprint.data.record is not None: if sprint.data.record is not None:
duration = timedelta(milliseconds=sprint.data.record.results.stats.finaltime).total_seconds() duration = timedelta(milliseconds=sprint.data.record.results.stats.finaltime).total_seconds()
@@ -62,12 +79,9 @@ async def make_query_image_v2(player: Player) -> bytes:
user=User( user=User(
id=user.ID, id=user.ID,
name=user.name.upper(), name=user.name.upper(),
bio=user_info.data.bio, country=user_info.data.country,
banner=str( role=user_info.data.role,
URL(f'http://{netloc}/host/resource/tetrio/banners/{user.ID}') % {'revision': banner_revision} botmaster=user_info.data.botmaster,
)
if banner_revision is not None and banner_revision != 0
else None,
avatar=str( avatar=str(
URL(f'http://{netloc}/host/resource/tetrio/avatars/{user.ID}') % {'revision': avatar_revision} URL(f'http://{netloc}/host/resource/tetrio/avatars/{user.ID}') % {'revision': avatar_revision}
) )
@@ -76,6 +90,15 @@ async def make_query_image_v2(player: Player) -> bytes:
type='identicon', type='identicon',
hash=md5(user.ID.encode()).hexdigest(), # noqa: S324 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=[ badges=[
Badge( Badge(
id=i.id, id=i.id,
@@ -85,12 +108,9 @@ async def make_query_image_v2(player: Player) -> bytes:
) )
for i in user_info.data.badges for i in user_info.data.badges
], ],
country=user_info.data.country,
role=user_info.data.role,
xp=user_info.data.xp, xp=user_info.data.xp,
friend_count=user_info.data.friend_count, ar=user_info.data.ar,
supporter_tier=user_info.data.supporter_tier, achievements=user_info.data.achievements,
bad_standing=user_info.data.badstanding or False,
playtime=play_time, playtime=play_time,
join_at=user_info.data.ts, join_at=user_info.data.ts,
), ),
@@ -113,6 +133,40 @@ async def make_query_image_v2(player: Player) -> bytes:
) )
if not isinstance(league.data, NeverPlayedData | InvalidData) if not isinstance(league.data, NeverPlayedData | InvalidData)
else None, 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( statistic=Statistic(
total=handling_special_value(user_info.data.gamesplayed), total=handling_special_value(user_info.data.gamesplayed),
wins=handling_special_value(user_info.data.gameswon), wins=handling_special_value(user_info.data.gameswon),
@@ -120,6 +174,7 @@ async def make_query_image_v2(player: Player) -> bytes:
sprint=Sprint( sprint=Sprint(
time=sprint_value, time=sprint_value,
global_rank=sprint.data.rank, global_rank=sprint.data.rank,
country_rank=sprint.data.rank_local,
play_at=sprint.data.record.ts, play_at=sprint.data.record.ts,
) )
if sprint.data.record is not None if sprint.data.record is not None
@@ -127,11 +182,13 @@ async def make_query_image_v2(player: Player) -> bytes:
blitz=Blitz( blitz=Blitz(
score=blitz.data.record.results.stats.score, score=blitz.data.record.results.stats.score,
global_rank=blitz.data.rank, global_rank=blitz.data.rank,
country_rank=blitz.data.rank_local,
play_at=blitz.data.record.ts, play_at=blitz.data.record.ts,
) )
if blitz.data.record is not None if blitz.data.record is not None
else None, else None,
zen=Zen(level=zen.data.level, score=zen.data.score), zen=Zen(level=zen.data.level, score=zen.data.score),
lang=get_lang(),
), ),
), ),
) as page_hash: ) as page_hash:

View File

@@ -12,6 +12,7 @@ from nonebot_plugin_apscheduler import scheduler
from nonebot_plugin_orm import get_session from nonebot_plugin_orm import get_session
from sqlalchemy import select from sqlalchemy import select
from ....config.config import config
from ....utils.exception import RequestError from ....utils.exception import RequestError
from ....utils.retry import retry from ....utils.retry import retry
from .. import alc from .. import alc
@@ -136,14 +137,16 @@ async def get_tetra_league_data() -> None:
await session.commit() await session.commit()
@driver.on_startup if not config.tetris.development:
async def _() -> None:
async with get_session() as session: @driver.on_startup
latest_time = await session.scalar( async def _() -> None:
select(TETRIOLeagueStats.update_time).order_by(TETRIOLeagueStats.id.desc()).limit(1) async with get_session() as session:
) latest_time = await session.scalar(
if latest_time is None or datetime.now(tz=UTC) - latest_time.replace(tzinfo=UTC) > timedelta(hours=6): select(TETRIOLeagueStats.update_time).order_by(TETRIOLeagueStats.id.desc()).limit(1)
await get_tetra_league_data() )
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 from . import all, detail # noqa: A004, E402

View File

@@ -10,13 +10,14 @@ from sqlalchemy.orm import selectinload
from ....db import trigger from ....db import trigger
from ....utils.host import HostPage, get_self_netloc from ....utils.host import HostPage, get_self_netloc
from ....utils.lang import get_lang
from ....utils.metrics import get_metrics from ....utils.metrics import get_metrics
from ....utils.render import render from ....utils.render import render
from ....utils.render.schemas.tetrio.rank.v1 import Data as DataV1 from ....utils.render.schemas.v1.tetrio.rank import Data as DataV1
from ....utils.render.schemas.tetrio.rank.v1 import ItemData as ItemDataV1 from ....utils.render.schemas.v1.tetrio.rank import ItemData as ItemDataV1
from ....utils.render.schemas.tetrio.rank.v2 import AverageData as AverageDataV2 from ....utils.render.schemas.v2.tetrio.rank import AverageData as AverageDataV2
from ....utils.render.schemas.tetrio.rank.v2 import Data as DataV2 from ....utils.render.schemas.v2.tetrio.rank import Data as DataV2
from ....utils.render.schemas.tetrio.rank.v2 import ItemData as ItemDataV2 from ....utils.render.schemas.v2.tetrio.rank import ItemData as ItemDataV2
from ....utils.screenshot import screenshot from ....utils.screenshot import screenshot
from .. import alc from .. import alc
from ..constant import GAME_TYPE 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) for i in zip(latest_data.fields, compare_data.fields, strict=True)
}, },
updated_at=latest_data.update_time, updated_at=latest_data.update_time,
lang=get_lang(),
), ),
) )
) as page_hash: ) 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) for i in zip(latest_data.fields, compare_data.fields, strict=True)
}, },
updated_at=latest_data.update_time, updated_at=latest_data.update_time,
lang=get_lang(),
), ),
) )
) as page_hash: ) as page_hash:

View File

@@ -12,9 +12,10 @@ from sqlalchemy.orm import selectinload
from ....db import trigger from ....db import trigger
from ....utils.host import HostPage, get_self_netloc from ....utils.host import HostPage, get_self_netloc
from ....utils.lang import get_lang
from ....utils.metrics import get_metrics from ....utils.metrics import get_metrics
from ....utils.render import render 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 ....utils.screenshot import screenshot
from .. import alc from .. import alc
from ..api.typedefs import ValidRank 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(), vs_holder=latest_data.high_vs.username.upper(),
), ),
updated_at=latest.update_time.replace(tzinfo=UTC).astimezone(ZoneInfo('Asia/Shanghai')), updated_at=latest.update_time.replace(tzinfo=UTC).astimezone(ZoneInfo('Asia/Shanghai')),
lang=get_lang(),
), ),
) )
) as page_hash: ) as page_hash:

View File

@@ -16,11 +16,12 @@ from ....db import query_bind_info, trigger
from ....i18n import Lang from ....i18n import Lang
from ....utils.exception import RecordNotFoundError from ....utils.exception import RecordNotFoundError
from ....utils.host import HostPage, get_self_netloc from ....utils.host import HostPage, get_self_netloc
from ....utils.lang import get_lang
from ....utils.metrics import get_metrics from ....utils.metrics import get_metrics
from ....utils.render import render from ....utils.render import render
from ....utils.render.schemas.base import Avatar from ....utils.render.schemas.base import Avatar
from ....utils.render.schemas.tetrio.record.base import Finesse, Max, Mini, Tspins, User from ....utils.render.schemas.v2.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.blitz import Record, Statistic
from ....utils.screenshot import screenshot from ....utils.screenshot import screenshot
from ....utils.typedefs import Me from ....utils.typedefs import Me
from .. import alc from .. import alc
@@ -145,6 +146,7 @@ async def make_blitz_image(player: Player) -> bytes:
level=stats.level, level=stats.level,
), ),
play_at=blitz.data.record.ts, play_at=blitz.data.record.ts,
lang=get_lang(),
), ),
) )
) as page_hash: ) as page_hash:

View File

@@ -16,11 +16,12 @@ from ....db import query_bind_info, trigger
from ....i18n import Lang from ....i18n import Lang
from ....utils.exception import RecordNotFoundError from ....utils.exception import RecordNotFoundError
from ....utils.host import HostPage, get_self_netloc from ....utils.host import HostPage, get_self_netloc
from ....utils.lang import get_lang
from ....utils.metrics import get_metrics from ....utils.metrics import get_metrics
from ....utils.render import render from ....utils.render import render
from ....utils.render.schemas.base import Avatar 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.v2.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.sprint import Record
from ....utils.screenshot import screenshot from ....utils.screenshot import screenshot
from ....utils.typedefs import Me from ....utils.typedefs import Me
from .. import alc from .. import alc
@@ -90,7 +91,7 @@ async def make_sprint_image(player: Player) -> bytes:
netloc = get_self_netloc() netloc = get_self_netloc()
async with HostPage( async with HostPage(
page=await render( page=await render(
'v2/tetrio/record/40l', 'v2/tetrio/record/sprint',
Record( Record(
type='best', type='best',
user=User( user=User(
@@ -145,6 +146,7 @@ async def make_sprint_image(player: Player) -> bytes:
), ),
), ),
play_at=sprint.data.record.ts, play_at=sprint.data.record.ts,
lang=get_lang(),
), ),
) )
) as page_hash: ) as page_hash:

View File

@@ -13,6 +13,7 @@ from yarl import URL
from ...db import query_bind_info, remove_bind, trigger from ...db import query_bind_info, remove_bind, trigger
from ...utils.host import HostPage, get_self_netloc from ...utils.host import HostPage, get_self_netloc
from ...utils.image import get_avatar from ...utils.image import get_avatar
from ...utils.lang import get_lang
from ...utils.render import Bind, render from ...utils.render import Bind, render
from ...utils.render.schemas.base import Avatar, People from ...utils.render.schemas.base import Avatar, People
from ...utils.screenshot import screenshot from ...utils.screenshot import screenshot
@@ -53,7 +54,7 @@ async def _(nb_user: User, event_session: EventSession, bot_info: UserInfo = Bot
'v1/binding', 'v1/binding',
Bind( Bind(
platform='TETR.IO', platform='TETR.IO',
status='unlink', type='unlink',
user=People( user=People(
avatar=str( avatar=str(
URL(f'http://{netloc}/host/resource/tetrio/avatars/{user.ID}') 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'), avatar=await get_avatar(bot_info, 'Data URI', '../../static/logo/logo.svg'),
name=bot_info.user_name, name=bot_info.user_name,
), ),
command='io绑定{游戏ID}', prompt='io绑定{游戏ID}',
lang=get_lang(),
), ),
) )
) as page_hash: ) as page_hash:

View File

@@ -8,6 +8,7 @@ from nonebot_plugin_userinfo import BotUserInfo, EventUserInfo, UserInfo
from ...db import BindStatus, create_or_update_bind, trigger from ...db import BindStatus, create_or_update_bind, trigger
from ...utils.host import HostPage, get_self_netloc from ...utils.host import HostPage, get_self_netloc
from ...utils.image import get_avatar from ...utils.image import get_avatar
from ...utils.lang import get_lang
from ...utils.render import Bind, render from ...utils.render import Bind, render
from ...utils.render.schemas.base import People from ...utils.render.schemas.base import People
from ...utils.screenshot import screenshot from ...utils.screenshot import screenshot
@@ -44,7 +45,7 @@ async def _(
'v1/binding', 'v1/binding',
Bind( Bind(
platform=GAME_TYPE, platform=GAME_TYPE,
status='unknown', type='unknown',
user=People( user=People(
avatar=await get_avatar(event_user_info, 'Data URI', None), avatar=await get_avatar(event_user_info, 'Data URI', None),
name=user.user_name, name=user.user_name,
@@ -53,7 +54,8 @@ async def _(
avatar=await get_avatar(bot_info, 'Data URI', '../../static/logo/logo.svg'), avatar=await get_avatar(bot_info, 'Data URI', '../../static/logo/logo.svg'),
name=bot_info.user_name, name=bot_info.user_name,
), ),
command='top查我', prompt='top查我',
lang=get_lang(),
), ),
) )
) as page_hash: ) as page_hash:

View File

@@ -11,12 +11,13 @@ from ...db import query_bind_info, trigger
from ...i18n import Lang from ...i18n import Lang
from ...utils.exception import FallbackError from ...utils.exception import FallbackError
from ...utils.host import HostPage, get_self_netloc from ...utils.host import HostPage, get_self_netloc
from ...utils.lang import get_lang
from ...utils.metrics import TetrisMetricsBasicWithLPM, get_metrics from ...utils.metrics import TetrisMetricsBasicWithLPM, get_metrics
from ...utils.render import render from ...utils.render import render
from ...utils.render.avatar import get_avatar from ...utils.render.avatar import get_avatar
from ...utils.render.schemas.base import People from ...utils.render.schemas.base import People, Trending
from ...utils.render.schemas.top_info import Data as InfoData from ...utils.render.schemas.v1.top.info import Data as InfoData
from ...utils.render.schemas.top_info import Info from ...utils.render.schemas.v1.top.info import Info
from ...utils.screenshot import screenshot from ...utils.screenshot import screenshot
from ...utils.typedefs import Me from ...utils.typedefs import Me
from . import alc from . import alc
@@ -79,8 +80,23 @@ async def make_query_image(profile: UserProfile) -> bytes:
'v1/top/info', 'v1/top/info',
Info( Info(
user=People(avatar=get_avatar(profile.user_name), name=profile.user_name), 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), today=InfoData(
history=InfoData(pps=history.pps, lpm=history.lpm, apm=history.apm, apl=history.apl), 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: ) as page_hash:

View File

@@ -9,6 +9,7 @@ from nonebot_plugin_waiter import suggest # type: ignore[import-untyped]
from ...db import query_bind_info, remove_bind, trigger from ...db import query_bind_info, remove_bind, trigger
from ...utils.host import HostPage, get_self_netloc from ...utils.host import HostPage, get_self_netloc
from ...utils.image import get_avatar from ...utils.image import get_avatar
from ...utils.lang import get_lang
from ...utils.render import Bind, render from ...utils.render import Bind, render
from ...utils.render.schemas.base import People from ...utils.render.schemas.base import People
from ...utils.screenshot import screenshot from ...utils.screenshot import screenshot
@@ -46,7 +47,7 @@ async def _(
'v1/binding', 'v1/binding',
Bind( Bind(
platform='TOP', platform='TOP',
status='unlink', type='unlink',
user=People( user=People(
avatar=await get_avatar(event_user_info, 'Data URI', None), avatar=await get_avatar(event_user_info, 'Data URI', None),
name=user.user_name, name=user.user_name,
@@ -55,7 +56,8 @@ async def _(
avatar=await get_avatar(bot_info, 'Data URI', '../../static/logo/logo.svg'), avatar=await get_avatar(bot_info, 'Data URI', '../../static/logo/logo.svg'),
name=bot_info.user_name, name=bot_info.user_name,
), ),
command='top绑定{游戏ID}', prompt='top绑定{游戏ID}',
lang=get_lang(),
), ),
) )
) as page_hash: ) as page_hash:

View File

@@ -8,6 +8,7 @@ from nonebot_plugin_userinfo import BotUserInfo, EventUserInfo, UserInfo
from ...db import BindStatus, create_or_update_bind, trigger from ...db import BindStatus, create_or_update_bind, trigger
from ...utils.host import HostPage, get_self_netloc from ...utils.host import HostPage, get_self_netloc
from ...utils.image import get_avatar from ...utils.image import get_avatar
from ...utils.lang import get_lang
from ...utils.render import Bind, render from ...utils.render import Bind, render
from ...utils.render.schemas.base import People from ...utils.render.schemas.base import People
from ...utils.screenshot import screenshot from ...utils.screenshot import screenshot
@@ -45,7 +46,7 @@ async def _(
'v1/binding', 'v1/binding',
Bind( Bind(
platform=GAME_TYPE, platform=GAME_TYPE,
status='unknown', type='unknown',
user=People( user=People(
avatar=await get_avatar(event_user_info, 'Data URI', None), name=user_info.data.name 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'), 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, name=bot_info.user_remark or bot_info.user_displayname or bot_info.user_name,
), ),
command='茶服查我', prompt='茶服查我',
lang=get_lang(),
), ),
) )
) as page_hash: ) as page_hash:

View File

@@ -1,7 +1,8 @@
from asyncio import gather from asyncio import gather
from datetime import timedelta from datetime import datetime, timedelta, timezone
from http import HTTPStatus from http import HTTPStatus
from typing import Literal, NamedTuple from typing import Literal, NamedTuple
from zoneinfo import ZoneInfo
from nonebot.adapters import Event from nonebot.adapters import Event
from nonebot.matcher import Matcher 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_session_orm import get_session_persist_id # type: ignore[import-untyped]
from nonebot_plugin_user import get_user from nonebot_plugin_user import get_user
from nonebot_plugin_userinfo import EventUserInfo, UserInfo from nonebot_plugin_userinfo import EventUserInfo, UserInfo
from sqlalchemy import select
from ...db import query_bind_info, trigger from ...db import query_bind_info, trigger
from ...i18n import Lang from ...i18n import Lang
from ...utils.chart import get_split, get_value_bounds, handle_history_data
from ...utils.exception import RequestError from ...utils.exception import RequestError
from ...utils.host import HostPage, get_self_netloc from ...utils.host import HostPage, get_self_netloc
from ...utils.image import get_avatar from ...utils.image import get_avatar
from ...utils.lang import get_lang
from ...utils.metrics import TetrisMetricsProWithLPMADPM, get_metrics from ...utils.metrics import TetrisMetricsProWithLPMADPM, get_metrics
from ...utils.render import render from ...utils.render import render
from ...utils.render.avatar import get_avatar as get_random_avatar from ...utils.render.avatar import get_avatar as get_random_avatar
from ...utils.render.schemas.base import People, Ranking from ...utils.render.schemas.base import HistoryData, People, Trending
from ...utils.render.schemas.tos_info import Info, Multiplayer, Radar 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.screenshot import screenshot
from ...utils.time_it import time_it
from ...utils.typedefs import Me, Number from ...utils.typedefs import Me, Number
from . import alc from . import alc
from .api import Player from .api import Player
from .api.models import TOSHistoricalData
from .api.schemas.user_info import UserInfoSuccess from .api.schemas.user_info import UserInfoSuccess
from .constant import GAME_TYPE from .constant import GAME_TYPE
@@ -148,6 +155,7 @@ async def _(account: Player, event_session: EventSession):
command_args=[], command_args=[],
): ):
user_info, game_data = await gather(account.get_info(), get_game_data(account)) 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: if game_data is not None:
await UniMessage.image(raw=await make_query_image(user_info, game_data, None)).finish() await UniMessage.image(raw=await make_query_image(user_info, game_data, None)).finish()
await make_query_text(user_info, game_data).finish() await make_query_text(user_info, game_data).finish()
@@ -156,7 +164,7 @@ async def _(account: Player, event_session: EventSession):
class GameData(NamedTuple): class GameData(NamedTuple):
game_num: int game_num: int
metrics: TetrisMetricsProWithLPMADPM metrics: TetrisMetricsProWithLPMADPM
OR: Number or_: Number
dspp: Number dspp: Number
ge: Number ge: Number
@@ -199,12 +207,49 @@ async def get_game_data(player: Player, query_num: int = 50) -> GameData | None:
return GameData( return GameData(
game_num=num, game_num=num,
metrics=metrics, metrics=metrics,
OR=total_offset / total_receive * 100, or_=total_offset / total_receive * 100,
dspp=total_dig / total_pieses, dspp=total_dig / total_pieses,
ge=2 * ((total_attack * total_dig) / total_pieses**2), 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: async def make_query_image(user_info: UserInfoSuccess, game_data: GameData, event_user_info: UserInfo | None) -> bytes:
metrics = game_data.metrics metrics = game_data.metrics
sprint_value = ( 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' if user_info.data.pb_sprint != '2147483647'
else 'N/A' 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( async with HostPage(
await render( await render(
'v1/tos/info', '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), else get_random_avatar(user_info.data.teaid),
name=user_info.data.name, name=user_info.data.name,
), ),
ranking=Ranking(rating=float(user_info.data.ranking), rd=round(float(user_info.data.rd_now), 2)),
multiplayer=Multiplayer( 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, lpm=metrics.lpm,
pps=metrics.pps,
lpm_trending=Trending.KEEP,
apm=metrics.apm, apm=metrics.apm,
apl=metrics.apl, apl=metrics.apl,
vs=metrics.vs, apm_trending=Trending.KEEP,
adpm=metrics.adpm, adpm=metrics.adpm,
vs=metrics.vs,
adpl=metrics.adpl, adpl=metrics.adpl,
), adpm_trending=Trending.KEEP,
radar=Radar(
app=(app := (metrics.apm / (60 * metrics.pps))), app=(app := (metrics.apm / (60 * metrics.pps))),
OR=game_data.OR, or_=game_data.or_,
dspp=game_data.dspp, dspp=game_data.dspp,
ci=150 * game_data.dspp - 125 * app + 50 * (metrics.vs / metrics.apm) - 25, ci=150 * game_data.dspp - 125 * app + 50 * (metrics.vs / metrics.apm) - 25,
ge=game_data.ge, ge=game_data.ge,
), ),
sprint=sprint_value, singleplayer=Singleplayer(
challenge=f'{int(user_info.data.pb_challenge):,}' if user_info.data.pb_challenge != '0' else 'N/A', sprint=sprint_value,
marathon=f'{int(user_info.data.pb_marathon):,}' if user_info.data.pb_marathon != '0' else 'N/A', 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: ) as page_hash:

View File

@@ -9,6 +9,7 @@ from nonebot_plugin_waiter import suggest # type: ignore[import-untyped]
from ...db import query_bind_info, remove_bind, trigger from ...db import query_bind_info, remove_bind, trigger
from ...utils.host import HostPage, get_self_netloc from ...utils.host import HostPage, get_self_netloc
from ...utils.image import get_avatar from ...utils.image import get_avatar
from ...utils.lang import get_lang
from ...utils.render import Bind, render from ...utils.render import Bind, render
from ...utils.render.avatar import get_avatar as get_random_avatar from ...utils.render.avatar import get_avatar as get_random_avatar
from ...utils.render.schemas.base import People from ...utils.render.schemas.base import People
@@ -47,7 +48,7 @@ async def _(
'v1/binding', 'v1/binding',
Bind( Bind(
platform='TOS', platform='TOS',
status='unlink', type='unlink',
user=People( user=People(
avatar=await get_avatar(event_user_info, 'Data URI', None) avatar=await get_avatar(event_user_info, 'Data URI', None)
if event_user_info is not 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'), avatar=await get_avatar(bot_info, 'Data URI', '../../static/logo/logo.svg'),
name=bot_info.user_name, name=bot_info.user_name,
), ),
command='茶服绑定{游戏ID}', prompt='茶服绑定{游戏ID}',
lang=get_lang(),
), ),
) )
) as page_hash: ) as page_hash:

View File

@@ -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"
}
}
} }
} }
} }

View File

@@ -11,6 +11,7 @@
{ {
"scope": "error", "scope": "error",
"types": [{ "subtype": "MessageFormatError", "types": ["TETR.IO", "TOS", "TOP"] }] "types": [{ "subtype": "MessageFormatError", "types": ["TETR.IO", "TOS", "TOP"] }]
} },
{ "scope": "template", "types": ["template_language"] }
] ]
} }

View File

@@ -12,5 +12,8 @@
"TOS": "Username/ID is invalid", "TOS": "Username/ID is invalid",
"TOP": "Username is invalid" "TOP": "Username is invalid"
} }
},
"template": {
"template_language": "en-US"
} }
} }

View File

@@ -1,7 +1,7 @@
# This file is @generated by tarina.lang CLI tool # This file is @generated by tarina.lang CLI tool
# It is not intended for manual editing. # 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: class InteractionWrong:
@@ -27,6 +27,11 @@ class Error:
MessageFormatError = ErrorMessageformaterror MessageFormatError = ErrorMessageformaterror
class Template:
template_language: LangItem = LangItem('template', 'template_language')
class Lang(LangModel): class Lang(LangModel):
interaction = Interaction interaction = Interaction
error = Error error = Error
template = Template

View File

@@ -10,5 +10,8 @@
"TOS": "用户名/ID不合法", "TOS": "用户名/ID不合法",
"TOP": "用户名不合法" "TOP": "用户名不合法"
} }
},
"template": {
"template_language": "zh-CN"
} }
} }

View File

@@ -10,6 +10,8 @@ from nonebot.log import logger
from playwright.__main__ import main from playwright.__main__ import main
from playwright.async_api import Browser, BrowserContext, async_playwright from playwright.async_api import Browser, BrowserContext, async_playwright
from ..config.config import config
driver = get_driver() driver = get_driver()
global_config = driver.config global_config = driver.config
@@ -76,6 +78,7 @@ class BrowserManager:
"""启动浏览器实例""" """启动浏览器实例"""
playwright = await async_playwright().start() playwright = await async_playwright().start()
cls._browser = await playwright.firefox.launch( cls._browser = await playwright.firefox.launch(
headless=not config.tetris.development,
firefox_user_prefs={ firefox_user_prefs={
'network.http.max-persistent-connections-per-server': 64, 'network.http.max-persistent-connections-per-server': 64,
}, },

View 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

View File

@@ -12,7 +12,7 @@ from nonebot import get_app, get_driver
from nonebot.log import logger from nonebot.log import logger
from yarl import URL 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 ..games.tetrio.api.cache import request
from .image import img_to_png from .image import img_to_png
from .templates import TEMPLATES_DIR from .templates import TEMPLATES_DIR
@@ -45,15 +45,21 @@ class HostPage:
async def __aenter__(self) -> str: async def __aenter__(self) -> str:
return self.page_hash return self.page_hash
async def __aexit__(self, exc_type, exc, tb) -> None: # noqa: ANN001 if not config.tetris.development:
self.pages.pop(self.page_hash, None)
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 @driver.on_startup
def _(): def _():
app.mount( app.mount(
'/host/assets', '/host/_nuxt',
StaticFiles(directory=TEMPLATES_DIR / 'assets'), StaticFiles(directory=TEMPLATES_DIR / '_nuxt'),
name='assets', name='assets',
) )
logger.success('assets mounted') logger.success('assets mounted')

View 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())

View File

@@ -4,20 +4,26 @@ from jinja2 import Environment, FileSystemLoader
from nonebot.compat import PYDANTIC_V2 from nonebot.compat import PYDANTIC_V2
from ..templates import TEMPLATES_DIR from ..templates import TEMPLATES_DIR
from .schemas.base import Base
from .schemas.bind import Bind from .schemas.bind import Bind
from .schemas.tetrio.rank.detail import Data as TETRIORankDetailData from .schemas.v1.tetrio.rank import Data as TETRIORankDataV1
from .schemas.tetrio.rank.v1 import Data as TETRIORankDataV1 from .schemas.v1.tetrio.user.info import Info as TETRIOUserInfoV1
from .schemas.tetrio.rank.v2 import Data as TETRIORankDataV2 from .schemas.v1.top.info import Info as TOPInfoV1
from .schemas.tetrio.record.blitz import Record as TETRIORecordBlitz from .schemas.v1.tos.info import Info as TOSInfoV1
from .schemas.tetrio.record.sprint import Record as TETRIORecordSprint from .schemas.v2.tetrio.rank import Data as TETRIORankDataV2
from .schemas.tetrio.user.info_v1 import Info as TETRIOUserInfoV1 from .schemas.v2.tetrio.rank.detail import Data as TETRIORankDetailDataV2
from .schemas.tetrio.user.info_v2 import Info as TETRIOUserInfoV2 from .schemas.v2.tetrio.record.blitz import Record as TETRIORecordBlitzV2
from .schemas.tetrio.user.list_v2 import List as TETRIOUserListV2 from .schemas.v2.tetrio.record.sprint import Record as TETRIORecordSprintV2
from .schemas.top_info import Info as TOPInfo from .schemas.v2.tetrio.tetra_league import Data as TETRIOTetraLeagueDataV2
from .schemas.tos_info import Info as TOSInfo from .schemas.v2.tetrio.user.info import Info as TETRIOUserInfoV2
from .schemas.v2.tetrio.user.list import List as TETRIOUserListV2
env = Environment( 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 @overload
async def render(render_type: Literal['v1/tetrio/rank'], data: TETRIORankDataV1) -> str: ... async def render(render_type: Literal['v1/tetrio/rank'], data: TETRIORankDataV1) -> str: ...
@overload @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 @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 @overload
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 @overload
async def render(render_type: Literal['v2/tetrio/user/list'], data: TETRIOUserListV2) -> str: ... 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( async def render(
render_type: Literal[ render_type: str,
'v1/binding', data: Base,
'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,
) -> 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

@@ -1,8 +1,14 @@
from datetime import datetime
from typing import Literal from typing import Literal
from pydantic import BaseModel 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): class Avatar(BaseModel):
@@ -18,3 +24,14 @@ class People(BaseModel):
class Ranking(BaseModel): class Ranking(BaseModel):
rating: Number rating: Number
rd: Number rd: Number
class HistoryData(BaseModel):
score: Number
record_at: datetime
class Trending(StrEnum):
UP = 'up'
KEEP = 'keep'
DOWN = 'down'

View File

@@ -1,13 +1,11 @@
from typing import Literal from typing import Literal
from pydantic import BaseModel from .base import Base, People
from .base import People
class Bind(BaseModel): class Bind(Base):
platform: Literal['TETR.IO', 'TOP', 'TOS'] platform: Literal['TETR.IO', 'TOP', 'TOS']
status: Literal['error', 'success', 'unknown', 'unlink', 'unverified'] type: Literal['success', 'unknown', 'unlink', 'unverified', 'error']
user: People user: People
bot: People bot: People
command: str prompt: str

View File

@@ -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

View File

@@ -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

View File

@@ -1,10 +0,0 @@
from datetime import datetime
from pydantic import BaseModel
from .....typedefs import Number
class TetraLeagueHistoryData(BaseModel):
record_at: datetime
tr: Number

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -3,6 +3,7 @@ from datetime import datetime
from pydantic import BaseModel from pydantic import BaseModel
from ......games.tetrio.api.typedefs import ValidRank from ......games.tetrio.api.typedefs import ValidRank
from ...base import Base
class ItemData(BaseModel): class ItemData(BaseModel):
@@ -11,6 +12,6 @@ class ItemData(BaseModel):
players: int players: int
class Data(BaseModel): class Data(Base):
items: dict[ValidRank, ItemData] items: dict[ValidRank, ItemData]
updated_at: datetime updated_at: datetime

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -3,7 +3,7 @@ from typing import Literal
from pydantic import BaseModel from pydantic import BaseModel
from ...base import People from ....base import Base, People
class User(People): class User(People):
@@ -61,7 +61,7 @@ class Statistic(BaseModel):
finesse: Finesse finesse: Finesse
class Record(BaseModel): class Record(Base):
type: Literal['best', 'personal_best', 'recent', 'disputed'] type: Literal['best', 'personal_best', 'recent', 'disputed']
user: User user: User

View File

@@ -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

View File

@@ -3,10 +3,9 @@ from typing import Literal
from pydantic import BaseModel from pydantic import BaseModel
from ......games.tetrio.api.typedefs import Rank from .......games.tetrio.api.typedefs import Rank
from .....typedefs import Number from ......typedefs import Number
from ...base import Avatar from ....base import Avatar, Base, HistoryData
from .base import TetraLeagueHistoryData
class Badge(BaseModel): class Badge(BaseModel):
@@ -22,6 +21,7 @@ class User(BaseModel):
country: str | None country: str | None
role: Literal['anon', 'user', 'bot', 'halfmod', 'mod', 'admin', 'sysop', 'hidden', 'banned'] role: Literal['anon', 'user', 'bot', 'halfmod', 'mod', 'admin', 'sysop', 'hidden', 'banned']
botmaster: str | None
avatar: str | Avatar avatar: str | Avatar
banner: str | None banner: str | None
@@ -36,6 +36,9 @@ class User(BaseModel):
badges: list[Badge] badges: list[Badge]
xp: Number xp: Number
ar: Number
achievements: list[int]
playtime: str | None playtime: str | None
join_at: datetime | None join_at: datetime | None
@@ -74,18 +77,20 @@ class TetraLeague(BaseModel):
decaying: bool decaying: bool
history: list[TetraLeagueHistoryData] | None history: list[HistoryData] | None
class Sprint(BaseModel): class Sprint(BaseModel):
time: str time: str
global_rank: int | None global_rank: Number | None
country_rank: Number | None
play_at: datetime play_at: datetime
class Blitz(BaseModel): class Blitz(BaseModel):
score: int score: Number
global_rank: int | None global_rank: int | None
country_rank: int | None
play_at: datetime play_at: datetime
@@ -94,9 +99,29 @@ class Zen(BaseModel):
score: int 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 user: User
tetra_league: TetraLeague | None tetra_league: TetraLeague | None
zenith: Zenith | None
zenithex: Zenith | None
statistic: Statistic | None statistic: Statistic | None
sprint: Sprint | None sprint: Sprint | None
blitz: Blitz | None blitz: Blitz | None

View File

@@ -1,24 +1,23 @@
from datetime import datetime
from pydantic import BaseModel from pydantic import BaseModel
from ......games.tetrio.api.typedefs import Rank from .......games.tetrio.api.typedefs import Rank
from .....typedefs import Number from ......typedefs import Number
from ...base import Avatar from ....base import Avatar, Base
class TetraLeague(BaseModel): class TetraLeague(BaseModel):
pps: Number
apm: Number
apl: Number
vs: Number | None
adpl: Number | None
rank: Rank rank: Rank
tr: Number tr: Number
glicko: Number | None glicko: Number | None
rd: Number | None rd: Number | None
decaying: bool decaying: bool
pps: Number
apm: Number
apl: Number
vs: Number | None
adpl: Number | None
class User(BaseModel): class User(BaseModel):
@@ -26,11 +25,14 @@ class User(BaseModel):
name: str name: str
avatar: str | Avatar avatar: str | Avatar
country: str | None country: str | None
tetra_league: TetraLeague
xp: Number xp: Number
join_at: datetime | None
class List(BaseModel): class Data(BaseModel):
user: User
tetra_league: TetraLeague
class List(Base):
show_index: bool show_index: bool
users: list[User] data: list[Data]

View File

@@ -16,6 +16,7 @@ async def screenshot(url: str) -> bytes:
context = await BrowserManager.get_context('screenshot', factory=context_factory) context = await BrowserManager.get_context('screenshot', factory=context_factory)
async with await context.new_page() as page: async with await context.new_page() as page:
await page.goto(url) await page.goto(url)
await page.wait_for_selector('#content')
size: ViewportSize = await page.evaluate(""" size: ViewportSize = await page.evaluate("""
() => { () => {
const element = document.querySelector('#content'); const element = document.querySelector('#content');
@@ -26,5 +27,4 @@ async def screenshot(url: str) -> bytes:
}; };
""") """)
await page.set_viewport_size(size) 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') return await page.locator('id=content').screenshot(animations='disabled', timeout=5000, type='png')

View File

@@ -30,7 +30,7 @@ async def download_templates(tag: str) -> Path:
tag = ( tag = (
( (
await client.get( 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('/') .url.path.strip('/')
@@ -43,7 +43,7 @@ async def download_templates(tag: str) -> Path:
async with ( async with (
client.stream( client.stream(
'GET', '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, follow_redirects=True,
) as response, ) as response,
aopen(path, 'wb') as file, aopen(path, 'wb') as file,
@@ -107,7 +107,7 @@ async def init_templates(tag: str) -> bool:
async def check_tag(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: async with AsyncClient(proxy=config.tetris.proxy.github or config.tetris.proxy.main) as client:
return ( 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 ).status_code != HTTPStatus.NOT_FOUND

View File

@@ -59,3 +59,5 @@ Me: TypeAlias = Literal[
'self', 'self',
'oneself', 'oneself',
] ]
Lang: TypeAlias = Literal['zh-CN', 'en-US']

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "nonebot-plugin-tetris-stats" name = "nonebot-plugin-tetris-stats"
version = "1.7.2" version = "1.8.2"
description = "一款基于 NoneBot2 的用于查询 Tetris 相关游戏数据的插件" description = "一款基于 NoneBot2 的用于查询 Tetris 相关游戏数据的插件"
readme = "README.md" readme = "README.md"
authors = [{ name = "shoucandanghehe", email = "wallfjjd@gmail.com" }] authors = [{ name = "shoucandanghehe", email = "wallfjjd@gmail.com" }]
@@ -29,6 +29,7 @@ dependencies = [
"pillow>=11.0.0", "pillow>=11.0.0",
"playwright>=1.48.0", "playwright>=1.48.0",
"rich>=13.9.3", "rich>=13.9.3",
"strenum>=0.4.15",
"yarl>=1.16.0", "yarl>=1.16.0",
] ]
classifiers = [ classifiers = [
@@ -57,6 +58,7 @@ dev = [
"nonebot-adapter-kaiheila>=0.3.4", "nonebot-adapter-kaiheila>=0.3.4",
"nonebot-adapter-onebot>=2.4.6", "nonebot-adapter-onebot>=2.4.6",
"nonebot-adapter-qq>=1.5.3", "nonebot-adapter-qq>=1.5.3",
"nonebot-plugin-tarina-lang-turbo>=0.1.1",
"ruff>=0.7.1", "ruff>=0.7.1",
] ]
typecheck = [ typecheck = [
@@ -158,7 +160,7 @@ defineConstant = { PYDANTIC_V2 = true }
typeCheckingMode = "standard" typeCheckingMode = "standard"
[tool.bumpversion] [tool.bumpversion]
current_version = "1.7.2" current_version = "1.8.2"
tag = true tag = true
sign_tags = true sign_tags = true
tag_name = "{new_version}" tag_name = "{new_version}"
@@ -175,4 +177,4 @@ asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "session" asyncio_default_fixture_loop_scope = "session"
[tool.nonebot] [tool.nonebot]
plugins = ["nonebot_plugin_tetris_stats"] plugins = ["nonebot_plugin_tetris_stats", "nonebot_plugin_tarina_lang_turbo"]

3100
uv.lock generated

File diff suppressed because it is too large Load Diff