mirror of
https://github.com/A-Minos/nonebot-plugin-tetris-stats.git
synced 2026-03-05 05:36:54 +08:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9806050e33 | |||
| 461327749f | |||
|
|
208582d313 | ||
|
|
90f655259d | ||
|
|
2ef400ca28 | ||
|
|
616a64bd6a | ||
|
|
bb8943d4c3 | ||
| 5e45db8cf5 | |||
|
|
2020deadac | ||
|
|
ce7bce6e20 | ||
|
|
d4b690f682 | ||
| c4bde71546 | |||
|
|
f56f993c69 | ||
|
|
cfcda6f597 | ||
|
|
96f5d4559d | ||
|
|
23f412b4f4 | ||
|
|
25b0d2bcdc | ||
|
|
a116f9901c | ||
|
|
82befd631e |
2
.github/workflows/Release.yml
vendored
2
.github/workflows/Release.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: astral-sh/setup-uv@v3
|
||||
- uses: astral-sh/setup-uv@v5
|
||||
name: Setup UV
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
4
.github/workflows/Test.yml
vendored
4
.github/workflows/Test.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-suffix: ${{ env.PYTHON_VERSION }}_${{ env.OS }}
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
run: uv run pytest --cov=nonebot_plugin_tetris_stats --cov-report xml
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
env_vars: OS,PYTHON_VERSION
|
||||
|
||||
2
.github/workflows/TypeCheck.yml
vendored
2
.github/workflows/TypeCheck.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: astral-sh/setup-uv@v3
|
||||
- uses: astral-sh/setup-uv@v5
|
||||
name: Setup UV
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
@@ -7,7 +7,7 @@ ci:
|
||||
autoupdate_commit_msg: ':arrow_up: auto update by pre-commit hooks'
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.7.3
|
||||
rev: v0.11.0
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
|
||||
@@ -10,6 +10,7 @@ require_plugins = {
|
||||
'nonebot_plugin_session',
|
||||
'nonebot_plugin_user',
|
||||
'nonebot_plugin_userinfo',
|
||||
'nonebot_plugin_waiter',
|
||||
}
|
||||
|
||||
for i in require_plugins:
|
||||
|
||||
@@ -11,7 +11,7 @@ from nonebot_plugin_orm import AsyncSession, get_session
|
||||
from nonebot_plugin_user import User
|
||||
from sqlalchemy import select
|
||||
|
||||
from ..utils.typing import AllCommandType, BaseCommandType, GameType, TETRIOCommandType
|
||||
from ..utils.typedefs import AllCommandType, BaseCommandType, GameType, TETRIOCommandType
|
||||
from .models import Bind, TriggerHistoricalData
|
||||
|
||||
UTC = timezone.utc
|
||||
@@ -63,6 +63,23 @@ async def create_or_update_bind(
|
||||
return status
|
||||
|
||||
|
||||
async def remove_bind(
|
||||
session: AsyncSession,
|
||||
user: User,
|
||||
game_platform: GameType,
|
||||
) -> bool:
|
||||
bind = await query_bind_info(
|
||||
session=session,
|
||||
user=user,
|
||||
game_platform=game_platform,
|
||||
)
|
||||
if bind is not None:
|
||||
await session.delete(bind)
|
||||
await session.commit()
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
T = TypeVar('T', 'TETRIOHistoricalData', 'TOPHistoricalData', 'TOSHistoricalData')
|
||||
|
||||
lock = Lock()
|
||||
|
||||
@@ -9,7 +9,7 @@ from sqlalchemy import JSON, DateTime, Dialect, String, TypeDecorator
|
||||
from sqlalchemy.orm import Mapped, MappedAsDataclass, mapped_column
|
||||
from typing_extensions import override
|
||||
|
||||
from ..utils.typing import AllCommandType, GameType
|
||||
from ..utils.typedefs import AllCommandType, GameType
|
||||
|
||||
|
||||
class PydanticType(TypeDecorator):
|
||||
|
||||
@@ -44,7 +44,7 @@ async def _(matcher: Matcher, account: MessageFormatError):
|
||||
|
||||
@alc.handle()
|
||||
async def _(matcher: Matcher, matches: AlcMatches):
|
||||
if matches.head_matched and matches.options != {} or matches.main_args == {}:
|
||||
if (matches.head_matched and matches.options != {}) or matches.main_args == {}:
|
||||
await matcher.finish(
|
||||
(f'{matches.error_info!r}\n' if matches.error_info is not None else '')
|
||||
+ f'输入"{matches.header_result} --help"查看帮助'
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import Generic, TypeVar
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ..utils.typing import GameType
|
||||
from ..utils.typedefs import GameType
|
||||
|
||||
T = TypeVar('T', bound=GameType)
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ command = Subcommand(
|
||||
)
|
||||
|
||||
|
||||
from . import bind, config, list, query, rank, record # noqa: E402
|
||||
from . import bind, config, list, query, rank, record, unbind # noqa: A004, E402
|
||||
|
||||
main_command.add(command)
|
||||
|
||||
@@ -35,4 +35,5 @@ __all__ = [
|
||||
'query',
|
||||
'rank',
|
||||
'record',
|
||||
'unbind',
|
||||
]
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from typing import Literal, overload
|
||||
from uuid import UUID
|
||||
|
||||
from nonebot import __version__ as __nonebot_version__
|
||||
from nonebot.compat import type_validate_json
|
||||
from yarl import URL
|
||||
|
||||
from ....utils.exception import RequestError
|
||||
from ....version import __version__
|
||||
from ..constant import BASE_URL
|
||||
from .cache import Cache
|
||||
from .schemas.base import FailedModel
|
||||
@@ -22,7 +24,12 @@ async def by(
|
||||
await get(
|
||||
BASE_URL / f'users/by/{by_type}',
|
||||
parameter,
|
||||
{'X-Session-ID': str(x_session_id)} if x_session_id is not None else None,
|
||||
{
|
||||
'X-Session-ID': str(x_session_id),
|
||||
'User-Agent': f'nonebot-plugin-tetris-stats/{__version__} (Windows NT 10.0; Win64; x64) NoneBot2/{__nonebot_version__}',
|
||||
}
|
||||
if x_session_id is not None
|
||||
else None,
|
||||
),
|
||||
)
|
||||
if isinstance(model, FailedModel):
|
||||
|
||||
@@ -7,7 +7,7 @@ from sqlalchemy.orm import Mapped, MappedAsDataclass, mapped_column
|
||||
|
||||
from ....db.models import PydanticType
|
||||
from .schemas.base import SuccessModel
|
||||
from .typing import Records, Summaries
|
||||
from .typedefs import Records, Summaries
|
||||
|
||||
|
||||
class TETRIOHistoricalData(MappedAsDataclass, Model):
|
||||
|
||||
@@ -27,7 +27,7 @@ from .schemas.summaries.base import User as SummariesUser
|
||||
from .schemas.summaries.league import LeagueSuccessModel
|
||||
from .schemas.user import User
|
||||
from .schemas.user_info import UserInfo, UserInfoSuccess
|
||||
from .typing import Records, Summaries
|
||||
from .typedefs import Records, Summaries
|
||||
|
||||
|
||||
class RecordModeType(str, Enum):
|
||||
@@ -46,7 +46,7 @@ class RecordKey(NamedTuple):
|
||||
record_type: RecordType
|
||||
|
||||
def to_records(self) -> Records:
|
||||
return cast(Records, f'{self.mode_type.value}_{self.record_type.value}')
|
||||
return cast('Records', f'{self.mode_type.value}_{self.record_type.value}')
|
||||
|
||||
|
||||
class Player:
|
||||
@@ -89,7 +89,7 @@ class Player:
|
||||
|
||||
@property
|
||||
def _request_user_parameter(self) -> str:
|
||||
return self.user_id or cast(str, self.user_name).lower()
|
||||
return self.user_id or cast('str', self.user_name).lower()
|
||||
|
||||
@property
|
||||
async def user(self) -> User:
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from ...typing import Prisecter
|
||||
from ...typedefs import Prisecter
|
||||
|
||||
|
||||
class AggregateStats(BaseModel):
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import Any
|
||||
from nonebot.compat import PYDANTIC_V2
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from ...typing import Prisecter
|
||||
from ...typedefs import Prisecter
|
||||
|
||||
|
||||
class Parameter(BaseModel):
|
||||
|
||||
@@ -3,11 +3,11 @@ from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from ...typing import Rank, ValidRank
|
||||
from ...typedefs import Rank, ValidRank
|
||||
from ..base import ArCounts, FailedModel, P, SuccessModel
|
||||
|
||||
|
||||
class League(BaseModel):
|
||||
class BaseLeague(BaseModel):
|
||||
gamesplayed: int
|
||||
gameswon: int
|
||||
tr: float
|
||||
@@ -16,13 +16,22 @@ class League(BaseModel):
|
||||
bestrank: ValidRank
|
||||
glicko: float
|
||||
rd: float
|
||||
apm: float
|
||||
pps: float
|
||||
vs: float
|
||||
decaying: bool
|
||||
|
||||
|
||||
class Entry(BaseModel):
|
||||
class InvalidLeague(BaseLeague):
|
||||
pps: float | None
|
||||
apm: None
|
||||
vs: None
|
||||
|
||||
|
||||
class League(BaseLeague):
|
||||
pps: float
|
||||
apm: float
|
||||
vs: float
|
||||
|
||||
|
||||
class BaseEntry(BaseModel):
|
||||
id: str = Field(..., alias='_id')
|
||||
username: str
|
||||
role: Literal['anon', 'user', 'bot', 'halfmod', 'mod', 'admin', 'sysop']
|
||||
@@ -30,7 +39,6 @@ class Entry(BaseModel):
|
||||
xp: float
|
||||
country: str | None = None
|
||||
supporter: bool | None = None
|
||||
league: League
|
||||
gamesplayed: int
|
||||
gameswon: int
|
||||
gametime: float
|
||||
@@ -39,8 +47,16 @@ class Entry(BaseModel):
|
||||
p: P
|
||||
|
||||
|
||||
class InvalidEntry(BaseEntry):
|
||||
league: InvalidLeague
|
||||
|
||||
|
||||
class Entry(BaseEntry):
|
||||
league: League
|
||||
|
||||
|
||||
class Data(BaseModel):
|
||||
entries: list[Entry]
|
||||
entries: list[Entry | InvalidEntry]
|
||||
|
||||
|
||||
class BySuccessModel(SuccessModel):
|
||||
|
||||
@@ -14,8 +14,8 @@ __all__ = [
|
||||
'SoloSuccessModel',
|
||||
'SummariesModel',
|
||||
'Zen',
|
||||
'ZenSuccessModel',
|
||||
'Zenith',
|
||||
'ZenithEx',
|
||||
'ZenithSuccessModel',
|
||||
'ZenSuccessModel',
|
||||
]
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import Literal
|
||||
from nonebot.compat import PYDANTIC_V2
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from ...typing import Rank, S1Rank, S1ValidRank
|
||||
from ...typedefs import Rank, S1Rank, S1ValidRank
|
||||
from ..base import SuccessModel
|
||||
|
||||
if PYDANTIC_V2:
|
||||
|
||||
@@ -11,7 +11,7 @@ from ...db import trigger
|
||||
from . import alc, command
|
||||
from .constant import GAME_TYPE
|
||||
from .models import TETRIOUserConfig
|
||||
from .typing import Template
|
||||
from .typedefs import Template
|
||||
|
||||
command.add(
|
||||
Subcommand(
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from re import compile
|
||||
from re import compile # noqa: A004
|
||||
from typing import Literal
|
||||
|
||||
from yarl import URL
|
||||
|
||||
from .api.typing import ValidRank
|
||||
from .api.typedefs import ValidRank
|
||||
|
||||
GAME_TYPE: Literal['IO'] = 'IO'
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from . import command
|
||||
from .api.leaderboards import by
|
||||
from .api.schemas.base import P
|
||||
from .api.schemas.leaderboards import Parameter
|
||||
from .api.schemas.leaderboards.by import Entry
|
||||
from .constant import GAME_TYPE
|
||||
|
||||
command.add(
|
||||
@@ -84,6 +85,7 @@ async def _(
|
||||
join_at=None,
|
||||
)
|
||||
for i in league.data.entries
|
||||
if isinstance(i, Entry)
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
@@ -7,8 +7,8 @@ from sqlalchemy.orm import Mapped, MappedAsDataclass, mapped_column, relationshi
|
||||
|
||||
from ...db.models import PydanticType
|
||||
from .api.schemas.leaderboards.by import BySuccessModel, Entry
|
||||
from .api.typing import ValidRank
|
||||
from .typing import Template
|
||||
from .api.typedefs import ValidRank
|
||||
from .typedefs import Template
|
||||
|
||||
|
||||
class TETRIOUserConfig(MappedAsDataclass, Model):
|
||||
|
||||
@@ -16,13 +16,13 @@ from sqlalchemy import select
|
||||
from ....db import query_bind_info, trigger
|
||||
from ....i18n import Lang
|
||||
from ....utils.exception import FallbackError
|
||||
from ....utils.typing import Me
|
||||
from ....utils.typedefs import Me
|
||||
from ... import add_block_handlers, alc
|
||||
from .. import command, get_player
|
||||
from ..api import Player
|
||||
from ..constant import GAME_TYPE
|
||||
from ..models import TETRIOUserConfig
|
||||
from ..typing import Template
|
||||
from ..typedefs import Template
|
||||
from .v1 import make_query_image_v1
|
||||
from .v2 import make_query_image_v2
|
||||
|
||||
|
||||
@@ -43,9 +43,9 @@ async def make_query_image_v2(player: Player) -> bytes:
|
||||
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'
|
||||
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'
|
||||
play_time = f'{game_time // 60:.0f}m {game_time % 60:.0f}s'
|
||||
else:
|
||||
play_time = f'{game_time:.0f}s'
|
||||
else:
|
||||
|
||||
@@ -25,7 +25,7 @@ from ..models import TETRIOLeagueHistorical, TETRIOLeagueStats, TETRIOLeagueStat
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..api.schemas.leaderboards.by import BySuccessModel
|
||||
from ..api.typing import Rank
|
||||
from ..api.typedefs import Rank
|
||||
|
||||
UTC = timezone.utc
|
||||
|
||||
@@ -95,7 +95,7 @@ async def get_tetra_league_data() -> None:
|
||||
|
||||
players: list[Entry] = []
|
||||
for result in results:
|
||||
players.extend(result.data.entries)
|
||||
players.extend([i for i in result.data.entries if isinstance(i, Entry)])
|
||||
players.sort(key=lambda x: x.league.tr, reverse=True)
|
||||
|
||||
rank_player_mapping: defaultdict[Rank, list[Entry]] = defaultdict(list)
|
||||
@@ -146,7 +146,7 @@ async def _() -> None:
|
||||
await get_tetra_league_data()
|
||||
|
||||
|
||||
from . import all, detail # noqa: E402
|
||||
from . import all, detail # noqa: A004, E402
|
||||
|
||||
base_command.add(command)
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ from ....utils.screenshot import screenshot
|
||||
from .. import alc
|
||||
from ..constant import GAME_TYPE
|
||||
from ..models import TETRIOLeagueStats
|
||||
from ..typing import Template
|
||||
from ..typedefs import Template
|
||||
from . import command
|
||||
|
||||
command.add(
|
||||
|
||||
@@ -17,7 +17,7 @@ from ....utils.render import render
|
||||
from ....utils.render.schemas.tetrio.rank.detail import Data, SpecialData
|
||||
from ....utils.screenshot import screenshot
|
||||
from .. import alc
|
||||
from ..api.typing import ValidRank
|
||||
from ..api.typedefs import ValidRank
|
||||
from ..constant import GAME_TYPE
|
||||
from ..models import TETRIOLeagueStats
|
||||
from . import command
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from arclet.alconna import Arg, ArgFlag
|
||||
from nonebot_plugin_alconna import Args, At, Subcommand
|
||||
|
||||
from ....utils.typing import Me
|
||||
from ....utils.typedefs import Me
|
||||
from .. import command as base_command
|
||||
from .. import get_player
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ from ....utils.render.schemas.base import Avatar
|
||||
from ....utils.render.schemas.tetrio.record.base import Finesse, Max, Mini, Tspins, User
|
||||
from ....utils.render.schemas.tetrio.record.blitz import Record, Statistic
|
||||
from ....utils.screenshot import screenshot
|
||||
from ....utils.typing import Me
|
||||
from ....utils.typedefs import Me
|
||||
from .. import alc
|
||||
from ..api.player import Player
|
||||
from ..constant import GAME_TYPE
|
||||
|
||||
@@ -22,7 +22,7 @@ from ....utils.render.schemas.base import Avatar
|
||||
from ....utils.render.schemas.tetrio.record.base import Finesse, Max, Mini, Statistic, Tspins, User
|
||||
from ....utils.render.schemas.tetrio.record.sprint import Record
|
||||
from ....utils.screenshot import screenshot
|
||||
from ....utils.typing import Me
|
||||
from ....utils.typedefs import Me
|
||||
from .. import alc
|
||||
from ..api.player import Player
|
||||
from ..constant import GAME_TYPE
|
||||
|
||||
75
nonebot_plugin_tetris_stats/games/tetrio/unbind.py
Normal file
75
nonebot_plugin_tetris_stats/games/tetrio/unbind.py
Normal file
@@ -0,0 +1,75 @@
|
||||
from hashlib import md5
|
||||
|
||||
from nonebot_plugin_alconna import 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
|
||||
from nonebot_plugin_userinfo import BotUserInfo, UserInfo
|
||||
from nonebot_plugin_waiter import suggest # type: ignore[import-untyped]
|
||||
from yarl import URL
|
||||
|
||||
from ...db import query_bind_info, remove_bind, trigger
|
||||
from ...utils.host import HostPage, get_self_netloc
|
||||
from ...utils.image import get_avatar
|
||||
from ...utils.render import Bind, render
|
||||
from ...utils.render.schemas.base import Avatar, People
|
||||
from ...utils.screenshot import screenshot
|
||||
from . import alc, command
|
||||
from .api import Player
|
||||
from .constant import GAME_TYPE
|
||||
|
||||
command.add(Subcommand('unbind', help_text='解除绑定 TETR.IO 账号'))
|
||||
|
||||
alc.shortcut(
|
||||
'(?i:io)(?i:解除绑定|解绑|unbind)',
|
||||
command='tstats TETR.IO unbind',
|
||||
humanized='io解绑',
|
||||
)
|
||||
|
||||
|
||||
@alc.assign('TETRIO.unbind')
|
||||
async def _(nb_user: User, event_session: EventSession, bot_info: UserInfo = BotUserInfo()): # noqa: B008
|
||||
async with (
|
||||
trigger(
|
||||
session_persist_id=await get_session_persist_id(event_session),
|
||||
game_platform=GAME_TYPE,
|
||||
command_type='unbind',
|
||||
command_args=[],
|
||||
),
|
||||
get_session() as session,
|
||||
):
|
||||
if (bind := await query_bind_info(session=session, user=nb_user, game_platform=GAME_TYPE)) is None:
|
||||
await UniMessage('您还未绑定 TETR.IO 账号').finish()
|
||||
resp = await suggest('您确定要解绑吗?', ['是', '否'])
|
||||
if resp is None or resp.extract_plain_text() == '否':
|
||||
return
|
||||
player = Player(user_id=bind.game_account, trust=True)
|
||||
user = await player.user
|
||||
netloc = get_self_netloc()
|
||||
async with HostPage(
|
||||
await render(
|
||||
'v1/binding',
|
||||
Bind(
|
||||
platform='TETR.IO',
|
||||
status='unlink',
|
||||
user=People(
|
||||
avatar=str(
|
||||
URL(f'http://{netloc}/host/resource/tetrio/avatars/{user.ID}')
|
||||
% {'revision': avatar_revision}
|
||||
)
|
||||
if (avatar_revision := (await player.avatar_revision)) is not None and avatar_revision != 0
|
||||
else Avatar(type='identicon', hash=md5(user.ID.encode()).hexdigest()), # noqa: S324
|
||||
name=user.name.upper(),
|
||||
),
|
||||
bot=People(
|
||||
avatar=await get_avatar(bot_info, 'Data URI', '../../static/logo/logo.svg'),
|
||||
name=bot_info.user_name,
|
||||
),
|
||||
command='io绑定{游戏ID}',
|
||||
),
|
||||
)
|
||||
) as page_hash:
|
||||
await UniMessage.image(raw=await screenshot(f'http://{netloc}/host/{page_hash}.html')).send()
|
||||
await remove_bind(session=session, user=nb_user, game_platform=GAME_TYPE)
|
||||
@@ -2,7 +2,7 @@ from arclet.alconna import Arg, ArgFlag
|
||||
from nonebot_plugin_alconna import Args, At, Subcommand
|
||||
|
||||
from ...utils.exception import MessageFormatError
|
||||
from ...utils.typing import Me
|
||||
from ...utils.typedefs import Me
|
||||
from .. import add_block_handlers, alc, command
|
||||
from .api import Player
|
||||
from .constant import USER_NAME
|
||||
@@ -29,6 +29,10 @@ command.add(
|
||||
),
|
||||
help_text='绑定 TOP 账号',
|
||||
),
|
||||
Subcommand(
|
||||
'unbind',
|
||||
help_text='解除绑定 TOP 账号',
|
||||
),
|
||||
Subcommand(
|
||||
'query',
|
||||
Args(
|
||||
@@ -51,9 +55,22 @@ command.add(
|
||||
)
|
||||
)
|
||||
|
||||
alc.shortcut('(?i:top)(?i:绑定|绑|bind)', {'command': 'tstats TOP bind', 'humanized': 'top绑定'})
|
||||
alc.shortcut('(?i:top)(?i:查询|查|query|stats)', {'command': 'tstats TOP query', 'humanized': 'top查'})
|
||||
alc.shortcut(
|
||||
'(?i:top)(?i:绑定|绑|bind)',
|
||||
command='tstats TOP bind',
|
||||
humanized='top绑定',
|
||||
)
|
||||
alc.shortcut(
|
||||
'(?i:top)(?i:解除绑定|解绑|unbind)',
|
||||
command='tstats TOP unbind',
|
||||
humanized='top解绑',
|
||||
)
|
||||
alc.shortcut(
|
||||
'(?i:top)(?i:查询|查|query|stats)',
|
||||
command='tstats TOP query',
|
||||
humanized='top查',
|
||||
)
|
||||
|
||||
add_block_handlers(alc.assign('TOP.query'))
|
||||
|
||||
from . import bind, query # noqa: E402, F401
|
||||
from . import bind, query, unbind # noqa: E402, F401
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from re import compile
|
||||
from re import compile # noqa: A004
|
||||
from typing import Literal
|
||||
|
||||
from yarl import URL
|
||||
|
||||
@@ -18,7 +18,7 @@ from ...utils.render.schemas.base import People
|
||||
from ...utils.render.schemas.top_info import Data as InfoData
|
||||
from ...utils.render.schemas.top_info import Info
|
||||
from ...utils.screenshot import screenshot
|
||||
from ...utils.typing import Me
|
||||
from ...utils.typedefs import Me
|
||||
from . import alc
|
||||
from .api import Player
|
||||
from .api.schemas.user_profile import Data, UserProfile
|
||||
|
||||
63
nonebot_plugin_tetris_stats/games/top/unbind.py
Normal file
63
nonebot_plugin_tetris_stats/games/top/unbind.py
Normal file
@@ -0,0 +1,63 @@
|
||||
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
|
||||
from nonebot_plugin_userinfo import BotUserInfo, EventUserInfo, UserInfo
|
||||
from nonebot_plugin_waiter import suggest # type: ignore[import-untyped]
|
||||
|
||||
from ...db import query_bind_info, remove_bind, trigger
|
||||
from ...utils.host import HostPage, get_self_netloc
|
||||
from ...utils.image import get_avatar
|
||||
from ...utils.render import Bind, render
|
||||
from ...utils.render.schemas.base import People
|
||||
from ...utils.screenshot import screenshot
|
||||
from . import alc
|
||||
from .api import Player
|
||||
from .constant import GAME_TYPE
|
||||
|
||||
|
||||
@alc.assign('TOP.unbind')
|
||||
async def _(
|
||||
nb_user: User,
|
||||
event_session: EventSession,
|
||||
event_user_info: UserInfo = EventUserInfo(), # noqa: B008
|
||||
bot_info: UserInfo = BotUserInfo(), # noqa: B008
|
||||
):
|
||||
async with (
|
||||
trigger(
|
||||
session_persist_id=await get_session_persist_id(event_session),
|
||||
game_platform=GAME_TYPE,
|
||||
command_type='unbind',
|
||||
command_args=[],
|
||||
),
|
||||
get_session() as session,
|
||||
):
|
||||
if (bind := await query_bind_info(session=session, user=nb_user, game_platform=GAME_TYPE)) is None:
|
||||
await UniMessage('您还未绑定 TOP 账号').finish()
|
||||
resp = await suggest('您确定要解绑吗?', ['是', '否'])
|
||||
if resp is None or resp.extract_plain_text() == '否':
|
||||
return
|
||||
player = Player(user_name=bind.game_account, trust=True)
|
||||
user = await player.user
|
||||
netloc = get_self_netloc()
|
||||
async with HostPage(
|
||||
await render(
|
||||
'v1/binding',
|
||||
Bind(
|
||||
platform='TOP',
|
||||
status='unlink',
|
||||
user=People(
|
||||
avatar=await get_avatar(event_user_info, 'Data URI', None),
|
||||
name=user.user_name,
|
||||
),
|
||||
bot=People(
|
||||
avatar=await get_avatar(bot_info, 'Data URI', '../../static/logo/logo.svg'),
|
||||
name=bot_info.user_name,
|
||||
),
|
||||
command='top绑定{游戏ID}',
|
||||
),
|
||||
)
|
||||
) as page_hash:
|
||||
await UniMessage.image(raw=await screenshot(f'http://{netloc}/host/{page_hash}.html')).send()
|
||||
await remove_bind(session=session, user=nb_user, game_platform=GAME_TYPE)
|
||||
@@ -2,7 +2,7 @@ from arclet.alconna import Arg, ArgFlag
|
||||
from nonebot_plugin_alconna import Args, At, Subcommand
|
||||
|
||||
from ...utils.exception import MessageFormatError
|
||||
from ...utils.typing import Me
|
||||
from ...utils.typedefs import Me
|
||||
from .. import add_block_handlers, alc, command
|
||||
from .api import Player
|
||||
from .constant import USER_NAME
|
||||
@@ -34,6 +34,10 @@ command.add(
|
||||
),
|
||||
help_text='绑定 茶服 账号',
|
||||
),
|
||||
Subcommand(
|
||||
'unbind',
|
||||
help_text='解除绑定 TOS 账号',
|
||||
),
|
||||
Subcommand(
|
||||
'query',
|
||||
Args(
|
||||
@@ -56,9 +60,22 @@ command.add(
|
||||
)
|
||||
)
|
||||
|
||||
alc.shortcut('(?i:tos|茶服)(?i:绑定|绑|bind)', {'command': 'tstats TOS bind', 'humanized': '茶服绑定'})
|
||||
alc.shortcut('(?i:tos|茶服)(?i:查询|查|query|stats)', {'command': 'tstats TOS query', 'humanized': '茶服查'})
|
||||
alc.shortcut(
|
||||
'(?i:tos|茶服)(?i:绑定|绑|bind)',
|
||||
command='tstats TOS bind',
|
||||
humanized='茶服绑定',
|
||||
)
|
||||
alc.shortcut(
|
||||
'(?i:tos|茶服)(?i:解除绑定|解绑|unbind)',
|
||||
command='tstats TOS unbind',
|
||||
humanized='茶服解绑',
|
||||
)
|
||||
alc.shortcut(
|
||||
'(?i:tos|茶服)(?i:查询|查|query|stats)',
|
||||
command='tstats TOS query',
|
||||
humanized='茶服查',
|
||||
)
|
||||
|
||||
add_block_handlers(alc.assign('TOS.query'))
|
||||
|
||||
from . import bind, query # noqa: E402, F401
|
||||
from . import bind, query, unbind # noqa: E402, F401
|
||||
|
||||
@@ -64,7 +64,7 @@ class Player:
|
||||
query = {'teaId': self.teaid}
|
||||
else:
|
||||
path = 'getUsernameInfo'
|
||||
query = {'username': cast(str, self.user_name)}
|
||||
query = {'username': cast('str', self.user_name)}
|
||||
raw_user_info = await request.failover_request(
|
||||
[i / path % query for i in BASE_URL], failover_code=[502], failover_exc=(TimeoutException,)
|
||||
)
|
||||
@@ -91,7 +91,7 @@ class Player:
|
||||
if self._user_profile.get(params) is None:
|
||||
raw_user_profile = await request.failover_request(
|
||||
[
|
||||
i / 'getProfile' % {'id': self.teaid or cast(str, self.user_name), **other_parameter}
|
||||
i / 'getProfile' % {'id': self.teaid or cast('str', self.user_name), **other_parameter}
|
||||
for i in BASE_URL
|
||||
],
|
||||
failover_code=[502],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from re import compile
|
||||
from re import compile # noqa: A004
|
||||
from typing import Literal
|
||||
|
||||
from yarl import URL
|
||||
|
||||
@@ -24,7 +24,7 @@ from ...utils.render.avatar import get_avatar as get_random_avatar
|
||||
from ...utils.render.schemas.base import People, Ranking
|
||||
from ...utils.render.schemas.tos_info import Info, Multiplayer, Radar
|
||||
from ...utils.screenshot import screenshot
|
||||
from ...utils.typing import Me, Number
|
||||
from ...utils.typedefs import Me, Number
|
||||
from . import alc
|
||||
from .api import Player
|
||||
from .api.schemas.user_info import UserInfoSuccess
|
||||
@@ -258,7 +258,7 @@ def make_query_text(user_info: UserInfoSuccess, game_data: GameData | None) -> U
|
||||
if user_data.ranked_games == '0':
|
||||
message += '暂无段位统计数据'
|
||||
else:
|
||||
message += f', 段位分 {round(float(user_data.rating_now),2)}±{round(float(user_data.rd_now),2)} ({round(float(user_data.vol_now),2)}) '
|
||||
message += f', 段位分 {round(float(user_data.rating_now), 2)}±{round(float(user_data.rd_now), 2)} ({round(float(user_data.vol_now), 2)}) '
|
||||
if game_data is None:
|
||||
message += ', 暂无游戏数据'
|
||||
else:
|
||||
@@ -266,7 +266,7 @@ def make_query_text(user_info: UserInfoSuccess, game_data: GameData | None) -> U
|
||||
message += f"\nL'PM: {game_data.metrics.lpm} ( {game_data.metrics.pps} pps )"
|
||||
message += f'\nAPM: {game_data.metrics.apm} ( x{game_data.metrics.apl} )'
|
||||
message += f'\nADPM: {game_data.metrics.adpm} ( x{game_data.metrics.adpl} ) ( {game_data.metrics.vs}vs )'
|
||||
message += f'\n40L: {float(user_data.pb_sprint)/1000:.2f}s' if user_data.pb_sprint != '2147483647' else ''
|
||||
message += f'\n40L: {float(user_data.pb_sprint) / 1000:.2f}s' if user_data.pb_sprint != '2147483647' else ''
|
||||
message += f'\nMarathon: {user_data.pb_marathon}' if user_data.pb_marathon != '0' else ''
|
||||
message += f'\nChallenge: {user_data.pb_challenge}' if user_data.pb_challenge != '0' else ''
|
||||
return UniMessage(message)
|
||||
|
||||
66
nonebot_plugin_tetris_stats/games/tos/unbind.py
Normal file
66
nonebot_plugin_tetris_stats/games/tos/unbind.py
Normal file
@@ -0,0 +1,66 @@
|
||||
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
|
||||
from nonebot_plugin_userinfo import BotUserInfo, EventUserInfo, UserInfo
|
||||
from nonebot_plugin_waiter import suggest # type: ignore[import-untyped]
|
||||
|
||||
from ...db import query_bind_info, remove_bind, trigger
|
||||
from ...utils.host import HostPage, get_self_netloc
|
||||
from ...utils.image import get_avatar
|
||||
from ...utils.render import Bind, render
|
||||
from ...utils.render.avatar import get_avatar as get_random_avatar
|
||||
from ...utils.render.schemas.base import People
|
||||
from ...utils.screenshot import screenshot
|
||||
from . import alc
|
||||
from .api import Player
|
||||
from .constant import GAME_TYPE
|
||||
|
||||
|
||||
@alc.assign('TOP.unbind')
|
||||
async def _(
|
||||
nb_user: User,
|
||||
event_session: EventSession,
|
||||
event_user_info: UserInfo = EventUserInfo(), # noqa: B008
|
||||
bot_info: UserInfo = BotUserInfo(), # noqa: B008
|
||||
):
|
||||
async with (
|
||||
trigger(
|
||||
session_persist_id=await get_session_persist_id(event_session),
|
||||
game_platform=GAME_TYPE,
|
||||
command_type='unbind',
|
||||
command_args=[],
|
||||
),
|
||||
get_session() as session,
|
||||
):
|
||||
if (bind := await query_bind_info(session=session, user=nb_user, game_platform=GAME_TYPE)) is None:
|
||||
await UniMessage('您还未绑定 TOP 账号').finish()
|
||||
resp = await suggest('您确定要解绑吗?', ['是', '否'])
|
||||
if resp is None or resp.extract_plain_text() == '否':
|
||||
return
|
||||
player = Player(user_name=bind.game_account, trust=True)
|
||||
user = await player.user
|
||||
netloc = get_self_netloc()
|
||||
async with HostPage(
|
||||
await render(
|
||||
'v1/binding',
|
||||
Bind(
|
||||
platform='TOS',
|
||||
status='unlink',
|
||||
user=People(
|
||||
avatar=await get_avatar(event_user_info, 'Data URI', None)
|
||||
if event_user_info is not None
|
||||
else get_random_avatar(user.teaid),
|
||||
name=user.name,
|
||||
),
|
||||
bot=People(
|
||||
avatar=await get_avatar(bot_info, 'Data URI', '../../static/logo/logo.svg'),
|
||||
name=bot_info.user_name,
|
||||
),
|
||||
command='茶服绑定{游戏ID}',
|
||||
),
|
||||
)
|
||||
) as page_hash:
|
||||
await UniMessage.image(raw=await screenshot(f'http://{netloc}/host/{page_hash}.html')).send()
|
||||
await remove_bind(session=session, user=nb_user, game_platform=GAME_TYPE)
|
||||
@@ -2,9 +2,9 @@ from functools import cache
|
||||
from hashlib import sha256
|
||||
from ipaddress import IPv4Address, IPv6Address
|
||||
from pathlib import Path as FilePath
|
||||
from typing import TYPE_CHECKING, ClassVar, Literal
|
||||
from typing import TYPE_CHECKING, Annotated, ClassVar, Literal
|
||||
|
||||
from aiofiles import open
|
||||
from aiofiles import open as aopen
|
||||
from fastapi import BackgroundTasks, FastAPI, Path, status
|
||||
from fastapi.responses import FileResponse, HTMLResponse, Response
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
@@ -69,9 +69,9 @@ def _(page_hash: str) -> HTMLResponse:
|
||||
@app.get('/host/resource/tetrio/{resource_type}/{user_id}', status_code=status.HTTP_200_OK)
|
||||
async def _(
|
||||
resource_type: Literal['avatars', 'banners'],
|
||||
user_id: Annotated[str, Path(regex=r'^[a-f0-9]{24}$')],
|
||||
revision: int,
|
||||
background_tasks: BackgroundTasks,
|
||||
user_id: str = Path(regex=r'^[a-f0-9]{24}$'),
|
||||
) -> Response:
|
||||
if not (path := CACHE_PATH / 'tetrio' / resource_type / f'{user_id}_{revision}.png').exists():
|
||||
image = img_to_png(
|
||||
@@ -87,7 +87,7 @@ async def _(
|
||||
|
||||
async def write_cache(path: FilePath, data: bytes) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
async with open(path, 'wb') as file:
|
||||
async with aopen(path, 'wb') as file:
|
||||
await file.write(data)
|
||||
|
||||
|
||||
|
||||
@@ -23,7 +23,9 @@ def limit(limit: timedelta) -> Callable[[Callable[P, Coroutine[Any, Any, T]]], C
|
||||
nonlocal last_call
|
||||
async with lock:
|
||||
if (diff := (time() - last_call)) < limit_seconds:
|
||||
logger.debug(f'func: {func.__name__} trigger limit, wait {(limit_time:=limit_seconds-diff):.3f}s')
|
||||
logger.debug(
|
||||
f'func: {func.__name__} trigger limit, wait {(limit_time := limit_seconds - diff):.3f}s'
|
||||
)
|
||||
await sleep(limit_time)
|
||||
last_call = time()
|
||||
return await func(*args, **kwargs)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import overload
|
||||
|
||||
from .typing import Number
|
||||
from .typedefs import Number
|
||||
|
||||
|
||||
class TetrisMetricsBaseWithPPS:
|
||||
|
||||
@@ -156,7 +156,7 @@ class SkinManager:
|
||||
|
||||
|
||||
class Skin(ABC):
|
||||
def __new__(cls, *args: Any, **kwargs: Any) -> Self: # noqa: ANN401, ARG003
|
||||
def __new__(cls, *args: Any, **kwargs: Any) -> Self: # noqa: ANN401, ARG004
|
||||
instance = super().__new__(cls)
|
||||
SkinManager.register(instance)
|
||||
return instance
|
||||
|
||||
@@ -2,7 +2,7 @@ from typing import Literal
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ...typing import Number
|
||||
from ...typedefs import Number
|
||||
|
||||
|
||||
class Avatar(BaseModel):
|
||||
|
||||
@@ -2,7 +2,7 @@ from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ......games.tetrio.api.typing import ValidRank
|
||||
from ......games.tetrio.api.typedefs import ValidRank
|
||||
|
||||
|
||||
class SpecialData(BaseModel):
|
||||
|
||||
@@ -2,7 +2,7 @@ from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ......games.tetrio.api.typing import ValidRank
|
||||
from ......games.tetrio.api.typedefs import ValidRank
|
||||
|
||||
|
||||
class ItemData(BaseModel):
|
||||
|
||||
@@ -2,7 +2,7 @@ from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ......games.tetrio.api.typing import ValidRank
|
||||
from ......games.tetrio.api.typedefs import ValidRank
|
||||
|
||||
|
||||
class AverageData(BaseModel):
|
||||
|
||||
@@ -2,7 +2,7 @@ from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .....typing import Number
|
||||
from .....typedefs import Number
|
||||
|
||||
|
||||
class TetraLeagueHistoryData(BaseModel):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ......games.tetrio.api.typing import Rank
|
||||
from .....typing import Number
|
||||
from ......games.tetrio.api.typedefs import Rank
|
||||
from .....typedefs import Number
|
||||
from ...base import People, Ranking
|
||||
from .base import TetraLeagueHistoryData
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ from typing import Literal
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ......games.tetrio.api.typing import Rank
|
||||
from .....typing import Number
|
||||
from ......games.tetrio.api.typedefs import Rank
|
||||
from .....typedefs import Number
|
||||
from ...base import Avatar
|
||||
from .base import TetraLeagueHistoryData
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ......games.tetrio.api.typing import Rank
|
||||
from .....typing import Number
|
||||
from ......games.tetrio.api.typedefs import Rank
|
||||
from .....typedefs import Number
|
||||
from ...base import Avatar
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ...typing import Number
|
||||
from ...typedefs import Number
|
||||
from .base import People
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from ...typing import Number
|
||||
from ...typedefs import Number
|
||||
from .base import People, Ranking
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ from collections.abc import Sequence
|
||||
from http import HTTPStatus
|
||||
from typing import Any
|
||||
|
||||
from fake_useragent import UserAgent
|
||||
from httpx import AsyncClient, HTTPError
|
||||
from msgspec import DecodeError, Struct, json
|
||||
from nonebot import get_driver
|
||||
@@ -113,6 +114,8 @@ class Request:
|
||||
def __init__(self, proxy: str | None) -> None:
|
||||
self.proxy = proxy
|
||||
self.anti_cloudflares: dict[str, AntiCloudflare] = {}
|
||||
self.client = AsyncClient(timeout=config.tetris.request_timeout, proxy=self.proxy)
|
||||
self.ua = UserAgent()
|
||||
|
||||
async def request(
|
||||
self,
|
||||
@@ -129,16 +132,20 @@ class Request:
|
||||
else:
|
||||
cookies = None
|
||||
headers = None
|
||||
headers = headers if extra_headers is None else extra_headers if headers is None else headers | extra_headers
|
||||
if headers is None:
|
||||
headers = {}
|
||||
if extra_headers:
|
||||
headers.update(extra_headers)
|
||||
headers.setdefault('User-Agent', self.ua.random)
|
||||
try:
|
||||
async with AsyncClient(cookies=cookies, timeout=config.tetris.request_timeout, proxy=self.proxy) as session:
|
||||
response = await session.get(str(url), headers=headers)
|
||||
if response.status_code != HTTPStatus.OK:
|
||||
msg = f'请求错误 code: {response.status_code} {HTTPStatus(response.status_code).phrase}\n{response.text}'
|
||||
raise RequestError(msg, status_code=response.status_code)
|
||||
if is_json:
|
||||
decoder.decode(response.content)
|
||||
return response.content
|
||||
response = await self.client.get(str(url), cookies=cookies, headers=headers)
|
||||
if response.status_code != HTTPStatus.OK:
|
||||
msg = (
|
||||
f'请求错误 code: {response.status_code} {HTTPStatus(response.status_code).phrase}\n{response.text}'
|
||||
)
|
||||
raise RequestError(msg, status_code=response.status_code)
|
||||
if is_json:
|
||||
decoder.decode(response.content)
|
||||
except HTTPError as e:
|
||||
msg = f'请求错误 \n{e!r}'
|
||||
raise RequestError(msg) from e
|
||||
@@ -146,6 +153,8 @@ class Request:
|
||||
if enable_anti_cloudflare and url.host is not None:
|
||||
return await self.anti_cloudflares.setdefault(url.host, AntiCloudflare(url.host))(str(url), self.proxy)
|
||||
raise
|
||||
else:
|
||||
return response.content
|
||||
|
||||
async def failover_request(
|
||||
self,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from playwright.async_api import BrowserContext, TimeoutError, ViewportSize
|
||||
from playwright.async_api import BrowserContext, TimeoutError, ViewportSize # noqa: A004
|
||||
|
||||
from ..config.config import config
|
||||
from .browser import BrowserManager
|
||||
|
||||
@@ -5,7 +5,7 @@ from shutil import rmtree
|
||||
from time import time_ns
|
||||
from zipfile import ZipFile
|
||||
|
||||
from aiofiles import open
|
||||
from aiofiles import open as aopen
|
||||
from httpx import AsyncClient
|
||||
from nonebot import get_driver
|
||||
from nonebot.log import logger
|
||||
@@ -46,7 +46,7 @@ async def download_templates(tag: str) -> Path:
|
||||
f'https://github.com/A-Minos/tetris-stats-templates/releases/download/{tag}/dist.zip',
|
||||
follow_redirects=True,
|
||||
) as response,
|
||||
open(path, 'wb') as file,
|
||||
aopen(path, 'wb') as file,
|
||||
):
|
||||
response.raise_for_status()
|
||||
progress.update(task_id, total=int(response.headers.get('content-length', 0)) or None)
|
||||
@@ -76,7 +76,7 @@ async def check_hash(hash_file_path: Path) -> bool:
|
||||
if not file_path.is_file():
|
||||
logger.error(f'{file_path.name} 不存在或不是文件')
|
||||
return False
|
||||
async with open(file_path, 'rb') as file:
|
||||
async with aopen(file_path, 'rb') as file:
|
||||
while True:
|
||||
chunk = await file.read(65535)
|
||||
if not chunk:
|
||||
|
||||
@@ -2,7 +2,7 @@ from typing import Literal, TypeAlias
|
||||
|
||||
Number: TypeAlias = float | int
|
||||
GameType: TypeAlias = Literal['IO', 'TOP', 'TOS']
|
||||
BaseCommandType: TypeAlias = Literal['bind', 'query']
|
||||
BaseCommandType: TypeAlias = Literal['bind', 'unbind', 'query']
|
||||
TETRIOCommandType: TypeAlias = BaseCommandType | Literal['rank', 'config', 'list', 'record']
|
||||
AllCommandType: TypeAlias = BaseCommandType | TETRIOCommandType
|
||||
Me: TypeAlias = Literal[
|
||||
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
@@ -5,18 +5,20 @@ settings:
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
devDependencies:
|
||||
prettier:
|
||||
specifier: ^3.3.3
|
||||
version: 3.3.3
|
||||
version: 3.5.3
|
||||
|
||||
packages:
|
||||
prettier@3.3.3:
|
||||
resolution:
|
||||
{ integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew== }
|
||||
engines: { node: '>=14' }
|
||||
|
||||
prettier@3.5.3:
|
||||
resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
|
||||
snapshots:
|
||||
prettier@3.3.3: {}
|
||||
|
||||
prettier@3.5.3: {}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "nonebot-plugin-tetris-stats"
|
||||
version = "1.6.3"
|
||||
version = "1.7.2"
|
||||
description = "一款基于 NoneBot2 的用于查询 Tetris 相关游戏数据的插件"
|
||||
readme = "README.md"
|
||||
authors = [{ name = "shoucandanghehe", email = "wallfjjd@gmail.com" }]
|
||||
@@ -10,6 +10,7 @@ dependencies = [
|
||||
"aiofiles>=24.1.0",
|
||||
"arclet-alconna<2",
|
||||
"async-lru>=2.0.4",
|
||||
"fake-useragent>=2.0.3",
|
||||
"httpx>=0.27.2",
|
||||
"jinja2>=3.1.4",
|
||||
"lxml>=5.3.0",
|
||||
@@ -22,6 +23,7 @@ dependencies = [
|
||||
"nonebot-plugin-session-orm>=0.2.0",
|
||||
"nonebot-plugin-user>=0.4.4",
|
||||
"nonebot-plugin-userinfo>=0.2.6",
|
||||
"nonebot-plugin-waiter>=0.8.0",
|
||||
"nonebot2[fastapi]>=2.3.3",
|
||||
"pandas>=2.2.3",
|
||||
"pillow>=11.0.0",
|
||||
@@ -132,8 +134,6 @@ select = [
|
||||
]
|
||||
ignore = [
|
||||
"E501", # 过长的行由 ruff format 处理, 剩余的都是字符串
|
||||
"ANN101", # 由 type checker 自动推断
|
||||
"ANN102", # 由 type checker 自动推断
|
||||
"ANN202", # 向 NoneBot 注册的函数
|
||||
"TRY003",
|
||||
"COM812", # 强制尾随逗号
|
||||
@@ -158,7 +158,7 @@ defineConstant = { PYDANTIC_V2 = true }
|
||||
typeCheckingMode = "standard"
|
||||
|
||||
[tool.bumpversion]
|
||||
current_version = "1.6.3"
|
||||
current_version = "1.7.2"
|
||||
tag = true
|
||||
sign_tags = true
|
||||
tag_name = "{new_version}"
|
||||
|
||||
19
uv.lock
generated
19
uv.lock
generated
@@ -756,6 +756,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/cd/ff116c050330c4b8c8ebc657f40501460c3e38905fb15993ef21f7d24208/expiringdictx-1.1.0-py3-none-any.whl", hash = "sha256:f5d38ae23b46a8f97da27ce0dd74e669430d43b2efe98232a4a81f10b43cde25", size = 7596 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fake-useragent"
|
||||
version = "2.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d6/a4/f8d204c20e7879c2c1fd1719095673f447a3111282bfe09c0a74a5ed5000/fake_useragent-2.0.3.tar.gz", hash = "sha256:af86a26ef8229efece8fed529b4aeb5b73747d889b60f01cd477b6f301df46e6", size = 194741 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/11/4f/a639b1dbdc557241e702eefb931ba24ba235c84f8fffdca3e272f096c6af/fake_useragent-2.0.3-py3-none-any.whl", hash = "sha256:8bae50abb72c309a5b3ae2f01a0b82426613fd5c4e2a04dca9332399ec44daa1", size = 201110 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.115.3"
|
||||
@@ -1818,13 +1827,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "nonebot-plugin-tetris-stats"
|
||||
version = "1.6.2"
|
||||
version = "1.6.3"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiocache" },
|
||||
{ name = "aiofiles" },
|
||||
{ name = "arclet-alconna" },
|
||||
{ name = "async-lru" },
|
||||
{ name = "fake-useragent" },
|
||||
{ name = "httpx" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "lxml" },
|
||||
@@ -1837,6 +1847,7 @@ dependencies = [
|
||||
{ name = "nonebot-plugin-session-orm" },
|
||||
{ name = "nonebot-plugin-user" },
|
||||
{ name = "nonebot-plugin-userinfo" },
|
||||
{ name = "nonebot-plugin-waiter" },
|
||||
{ name = "nonebot2", extra = ["fastapi"] },
|
||||
{ name = "pandas" },
|
||||
{ name = "pillow" },
|
||||
@@ -1845,7 +1856,7 @@ dependencies = [
|
||||
{ name = "yarl" },
|
||||
]
|
||||
|
||||
[package.dependency-groups]
|
||||
[package.dev-dependencies]
|
||||
debug = [
|
||||
{ name = "matplotlib" },
|
||||
{ name = "memory-profiler" },
|
||||
@@ -1886,6 +1897,7 @@ requires-dist = [
|
||||
{ name = "aiofiles", specifier = ">=24.1.0" },
|
||||
{ name = "arclet-alconna", specifier = "<2" },
|
||||
{ name = "async-lru", specifier = ">=2.0.4" },
|
||||
{ name = "fake-useragent", specifier = ">=2.0.3" },
|
||||
{ name = "httpx", specifier = ">=0.27.2" },
|
||||
{ name = "jinja2", specifier = ">=3.1.4" },
|
||||
{ name = "lxml", specifier = ">=5.3.0" },
|
||||
@@ -1898,6 +1910,7 @@ requires-dist = [
|
||||
{ name = "nonebot-plugin-session-orm", specifier = ">=0.2.0" },
|
||||
{ name = "nonebot-plugin-user", specifier = ">=0.4.4" },
|
||||
{ name = "nonebot-plugin-userinfo", specifier = ">=0.2.6" },
|
||||
{ name = "nonebot-plugin-waiter", specifier = ">=0.8.0" },
|
||||
{ name = "nonebot2", extras = ["fastapi"], specifier = ">=2.3.3" },
|
||||
{ name = "pandas", specifier = ">=2.2.3" },
|
||||
{ name = "pillow", specifier = ">=11.0.0" },
|
||||
@@ -1906,7 +1919,7 @@ requires-dist = [
|
||||
{ name = "yarl", specifier = ">=1.16.0" },
|
||||
]
|
||||
|
||||
[package.metadata.dependency-groups]
|
||||
[package.metadata.requires-dev]
|
||||
debug = [
|
||||
{ name = "matplotlib", specifier = ">=3.9.2" },
|
||||
{ name = "memory-profiler", specifier = ">=0.61.0" },
|
||||
|
||||
Reference in New Issue
Block a user