Compare commits

..

32 Commits
1.4.1 ... 1.4.4

Author SHA1 Message Date
5117e7dbd9 🔖 1.4.4 2024-08-06 20:12:20 +08:00
4bb00cdeb7 👽️ 移除茶服不可用地址 2024-08-06 20:11:50 +08:00
b7cbe2b2a0 🔥 删除不必要的类型转换
上游修了hhh
2024-08-06 16:35:35 +08:00
8bb460fce0 ⬆️ 更新依赖 2024-08-06 16:34:14 +08:00
41bbcdb66c 🔖 1.4.3 2024-08-06 15:46:11 +08:00
160d81476a 🔥 删除不需要的 type: ignore 2024-08-06 15:35:53 +08:00
1e5b00a280 初步重新适配 TETR.IO query 2024-08-06 15:29:32 +08:00
ee53b92559 🔥 删除不需要的函数调用 2024-08-06 15:28:26 +08:00
cd9d29b748 🚨 修复 pyright 类型报错 2024-08-06 15:27:42 +08:00
214ebc5073 移除对 arclet-alconna 的显式依赖声明 2024-08-06 13:41:13 +08:00
485706267e 🐛 更新 TETR.IO summaries solo 模型 2024-08-06 01:34:07 +08:00
12cb5193b3 🎨 优化模板模型路径
~~真的是优化吗~~
2024-08-06 00:03:02 +08:00
dependabot[bot]
461d3450d6 ⬆️ Bump ruff from 0.5.5 to 0.5.6 (#386)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.5.5 to 0.5.6.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.5.5...0.5.6)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-05 15:55:56 +00:00
dependabot[bot]
64d77dbff2 ⬆️ Bump pandas-stubs from 2.2.2.240603 to 2.2.2.240805 (#385)
Bumps [pandas-stubs](https://github.com/pandas-dev/pandas-stubs) from 2.2.2.240603 to 2.2.2.240805.
- [Changelog](https://github.com/pandas-dev/pandas-stubs/blob/main/docs/release_procedure.md)
- [Commits](https://github.com/pandas-dev/pandas-stubs/compare/v2.2.2.240603...v2.2.2.240805)

---
updated-dependencies:
- dependency-name: pandas-stubs
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-05 15:52:59 +00:00
dependabot[bot]
e5b4d3bc08 ⬆️ Bump arclet-alconna from 1.8.19 to 1.8.21 (#387)
Bumps [arclet-alconna](https://github.com/ArcletProject/Alconna) from 1.8.19 to 1.8.21.
- [Release notes](https://github.com/ArcletProject/Alconna/releases)
- [Changelog](https://github.com/ArcletProject/Alconna/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/ArcletProject/Alconna/compare/v1.8.19...v1.8.21)

---
updated-dependencies:
- dependency-name: arclet-alconna
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-05 15:49:32 +00:00
dependabot[bot]
4208018caf ⬆️ Bump nonebot-plugin-localstore from 0.7.0 to 0.7.1 (#384)
Bumps [nonebot-plugin-localstore](https://github.com/nonebot/plugin-localstore) from 0.7.0 to 0.7.1.
- [Release notes](https://github.com/nonebot/plugin-localstore/releases)
- [Commits](https://github.com/nonebot/plugin-localstore/compare/v0.7.0...v0.7.1)

---
updated-dependencies:
- dependency-name: nonebot-plugin-localstore
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-05 15:45:53 +00:00
dependabot[bot]
5032a3eb9a ⬆️ Bump nonebot-plugin-session from 0.3.1 to 0.3.2 (#383)
Bumps [nonebot-plugin-session](https://github.com/noneplugin/nonebot-plugin-session) from 0.3.1 to 0.3.2.
- [Release notes](https://github.com/noneplugin/nonebot-plugin-session/releases)
- [Commits](https://github.com/noneplugin/nonebot-plugin-session/compare/v0.3.1...v0.3.2)

---
updated-dependencies:
- dependency-name: nonebot-plugin-session
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-05 23:42:23 +08:00
dependabot[bot]
bf9a9953dd ⬆️ Bump nonebot-adapter-qq from 1.4.4 to 1.5.0 (#381)
Bumps [nonebot-adapter-qq](https://github.com/nonebot/adapter-qq) from 1.4.4 to 1.5.0.
- [Release notes](https://github.com/nonebot/adapter-qq/releases)
- [Commits](https://github.com/nonebot/adapter-qq/compare/v1.4.4...v1.5.0)

---
updated-dependencies:
- dependency-name: nonebot-adapter-qq
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-05 04:10:49 +00:00
dependabot[bot]
85feb9cb41 ⬆️ Bump mypy from 1.11.0 to 1.11.1 (#382)
Bumps [mypy](https://github.com/python/mypy) from 1.11.0 to 1.11.1.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.11...v1.11.1)

---
updated-dependencies:
- dependency-name: mypy
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-05 04:07:14 +00:00
5a7c54528c 🐛 修正 record 使用的 type 2024-08-05 12:02:50 +08:00
afce74afe8 修改命令注册逻辑 2024-08-05 11:56:48 +08:00
435850819c 🔖 1.4.2 2024-08-04 19:57:47 +08:00
6f439ad357 适配新模板 2024-08-04 19:22:36 +08:00
b74cc1f4a0 🐛 修复 TETR.IO 获取 user 时出现 UnboundLocalError 2024-08-04 19:21:52 +08:00
1a1c2675d1 再次更新模板仓库处理逻辑 2024-08-03 23:52:45 +08:00
1f02c107f5 AR排行榜 API 模型 2024-08-03 16:47:57 +08:00
89c319a500 完善 PluginMetadata 2024-08-02 22:46:00 +08:00
56f9a69c4d 🙈 更新 .gitignore 2024-08-02 22:19:59 +08:00
50431fe7cb 新赛季排行榜 API 模型 2024-08-02 22:15:46 +08:00
71ad53a1f9 适配 TETR.IO rank v1 模板 2024-08-02 22:15:46 +08:00
820393f216 🎨 减少两个 overload 2024-08-02 22:15:45 +08:00
27994cea6b 🗃️ 清空 TETR.IO 旧赛季数据 2024-08-02 22:15:45 +08:00
48 changed files with 1300 additions and 1070 deletions

2
.gitignore vendored
View File

@@ -20,3 +20,5 @@ bot.py
TODO
*.fish
extracted_skin_mino_*
sample_*
TODO*

View File

@@ -1,14 +1,19 @@
from nonebot import require
from nonebot.plugin import PluginMetadata
from nonebot.plugin import PluginMetadata, inherit_supported_adapters
require('nonebot_plugin_alconna')
require('nonebot_plugin_apscheduler')
require('nonebot_plugin_localstore')
require('nonebot_plugin_orm')
require('nonebot_plugin_session_orm')
require('nonebot_plugin_session')
require('nonebot_plugin_user')
require('nonebot_plugin_userinfo')
require_plugins = {
'nonebot_plugin_alconna',
'nonebot_plugin_apscheduler',
'nonebot_plugin_localstore',
'nonebot_plugin_orm',
'nonebot_plugin_session_orm',
'nonebot_plugin_session',
'nonebot_plugin_user',
'nonebot_plugin_userinfo',
}
for i in require_plugins:
require(i)
from nonebot_plugin_alconna import namespace # noqa: E402
@@ -16,6 +21,7 @@ with namespace('tetris_stats') as ns:
ns.enable_message_cache = False
from .config import migrations # noqa: E402
from .config.config import Config # noqa: E402
__plugin_meta__ = PluginMetadata(
name='Tetris Stats',
@@ -23,6 +29,8 @@ __plugin_meta__ = PluginMetadata(
usage='发送 tstats --help 查询使用方法',
type='application',
homepage='https://github.com/A-minos/nonebot-plugin-tetris-stats',
config=Config,
supported_adapters=inherit_supported_adapters(*require_plugins),
extra={
'orm_version_location': migrations,
},

View File

@@ -1,6 +1,6 @@
from pathlib import Path
from nonebot_plugin_localstore import get_cache_dir # type: ignore[import-untyped]
from nonebot_plugin_localstore import get_cache_dir
from pydantic import BaseModel
CACHE_PATH: Path = get_cache_dir('nonebot_plugin_tetris_stats')

View File

@@ -0,0 +1,91 @@
"""TETR.IO new season
迁移 ID: f5b4a6d1325b
父迁移: a1195e989cc6
创建时间: 2024-08-01 20:44:48.644912
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import sqlalchemy as sa
from alembic import op
if TYPE_CHECKING:
from collections.abc import Sequence
revision: str = 'f5b4a6d1325b'
down_revision: str | Sequence[str] | None = 'a1195e989cc6'
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade(name: str = '') -> None:
if name:
return
with op.batch_alter_table('nonebot_plugin_tetris_stats_iorank', schema=None) as batch_op:
batch_op.drop_index('ix_nonebot_plugin_tetris_stats_iorank_file_hash')
batch_op.drop_index('ix_nonebot_plugin_tetris_stats_iorank_rank')
batch_op.drop_index('ix_nonebot_plugin_tetris_stats_iorank_update_time')
op.drop_table('nonebot_plugin_tetris_stats_iorank')
with op.batch_alter_table('nonebot_plugin_tetris_stats_tetriohistoricaldata', schema=None) as batch_op:
batch_op.drop_index('ix_nonebot_plugin_tetris_stats_tetriohistoricaldata_api_type')
batch_op.drop_index('ix_nonebot_plugin_tetris_stats_tetriohistoricaldata_update_time')
batch_op.drop_index('ix_nonebot_plugin_tetris_stats_tetriohistoricaldata_user_unique_identifier')
op.drop_table('nonebot_plugin_tetris_stats_tetriohistoricaldata')
op.create_table(
'nonebot_plugin_tetris_stats_tetriohistoricaldata',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_unique_identifier', sa.String(length=24), nullable=False),
sa.Column('api_type', sa.String(length=16), nullable=False),
sa.Column('data', sa.JSON(), nullable=False),
sa.Column('update_time', sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('pk_nonebot_plugin_tetris_stats_tetriohistoricaldata')),
info={'bind_key': 'nonebot_plugin_tetris_stats'},
)
with op.batch_alter_table('nonebot_plugin_tetris_stats_tetriohistoricaldata', schema=None) as batch_op:
batch_op.create_index(
batch_op.f('ix_nonebot_plugin_tetris_stats_tetriohistoricaldata_api_type'), ['api_type'], unique=False
)
batch_op.create_index(
batch_op.f('ix_nonebot_plugin_tetris_stats_tetriohistoricaldata_update_time'), ['update_time'], unique=False
)
batch_op.create_index(
batch_op.f('ix_nonebot_plugin_tetris_stats_tetriohistoricaldata_user_unique_identifier'),
['user_unique_identifier'],
unique=False,
)
def downgrade(name: str = '') -> None:
if name:
return
op.create_table(
'nonebot_plugin_tetris_stats_iorank',
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('rank', sa.VARCHAR(length=2), nullable=False),
sa.Column('tr_line', sa.FLOAT(), nullable=False),
sa.Column('player_count', sa.INTEGER(), nullable=False),
sa.Column('low_pps', sa.JSON(), nullable=False),
sa.Column('low_apm', sa.JSON(), nullable=False),
sa.Column('low_vs', sa.JSON(), nullable=False),
sa.Column('avg_pps', sa.FLOAT(), nullable=False),
sa.Column('avg_apm', sa.FLOAT(), nullable=False),
sa.Column('avg_vs', sa.FLOAT(), nullable=False),
sa.Column('high_pps', sa.JSON(), nullable=False),
sa.Column('high_apm', sa.JSON(), nullable=False),
sa.Column('high_vs', sa.JSON(), nullable=False),
sa.Column('update_time', sa.DATETIME(), nullable=False),
sa.Column('file_hash', sa.VARCHAR(length=128), nullable=True),
sa.PrimaryKeyConstraint('id', name='pk_nonebot_plugin_tetris_stats_iorank'),
)
with op.batch_alter_table('nonebot_plugin_tetris_stats_iorank', schema=None) as batch_op:
batch_op.create_index('ix_nonebot_plugin_tetris_stats_iorank_update_time', ['update_time'], unique=False)
batch_op.create_index('ix_nonebot_plugin_tetris_stats_iorank_rank', ['rank'], unique=False)
batch_op.create_index('ix_nonebot_plugin_tetris_stats_iorank_file_hash', ['file_hash'], unique=False)

View File

@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Literal, TypeVar, overload
from nonebot.exception import FinishedException
from nonebot.log import logger
from nonebot_plugin_orm import AsyncSession, get_session
from nonebot_plugin_user import User # type: ignore[import-untyped]
from nonebot_plugin_user import User
from sqlalchemy import select
from ..utils.typing import AllCommandType, BaseCommandType, GameType, TETRIOCommandType

View File

@@ -1,16 +1,10 @@
from arclet.alconna import Arg, ArgFlag, Args, Option, Subcommand
from nonebot_plugin_alconna import At
from nonebot_plugin_alconna import Subcommand
from ...utils.exception import MessageFormatError
from ...utils.typing import Me
# from .. import add_block_handlers, alc, command
from .. import alc, command
from .. import alc
from .. import command as main_command
from .api import Player
# from .api.typing import ValidRank
from .constant import USER_ID, USER_NAME
from .typing import Template
def get_player(user_id_or_name: str) -> Player | MessageFormatError:
@@ -21,171 +15,22 @@ def get_player(user_id_or_name: str) -> Player | MessageFormatError:
return MessageFormatError('用户名/ID不合法')
command.add(
Subcommand(
'TETR.IO',
Subcommand(
'bind',
Args(
Arg(
'account',
get_player,
notice='TETR.IO 用户名 / ID',
flags=[ArgFlag.HIDDEN],
)
),
help_text='绑定 TETR.IO 账号',
),
# Subcommand(
# 'query',
# Args(
# Arg(
# 'target',
# At | Me,
# notice='@想要查询的人 / 自己',
# flags=[ArgFlag.HIDDEN, ArgFlag.OPTIONAL],
# ),
# Arg(
# 'account',
# get_player,
# notice='TETR.IO 用户名 / ID',
# flags=[ArgFlag.HIDDEN, ArgFlag.OPTIONAL],
# ),
# ),
# Option(
# '--template',
# Arg('template', Template),
# alias=['-T'],
# help_text='要使用的查询模板',
# ),
# help_text='查询 TETR.IO 游戏信息',
# ),
Subcommand(
'record',
Option(
'--40l',
dest='sprint',
),
Option(
'--blitz',
dest='blitz',
),
Args(
Arg(
'target',
At | Me,
notice='@想要查询的人 / 自己',
flags=[ArgFlag.HIDDEN, ArgFlag.OPTIONAL],
),
Arg(
'account',
get_player,
notice='TETR.IO 用户名 / ID',
flags=[ArgFlag.HIDDEN, ArgFlag.OPTIONAL],
),
),
),
# Subcommand(
# 'list',
# Option('--max-tr', Arg('max_tr', float), help_text='TR的上限'),
# Option('--min-tr', Arg('min_tr', float), help_text='TR的下限'),
# Option('--limit', Arg('limit', int), help_text='查询数量'),
# Option('--country', Arg('country', str), help_text='国家代码'),
# help_text='查询 TETR.IO 段位排行榜',
# ),
# Subcommand(
# 'rank',
# Subcommand(
# '--all',
# Option(
# '--template',
# Arg('template', Template),
# alias=['-T'],
# help_text='要使用的查询模板',
# ),
# dest='all',
# ),
# Option(
# '--detail',
# Arg('rank', ValidRank),
# alias=['-D'],
# ),
# help_text='查询 TETR.IO 段位信息',
# ),
Subcommand(
'config',
Option(
'--default-template',
Arg('template', Template),
alias=['-DT', 'DefaultTemplate'],
),
),
alias=['TETRIO', 'tetr.io', 'tetrio', 'io'],
dest='TETRIO',
help_text='TETR.IO 游戏相关指令',
)
command = Subcommand(
'TETR.IO',
alias=['TETRIO', 'tetr.io', 'tetrio', 'io'],
dest='TETRIO',
help_text='TETR.IO 游戏相关指令',
)
# def rank_wrapper(slot: int | str, content: str | None):
# if slot == 'rank' and not content:
# return '--all'
# if content is not None:
# return f'--detail {content.lower()}'
# return content
from . import bind, config, query, record # noqa: E402
alc.shortcut(
'(?i:io)(?i:绑定|绑|bind)',
command='tstats TETR.IO bind',
humanized='io绑定',
)
# alc.shortcut(
# '(?i:io)(?i:查询|查|query|stats)',
# command='tstats TETR.IO query',
# humanized='io查',
# )
alc.shortcut(
'(?i:io)(?i:记录|record)(?i:40l)',
command='tstats TETR.IO record --40l',
humanized='io记录40l',
)
alc.shortcut(
'(?i:io)(?i:记录|record)(?i:blitz)',
command='tstats TETR.IO record --blitz',
humanized='io记录blitz',
)
# alc.shortcut(
# r'(?i:io)(?i:段位|段|rank)\s*(?P<rank>[a-zA-Z+-]{0,2})',
# command='tstats TETR.IO rank {rank}',
# humanized='iorank',
# fuzzy=False,
# wrapper=rank_wrapper,
# )
alc.shortcut(
'(?i:io)(?i:配置|配|config)',
command='tstats TETR.IO config',
humanized='io配置',
)
# alc.shortcut(
# 'fkosk',
# command='tstats TETR.IO query',
# arguments=['我'],
# fuzzy=False,
# humanized='An Easter egg!',
# )
# add_block_handlers(alc.assign('TETRIO.query'))
# from . import bind, config, list, query, rank, record
from . import bind, config, record # noqa: E402
main_command.add(command)
__all__ = [
'alc',
'bind',
'config',
# 'list',
# 'query',
# 'rank',
'query',
'record',
]

View File

@@ -83,8 +83,8 @@ class Player:
ID=user_info.data.id,
name=user_info.data.username,
)
self.user_id = user_info.data.id
self.user_name = user_info.data.username
self.user_id = self.__user.ID
self.user_name = self.__user.name
return self.__user
async def get_info(self) -> UserInfoSuccess:
@@ -108,13 +108,9 @@ class Player:
return self._user_info
@overload
async def get_summaries(self, summaries_type: Literal['40l']) -> SoloSuccessModel: ...
async def get_summaries(self, summaries_type: Literal['40l', 'blitz']) -> SoloSuccessModel: ...
@overload
async def get_summaries(self, summaries_type: Literal['blitz']) -> SoloSuccessModel: ...
@overload
async def get_summaries(self, summaries_type: Literal['zenith']) -> ZenithSuccessModel: ...
@overload
async def get_summaries(self, summaries_type: Literal['zenithex']) -> ZenithSuccessModel: ...
async def get_summaries(self, summaries_type: Literal['zenith', 'zenithex']) -> ZenithSuccessModel: ...
@overload
async def get_summaries(self, summaries_type: Literal['zen']) -> ZenSuccessModel: ...
@overload

View File

@@ -4,6 +4,12 @@ from typing import Literal
from pydantic import BaseModel
class P(BaseModel): # what is P
pri: float
sec: float
ter: float
class Cache(BaseModel):
status: str
cached_at: datetime

View File

@@ -0,0 +1,27 @@
from pydantic import BaseModel, Field
from ..base import SuccessModel
from .base import Entry as BaseEntry
class ArCounts(BaseModel):
bronze: int | None = Field(None, alias='1')
silver: int | None = Field(None, alias='2')
gold: int | None = Field(None, alias='3')
platinum: int | None = Field(None, alias='4')
diamond: int | None = Field(None, alias='5')
issued: int | None = Field(None, alias='100')
top10: int | None = Field(None, alias='t10')
class Entry(BaseEntry):
ar: int
ar_counts: ArCounts
class Data(BaseModel):
entries: list[Entry]
class ArSuccessModel(SuccessModel):
data: Data

View File

@@ -0,0 +1,30 @@
from datetime import datetime
from pydantic import BaseModel, Field
from ...typing import Rank
from ..base import P
class League(BaseModel):
gamesplayed: int
gameswon: int
rating: int
rank: Rank
decaying: bool
class Entry(BaseModel):
id: str = Field(..., alias='_id')
username: str
role: str
xp: float
league: League
supporter: bool | None = None
verified: bool
country: str | None = None
ts: datetime
gamesplayed: int
gameswon: int
gametime: float
p: P

View File

@@ -0,0 +1,12 @@
from pydantic import BaseModel
from ..base import SuccessModel
from ..summaries.solo import Record
class Data(BaseModel):
entries: list[Record]
class SoloSuccessModel(SuccessModel):
data: Data

View File

@@ -0,0 +1,12 @@
from pydantic import BaseModel
from ..base import SuccessModel
from .base import Entry
class Data(BaseModel):
entries: list[Entry]
class XpSuccessModel(SuccessModel):
data: Data

View File

@@ -0,0 +1,12 @@
from pydantic import BaseModel
from ..base import SuccessModel
from ..summaries.zenith import Record
class Data(BaseModel):
entries: list[Record]
class ZenithSuccessModel(SuccessModel):
data: Data

View File

@@ -4,9 +4,9 @@ from pydantic import BaseModel
class User(BaseModel):
id: str
username: str
avatar_revision: int
banner_revision: int
country: str
avatar_revision: int | None
banner_revision: int | None
country: str | None
verified: int
supporter: int
@@ -21,9 +21,3 @@ class Finesse(BaseModel):
combo: int
faults: int
perfectpieces: int
class P(BaseModel): # what is P
pri: float
sec: float
ter: float

View File

@@ -3,8 +3,8 @@ from typing import Literal, TypeAlias
from pydantic import BaseModel, Field
from ..base import FailedModel, SuccessModel
from .base import AggregateStats, Finesse, P, User
from ..base import FailedModel, P, SuccessModel
from .base import AggregateStats, Finesse, User
class Time(BaseModel):
@@ -34,18 +34,18 @@ class Clears(BaseModel):
class Garbage(BaseModel):
sent: int
received: int
attack: int
attack: int | None
cleared: int
class Stats(BaseModel):
seed: int
seed: int | None = None # ?: 不知道是之后都没有了还是还会有
lines: int
level_lines: int
level_lines_needed: int
inputs: int
holds: int
time: Time
time: Time | None = None # ?: 不知道是之后都没有了还是还会有
score: int
zenlevel: int
zenprogress: int

View File

@@ -3,8 +3,8 @@ from typing import Literal, TypeAlias
from pydantic import BaseModel, Field
from ..base import FailedModel, SuccessModel
from .base import AggregateStats, Finesse, P, User
from ..base import FailedModel, P, SuccessModel
from .base import AggregateStats, Finesse, User
class Clears(BaseModel):
@@ -76,7 +76,7 @@ class Stats(BaseModel):
kills: int
finesse: Finesse
zenith: _Zenith
finaltime: int
finaltime: float
class Results(BaseModel):

View File

@@ -1,13 +1,14 @@
from asyncio import gather
from hashlib import md5
from urllib.parse import urlencode
from arclet.alconna import Arg, ArgFlag
from nonebot_plugin_alconna import Args, Subcommand
from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_orm import get_session
from nonebot_plugin_session import EventSession # type: ignore[import-untyped]
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 User # type: ignore[import-untyped]
from nonebot_plugin_userinfo import BotUserInfo, UserInfo # type: ignore[import-untyped]
from nonebot_plugin_user import User
from nonebot_plugin_userinfo import BotUserInfo, UserInfo
from ...db import BindStatus, create_or_update_bind, trigger
from ...utils.host import HostPage, get_self_netloc
@@ -15,10 +16,31 @@ from ...utils.image import get_avatar
from ...utils.render import Bind, render
from ...utils.render.schemas.base import Avatar, People
from ...utils.screenshot import screenshot
from . import alc
from . import alc, command, get_player
from .api import Player
from .constant import GAME_TYPE
command.add(
Subcommand(
'bind',
Args(
Arg(
'account',
get_player,
notice='TETR.IO 用户名 / ID',
flags=[ArgFlag.HIDDEN],
)
),
help_text='绑定 TETR.IO 账号',
)
)
alc.shortcut(
'(?i:io)(?i:绑定|绑|bind)',
command='tstats TETR.IO bind',
humanized='io绑定',
)
@alc.assign('TETRIO.bind')
async def _(nb_user: User, account: Player, event_session: EventSession, bot_info: UserInfo = BotUserInfo()): # noqa: B008
@@ -28,7 +50,7 @@ async def _(nb_user: User, account: Player, event_session: EventSession, bot_inf
command_type='bind',
command_args=[],
):
user, user_info = await gather(account.user, account.get_info())
user = await account.user
async with get_session() as session:
bind_status = await create_or_update_bind(
session=session,

View File

@@ -1,16 +1,37 @@
from arclet.alconna import Arg
from nonebot_plugin_alconna import Option, Subcommand
from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_orm import async_scoped_session
from nonebot_plugin_session import EventSession # type: ignore[import-untyped]
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 User # type: ignore[import-untyped]
from nonebot_plugin_user import User
from sqlalchemy import select
from ...db import trigger
from . import alc
from . import alc, command
from .constant import GAME_TYPE
from .models import TETRIOUserConfig
from .typing import Template
command.add(
Subcommand(
'config',
Option(
'--default-template',
Arg('template', Template, notice='模板版本'),
alias=['-DT', 'DefaultTemplate'],
help_text='设置默认查询模板',
),
help_text='TETR.IO 查询个性化配置',
),
)
alc.shortcut(
'(?i:io)(?i:配置|配|config)',
command='tstats TETR.IO config',
humanized='io配置',
)
@alc.assign('TETRIO.config')
async def _(user: User, session: async_scoped_session, event_session: EventSession, template: Template):

View File

@@ -1,34 +1,10 @@
from datetime import datetime
from nonebot_plugin_orm import Model
from sqlalchemy import JSON, DateTime, String
from sqlalchemy import String
from sqlalchemy.orm import Mapped, MappedAsDataclass, mapped_column
from .api.typing import ValidRank
from .typing import Template
class IORank(MappedAsDataclass, Model):
id: Mapped[int] = mapped_column(init=False, primary_key=True)
rank: Mapped[ValidRank] = mapped_column(String(2), index=True)
tr_line: Mapped[float]
player_count: Mapped[int]
low_pps: Mapped[tuple[dict[str, str], float]] = mapped_column(JSON)
low_apm: Mapped[tuple[dict[str, str], float]] = mapped_column(JSON)
low_vs: Mapped[tuple[dict[str, str], float]] = mapped_column(JSON)
avg_pps: Mapped[float]
avg_apm: Mapped[float]
avg_vs: Mapped[float]
high_pps: Mapped[tuple[dict[str, str], float]] = mapped_column(JSON)
high_apm: Mapped[tuple[dict[str, str], float]] = mapped_column(JSON)
high_vs: Mapped[tuple[dict[str, str], float]] = mapped_column(JSON)
update_time: Mapped[datetime] = mapped_column(
DateTime,
index=True,
)
file_hash: Mapped[str | None] = mapped_column(String(128), index=True)
class TETRIOUserConfig(MappedAsDataclass, Model):
id: Mapped[int] = mapped_column(primary_key=True)
query_template: Mapped[Template] = mapped_column(String(2))

View File

@@ -0,0 +1,241 @@
from asyncio import gather
from datetime import datetime, timedelta, timezone
from hashlib import md5
from typing import TYPE_CHECKING, TypeVar
from urllib.parse import urlencode
from arclet.alconna import Arg, ArgFlag
from nonebot import get_driver
from nonebot.adapters import Event
from nonebot.matcher import Matcher
from nonebot_plugin_alconna import Args, At, Option, Subcommand
from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_orm import get_session
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 User as NBUser
from nonebot_plugin_user import get_user
from sqlalchemy import select
from ...db import query_bind_info, trigger
from ...utils.host import HostPage, get_self_netloc
from ...utils.render import render
from ...utils.render.schemas.base import Avatar
from ...utils.render.schemas.tetrio.user.info_v2 import Badge, Blitz, Sprint, Statistic, Zen
from ...utils.render.schemas.tetrio.user.info_v2 import Info as V2TemplateInfo
from ...utils.render.schemas.tetrio.user.info_v2 import User as V2TemplateUser
from ...utils.screenshot import screenshot
from ...utils.typing import Me
from .. import add_block_handlers, alc
from ..constant import CANT_VERIFY_MESSAGE
from . import command, get_player
from .api import Player
from .constant import GAME_TYPE
from .models import TETRIOUserConfig
from .typing import Template
if TYPE_CHECKING:
from .api.schemas.summaries import SoloSuccessModel, ZenSuccessModel
from .api.schemas.user import User
from .api.schemas.user_info import UserInfoSuccess
UTC = timezone.utc
driver = get_driver()
command.add(
Subcommand(
'query',
Args(
Arg(
'target',
At | Me,
notice='@想要查询的人 / 自己',
flags=[ArgFlag.HIDDEN, ArgFlag.OPTIONAL],
),
Arg(
'account',
get_player,
notice='TETR.IO 用户名 / ID',
flags=[ArgFlag.HIDDEN, ArgFlag.OPTIONAL],
),
),
Option(
'--template',
Arg('template', Template),
alias=['-T'],
help_text='要使用的查询模板',
),
help_text='查询 TETR.IO 游戏信息',
),
)
alc.shortcut(
'(?i:io)(?i:查询|查|query|stats)',
command='tstats TETR.IO query',
humanized='io查',
)
alc.shortcut(
'fkosk',
command='tstats TETR.IO query',
arguments=[''],
fuzzy=False,
humanized='An Easter egg!',
)
add_block_handlers(alc.assign('TETRIO.query'))
@alc.assign('TETRIO.query')
async def _( # noqa: PLR0913
user: NBUser,
event: Event,
matcher: Matcher,
target: At | Me,
event_session: EventSession,
template: Template | None = None,
):
async with trigger(
session_persist_id=await get_session_persist_id(event_session),
game_platform=GAME_TYPE,
command_type='query',
command_args=[f'--default-template {template}'] if template is not None else [],
):
async with get_session() as session:
bind = await query_bind_info(
session=session,
user=await get_user(
event_session.platform, target.target if isinstance(target, At) else event.get_user_id()
),
game_platform=GAME_TYPE,
)
if template is None:
template = await session.scalar(
select(TETRIOUserConfig.query_template).where(TETRIOUserConfig.id == user.id)
)
if bind is None:
await matcher.finish('未查询到绑定信息')
message = UniMessage(CANT_VERIFY_MESSAGE)
player = Player(user_id=bind.game_account, trust=True)
await (message + UniMessage.image(raw=await make_query_image_v2(player))).finish()
@alc.assign('TETRIO.query')
async def _(user: NBUser, account: Player, event_session: EventSession, template: Template | None = None):
async with trigger(
session_persist_id=await get_session_persist_id(event_session),
game_platform=GAME_TYPE,
command_type='query',
command_args=[f'--default-template {template}'] if template is not None else [],
):
async with get_session() as session:
if template is None:
template = await session.scalar(
select(TETRIOUserConfig.query_template).where(TETRIOUserConfig.id == user.id)
)
await (UniMessage.image(raw=await make_query_image_v2(account))).finish()
N = TypeVar('N', int, float)
def handling_special_value(value: N) -> N | None:
return value if value != -1 else None
async def make_query_image_v2(player: Player) -> bytes:
user: User
user_info: UserInfoSuccess
sprint: SoloSuccessModel
blitz: SoloSuccessModel
zen: ZenSuccessModel
avatar_revision: int | None
banner_revision: int | None
# [todo) 有没有什么办法能让这类型推导成功)
user, user_info, sprint, blitz, zen, avatar_revision, banner_revision = await gather( # type: ignore[assignment]
player.user,
player.get_info(),
player.sprint,
player.blitz,
player.zen,
player.avatar_revision,
player.banner_revision,
)
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
else:
sprint_value = 'N/A'
play_time: str | None
if (game_time := handling_special_value(user_info.data.gametime)) is not None:
if game_time // 3600 > 0:
play_time = f'{game_time//3600:.0f}h {game_time % 3600 // 60:.0f}m {game_time % 60:.0f}s'
elif game_time // 60 > 0:
play_time = f'{game_time//60:.0f}m {game_time % 60:.0f}s'
else:
play_time = f'{game_time:.0f}s'
else:
play_time = game_time
netloc = get_self_netloc()
async with HostPage(
await render(
'v2/tetrio/user/info',
V2TemplateInfo(
user=V2TemplateUser(
id=user.ID,
name=user.name.upper(),
bio=user_info.data.bio,
banner=f'http://{netloc}/host/resource/tetrio/banners/{user.ID}?{urlencode({"revision": banner_revision})}'
if banner_revision is not None and banner_revision != 0
else None,
avatar=f'http://{netloc}/host/resource/tetrio/avatars/{user.ID}?{urlencode({"revision": avatar_revision})}'
if avatar_revision is not None and avatar_revision != 0
else Avatar(
type='identicon',
hash=md5(user.ID.encode()).hexdigest(), # noqa: S324
),
badges=[
Badge(
id=i.id,
description=i.label,
group=i.group,
receive_at=i.ts if isinstance(i.ts, datetime) else None,
)
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,
verified=user_info.data.verified,
playtime=play_time,
join_at=user_info.data.ts,
),
tetra_league=None,
statistic=Statistic(
total=handling_special_value(user_info.data.gamesplayed),
wins=handling_special_value(user_info.data.gameswon),
),
sprint=Sprint(
time=sprint_value,
global_rank=sprint.data.rank,
play_at=sprint.data.record.ts,
)
if sprint.data.record is not None
else None,
blitz=Blitz(
score=blitz.data.record.results.stats.score,
global_rank=blitz.data.rank,
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),
),
),
) as page_hash:
return await screenshot(f'http://{netloc}/host/{page_hash}.html')

View File

@@ -1,4 +1,31 @@
from . import blitz, sprint
from arclet.alconna import Arg, ArgFlag
from nonebot_plugin_alconna import Args, At, Subcommand
from ....utils.typing import Me
from .. import command as base_command
from .. import get_player
command = Subcommand(
'record',
Args(
Arg(
'target',
At | Me,
notice='@想要查询的人 / 自己',
flags=[ArgFlag.HIDDEN, ArgFlag.OPTIONAL],
),
Arg(
'account',
get_player,
notice='TETR.IO 用户名 / ID',
flags=[ArgFlag.HIDDEN, ArgFlag.OPTIONAL],
),
),
)
from . import blitz, sprint # noqa: E402
base_command.add(command)
__all__ = [
'blitz',

View File

@@ -5,12 +5,12 @@ from urllib.parse import urlencode
from nonebot.adapters import Event
from nonebot.matcher import Matcher
from nonebot_plugin_alconna import At
from nonebot_plugin_alconna import At, Option
from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_orm import get_session
from nonebot_plugin_session import EventSession # type: ignore[import-untyped]
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 # type: ignore[import-untyped]
from nonebot_plugin_user import get_user
from ....db import query_bind_info, trigger
from ....utils.exception import RecordNotFoundError
@@ -18,14 +18,23 @@ from ....utils.host import HostPage, get_self_netloc
from ....utils.metrics import get_metrics
from ....utils.render import render
from ....utils.render.schemas.base import Avatar
from ....utils.render.schemas.tetrio.tetrio_record_base import Finesse, Max, Mini, Tspins, User
from ....utils.render.schemas.tetrio.tetrio_record_blitz import Record, Statistic
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.screenshot import screenshot
from ....utils.typing import Me
from ...constant import CANT_VERIFY_MESSAGE
from .. import alc
from ..api.player import Player
from ..constant import GAME_TYPE
from . import command
command.add(Option('--blitz', dest='blitz'))
alc.shortcut(
'(?i:io)(?i:记录|record)(?i:blitz)',
command='tstats TETR.IO record --blitz',
humanized='io记录blitz',
)
@alc.assign('TETRIO.record.blitz')
@@ -81,6 +90,7 @@ async def make_blitz_image(player: Player) -> bytes:
page=await render(
'v2/tetrio/record/blitz',
Record(
type='best',
user=User(
id=user.ID,
name=user.name.upper(),
@@ -93,6 +103,7 @@ async def make_blitz_image(player: Player) -> bytes:
),
replay_id=blitz.data.record.replayid,
rank=blitz.data.rank,
personal_rank=1,
statistic=Statistic(
keys=stats.inputs,
kpp=round(stats.inputs / stats.piecesplaced, 2),

View File

@@ -5,12 +5,12 @@ from urllib.parse import urlencode
from nonebot.adapters import Event
from nonebot.matcher import Matcher
from nonebot_plugin_alconna import At
from nonebot_plugin_alconna import At, Option
from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_orm import get_session
from nonebot_plugin_session import EventSession # type: ignore[import-untyped]
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 # type: ignore[import-untyped]
from nonebot_plugin_user import get_user
from ....db import query_bind_info, trigger
from ....utils.exception import RecordNotFoundError
@@ -18,14 +18,23 @@ from ....utils.host import HostPage, get_self_netloc
from ....utils.metrics import get_metrics
from ....utils.render import render
from ....utils.render.schemas.base import Avatar
from ....utils.render.schemas.tetrio.tetrio_record_base import Finesse, Max, Mini, Tspins, User
from ....utils.render.schemas.tetrio.tetrio_record_sprint import Record, Statistic
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.screenshot import screenshot
from ....utils.typing import Me
from ...constant import CANT_VERIFY_MESSAGE
from .. import alc
from ..api.player import Player
from ..constant import GAME_TYPE
from . import command
command.add(Option('--40l', dest='sprint'))
alc.shortcut(
'(?i:io)(?i:记录|record)(?i:40l)',
command='tstats TETR.IO record --40l',
humanized='io记录40l',
)
@alc.assign('TETRIO.record.sprint')
@@ -82,6 +91,7 @@ async def make_sprint_image(player: Player) -> bytes:
page=await render(
'v2/tetrio/record/40l',
Record(
type='best',
user=User(
id=user.ID,
name=user.name.upper(),
@@ -95,6 +105,7 @@ async def make_sprint_image(player: Player) -> bytes:
time=sprint_value,
replay_id=sprint.data.record.replayid,
rank=sprint.data.rank,
personal_rank=1,
statistic=Statistic(
keys=stats.inputs,
kpp=round(stats.inputs / stats.piecesplaced, 2),

View File

@@ -1,5 +1,5 @@
from arclet.alconna import Arg, ArgFlag, Args, Subcommand
from nonebot_plugin_alconna import At
from arclet.alconna import Arg, ArgFlag
from nonebot_plugin_alconna import Args, At, Subcommand
from ...utils.exception import MessageFormatError
from ...utils.typing import Me

View File

@@ -1,9 +1,9 @@
from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_orm import get_session
from nonebot_plugin_session import EventSession # type: ignore[import-untyped]
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 User # type: ignore[import-untyped]
from nonebot_plugin_userinfo import BotUserInfo, EventUserInfo, UserInfo # type: ignore[import-untyped]
from nonebot_plugin_user import User
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

View File

@@ -3,9 +3,9 @@ from nonebot.matcher import Matcher
from nonebot_plugin_alconna import At
from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_orm import get_session
from nonebot_plugin_session import EventSession # type: ignore[import-untyped]
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 # type: ignore[import-untyped]
from nonebot_plugin_user import get_user
from ...db import query_bind_info, trigger
from ...utils.metrics import get_metrics

View File

@@ -1,5 +1,5 @@
from arclet.alconna import Arg, ArgFlag, Args, Subcommand
from nonebot_plugin_alconna import At
from arclet.alconna import Arg, ArgFlag
from nonebot_plugin_alconna import Args, At, Subcommand
from ...utils.exception import MessageFormatError
from ...utils.typing import Me

View File

@@ -1,9 +1,9 @@
from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_orm import get_session
from nonebot_plugin_session import EventSession # type: ignore[import-untyped]
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 User # type: ignore[import-untyped]
from nonebot_plugin_userinfo import BotUserInfo, EventUserInfo, UserInfo # type: ignore[import-untyped]
from nonebot_plugin_user import User
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

View File

@@ -6,9 +6,6 @@ GAME_TYPE: Literal['TOS'] = 'TOS'
BASE_URL = {
'https://teatube.cn:8888/',
'http://cafuuchino1.studio26f.org:19970',
'http://cafuuchino2.studio26f.org:19970',
'http://cafuuchino3.studio26f.org:19970',
'http://cafuuchino4.studio26f.org:19970',
}
USER_NAME = compile(

View File

@@ -8,10 +8,10 @@ from nonebot.matcher import Matcher
from nonebot_plugin_alconna import At
from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_orm import get_session
from nonebot_plugin_session import EventSession # type: ignore[import-untyped]
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 # type: ignore[import-untyped]
from nonebot_plugin_userinfo import EventUserInfo, UserInfo # type: ignore[import-untyped]
from nonebot_plugin_user import get_user
from nonebot_plugin_userinfo import EventUserInfo, UserInfo
from ...db import query_bind_info, trigger
from ...utils.exception import RequestError

View File

@@ -11,7 +11,7 @@ from nonebot.log import logger
from ..config.config import CACHE_PATH
from .image import img_to_png
from .request import Request
from .templates import templates_dir
from .templates import TEMPLATES_DIR
if TYPE_CHECKING:
from pydantic import IPvAnyAddress
@@ -48,7 +48,7 @@ class HostPage:
def _():
app.mount(
'/host/assets',
StaticFiles(directory=templates_dir / 'assets'),
StaticFiles(directory=TEMPLATES_DIR / 'assets'),
name='assets',
)
logger.success('assets mounted')

View File

@@ -2,7 +2,7 @@ from base64 import b64encode
from io import BytesIO
from typing import Literal, overload
from nonebot_plugin_userinfo import UserInfo # type: ignore[import-untyped]
from nonebot_plugin_userinfo import UserInfo
from PIL import Image

View File

@@ -3,27 +3,30 @@ from typing import Literal, overload
from jinja2 import Environment, FileSystemLoader
from nonebot.compat import PYDANTIC_V2
from ..templates import templates_dir
from ..templates import TEMPLATES_DIR
from .schemas.bind import Bind
from .schemas.tetrio.tetrio_info import Info as TETRIOInfo
from .schemas.tetrio.tetrio_rank import Data as TETRIORankData
from .schemas.tetrio.tetrio_rank_detail import Data as TETRIORankDetailData
from .schemas.tetrio.tetrio_record_blitz import Record as TETRIORecordBlitz
from .schemas.tetrio.tetrio_record_sprint import Record as TETRIORecordSprint
from .schemas.tetrio.tetrio_user_info_v2 import Info as TETRIOUserInfoV2
from .schemas.tetrio.tetrio_user_list_v2 import List as TETRIOUserListV2
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
env = Environment(
loader=FileSystemLoader(templates_dir), autoescape=True, trim_blocks=True, lstrip_blocks=True, enable_async=True
loader=FileSystemLoader(TEMPLATES_DIR), autoescape=True, trim_blocks=True, lstrip_blocks=True, enable_async=True
)
@overload
async def render(render_type: Literal['v1/binding'], data: Bind) -> str: ...
@overload
async def render(render_type: Literal['v1/tetrio/info'], data: TETRIOInfo) -> str: ...
async def render(render_type: Literal['v1/tetrio/info'], data: TETRIOUserInfoV1) -> str: ...
@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: ...
@overload
@@ -37,7 +40,7 @@ async def render(render_type: Literal['v2/tetrio/record/40l'], data: TETRIORecor
@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: TETRIORankData) -> str: ...
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: ...
@@ -46,6 +49,7 @@ async def render(
render_type: Literal[
'v1/binding',
'v1/tetrio/info',
'v1/tetrio/rank',
'v1/top/info',
'v1/tos/info',
'v2/tetrio/user/info',
@@ -56,14 +60,15 @@ async def render(
'v2/tetrio/rank/detail',
],
data: Bind
| TETRIOInfo
| TETRIOUserInfoV1
| TETRIORankDataV1
| TOPInfo
| TOSInfo
| TETRIOUserInfoV2
| TETRIOUserListV2
| TETRIORecordSprint
| TETRIORecordBlitz
| TETRIORankData
| TETRIORankDataV2
| TETRIORankDetailData,
) -> str:
if PYDANTIC_V2:

View File

@@ -2,7 +2,7 @@ from datetime import datetime
from pydantic import BaseModel
from .....games.tetrio.api.typing import ValidRank
from ......games.tetrio.api.typing import ValidRank
class SpecialData(BaseModel):

View File

@@ -0,0 +1,16 @@
from datetime import datetime
from pydantic import BaseModel
from ......games.tetrio.api.typing import ValidRank
class ItemData(BaseModel):
trending: float
require_tr: float
players: int
class Data(BaseModel):
items: dict[ValidRank, ItemData]
updated_at: datetime

View File

@@ -2,7 +2,7 @@ from datetime import datetime
from pydantic import BaseModel
from .....games.tetrio.api.typing import ValidRank
from ......games.tetrio.api.typing import ValidRank
class AverageData(BaseModel):

View File

@@ -1,6 +1,9 @@
from datetime import datetime
from typing import Literal
from pydantic import BaseModel
from ..base import People
from ...base import People
class User(People):
@@ -32,7 +35,7 @@ class Finesse(BaseModel):
accuracy: float
class RecordStatistic(BaseModel):
class Statistic(BaseModel):
keys: int
kpp: float
kps: float
@@ -56,3 +59,17 @@ class RecordStatistic(BaseModel):
all_clear: int
finesse: Finesse
class Record(BaseModel):
type: Literal['best', 'personal_best', 'recent', 'disputed']
user: User
replay_id: str
rank: int | None
personal_rank: int | None
statistic: Statistic
play_at: datetime

View File

@@ -0,0 +1,12 @@
from .base import Record as BaseRecord
from .base import Statistic as BaseStatistic
class Statistic(BaseStatistic):
spp: float
level: int
class Record(BaseRecord):
statistic: Statistic

View File

@@ -0,0 +1,5 @@
from .base import Record as BaseRecord
class Record(BaseRecord):
time: str

View File

@@ -1,22 +0,0 @@
from datetime import datetime
from pydantic import BaseModel
from .tetrio_record_base import RecordStatistic, User
class Statistic(RecordStatistic):
spp: float
level: int
class Record(BaseModel):
user: User
replay_id: str
rank: int | None
statistic: Statistic
play_at: datetime

View File

@@ -1,18 +0,0 @@
from datetime import datetime
from pydantic import BaseModel
from .tetrio_record_base import RecordStatistic as Statistic
from .tetrio_record_base import User
class Record(BaseModel):
user: User
time: str
replay_id: str
rank: int | None
statistic: Statistic
play_at: datetime

View File

@@ -2,7 +2,7 @@ from datetime import datetime
from pydantic import BaseModel
from ....typing import Number
from .....typing import Number
class TetraLeagueHistoryData(BaseModel):

View File

@@ -1,8 +1,8 @@
from pydantic import BaseModel
from .....games.tetrio.api.typing import Rank
from ....typing import Number
from ..base import People, Ranking
from ......games.tetrio.api.typing import Rank
from .....typing import Number
from ...base import People, Ranking
from .base import TetraLeagueHistoryData

View File

@@ -3,10 +3,10 @@ from typing import Literal
from pydantic import BaseModel
from .....games.tetrio.api.schemas.user_records import Zen
from .....games.tetrio.api.typing import Rank
from ....typing import Number
from ..base import Avatar
from ......games.tetrio.api.schemas.user_records import Zen
from ......games.tetrio.api.typing import Rank, ValidRank
from .....typing import Number
from ...base import Avatar
from .base import TetraLeagueHistoryData
@@ -54,20 +54,20 @@ class TetraLeagueStatistic(BaseModel):
class TetraLeague(BaseModel):
rank: Rank
highest_rank: Rank
highest_rank: ValidRank
tr: Number
glicko: Number
rd: Number
glicko: Number | None
rd: Number | None
global_rank: int | None
country_rank: int | None
pps: Number
pps: Number | None
apm: Number
apl: Number
apm: Number | None
apl: Number | None
vs: Number | None
adpl: Number | None
@@ -76,7 +76,7 @@ class TetraLeague(BaseModel):
decaying: bool
history: list[TetraLeagueHistoryData]
history: list[TetraLeagueHistoryData] | None
class Sprint(BaseModel):
@@ -97,4 +97,4 @@ class Info(BaseModel):
statistic: Statistic | None
sprint: Sprint | None
blitz: Blitz | None
zen: Zen
zen: Zen | None

View File

@@ -2,9 +2,9 @@ from datetime import datetime
from pydantic import BaseModel
from .....games.tetrio.api.typing import Rank
from ....typing import Number
from ..base import Avatar
from ......games.tetrio.api.typing import Rank
from .....typing import Number
from ...base import Avatar
class TetraLeague(BaseModel):

View File

@@ -1,186 +1,130 @@
from asyncio.subprocess import PIPE, Process, create_subprocess_exec
from enum import Enum, auto
from hashlib import sha256
from http import HTTPStatus
from pathlib import Path
from shutil import rmtree
from typing import NamedTuple
from time import time_ns
from zipfile import ZipFile
from aiofiles import open
from httpx import AsyncClient
from nonebot import get_driver
from nonebot.log import logger
from nonebot.permission import SUPERUSER
from nonebot_plugin_alconna import Alconna, Args, Option, on_alconna
from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_localstore import get_data_dir # type: ignore[import-untyped]
from nonebot_plugin_localstore import get_cache_file, get_data_dir
from rich.progress import Progress
driver = get_driver()
templates_dir = get_data_dir('nonebot_plugin_tetris_stats') / 'templates'
TEMPLATES_DIR = get_data_dir('nonebot_plugin_tetris_stats') / 'templates'
alc = on_alconna(Alconna('更新模板', Option('--revision', Args['revision', str], alias={'-R'})), permission=SUPERUSER)
logger.level('GIT', no=10, color='<blue>')
async def download_templates(tag: str) -> Path:
logger.info(f'开始下载模板 {tag}')
async with AsyncClient() as client:
if tag == 'latest':
logger.info('目标为 latest, 正在获取最新版本号')
tag = (
(
await client.get(
'https://github.com/A-Minos/tetris-stats-templates/releases/latest', follow_redirects=True
)
)
.url.path.strip('/')
.rsplit('/', 1)[-1]
)
logger.success(f'获取到的最新版本号: {tag}')
path = get_cache_file('nonebot_plugin_tetris_stats', f'dist_{time_ns()}.zip')
with Progress() as progress:
task_id = progress.add_task('[red]Downloading...', total=None)
async with (
client.stream(
'GET',
f'https://github.com/A-Minos/tetris-stats-templates/releases/download/{tag}/dist.zip',
follow_redirects=True,
) as response,
open(path, 'wb') as file,
):
response.raise_for_status()
progress.update(task_id, total=int(response.headers.get('content-length', 0)) or None)
async for chunk in response.aiter_bytes():
await file.write(chunk)
progress.update(task_id, advance=len(chunk))
logger.success('模板下载完成')
return path
class Status(Enum):
OK = auto()
NOT_EXIST = auto()
NOT_INITIALIZATION = auto()
async def unzip_templates(zip_path: Path) -> Path:
logger.info('开始解压模板')
temp_path = TEMPLATES_DIR.parent / f'temp_{time_ns()}'
with ZipFile(zip_path) as zip_file:
zip_file.extractall(temp_path)
zip_path.unlink()
logger.success('模板解压完成')
return temp_path
class Output(NamedTuple):
stdout: list[str]
stderr: list[str]
async def parse_log(proc: Process) -> Output:
stdout, stderr = await proc.communicate()
for i in (out := stdout.decode().splitlines()):
logger.log('GIT', f'stdout: {i}')
# stderr 可能是 None
for i in (err := (stderr or b'').decode().splitlines()):
logger.log('GIT', f'stderr: {i}')
return Output(out, err)
async def check_git() -> None:
try:
await parse_log(await create_subprocess_exec('git', '--version', stdout=PIPE))
except FileNotFoundError as e:
msg = '未找到 git, 请确保 git 已安装并在环境变量中\n安装步骤请参阅: https://git-scm.com/book/zh/v2/%E8%B5%B7%E6%AD%A5-%E5%AE%89%E8%A3%85-Git'
raise RuntimeError(msg) from e
async def check_repo(repo_path: Path) -> Status:
if not repo_path.exists():
return Status.NOT_EXIST
proc = await create_subprocess_exec(
'git', 'rev-parse', '--is-inside-work-tree', stdout=PIPE, stderr=PIPE, cwd=repo_path
)
await parse_log(proc)
if proc.returncode != 0:
return Status.NOT_INITIALIZATION
return Status.OK
async def clone_repo(repo_url: str, repo_path: Path, branch: str | None = None, depth: int | None = 1) -> bool:
args: list[str | Path] = ['git', 'clone', repo_url, repo_path]
if branch is not None:
args.extend(['-b', branch])
if depth is not None:
args.append(f'--depth={depth}')
proc = await create_subprocess_exec(*args, stdout=PIPE, stderr=PIPE)
await parse_log(proc)
return proc.returncode == 0
async def checkout(revision: str, repo_path: Path) -> bool:
proc = await create_subprocess_exec('git', 'checkout', revision, stdout=PIPE, stderr=PIPE, cwd=repo_path)
await parse_log(proc)
return proc.returncode == 0
async def init_templates() -> None:
await check_git()
status = await check_repo(templates_dir)
if status == Status.OK:
return
if status == Status.NOT_EXIST:
logger.info('模板仓库不存在, 正在尝试初始化...')
if status == Status.NOT_INITIALIZATION:
logger.warning('模板仓库状态异常, 尝试重新初始化')
rmtree(templates_dir)
if not await clone_repo(
repo_url='https://github.com/A-Minos/tetris-stats-templates', repo_path=templates_dir, branch='gh-pages'
):
msg = '模板仓库初始化失败'
raise RuntimeError(msg)
logger.success('模板仓库初始化成功')
async def update_templates(repo_path: Path) -> bool:
logger.info('开始更新模板仓库...')
logger.info('拉取最新提交')
proc = await create_subprocess_exec('git', 'fetch', '--all', '--tags', stdout=PIPE, stderr=PIPE, cwd=repo_path)
await parse_log(proc)
if proc.returncode != 0:
logger.error('拉取最新提交失败')
return False
logger.success('拉取最新提交成功')
async def check_hash(hash_file_path: Path) -> bool:
logger.info('开始校验模板哈希值')
for i in hash_file_path.read_text().splitlines():
file_sha256, file_relative_path = i.split(maxsplit=1)
file_path = hash_file_path.parent / file_relative_path
hasher = sha256()
if not file_path.is_file():
logger.error(f'{file_path.name} 不存在或不是文件')
return False
async with open(file_path, 'rb') as file:
while True:
chunk = await file.read(65535)
if not chunk:
break
hasher.update(chunk)
if hasher.hexdigest() != file_sha256:
logger.error(f'{file_path.name} hash 不匹配')
return False
logger.debug(f'{file_path.name} hash 匹配成功')
logger.success('模板哈希值校验成功')
return True
async def check_commit_hash(commit_hash: str, repo_path: Path, branch: str | None = None) -> bool:
output = await parse_log(
proc := await create_subprocess_exec(
'git', 'branch', '--contains', commit_hash, stdout=PIPE, stderr=PIPE, cwd=repo_path
)
)
return (
proc.returncode == 0
and len(output.stdout) > 0
and (branch is None or branch in output.stdout[0] or 'HEAD detached at' in output.stdout[0])
)
async def init_templates(tag: str) -> bool:
logger.info(f'开始初始化模板 {tag}')
temp_path = await unzip_templates(await download_templates(tag))
if not await check_hash(temp_path / 'hash.sha256'):
rmtree(temp_path)
return False
if TEMPLATES_DIR.exists():
logger.info('清除旧模板文件')
rmtree(TEMPLATES_DIR)
temp_path.rename(TEMPLATES_DIR)
logger.info('模板初始化完成')
return True
async def handle_tag(tag: str) -> str | None:
tags = (
await parse_log(await create_subprocess_exec('git', 'tag', stdout=PIPE, stderr=PIPE, cwd=templates_dir))
).stdout
if tag not in tags:
logger.debug(f'{tag} 不为 tag')
return None
logger.info(f'{tag} 为 tag, 正在尝试 checkout 到 tag 对应的 gh-pages commit')
tag_commit_hash = (
(
await parse_log(
await create_subprocess_exec(
'git', 'show-ref', '--tags', tag, stdout=PIPE, stderr=PIPE, cwd=templates_dir
)
)
)
.stdout[0]
.split(maxsplit=1)[0]
)
logger.success(f'tag 的 commit 为 {tag_commit_hash}')
commit_hash = (
await parse_log(
await create_subprocess_exec(
'git',
'log',
'gh-pages',
'--grep',
f'deploy: {tag_commit_hash}',
'--pretty=format:%H',
stdout=PIPE,
stderr=PIPE,
cwd=templates_dir,
)
)
).stdout[0]
logger.info(f'找到疑似的 gh-pages commit {commit_hash}')
if await check_commit_hash(commit_hash, templates_dir, branch='gh-pages'):
logger.success('验证成功')
return commit_hash
logger.error('验证失败')
return None
@alc.handle()
async def _(revision: str):
if not await update_templates(templates_dir):
msg = '模板仓库更新失败'
logger.error(msg)
await UniMessage(msg).finish()
commit_hash = await handle_tag(revision)
if commit_hash is not None:
if await checkout(commit_hash, templates_dir):
msg = f'模板成功 checkout 到 {commit_hash}'
logger.success(msg)
await alc.finish(msg)
else:
logger.error('checkout 失败')
await alc.finish('checkout 失败')
async def check_tag(tag: str) -> bool:
async with AsyncClient() as client:
return (
await client.get(f'https://github.com/A-Minos/tetris-stats-templates/releases/tag/{tag}')
).status_code != HTTPStatus.NOT_FOUND
@driver.on_startup
async def _():
await init_templates()
if (path := (TEMPLATES_DIR / 'hash.sha256')).is_file() and await check_hash(path):
logger.success('模板验证成功')
return
if not await init_templates('latest'):
msg = '模板初始化失败'
raise RuntimeError(msg)
@alc.handle()
async def _(revision: str | None = None):
if revision is not None and not await check_tag(revision):
await alc.finish(f'{revision} 不是模板仓库中的有效标签')
logger.info('开始更新模板')
if await init_templates(revision or 'latest'):
await alc.finish('更新模板成功')
await alc.finish('更新模板失败')

1056
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = 'nonebot-plugin-tetris-stats'
version = '1.4.1'
version = '1.4.4'
description = '一款基于 NoneBot2 的用于查询 Tetris 相关游戏数据的插件'
authors = ['scdhh <wallfjjd@gmail.com>']
readme = 'README.md'
@@ -21,7 +21,6 @@ nonebot-plugin-user = ">=0.2,<0.4"
nonebot-plugin-userinfo = "^0.2.4"
aiocache = "^0.12.2"
aiofiles = ">=23.2.1,<25.0.0"
arclet-alconna = "^1.8.19"
async-lru = "^2.0.4"
httpx = "^0.27.0"
jinja2 = "^3.1.3"
@@ -131,3 +130,4 @@ quote-style = 'single'
[tool.nonebot]
plugins = ['nonebot_plugin_tetris_stats']
# plugins = ['test_datetime']