Compare commits

..

32 Commits

Author SHA1 Message Date
7133cd9384 🔖 1.4.17 2024-08-20 08:58:31 +08:00
pre-commit-ci[bot]
406bc7674e ⬆️ auto update by pre-commit hooks (#406)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.5.7 → v0.6.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.7...v0.6.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-08-19 23:40:59 +00:00
呵呵です
259b38fda5 支持设置代理 (#407)
*  添加依赖 yarl

*  添加依赖 msgspec

*  移除依赖 ujson

* ♻️ 重构 request 使其支持分别设置代理

* ♻️ 重构 resource 接口

* ️ 不再重复获取 Config

* ♻️ 使用 yarl 替换 urllib.parse

* ️ 给 get_self_netloc 加个 cache

*  request 使用 proxy

*  更新模板使用 proxy

* 🐛 修复删除 ujson 依赖后 迁移脚本报错的bug
2024-08-19 23:37:51 +00:00
dependabot[bot]
414345ae5c ⬆️ Bump ruff from 0.6.0 to 0.6.1 (#405)
* ⬆️ Bump ruff from 0.6.0 to 0.6.1

Bumps [ruff](https://github.com/astral-sh/ruff) from 0.6.0 to 0.6.1.
- [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.6.0...0.6.1)

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

Signed-off-by: dependabot[bot] <support@github.com>

* 🚨 auto fix by pre-commit hooks

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-08-19 17:51:20 +00:00
dependabot[bot]
341cbd86cd ⬆️ Bump nonebot-plugin-user from 0.4.1 to 0.4.2 (#404)
* ⬆️ Bump nonebot-plugin-user from 0.4.1 to 0.4.2

Bumps [nonebot-plugin-user](https://github.com/he0119/nonebot-plugin-user) from 0.4.1 to 0.4.2.
- [Release notes](https://github.com/he0119/nonebot-plugin-user/releases)
- [Changelog](https://github.com/he0119/nonebot-plugin-user/blob/main/CHANGELOG.md)
- [Commits](https://github.com/he0119/nonebot-plugin-user/compare/v0.4.1...v0.4.2)

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

Signed-off-by: dependabot[bot] <support@github.com>

* 🚨 auto fix by pre-commit hooks

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-08-19 16:06:57 +00:00
dependabot[bot]
bf7804738e ⬆️ Bump nonebot-adapter-qq from 1.5.0 to 1.5.1 (#403)
* ⬆️ Bump nonebot-adapter-qq from 1.5.0 to 1.5.1

Bumps [nonebot-adapter-qq](https://github.com/nonebot/adapter-qq) from 1.5.0 to 1.5.1.
- [Release notes](https://github.com/nonebot/adapter-qq/releases)
- [Commits](https://github.com/nonebot/adapter-qq/compare/v1.5.0...v1.5.1)

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

Signed-off-by: dependabot[bot] <support@github.com>

* 🚨 auto fix by pre-commit hooks

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-08-19 15:52:37 +00:00
dependabot[bot]
553f373671 ⬆️ Bump nonebot2 from 2.3.2 to 2.3.3 (#402)
* ⬆️ Bump nonebot2 from 2.3.2 to 2.3.3

Bumps [nonebot2](https://github.com/nonebot/nonebot2) from 2.3.2 to 2.3.3.
- [Release notes](https://github.com/nonebot/nonebot2/releases)
- [Changelog](https://github.com/nonebot/nonebot2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nonebot/nonebot2/compare/v2.3.2...v2.3.3)

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

Signed-off-by: dependabot[bot] <support@github.com>

* 🚨 auto fix by pre-commit hooks

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-08-19 15:49:27 +00:00
e53e164a52 🔖 1.4.16 2024-08-18 01:25:43 +08:00
2cd7d89c3e 截图前等待 networkidle
还是得等)
2024-08-18 01:19:03 +08:00
b8b6d5f6c8 🔖 1.4.15 2024-08-17 22:41:32 +08:00
7a44c0dca5 🐛 修 s1 没打的爆炸 2024-08-17 22:40:47 +08:00
4155d8eb42 🔖 1.4.14 2024-08-17 19:50:52 +08:00
4cc942d226 🐛 修 40l 无 hold 爆炸 2024-08-17 19:50:25 +08:00
996dd565d8 🔖 1.4.13 2024-08-17 18:43:11 +08:00
5b0660e45b 🐛 修第一赛季最后没有段位爆炸 2024-08-17 18:41:31 +08:00
8d1ebc06d1 🔖 1.4.12 2024-08-17 05:07:27 +08:00
c57aa48048 🐛 修没打过的爆炸 2024-08-17 05:06:59 +08:00
ad90562fdf 🐛 修国家为空爆炸 2024-08-17 04:45:06 +08:00
cbc96fc09e 🔖 1.4.11 2024-08-17 04:37:18 +08:00
8e10cfe0d0 🐛 修最佳段位为 z 爆炸 2024-08-17 04:31:14 +08:00
d192f0506d 🔖 1.4.10 2024-08-17 04:21:57 +08:00
44aed656b8 🐛 忘记 push schema 2024-08-17 04:21:33 +08:00
feb662b980 🔖 1.4.9 2024-08-17 04:17:57 +08:00
ed6eb9a5cf 💩 迅速的适配第二赛季 2024-08-17 04:17:41 +08:00
25e281a4c5 🎨 localstore 一律从 config 导入常量使用 2024-08-16 18:55:13 +08:00
a2d69b9113 ️ 尝试提高截图性能 2024-08-16 18:53:12 +08:00
c8907a47a4 💥 插件配置现在使用 ScopedConfig 2024-08-16 18:52:47 +08:00
9fb176b4bc 确保同一个账号生成的随机头像一致 2024-08-16 03:42:11 +08:00
dependabot[bot]
53740265b6 ⬆️ Bump ruff from 0.5.7 to 0.6.0 (#401)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.5.7 to 0.6.0.
- [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.7...0.6.0)

---
updated-dependencies:
- dependency-name: ruff
  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-15 19:16:44 +00:00
dependabot[bot]
e6119074ce ⬆️ Bump nonebot-plugin-user from 0.4.0 to 0.4.1 (#400)
Bumps [nonebot-plugin-user](https://github.com/he0119/nonebot-plugin-user) from 0.4.0 to 0.4.1.
- [Release notes](https://github.com/he0119/nonebot-plugin-user/releases)
- [Changelog](https://github.com/he0119/nonebot-plugin-user/blob/main/CHANGELOG.md)
- [Commits](https://github.com/he0119/nonebot-plugin-user/compare/v0.4.0...v0.4.1)

---
updated-dependencies:
- dependency-name: nonebot-plugin-user
  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-16 03:12:41 +08:00
f7a2e89274 🔖 1.4.8 2024-08-15 17:39:59 +08:00
3fe5a19c4a 🐛 修复 top 没有 recent games 时 出现 0 长 list 的 bug 2024-08-15 17:39:28 +08:00
34 changed files with 474 additions and 339 deletions

View File

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

View File

@@ -1,13 +1,27 @@
from pathlib import Path from nonebot import get_plugin_config
from nonebot_plugin_localstore import get_cache_dir, get_data_dir
from pydantic import BaseModel, Field
from nonebot_plugin_localstore import get_cache_dir CACHE_PATH = get_cache_dir('nonebot_plugin_tetris_stats')
from pydantic import BaseModel DATA_PATH = get_data_dir('nonebot_plugin_tetris_stats')
CACHE_PATH: Path = get_cache_dir('nonebot_plugin_tetris_stats')
class Proxy(BaseModel):
main: str | None = None
github: str | None = None
tetrio: str | None = None
tos: str | None = None
top: str | None = None
class ScopedConfig(BaseModel):
request_timeout: float = 30.0
screenshot_quality: float = 2
proxy: Proxy = Field(default_factory=Proxy)
class Config(BaseModel): class Config(BaseModel):
"""配置类""" tetris: ScopedConfig = Field(default_factory=ScopedConfig)
tetris_req_timeout: float = 30.0
tetris_screenshot_quality: float = 2 config = get_plugin_config(Config)

View File

@@ -19,7 +19,6 @@ from sqlalchemy import desc, select
from sqlalchemy.dialects import sqlite from sqlalchemy.dialects import sqlite
from sqlalchemy.ext.automap import automap_base from sqlalchemy.ext.automap import automap_base
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from ujson import dumps, loads
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Sequence from collections.abc import Sequence
@@ -31,6 +30,8 @@ depends_on: str | Sequence[str] | None = None
def migrate_old_data() -> None: def migrate_old_data() -> None:
from json import dumps, loads
Base = automap_base() # noqa: N806 Base = automap_base() # noqa: N806
Base.prepare(autoload_with=op.get_bind()) Base.prepare(autoload_with=op.get_bind())
OldHistoricalData = Base.classes.nonebot_plugin_tetris_stats_historicaldata # noqa: N806 OldHistoricalData = Base.classes.nonebot_plugin_tetris_stats_historicaldata # noqa: N806

View File

@@ -13,7 +13,6 @@ from typing import TYPE_CHECKING
from alembic import op from alembic import op
from sqlalchemy.ext.automap import automap_base from sqlalchemy.ext.automap import automap_base
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from ujson import dumps, loads
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Sequence from collections.abc import Sequence
@@ -27,6 +26,7 @@ depends_on: str | Sequence[str] | None = None
def upgrade(name: str = '') -> None: def upgrade(name: str = '') -> None:
if name: if name:
return return
from json import dumps, loads
Base = automap_base() # noqa: N806 Base = automap_base() # noqa: N806
connection = op.get_bind() connection = op.get_bind()
@@ -50,6 +50,7 @@ def upgrade(name: str = '') -> None:
def downgrade(name: str = '') -> None: def downgrade(name: str = '') -> None:
if name: if name:
return return
from json import dumps, loads
Base = automap_base() # noqa: N806 Base = automap_base() # noqa: N806
connection = op.get_bind() connection = op.get_bind()

View File

@@ -6,25 +6,29 @@ from weakref import WeakValueDictionary
from aiocache import Cache as ACache # type: ignore[import-untyped] from aiocache import Cache as ACache # type: ignore[import-untyped]
from nonebot.compat import type_validate_json from nonebot.compat import type_validate_json
from nonebot.log import logger from nonebot.log import logger
from yarl import URL
from ....config.config import config
from ....utils.request import Request from ....utils.request import Request
from .schemas.base import FailedModel, SuccessModel from .schemas.base import FailedModel, SuccessModel
UTC = timezone.utc UTC = timezone.utc
request = Request(config.tetris.proxy.tetrio or config.tetris.proxy.main)
class Cache: class Cache:
cache = ACache(ACache.MEMORY) cache = ACache(ACache.MEMORY)
task: ClassVar[WeakValueDictionary[str, Lock]] = WeakValueDictionary() task: ClassVar[WeakValueDictionary[URL, Lock]] = WeakValueDictionary()
@classmethod @classmethod
async def get(cls, url: str) -> bytes: async def get(cls, url: URL) -> bytes:
lock = cls.task.setdefault(url, Lock()) lock = cls.task.setdefault(url, Lock())
async with lock: async with lock:
if (cached_data := await cls.cache.get(url)) is not None: if (cached_data := await cls.cache.get(url)) is not None:
logger.debug(f'{url}: Cache hit!') logger.debug(f'{url}: Cache hit!')
return cached_data return cached_data
response_data = await Request.request(url) response_data = await request.request(url)
parsed_data: SuccessModel | FailedModel = type_validate_json(SuccessModel | FailedModel, response_data) # type: ignore[arg-type] parsed_data: SuccessModel | FailedModel = type_validate_json(SuccessModel | FailedModel, response_data) # type: ignore[arg-type]
if isinstance(parsed_data, SuccessModel): if isinstance(parsed_data, SuccessModel):
await cls.cache.add( await cls.cache.add(

View File

@@ -7,7 +7,6 @@ from nonebot.compat import type_validate_json
from ....db import anti_duplicate_add from ....db import anti_duplicate_add
from ....utils.exception import RequestError from ....utils.exception import RequestError
from ....utils.request import splice_url
from ..constant import BASE_URL, USER_ID, USER_NAME from ..constant import BASE_URL, USER_ID, USER_NAME
from .cache import Cache from .cache import Cache
from .models import TETRIOHistoricalData from .models import TETRIOHistoricalData
@@ -24,6 +23,7 @@ from .schemas.summaries import (
SoloSuccessModel as SummariesSoloSuccessModel, SoloSuccessModel as SummariesSoloSuccessModel,
) )
from .schemas.summaries.base import User as SummariesUser from .schemas.summaries.base import User as SummariesUser
from .schemas.summaries.league import LeagueSuccessModel
from .schemas.user import User from .schemas.user import User
from .schemas.user_info import UserInfo, UserInfoSuccess from .schemas.user_info import UserInfo, UserInfoSuccess
from .typing import Records, Summaries from .typing import Records, Summaries
@@ -55,6 +55,7 @@ class Player:
'blitz': SummariesSoloSuccessModel, 'blitz': SummariesSoloSuccessModel,
'zenith': ZenithSuccessModel, 'zenith': ZenithSuccessModel,
'zenithex': ZenithSuccessModel, 'zenithex': ZenithSuccessModel,
'league': LeagueSuccessModel,
'zen': ZenSuccessModel, 'zen': ZenSuccessModel,
'achievements': AchievementsSuccessModel, 'achievements': AchievementsSuccessModel,
} }
@@ -86,12 +87,7 @@ class Player:
@property @property
def _request_user_parameter(self) -> str: def _request_user_parameter(self) -> str:
if self.user_id is not None: return self.user_id or cast(str, self.user_name).lower()
return self.user_id
if self.user_name is not None:
return self.user_name.lower()
msg = 'Invalid user'
raise ValueError(msg)
@property @property
async def user(self) -> User: async def user(self) -> User:
@@ -115,7 +111,7 @@ class Player:
async def get_info(self) -> UserInfoSuccess: async def get_info(self) -> UserInfoSuccess:
"""Get User Info""" """Get User Info"""
if self._user_info is None: if self._user_info is None:
raw_user_info = await Cache.get(splice_url([BASE_URL, 'users/', f'{self._request_user_parameter}'])) raw_user_info = await Cache.get(BASE_URL / 'users' / self._request_user_parameter)
user_info: UserInfo = type_validate_json(UserInfo, raw_user_info) # type: ignore[arg-type] user_info: UserInfo = type_validate_json(UserInfo, raw_user_info) # type: ignore[arg-type]
if isinstance(user_info, FailedModel): if isinstance(user_info, FailedModel):
msg = f'用户信息请求错误:\n{user_info.error}' msg = f'用户信息请求错误:\n{user_info.error}'
@@ -138,12 +134,14 @@ class Player:
@overload @overload
async def get_summaries(self, summaries_type: Literal['zen']) -> ZenSuccessModel: ... async def get_summaries(self, summaries_type: Literal['zen']) -> ZenSuccessModel: ...
@overload @overload
async def get_summaries(self, summaries_type: Literal['league']) -> LeagueSuccessModel: ...
@overload
async def get_summaries(self, summaries_type: Literal['achievements']) -> AchievementsSuccessModel: ... async def get_summaries(self, summaries_type: Literal['achievements']) -> AchievementsSuccessModel: ...
async def get_summaries(self, summaries_type: Summaries) -> SummariesModel: async def get_summaries(self, summaries_type: Summaries) -> SummariesModel:
if summaries_type not in self._summaries: if summaries_type not in self._summaries:
raw_summaries = await Cache.get( raw_summaries = await Cache.get(
splice_url([BASE_URL, 'users/', f'{self._request_user_parameter}/', 'summaries/', summaries_type]) BASE_URL / 'users' / self._request_user_parameter / 'summaries' / summaries_type
) )
summaries: SummariesModel | FailedModel = type_validate_json( summaries: SummariesModel | FailedModel = type_validate_json(
self.__SUMMARIES_MAPPING[summaries_type] | FailedModel, # type: ignore[arg-type] self.__SUMMARIES_MAPPING[summaries_type] | FailedModel, # type: ignore[arg-type]
@@ -164,20 +162,21 @@ class Player:
return self._summaries[summaries_type] return self._summaries[summaries_type]
@property @property
@alru_cache
async def sprint(self) -> SummariesSoloSuccessModel: async def sprint(self) -> SummariesSoloSuccessModel:
return await self.get_summaries('40l') return await self.get_summaries('40l')
@property @property
@alru_cache
async def blitz(self) -> SummariesSoloSuccessModel: async def blitz(self) -> SummariesSoloSuccessModel:
return await self.get_summaries('blitz') return await self.get_summaries('blitz')
@property @property
@alru_cache
async def zen(self) -> ZenSuccessModel: async def zen(self) -> ZenSuccessModel:
return await self.get_summaries('zen') return await self.get_summaries('zen')
@property
async def league(self) -> LeagueSuccessModel:
return await self.get_summaries('league')
async def _get_local_summaries_user(self) -> SummariesUser | None: async def _get_local_summaries_user(self) -> SummariesUser | None:
allow_summaries: set[Literal['40l', 'blitz', 'zenith', 'zenithex']] = { allow_summaries: set[Literal['40l', 'blitz', 'zenith', 'zenithex']] = {
'40l', '40l',
@@ -212,16 +211,7 @@ class Player:
async def get_records(self, mode_type: RecordModeType, records_type: RecordType) -> RecordsSoloSuccessModel: async def get_records(self, mode_type: RecordModeType, records_type: RecordType) -> RecordsSoloSuccessModel:
if (record_key := RecordKey(mode_type, records_type)) not in self._records: if (record_key := RecordKey(mode_type, records_type)) not in self._records:
raw_records = await Cache.get( raw_records = await Cache.get(
splice_url( BASE_URL / 'users' / self._request_user_parameter / 'records' / mode_type / records_type,
[
BASE_URL,
'users/',
f'{self._request_user_parameter}/',
'records/',
f'{mode_type}/',
records_type,
]
)
) )
records: RecordsSoloSuccessModel | FailedModel = type_validate_json(SoloRecord, raw_records) # type: ignore[arg-type] records: RecordsSoloSuccessModel | FailedModel = type_validate_json(SoloRecord, raw_records) # type: ignore[arg-type]
if isinstance(records, FailedModel): if isinstance(records, FailedModel):

View File

@@ -21,7 +21,7 @@ class Stats(BaseModel):
level_lines: int level_lines: int
level_lines_needed: int level_lines_needed: int
inputs: int inputs: int
holds: int holds: int = 0
time: Time | None = None # ?: 不知道是之后都没有了还是还会有 time: Time | None = None # ?: 不知道是之后都没有了还是还会有
score: int score: int
zenlevel: int zenlevel: int

View File

@@ -1,19 +1,21 @@
from .achievements import Achievements, AchievementsSuccessModel from .achievements import Achievements, AchievementsSuccessModel
from .league import LeagueSuccessModel
from .solo import Solo, SoloSuccessModel from .solo import Solo, SoloSuccessModel
from .zen import Zen, ZenSuccessModel from .zen import Zen, ZenSuccessModel
from .zenith import Zenith, ZenithEx, ZenithSuccessModel from .zenith import Zenith, ZenithEx, ZenithSuccessModel
SummariesModel = AchievementsSuccessModel | SoloSuccessModel | ZenSuccessModel | ZenithSuccessModel SummariesModel = AchievementsSuccessModel | SoloSuccessModel | ZenSuccessModel | LeagueSuccessModel | ZenithSuccessModel
__all__ = [ __all__ = [
'Achievements', 'Achievements',
'AchievementsSuccessModel', 'AchievementsSuccessModel',
'LeagueSuccessModel',
'Solo', 'Solo',
'SoloSuccessModel', 'SoloSuccessModel',
'SummariesModel',
'Zen', 'Zen',
'ZenSuccessModel',
'Zenith', 'Zenith',
'ZenithEx', 'ZenithEx',
'ZenithSuccessModel', 'ZenithSuccessModel',
'SummariesModel', 'ZenSuccessModel',
] ]

View File

@@ -7,5 +7,5 @@ class User(BaseModel):
avatar_revision: int | None avatar_revision: int | None
banner_revision: int | None banner_revision: int | None
country: str | None country: str | None
verified: int verified: int | None = None
supporter: int supporter: int

View File

@@ -0,0 +1,102 @@
from typing import Literal
from pydantic import BaseModel, Field
from ...typing import Rank, S1Rank, S1ValidRank
from ..base import SuccessModel
class PastInner(BaseModel):
season: str
username: str
country: str | None = None
placement: int | None = None
gamesplayed: int
gameswon: int
glicko: float
gxe: float
tr: float
rd: float
rank: S1Rank
bestrank: S1ValidRank
ranked: bool
apm: float
pps: float
vs: float
class Past(BaseModel):
first: PastInner | None = Field(default=None, alias='1')
class BaseData(BaseModel):
decaying: bool
past: Past
class NeverPlayedData(BaseData):
gamesplayed: Literal[0]
gameswon: Literal[0]
glicko: Literal[-1]
rd: Literal[-1]
gxe: Literal[-1]
tr: Literal[-1]
rank: Literal['z']
apm: None = None
pps: None = None
vs: None = None
standing: Literal[-1]
standing_local: Literal[-1]
prev_rank: None
prev_at: Literal[-1]
next_rank: None
next_at: Literal[-1]
percentile: Literal[-1]
percentile_rank: Literal['z']
class NeverRatedData(BaseData):
gamesplayed: Literal[1, 2, 3, 4, 5, 6, 7, 8, 9]
gameswon: int
glicko: Literal[-1]
rd: Literal[-1]
gxe: Literal[-1]
tr: Literal[-1]
apm: float
pps: float
vs: float
rank: Literal['z']
standing: Literal[-1]
standing_local: Literal[-1]
prev_rank: None
prev_at: Literal[-1]
next_rank: None
next_at: Literal[-1]
percentile: Literal[-1]
percentile_rank: Literal['z']
class RatedData(BaseData):
gamesplayed: int
gameswon: int
glicko: float
rd: float
gxe: float
tr: float
rank: Rank
bestrank: Rank
standing: int
apm: float
pps: float
vs: float
standing_local: int
prev_rank: Rank | None = None
prev_at: int
next_rank: Rank | None = None
next_at: int
percentile: float
percentile_rank: str
class LeagueSuccessModel(SuccessModel):
data: NeverPlayedData | NeverRatedData | RatedData

View File

@@ -42,7 +42,7 @@ class Data(BaseModel):
badstanding: bool | None = None badstanding: bool | None = None
supporter: bool | None = None # osk说是必有, 但实际上不是 fkosk supporter: bool | None = None # osk说是必有, 但实际上不是 fkosk
supporter_tier: int supporter_tier: int
verified: bool verified: bool | None = None
avatar_revision: int | None = None avatar_revision: int | None = None
"""This user's avatar ID. Get their avatar at """This user's avatar ID. Get their avatar at

View File

@@ -1,27 +1,26 @@
from typing import Literal, NamedTuple, TypedDict, overload from typing import Literal, NamedTuple, overload
from urllib.parse import urlencode
from msgspec import Struct, to_builtins
from nonebot.compat import type_validate_json from nonebot.compat import type_validate_json
from ....utils.exception import RequestError from ....utils.exception import RequestError
from ....utils.request import splice_url
from ..constant import BASE_URL from ..constant import BASE_URL
from .cache import Cache from .cache import Cache
from .schemas.base import FailedModel from .schemas.base import FailedModel
from .schemas.tetra_league import TetraLeague, TetraLeagueSuccess from .schemas.tetra_league import TetraLeague, TetraLeagueSuccess
class Parameter(TypedDict, total=False): class Parameter(Struct, omit_defaults=True):
after: float after: float | None = None
before: float before: float | None = None
limit: int limit: int | None = None
country: str country: str | None = None
async def leaderboard(parameter: Parameter | None = None) -> TetraLeagueSuccess: async def leaderboard(parameter: Parameter | None = None) -> TetraLeagueSuccess:
league: TetraLeague = type_validate_json( league: TetraLeague = type_validate_json(
TetraLeague, # type: ignore[arg-type] TetraLeague, # type: ignore[arg-type]
(await Cache.get(splice_url([BASE_URL, 'users/lists/league', f'?{urlencode(parameter or {})}']))), (await Cache.get(BASE_URL / 'users/lists/league' % to_builtins(parameter))),
) )
if isinstance(league, FailedModel): if isinstance(league, FailedModel):
msg = f'排行榜数据请求错误:\n{league.error}' msg = f'排行榜数据请求错误:\n{league.error}'
@@ -45,8 +44,9 @@ async def full_export(*, with_original: Literal[True]) -> FullExport: ...
async def full_export(*, with_original: bool) -> TetraLeagueSuccess | FullExport: async def full_export(*, with_original: bool) -> TetraLeagueSuccess | FullExport:
full: TetraLeague = type_validate_json( full: TetraLeague = type_validate_json(
TetraLeague, # type: ignore[arg-type] TetraLeague, # type: ignore[arg-type]
(data := await Cache.get(splice_url([BASE_URL, 'users/lists/league/all']))), (data := await Cache.get(BASE_URL / 'users/lists/league/all')),
) )
if isinstance(full, FailedModel): if isinstance(full, FailedModel):
msg = f'排行榜数据请求错误:\n{full.error}' msg = f'排行榜数据请求错误:\n{full.error}'
raise RequestError(msg) raise RequestError(msg)

View File

@@ -1,6 +1,7 @@
from typing import Literal from typing import Literal
ValidRank = Literal[ S1ValidRank = Literal[
'x+',
'x', 'x',
'u', 'u',
'ss', 'ss',
@@ -19,7 +20,9 @@ ValidRank = Literal[
'd+', 'd+',
'd', 'd',
] ]
S1Rank = S1ValidRank | Literal['z']
ValidRank = Literal['x+'] | S1ValidRank
Rank = ValidRank | Literal['z'] # 未定级 Rank = ValidRank | Literal['z'] # 未定级
Summaries = Literal[ Summaries = Literal[
@@ -27,7 +30,7 @@ Summaries = Literal[
'blitz', 'blitz',
'zenith', 'zenith',
'zenithex', 'zenithex',
# 'league', # 等待正式赛季开始 'league',
'zen', 'zen',
'achievements', 'achievements',
] ]

View File

@@ -1,5 +1,4 @@
from hashlib import md5 from hashlib import md5
from urllib.parse import urlencode
from arclet.alconna import Arg, ArgFlag from arclet.alconna import Arg, ArgFlag
from nonebot_plugin_alconna import Args, Subcommand from nonebot_plugin_alconna import Args, Subcommand
@@ -9,6 +8,7 @@ from nonebot_plugin_session import EventSession
from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[import-untyped] from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[import-untyped]
from nonebot_plugin_user import User from nonebot_plugin_user import User
from nonebot_plugin_userinfo import BotUserInfo, UserInfo from nonebot_plugin_userinfo import BotUserInfo, UserInfo
from yarl import URL
from ...db import BindStatus, create_or_update_bind, trigger from ...db import BindStatus, create_or_update_bind, trigger
from ...utils.host import HostPage, get_self_netloc from ...utils.host import HostPage, get_self_netloc
@@ -67,7 +67,10 @@ async def _(nb_user: User, account: Player, event_session: EventSession, bot_inf
platform='TETR.IO', platform='TETR.IO',
status='unknown', status='unknown',
user=People( user=People(
avatar=f'http://{netloc}/host/resource/tetrio/avatars/{user.ID}?{urlencode({"revision": avatar_revision})}' avatar=str(
URL(f'http://{netloc}/host/resource/tetrio/avatars/{user.ID}')
% {'revision': avatar_revision}
)
if (avatar_revision := (await account.avatar_revision)) is not None and avatar_revision != 0 if (avatar_revision := (await account.avatar_revision)) is not None and avatar_revision != 0
else Avatar(type='identicon', hash=md5(user.ID.encode()).hexdigest()), # noqa: S324 else Avatar(type='identicon', hash=md5(user.ID.encode()).hexdigest()), # noqa: S324
name=user.name.upper(), name=user.name.upper(),

View File

@@ -1,11 +1,13 @@
from re import compile from re import compile
from typing import Literal from typing import Literal
from yarl import URL
from .api.typing import ValidRank from .api.typing import ValidRank
GAME_TYPE: Literal['IO'] = 'IO' GAME_TYPE: Literal['IO'] = 'IO'
BASE_URL = 'https://ch.tetr.io/api/' BASE_URL = URL('https://ch.tetr.io/api/')
RANK_PERCENTILE: dict[ValidRank, float] = { RANK_PERCENTILE: dict[ValidRank, float] = {
'x': 1, 'x': 1,

View File

@@ -2,7 +2,6 @@ from asyncio import gather
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from hashlib import md5 from hashlib import md5
from typing import TYPE_CHECKING, TypeVar from typing import TYPE_CHECKING, TypeVar
from urllib.parse import urlencode
from arclet.alconna import Arg, ArgFlag from arclet.alconna import Arg, ArgFlag
from nonebot import get_driver from nonebot import get_driver
@@ -16,12 +15,22 @@ from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[im
from nonebot_plugin_user import User as NBUser from nonebot_plugin_user import User as NBUser
from nonebot_plugin_user import get_user from nonebot_plugin_user import get_user
from sqlalchemy import select from sqlalchemy import select
from yarl import URL
from ...db import query_bind_info, trigger from ...db import query_bind_info, trigger
from ...utils.host import HostPage, get_self_netloc from ...utils.host import HostPage, get_self_netloc
from ...utils.metrics import get_metrics
from ...utils.render import render from ...utils.render import render
from ...utils.render.schemas.base import Avatar from ...utils.render.schemas.base import Avatar
from ...utils.render.schemas.tetrio.user.info_v2 import Badge, Blitz, Sprint, Statistic, Zen from ...utils.render.schemas.tetrio.user.info_v2 import (
Badge,
Blitz,
Sprint,
Statistic,
TetraLeague,
TetraLeagueStatistic,
Zen,
)
from ...utils.render.schemas.tetrio.user.info_v2 import Info as V2TemplateInfo 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.render.schemas.tetrio.user.info_v2 import User as V2TemplateUser
from ...utils.screenshot import screenshot from ...utils.screenshot import screenshot
@@ -30,6 +39,7 @@ from .. import add_block_handlers, alc
from ..constant import CANT_VERIFY_MESSAGE from ..constant import CANT_VERIFY_MESSAGE
from . import command, get_player from . import command, get_player
from .api import Player from .api import Player
from .api.schemas.summaries.league import LeagueSuccessModel, NeverPlayedData, NeverRatedData
from .constant import GAME_TYPE from .constant import GAME_TYPE
from .models import TETRIOUserConfig from .models import TETRIOUserConfig
from .typing import Template from .typing import Template
@@ -146,15 +156,17 @@ def handling_special_value(value: N) -> N | None:
async def make_query_image_v2(player: Player) -> bytes: async def make_query_image_v2(player: Player) -> bytes:
user: User user: User
user_info: UserInfoSuccess user_info: UserInfoSuccess
league: LeagueSuccessModel
sprint: SoloSuccessModel sprint: SoloSuccessModel
blitz: SoloSuccessModel blitz: SoloSuccessModel
zen: ZenSuccessModel zen: ZenSuccessModel
avatar_revision: int | None avatar_revision: int | None
banner_revision: int | None banner_revision: int | None
# TODO)) 有没有什么办法能让这类型推导成功) # TODO)) 有没有什么办法能让这类型推导成功)
user, user_info, sprint, blitz, zen, avatar_revision, banner_revision = await gather( # type: ignore[assignment] user, user_info, league, sprint, blitz, zen, avatar_revision, banner_revision = await gather( # type: ignore[assignment]
player.user, player.user,
player.get_info(), player.get_info(),
player.league,
player.sprint, player.sprint,
player.blitz, player.blitz,
player.zen, player.zen,
@@ -187,10 +199,14 @@ async def make_query_image_v2(player: Player) -> bytes:
id=user.ID, id=user.ID,
name=user.name.upper(), name=user.name.upper(),
bio=user_info.data.bio, bio=user_info.data.bio,
banner=f'http://{netloc}/host/resource/tetrio/banners/{user.ID}?{urlencode({"revision": banner_revision})}' banner=str(
URL(f'http://{netloc}/host/resource/tetrio/banners/{user.ID}') % {'revision': banner_revision}
)
if banner_revision is not None and banner_revision != 0 if banner_revision is not None and banner_revision != 0
else None, else None,
avatar=f'http://{netloc}/host/resource/tetrio/avatars/{user.ID}?{urlencode({"revision": avatar_revision})}' avatar=str(
URL(f'http://{netloc}/host/resource/tetrio/avatars/{user.ID}') % {'revision': avatar_revision}
)
if avatar_revision is not None and avatar_revision != 0 if avatar_revision is not None and avatar_revision != 0
else Avatar( else Avatar(
type='identicon', type='identicon',
@@ -211,11 +227,29 @@ async def make_query_image_v2(player: Player) -> bytes:
friend_count=user_info.data.friend_count, friend_count=user_info.data.friend_count,
supporter_tier=user_info.data.supporter_tier, supporter_tier=user_info.data.supporter_tier,
bad_standing=user_info.data.badstanding or False, bad_standing=user_info.data.badstanding or False,
verified=user_info.data.verified, verified=user_info.data.verified or False,
playtime=play_time, playtime=play_time,
join_at=user_info.data.ts, join_at=user_info.data.ts,
), ),
tetra_league=None, tetra_league=TetraLeague(
rank=league.data.rank,
highest_rank='z' if isinstance(league.data, NeverRatedData) else league.data.bestrank,
tr=round(league.data.tr, 2),
glicko=round(league.data.glicko, 2),
rd=round(league.data.rd, 2),
global_rank=league.data.standing,
country_rank=league.data.standing_local,
pps=(metrics := get_metrics(pps=league.data.pps, apm=league.data.apm, vs=league.data.vs)).pps,
apm=metrics.apm,
apl=metrics.apl,
vs=metrics.vs,
adpl=metrics.adpl,
statistic=TetraLeagueStatistic(total=league.data.gamesplayed, wins=league.data.gameswon),
decaying=league.data.decaying,
history=None,
)
if not isinstance(league.data, NeverPlayedData)
else None,
statistic=Statistic( statistic=Statistic(
total=handling_special_value(user_info.data.gamesplayed), total=handling_special_value(user_info.data.gamesplayed),
wins=handling_special_value(user_info.data.gameswon), wins=handling_special_value(user_info.data.gameswon),

View File

@@ -1,7 +1,6 @@
from asyncio import gather from asyncio import gather
from datetime import timedelta from datetime import timedelta
from hashlib import md5 from hashlib import md5
from urllib.parse import urlencode
from nonebot.adapters import Event from nonebot.adapters import Event
from nonebot.matcher import Matcher from nonebot.matcher import Matcher
@@ -11,6 +10,7 @@ from nonebot_plugin_orm import get_session
from nonebot_plugin_session import EventSession from nonebot_plugin_session import EventSession
from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[import-untyped] from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[import-untyped]
from nonebot_plugin_user import get_user from nonebot_plugin_user import get_user
from yarl import URL
from ....db import query_bind_info, trigger from ....db import query_bind_info, trigger
from ....utils.exception import RecordNotFoundError from ....utils.exception import RecordNotFoundError
@@ -94,7 +94,9 @@ async def make_blitz_image(player: Player) -> bytes:
user=User( user=User(
id=user.ID, id=user.ID,
name=user.name.upper(), name=user.name.upper(),
avatar=f'http://{netloc}/host/resource/tetrio/avatars/{user.ID}?{urlencode({"revision": avatar_revision})}' 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 if (avatar_revision := (await player.avatar_revision)) is not None and avatar_revision != 0
else Avatar( else Avatar(
type='identicon', type='identicon',

View File

@@ -1,7 +1,6 @@
from asyncio import gather from asyncio import gather
from datetime import timedelta from datetime import timedelta
from hashlib import md5 from hashlib import md5
from urllib.parse import urlencode
from nonebot.adapters import Event from nonebot.adapters import Event
from nonebot.matcher import Matcher from nonebot.matcher import Matcher
@@ -11,6 +10,7 @@ from nonebot_plugin_orm import get_session
from nonebot_plugin_session import EventSession from nonebot_plugin_session import EventSession
from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[import-untyped] from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[import-untyped]
from nonebot_plugin_user import get_user from nonebot_plugin_user import get_user
from yarl import URL
from ....db import query_bind_info, trigger from ....db import query_bind_info, trigger
from ....utils.exception import RecordNotFoundError from ....utils.exception import RecordNotFoundError
@@ -95,7 +95,9 @@ async def make_sprint_image(player: Player) -> bytes:
user=User( user=User(
id=user.ID, id=user.ID,
name=user.name.upper(), name=user.name.upper(),
avatar=f'http://{netloc}/host/resource/tetrio/avatars/{user.ID}?{urlencode({"revision": avatar_revision})}' 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 if (avatar_revision := (await player.avatar_revision)) is not None and avatar_revision != 0
else Avatar( else Avatar(
type='identicon', type='identicon',

View File

@@ -1,13 +1,13 @@
from contextlib import suppress from contextlib import suppress
from datetime import datetime, timezone from datetime import datetime, timezone
from io import StringIO from io import StringIO
from urllib.parse import urlencode
from lxml import etree from lxml import etree
from pandas import read_html from pandas import read_html
from ....config.config import config
from ....db import anti_duplicate_add from ....db import anti_duplicate_add
from ....utils.request import Request, splice_url from ....utils.request import Request
from ..constant import BASE_URL, USER_NAME from ..constant import BASE_URL, USER_NAME
from .models import TOPHistoricalData from .models import TOPHistoricalData
from .schemas.user import User from .schemas.user import User
@@ -15,6 +15,8 @@ from .schemas.user_profile import Data, UserProfile
UTC = timezone.utc UTC = timezone.utc
request = Request(config.tetris.proxy.top or config.tetris.proxy.main)
class Player: class Player:
def __init__(self, *, user_name: str, trust: bool = False) -> None: def __init__(self, *, user_name: str, trust: bool = False) -> None:
@@ -35,8 +37,7 @@ class Player:
async def get_profile(self) -> UserProfile: async def get_profile(self) -> UserProfile:
"""获取用户信息""" """获取用户信息"""
if self._user_profile is None: if self._user_profile is None:
url = splice_url([BASE_URL, 'profile.php', f'?{urlencode({"user":self.user_name})}']) raw_user_profile = await request.request(BASE_URL / 'profile.php' % {'user': self.user_name}, is_json=False)
raw_user_profile = await Request.request(url, is_json=False)
self._user_profile = self._parse_profile(raw_user_profile) self._user_profile = self._parse_profile(raw_user_profile)
await anti_duplicate_add( await anti_duplicate_add(
TOPHistoricalData( TOPHistoricalData(
@@ -68,4 +69,4 @@ class Player:
total: list[Data] = [] total: list[Data] = []
for _, value in dataframe.iterrows(): for _, value in dataframe.iterrows():
total.append(Data(lpm=value['lpm'], apm=value['apm'])) total.append(Data(lpm=value['lpm'], apm=value['apm']))
return UserProfile(user_name=user_name, today=today, total=total) return UserProfile(user_name=user_name, today=today, total=total or None)

View File

@@ -1,8 +1,10 @@
from re import compile from re import compile
from typing import Literal from typing import Literal
from yarl import URL
GAME_TYPE: Literal['TOP'] = 'TOP' GAME_TYPE: Literal['TOP'] = 'TOP'
BASE_URL = 'http://tetrisonline.pl/top/' BASE_URL = URL('http://tetrisonline.pl/top/')
USER_NAME = compile(r'^[a-zA-Z0-9_]{1,16}$') USER_NAME = compile(r'^[a-zA-Z0-9_]{1,16}$')

View File

@@ -78,7 +78,7 @@ async def make_query_image(profile: UserProfile) -> bytes:
await render( await render(
'v1/top/info', 'v1/top/info',
Info( Info(
user=People(avatar=get_avatar(), name=profile.user_name), user=People(avatar=get_avatar(profile.user_name), name=profile.user_name),
today=InfoData(pps=today.pps, lpm=today.lpm, apm=today.apm, apl=today.apl), today=InfoData(pps=today.pps, lpm=today.lpm, apm=today.apm, apl=today.apl),
history=InfoData(pps=history.pps, lpm=history.lpm, apm=history.apm, apl=history.apl), history=InfoData(pps=history.pps, lpm=history.lpm, apm=history.apm, apl=history.apl),
), ),

View File

@@ -1,13 +1,14 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import overload from typing import cast, overload
from urllib.parse import urlencode
from httpx import TimeoutException from httpx import TimeoutException
from nonebot.compat import type_validate_json from nonebot.compat import type_validate_json
from yarl import URL
from ....config.config import config
from ....db import anti_duplicate_add from ....db import anti_duplicate_add
from ....utils.exception import RequestError from ....utils.exception import RequestError
from ....utils.request import Request, splice_url from ....utils.request import Request
from ..constant import BASE_URL, USER_NAME from ..constant import BASE_URL, USER_NAME
from .models import TOSHistoricalData from .models import TOSHistoricalData
from .schemas.user import User from .schemas.user import User
@@ -16,6 +17,8 @@ from .schemas.user_profile import UserProfile
UTC = timezone.utc UTC = timezone.utc
request = Request(config.tetris.proxy.tos or config.tetris.proxy.main)
class Player: class Player:
@overload @overload
@@ -56,29 +59,14 @@ class Player:
async def get_info(self) -> UserInfoSuccess: async def get_info(self) -> UserInfoSuccess:
"""获取用户信息""" """获取用户信息"""
if self._user_info is None: if self._user_info is None:
if self.teaid is not None: path = str(
url = [ URL('getTeaIdInfo') % {'teaId': self.teaid}
splice_url( if self.teaid is not None
[ else URL('getUsernameInfo') % {'username': cast(str, self.user_name)}
i, )
'getTeaIdInfo', raw_user_info = await request.failover_request(
f'?{urlencode({"teaId":self.teaid})}', [i / path for i in BASE_URL], failover_code=[502], failover_exc=(TimeoutException,)
] )
)
for i in BASE_URL
]
else:
url = [
splice_url(
[
i,
'getUsernameInfo',
f'?{urlencode({"username":self.user_name})}',
]
)
for i in BASE_URL
]
raw_user_info = await Request.failover_request(url, failover_code=[502], failover_exc=(TimeoutException,))
user_info: UserInfo = type_validate_json(UserInfo, raw_user_info) # type: ignore[arg-type] user_info: UserInfo = type_validate_json(UserInfo, raw_user_info) # type: ignore[arg-type]
if not isinstance(user_info, UserInfoSuccess): if not isinstance(user_info, UserInfoSuccess):
msg = f'用户信息请求错误:\n{user_info.error}' msg = f'用户信息请求错误:\n{user_info.error}'
@@ -98,17 +86,11 @@ class Player:
"""获取用户数据""" """获取用户数据"""
if other_parameter is None: if other_parameter is None:
other_parameter = {} other_parameter = {}
params = urlencode(dict(sorted(other_parameter.items()))) params = (URL('') % dict(sorted(other_parameter.items()))).human_repr()
if self._user_profile.get(params) is None: if self._user_profile.get(params) is None:
raw_user_profile = await Request.failover_request( raw_user_profile = await request.failover_request(
[ [
splice_url( i / 'getProfile' % {'id': self.teaid or cast(str, self.user_name), **other_parameter}
[
i,
'getProfile',
f'?{urlencode({"id":self.teaid or self.user_name,**other_parameter})}',
]
)
for i in BASE_URL for i in BASE_URL
], ],
failover_code=[502], failover_code=[502],

View File

@@ -1,11 +1,13 @@
from re import compile from re import compile
from typing import Literal from typing import Literal
from yarl import URL
GAME_TYPE: Literal['TOS'] = 'TOS' GAME_TYPE: Literal['TOS'] = 'TOS'
BASE_URL = { BASE_URL = {
'https://teatube.cn:8888/', URL('https://teatube.cn:8888/'),
'http://cafuuchino1.studio26f.org:19970', URL('http://cafuuchino1.studio26f.org:19970'),
} }
USER_NAME = compile( USER_NAME = compile(

View File

@@ -223,7 +223,7 @@ async def make_query_image(user_info: UserInfoSuccess, game_data: GameData, even
user=People( user=People(
avatar=await get_avatar(event_user_info, 'Data URI', None) avatar=await get_avatar(event_user_info, 'Data URI', None)
if event_user_info is not None if event_user_info is not None
else get_random_avatar(), else get_random_avatar(user_info.data.teaid),
name=user_info.data.name, name=user_info.data.name,
), ),
ranking=Ranking(rating=float(user_info.data.ranking), rd=round(float(user_info.data.rd_now), 2)), ranking=Ranking(rating=float(user_info.data.ranking), rd=round(float(user_info.data.rd_now), 2)),

View File

@@ -1,16 +1,20 @@
from functools import cache
from hashlib import sha256 from hashlib import sha256
from ipaddress import IPv4Address, IPv6Address from ipaddress import IPv4Address, IPv6Address
from pathlib import Path as FilePath
from typing import TYPE_CHECKING, ClassVar, Literal from typing import TYPE_CHECKING, ClassVar, Literal
from fastapi import FastAPI, Path, status from aiofiles import open
from fastapi import BackgroundTasks, FastAPI, Path, status
from fastapi.responses import FileResponse, HTMLResponse, Response from fastapi.responses import FileResponse, HTMLResponse, Response
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from nonebot import get_app, get_driver from nonebot import get_app, get_driver
from nonebot.log import logger from nonebot.log import logger
from yarl import URL
from ..config.config import CACHE_PATH from ..config.config import CACHE_PATH
from ..games.tetrio.api.cache import request
from .image import img_to_png from .image import img_to_png
from .request import Request
from .templates import TEMPLATES_DIR from .templates import TEMPLATES_DIR
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -22,6 +26,7 @@ driver = get_driver()
global_config = driver.config global_config = driver.config
BASE_URL = URL('https://tetr.io/user-content/')
if not isinstance(app, FastAPI): if not isinstance(app, FastAPI):
msg = '本插件需要 FastAPI 驱动器才能运行' msg = '本插件需要 FastAPI 驱动器才能运行'
@@ -63,20 +68,30 @@ def _(page_hash: str) -> HTMLResponse:
@app.get('/host/resource/tetrio/{resource_type}/{user_id}', status_code=status.HTTP_200_OK) @app.get('/host/resource/tetrio/{resource_type}/{user_id}', status_code=status.HTTP_200_OK)
async def _( async def _(
resource_type: Literal['avatars', 'banners'], revision: int, user_id: str = Path(regex=r'^[a-f0-9]{24}$') resource_type: Literal['avatars', 'banners'],
revision: int,
background_tasks: BackgroundTasks,
user_id: str = Path(regex=r'^[a-f0-9]{24}$'),
) -> Response: ) -> Response:
if not (path := CACHE_PATH / 'tetrio' / resource_type / f'{user_id}_{revision}.png').exists(): if not (path := CACHE_PATH / 'tetrio' / resource_type / f'{user_id}_{revision}.png').exists():
path.parent.mkdir(parents=True, exist_ok=True) image = img_to_png(
path.write_bytes( await request.request(
img_to_png( BASE_URL / resource_type / f'{user_id}.jpg' % {'rv': revision},
await Request.request( is_json=False,
f'https://tetr.io/user-content/{resource_type}/{user_id}.jpg?rv={revision}', is_json=False
)
) )
) )
background_tasks.add_task(write_cache, path=path, data=image)
return Response(content=image, media_type='image/png')
return FileResponse(path) return FileResponse(path)
async def write_cache(path: FilePath, data: bytes) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
async with open(path, 'wb') as file:
await file.write(data)
@cache
def get_self_netloc() -> str: def get_self_netloc() -> str:
host: IPv4Address | IPv6Address | IPvAnyAddress = global_config.host host: IPv4Address | IPv6Address | IPvAnyAddress = global_config.host
if isinstance(host, IPv4Address): if isinstance(host, IPv4Address):

View File

@@ -1,6 +1,6 @@
from base64 import b64encode from base64 import b64encode
from io import BytesIO from io import BytesIO
from random import choice, randint from random import Random
from PIL import Image from PIL import Image
from PIL.Image import Resampling from PIL.Image import Resampling
@@ -8,12 +8,13 @@ from PIL.Image import Resampling
from .draw import PIECE_MEMBERS, SkinManager from .draw import PIECE_MEMBERS, SkinManager
def get_avatar() -> str: def get_avatar(send: float | str | bytes | bytearray | None = None) -> str:
random = Random(send) # noqa: S311
skin = ( skin = (
SkinManager.get_skin() SkinManager.get_skin(send)
.get_piece(choice(PIECE_MEMBERS)) # noqa: S311 .get_piece(random.choice(PIECE_MEMBERS))
.rotate( .rotate(
randint(-360, 360), # noqa: S311 random.randint(-360, 360),
expand=True, expand=True,
resample=Resampling.BICUBIC, resample=Resampling.BICUBIC,
) )

View File

@@ -1,6 +1,6 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from enum import Enum from enum import Enum
from random import choice from random import Random
from typing import Any, ClassVar from typing import Any, ClassVar
from PIL.Image import Image from PIL.Image import Image
@@ -151,8 +151,8 @@ class SkinManager:
cls.skin.append(skin) cls.skin.append(skin)
@classmethod @classmethod
def get_skin(cls) -> 'Skin': def get_skin(cls, send: float | str | bytes | bytearray | None = None) -> 'Skin':
return choice(cls.skin) # noqa: S311 return Random(send).choice(cls.skin) # noqa: S311
class Skin(ABC): class Skin(ABC):

View File

@@ -90,5 +90,5 @@ class TechSkin(Skin):
@driver.on_startup @driver.on_startup
def _(): def _():
path = Path(__file__).parent / 'skins' path = Path(__file__).parent / 'skins'
for i in path.iterdir(): for i in sorted(path.iterdir()):
TechSkin(i) TechSkin(i)

View File

@@ -3,7 +3,7 @@ from typing import Literal
from pydantic import BaseModel from pydantic import BaseModel
from ......games.tetrio.api.typing import Rank, ValidRank from ......games.tetrio.api.typing import Rank
from .....typing import Number from .....typing import Number
from ...base import Avatar from ...base import Avatar
from .base import TetraLeagueHistoryData from .base import TetraLeagueHistoryData
@@ -53,7 +53,7 @@ class TetraLeagueStatistic(BaseModel):
class TetraLeague(BaseModel): class TetraLeague(BaseModel):
rank: Rank rank: Rank
highest_rank: ValidRank highest_rank: Rank
tr: Number tr: Number

View File

@@ -1,54 +1,79 @@
from collections.abc import Sequence from collections.abc import Sequence
from http import HTTPStatus from http import HTTPStatus
from urllib.parse import urljoin, urlparse from typing import Any
from aiofiles import open
from httpx import AsyncClient, HTTPError from httpx import AsyncClient, HTTPError
from nonebot import get_driver, get_plugin_config from msgspec import DecodeError, Struct, json
from nonebot import get_driver
from nonebot.log import logger from nonebot.log import logger
from playwright.async_api import Response from playwright.async_api import Response
from ujson import JSONDecodeError, dumps, loads from yarl import URL
from ..config.config import CACHE_PATH, Config from ..config.config import CACHE_PATH, config
from .browser import BrowserManager from .browser import BrowserManager
from .exception import RequestError from .exception import RequestError
driver = get_driver() driver = get_driver()
config = get_plugin_config(Config)
@driver.on_startup class CloudflareCache(Struct):
async def _(): headers: dict[str, Any] | None = None
await Request.init_cache() cookies: dict[str, Any] | None = None
await Request.read_cache()
@driver.on_shutdown encoder = json.Encoder()
async def _(): decoder = json.Decoder()
await Request.write_cache()
def splice_url(url_list: list[str]) -> str: class AntiCloudflare:
url = '' cache_decoder = json.Decoder(type=CloudflareCache)
if len(url_list):
url = url_list.pop(0)
for i in url_list:
url = urljoin(url, i)
return url
def __init__(self, domain_suffix: str) -> None:
self.domain_suffix = domain_suffix
self.cache_path = CACHE_PATH / f'{self.domain_suffix}_cloudflare_cache.json'
self._headers: dict | None = None
self._cookies: dict | None = None
self.read_cache()
class Request: def read_cache(self) -> None:
"""网络请求相关类""" """读取缓存文件"""
try:
cache: CloudflareCache = self.cache_decoder.decode(self.cache_path.read_text(encoding='UTF-8'))
self._headers = cache.headers
self._cookies = cache.cookies
except (OSError, DecodeError):
self.cache_path.unlink()
self.write_cache()
_CACHE_FILE = CACHE_PATH / 'cloudflare_cache.json' def write_cache(self) -> None:
_headers: dict | None = None """写入缓存文件"""
_cookies: dict | None = None self.cache_path.write_bytes(json.encode(CloudflareCache(headers=self.headers, cookies=self.cookies)))
@classmethod @property
async def _anti_cloudflare(cls, url: str) -> bytes: def headers(self) -> dict | None:
return self._headers
@headers.setter
def headers(self, value: dict | None) -> None:
self._headers = value
self.write_cache()
@property
def cookies(self) -> dict | None:
return self._cookies
@cookies.setter
def cookies(self, value: dict | None) -> None:
self._cookies = value
self.write_cache()
async def __call__(self, url: str, proxy: str | None = None) -> bytes:
"""用firefox硬穿五秒盾""" """用firefox硬穿五秒盾"""
browser = await BrowserManager.get_browser() browser = await BrowserManager.get_browser()
async with await browser.new_context() as context, await context.new_page() as page: async with (
await browser.new_context(proxy={'server': proxy} if proxy is not None else None) as context,
await context.new_page() as page,
):
response = await page.goto(url) response = await page.goto(url)
attempts = 0 attempts = 0
while attempts < 60: # noqa: PLR2004 while attempts < 60: # noqa: PLR2004
@@ -61,84 +86,68 @@ class Request:
logger.warning('疑似触发了 Cloudflare 的验证码') logger.warning('疑似触发了 Cloudflare 的验证码')
break break
try: try:
loads(text) decoder.decode(text)
except JSONDecodeError: except DecodeError:
await page.wait_for_timeout(1000) await page.wait_for_timeout(1000)
else: else:
if not isinstance(response, Response): if not isinstance(response, Response):
msg = 'api请求失败' msg = 'api请求失败'
raise RequestError(msg) raise RequestError(msg)
cls._headers = await response.request.all_headers() self.headers = await response.request.all_headers()
try: try:
cls._cookies = { self.cookies = {
name: value name: value
for i in await context.cookies() for i in await context.cookies()
if (name := i.get('name')) is not None and (value := i.get('value')) is not None if (name := i.get('name')) is not None and (value := i.get('value')) is not None
} }
except KeyError: except KeyError:
cls._cookies = None self.cookies = None
return await response.body() return await response.body()
msg = '绕过五秒盾失败' msg = '绕过五秒盾失败'
raise RequestError(msg) raise RequestError(msg)
@classmethod
async def init_cache(cls) -> None:
"""初始化缓存文件"""
if not cls._CACHE_FILE.exists():
async with open(file=cls._CACHE_FILE, mode='w', encoding='UTF-8') as file:
await file.write(dumps({'headers': cls._headers, 'cookies': cls._cookies}))
@classmethod class Request:
async def read_cache(cls) -> None: """网络请求相关类"""
"""读取缓存文件"""
try:
async with open(file=cls._CACHE_FILE, mode='r', encoding='UTF-8') as file:
json = loads(await file.read())
except FileNotFoundError:
await cls.init_cache()
except (PermissionError, JSONDecodeError):
cls._CACHE_FILE.unlink()
await cls.init_cache()
else:
cls._headers = json['headers']
cls._cookies = json['cookies']
@classmethod def __init__(self, proxy: str | None) -> None:
async def write_cache(cls) -> None: self.proxy = proxy
"""写入缓存文件""" self.anti_cloudflares: dict[str, AntiCloudflare] = {}
try:
async with open(file=cls._CACHE_FILE, mode='r+', encoding='UTF-8') as file:
await file.write(dumps({'headers': cls._headers, 'cookies': cls._cookies}))
except FileNotFoundError:
await cls.init_cache()
except (PermissionError, JSONDecodeError):
cls._CACHE_FILE.unlink()
await cls.init_cache()
@classmethod async def request(
async def request(cls, url: str, *, is_json: bool = True) -> bytes: self,
url: URL,
*,
is_json: bool = True,
enable_anti_cloudflare: bool = False,
) -> bytes:
"""请求api""" """请求api"""
if (anti_cloudflare := self.anti_cloudflares.get(url.host or '')) is not None:
cookies = anti_cloudflare.cookies
headers = anti_cloudflare.headers
else:
cookies = None
headers = None
try: try:
async with AsyncClient(cookies=cls._cookies, timeout=config.tetris_req_timeout) as session: async with AsyncClient(cookies=cookies, timeout=config.tetris.request_timeout, proxy=self.proxy) as session:
response = await session.get(url, headers=cls._headers) response = await session.get(str(url), headers=headers)
if response.status_code != HTTPStatus.OK: if response.status_code != HTTPStatus.OK:
msg = f'请求错误 code: {response.status_code} {HTTPStatus(response.status_code).phrase}\n{response.text}' msg = f'请求错误 code: {response.status_code} {HTTPStatus(response.status_code).phrase}\n{response.text}'
raise RequestError(msg, status_code=response.status_code) raise RequestError(msg, status_code=response.status_code)
if is_json: if is_json:
loads(response.content) decoder.decode(response.content)
return response.content return response.content
except HTTPError as e: except HTTPError as e:
msg = f'请求错误 \n{e!r}' msg = f'请求错误 \n{e!r}'
raise RequestError(msg) from e raise RequestError(msg) from e
except JSONDecodeError: except DecodeError: # 由于捕获的是 DecodeError 所以一定是 is_json = True
if urlparse(url).netloc.lower().endswith('tetr.io'): if enable_anti_cloudflare and url.host is not None:
return await cls._anti_cloudflare(url) return await self.anti_cloudflares.setdefault(url.host, AntiCloudflare(url.host))(str(url), self.proxy)
raise raise
@classmethod
async def failover_request( async def failover_request(
cls, self,
urls: Sequence[str], urls: Sequence[URL],
*, *,
failover_code: Sequence[int], failover_code: Sequence[int],
failover_exc: tuple[type[BaseException], ...], failover_exc: tuple[type[BaseException], ...],
@@ -148,7 +157,7 @@ class Request:
for i in urls: for i in urls:
logger.debug(f'尝试请求 {i}') logger.debug(f'尝试请求 {i}')
try: try:
return await cls.request(i, is_json=is_json) return await self.request(i, is_json=is_json)
except RequestError as e: except RequestError as e:
if e.status_code in failover_code: # 如果状态码在 failover_code 中, 则继续尝试下一个URL if e.status_code in failover_code: # 如果状态码在 failover_code 中, 则继续尝试下一个URL
error_list.append(e) error_list.append(e)

View File

@@ -1,23 +1,19 @@
from nonebot import get_plugin_config
from playwright.async_api import TimeoutError, ViewportSize from playwright.async_api import TimeoutError, ViewportSize
from ..config.config import Config from ..config.config import config
from .browser import BrowserManager from .browser import BrowserManager
from .retry import retry from .retry import retry
from .time_it import time_it from .time_it import time_it
config = get_plugin_config(Config)
@retry(exception_type=TimeoutError, reply='截图失败, 重试中') @retry(exception_type=TimeoutError, reply='截图失败, 重试中')
@time_it @time_it
async def screenshot(url: str) -> bytes: async def screenshot(url: str) -> bytes:
browser = await BrowserManager.get_browser() browser = await BrowserManager.get_browser()
async with ( async with (
await browser.new_page(device_scale_factor=config.tetris_screenshot_quality) as page, await browser.new_page(device_scale_factor=config.tetris.screenshot_quality) as page,
): ):
await page.goto(url) await page.goto(url)
await page.wait_for_load_state('networkidle')
size: ViewportSize = await page.evaluate(""" size: ViewportSize = await page.evaluate("""
() => { () => {
const element = document.querySelector('#content'); const element = document.querySelector('#content');
@@ -28,4 +24,5 @@ async def screenshot(url: str) -> bytes:
}; };
""") """)
await page.set_viewport_size(size) await page.set_viewport_size(size)
return await page.locator('id=content').screenshot(timeout=5000, type='png') await page.wait_for_load_state('networkidle')
return await page.locator('id=content').screenshot(animations='disabled', timeout=5000, type='png')

View File

@@ -11,19 +11,20 @@ from nonebot import get_driver
from nonebot.log import logger from nonebot.log import logger
from nonebot.permission import SUPERUSER from nonebot.permission import SUPERUSER
from nonebot_plugin_alconna import Alconna, Args, Option, on_alconna from nonebot_plugin_alconna import Alconna, Args, Option, on_alconna
from nonebot_plugin_localstore import get_cache_file, get_data_dir
from rich.progress import Progress from rich.progress import Progress
from ..config.config import CACHE_PATH, DATA_PATH, config
driver = get_driver() driver = get_driver()
TEMPLATES_DIR = get_data_dir('nonebot_plugin_tetris_stats') / 'templates' TEMPLATES_DIR = DATA_PATH / 'templates'
alc = on_alconna(Alconna('更新模板', Option('--revision', Args['revision', str], alias={'-R'})), permission=SUPERUSER) alc = on_alconna(Alconna('更新模板', Option('--revision', Args['revision', str], alias={'-R'})), permission=SUPERUSER)
async def download_templates(tag: str) -> Path: async def download_templates(tag: str) -> Path:
logger.info(f'开始下载模板 {tag}') logger.info(f'开始下载模板 {tag}')
async with AsyncClient() as client: async with AsyncClient(proxy=config.tetris.proxy.github or config.tetris.proxy.main) as client:
if tag == 'latest': if tag == 'latest':
logger.info('目标为 latest, 正在获取最新版本号') logger.info('目标为 latest, 正在获取最新版本号')
tag = ( tag = (
@@ -36,7 +37,7 @@ async def download_templates(tag: str) -> Path:
.rsplit('/', 1)[-1] .rsplit('/', 1)[-1]
) )
logger.success(f'获取到的最新版本号: {tag}') logger.success(f'获取到的最新版本号: {tag}')
path = get_cache_file('nonebot_plugin_tetris_stats', f'dist_{time_ns()}.zip') path = CACHE_PATH / f'dist_{time_ns()}.zip'
with Progress() as progress: with Progress() as progress:
task_id = progress.add_task('[red]Downloading...', total=None) task_id = progress.add_task('[red]Downloading...', total=None)
async with ( async with (
@@ -104,7 +105,7 @@ async def init_templates(tag: str) -> bool:
async def check_tag(tag: str) -> bool: async def check_tag(tag: str) -> bool:
async with AsyncClient() as client: async with AsyncClient(proxy=config.tetris.proxy.github or config.tetris.proxy.main) as client:
return ( return (
await client.get(f'https://github.com/A-Minos/tetris-stats-templates/releases/tag/{tag}') await client.get(f'https://github.com/A-Minos/tetris-stats-templates/releases/tag/{tag}')
).status_code != HTTPStatus.NOT_FOUND ).status_code != HTTPStatus.NOT_FOUND

197
poetry.lock generated
View File

@@ -1559,6 +1559,58 @@ files = [
{file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"},
] ]
[[package]]
name = "msgspec"
version = "0.18.6"
description = "A fast serialization and validation library, with builtin support for JSON, MessagePack, YAML, and TOML."
optional = false
python-versions = ">=3.8"
files = [
{file = "msgspec-0.18.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:77f30b0234eceeff0f651119b9821ce80949b4d667ad38f3bfed0d0ebf9d6d8f"},
{file = "msgspec-0.18.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a76b60e501b3932782a9da039bd1cd552b7d8dec54ce38332b87136c64852dd"},
{file = "msgspec-0.18.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06acbd6edf175bee0e36295d6b0302c6de3aaf61246b46f9549ca0041a9d7177"},
{file = "msgspec-0.18.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40a4df891676d9c28a67c2cc39947c33de516335680d1316a89e8f7218660410"},
{file = "msgspec-0.18.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a6896f4cd5b4b7d688018805520769a8446df911eb93b421c6c68155cdf9dd5a"},
{file = "msgspec-0.18.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3ac4dd63fd5309dd42a8c8c36c1563531069152be7819518be0a9d03be9788e4"},
{file = "msgspec-0.18.6-cp310-cp310-win_amd64.whl", hash = "sha256:fda4c357145cf0b760000c4ad597e19b53adf01382b711f281720a10a0fe72b7"},
{file = "msgspec-0.18.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e77e56ffe2701e83a96e35770c6adb655ffc074d530018d1b584a8e635b4f36f"},
{file = "msgspec-0.18.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d5351afb216b743df4b6b147691523697ff3a2fc5f3d54f771e91219f5c23aaa"},
{file = "msgspec-0.18.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3232fabacef86fe8323cecbe99abbc5c02f7698e3f5f2e248e3480b66a3596b"},
{file = "msgspec-0.18.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3b524df6ea9998bbc99ea6ee4d0276a101bcc1aa8d14887bb823914d9f60d07"},
{file = "msgspec-0.18.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:37f67c1d81272131895bb20d388dd8d341390acd0e192a55ab02d4d6468b434c"},
{file = "msgspec-0.18.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d0feb7a03d971c1c0353de1a8fe30bb6579c2dc5ccf29b5f7c7ab01172010492"},
{file = "msgspec-0.18.6-cp311-cp311-win_amd64.whl", hash = "sha256:41cf758d3f40428c235c0f27bc6f322d43063bc32da7b9643e3f805c21ed57b4"},
{file = "msgspec-0.18.6-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d86f5071fe33e19500920333c11e2267a31942d18fed4d9de5bc2fbab267d28c"},
{file = "msgspec-0.18.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce13981bfa06f5eb126a3a5a38b1976bddb49a36e4f46d8e6edecf33ccf11df1"},
{file = "msgspec-0.18.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e97dec6932ad5e3ee1e3c14718638ba333befc45e0661caa57033cd4cc489466"},
{file = "msgspec-0.18.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad237100393f637b297926cae1868b0d500f764ccd2f0623a380e2bcfb2809ca"},
{file = "msgspec-0.18.6-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db1d8626748fa5d29bbd15da58b2d73af25b10aa98abf85aab8028119188ed57"},
{file = "msgspec-0.18.6-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d70cb3d00d9f4de14d0b31d38dfe60c88ae16f3182988246a9861259c6722af6"},
{file = "msgspec-0.18.6-cp312-cp312-win_amd64.whl", hash = "sha256:1003c20bfe9c6114cc16ea5db9c5466e49fae3d7f5e2e59cb70693190ad34da0"},
{file = "msgspec-0.18.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f7d9faed6dfff654a9ca7d9b0068456517f63dbc3aa704a527f493b9200b210a"},
{file = "msgspec-0.18.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9da21f804c1a1471f26d32b5d9bc0480450ea77fbb8d9db431463ab64aaac2cf"},
{file = "msgspec-0.18.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46eb2f6b22b0e61c137e65795b97dc515860bf6ec761d8fb65fdb62aa094ba61"},
{file = "msgspec-0.18.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8355b55c80ac3e04885d72db515817d9fbb0def3bab936bba104e99ad22cf46"},
{file = "msgspec-0.18.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9080eb12b8f59e177bd1eb5c21e24dd2ba2fa88a1dbc9a98e05ad7779b54c681"},
{file = "msgspec-0.18.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cc001cf39becf8d2dcd3f413a4797c55009b3a3cdbf78a8bf5a7ca8fdb76032c"},
{file = "msgspec-0.18.6-cp38-cp38-win_amd64.whl", hash = "sha256:fac5834e14ac4da1fca373753e0c4ec9c8069d1fe5f534fa5208453b6065d5be"},
{file = "msgspec-0.18.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:974d3520fcc6b824a6dedbdf2b411df31a73e6e7414301abac62e6b8d03791b4"},
{file = "msgspec-0.18.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fd62e5818731a66aaa8e9b0a1e5543dc979a46278da01e85c3c9a1a4f047ef7e"},
{file = "msgspec-0.18.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7481355a1adcf1f08dedd9311193c674ffb8bf7b79314b4314752b89a2cf7f1c"},
{file = "msgspec-0.18.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6aa85198f8f154cf35d6f979998f6dadd3dc46a8a8c714632f53f5d65b315c07"},
{file = "msgspec-0.18.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e24539b25c85c8f0597274f11061c102ad6b0c56af053373ba4629772b407be"},
{file = "msgspec-0.18.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c61ee4d3be03ea9cd089f7c8e36158786cd06e51fbb62529276452bbf2d52ece"},
{file = "msgspec-0.18.6-cp39-cp39-win_amd64.whl", hash = "sha256:b5c390b0b0b7da879520d4ae26044d74aeee5144f83087eb7842ba59c02bc090"},
{file = "msgspec-0.18.6.tar.gz", hash = "sha256:a59fc3b4fcdb972d09138cb516dbde600c99d07c38fd9372a6ef500d2d031b4e"},
]
[package.extras]
dev = ["attrs", "coverage", "furo", "gcovr", "ipython", "msgpack", "mypy", "pre-commit", "pyright", "pytest", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "tomli", "tomli-w"]
doc = ["furo", "ipython", "sphinx", "sphinx-copybutton", "sphinx-design"]
test = ["attrs", "msgpack", "mypy", "pyright", "pytest", "pyyaml", "tomli", "tomli-w"]
toml = ["tomli", "tomli-w"]
yaml = ["pyyaml"]
[[package]] [[package]]
name = "multidict" name = "multidict"
version = "6.0.5" version = "6.0.5"
@@ -1779,13 +1831,13 @@ typing-extensions = ">=4.0.0,<5.0.0"
[[package]] [[package]]
name = "nonebot-adapter-qq" name = "nonebot-adapter-qq"
version = "1.5.0" version = "1.5.1"
description = "QQ adapter for nonebot2" description = "QQ adapter for nonebot2"
optional = false optional = false
python-versions = "<4.0,>=3.9" python-versions = "<4.0,>=3.9"
files = [ files = [
{file = "nonebot_adapter_qq-1.5.0-py3-none-any.whl", hash = "sha256:27e6bcbc733d41102c085c844008aa5f2651a5cb1b9f17a3972bb5548ee0b695"}, {file = "nonebot_adapter_qq-1.5.1-py3-none-any.whl", hash = "sha256:d98a264087e2e92024673cbbefc963804b4a85b680599d9bebc5d3c606c8cd22"},
{file = "nonebot_adapter_qq-1.5.0.tar.gz", hash = "sha256:1f46389389f99b19d1447c6032a34d6a7c0d1876468b63152e2ec68c3a042e29"}, {file = "nonebot_adapter_qq-1.5.1.tar.gz", hash = "sha256:02cd9c6204fa8a711569fd59fd518826fb484a3ad5bcb45868a754091005a6ea"},
] ]
[package.dependencies] [package.dependencies]
@@ -1927,13 +1979,13 @@ nonebot2 = {version = ">=2.2.0,<3.0.0", extras = ["fastapi"]}
[[package]] [[package]]
name = "nonebot-plugin-user" name = "nonebot-plugin-user"
version = "0.4.0" version = "0.4.2"
description = "适用于 Nonebot2 的用户插件" description = "适用于 Nonebot2 的用户插件"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
files = [ files = [
{file = "nonebot_plugin_user-0.4.0-py3-none-any.whl", hash = "sha256:aa152cc6159c4f09940cb809d80688581c2d826cd4aa3ebc0171f6e8b31ba7f5"}, {file = "nonebot_plugin_user-0.4.2-py3-none-any.whl", hash = "sha256:c7312109614cb40e1887e534deb1e81dd213b2304dd34cba738b35580971cc9f"},
{file = "nonebot_plugin_user-0.4.0.tar.gz", hash = "sha256:d75b87b9f4ebc301106ede6da106ea3e352e186899fb03221030476133cd915b"}, {file = "nonebot_plugin_user-0.4.2.tar.gz", hash = "sha256:422062dfa97d8fbd8dbd11ab46b240e86d90fcca3d991886596c3332e4211694"},
] ]
[package.dependencies] [package.dependencies]
@@ -1977,13 +2029,13 @@ nonebot2 = ">=2.3.0"
[[package]] [[package]]
name = "nonebot2" name = "nonebot2"
version = "2.3.2" version = "2.3.3"
description = "An asynchronous python bot framework." description = "An asynchronous python bot framework."
optional = false optional = false
python-versions = "<4.0,>=3.9" python-versions = "<4.0,>=3.9"
files = [ files = [
{file = "nonebot2-2.3.2-py3-none-any.whl", hash = "sha256:c51aa3c1f23d8062ce6d13c8423dcb9a8bf0c44f21687916095f825da79a9a55"}, {file = "nonebot2-2.3.3-py3-none-any.whl", hash = "sha256:5bc8d073091347f29c4a1a2f927c24a8941e5d286c77139376259318b9bbfc68"},
{file = "nonebot2-2.3.2.tar.gz", hash = "sha256:af52e27e03e7fe147f2b642151eec81f264d058efe53b974eb08b5d90177cd14"}, {file = "nonebot2-2.3.3.tar.gz", hash = "sha256:4fa7707de5d708c27cc49493bc78a07fee2ba01f5516835a2ea5fbebb49b9dfa"},
] ]
[package.dependencies] [package.dependencies]
@@ -2706,29 +2758,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.5.7" version = "0.6.1"
description = "An extremely fast Python linter and code formatter, written in Rust." description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "ruff-0.5.7-py3-none-linux_armv6l.whl", hash = "sha256:548992d342fc404ee2e15a242cdbea4f8e39a52f2e7752d0e4cbe88d2d2f416a"}, {file = "ruff-0.6.1-py3-none-linux_armv6l.whl", hash = "sha256:b4bb7de6a24169dc023f992718a9417380301b0c2da0fe85919f47264fb8add9"},
{file = "ruff-0.5.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00cc8872331055ee017c4f1071a8a31ca0809ccc0657da1d154a1d2abac5c0be"}, {file = "ruff-0.6.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:45efaae53b360c81043e311cdec8a7696420b3d3e8935202c2846e7a97d4edae"},
{file = "ruff-0.5.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf3d86a1fdac1aec8a3417a63587d93f906c678bb9ed0b796da7b59c1114a1e"}, {file = "ruff-0.6.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:bc60c7d71b732c8fa73cf995efc0c836a2fd8b9810e115be8babb24ae87e0850"},
{file = "ruff-0.5.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a01c34400097b06cf8a6e61b35d6d456d5bd1ae6961542de18ec81eaf33b4cb8"}, {file = "ruff-0.6.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c7477c3b9da822e2db0b4e0b59e61b8a23e87886e727b327e7dcaf06213c5cf"},
{file = "ruff-0.5.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcc8054f1a717e2213500edaddcf1dbb0abad40d98e1bd9d0ad364f75c763eea"}, {file = "ruff-0.6.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a0af7ab3f86e3dc9f157a928e08e26c4b40707d0612b01cd577cc84b8905cc9"},
{file = "ruff-0.5.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f70284e73f36558ef51602254451e50dd6cc479f8b6f8413a95fcb5db4a55fc"}, {file = "ruff-0.6.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:392688dbb50fecf1bf7126731c90c11a9df1c3a4cdc3f481b53e851da5634fa5"},
{file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a78ad870ae3c460394fc95437d43deb5c04b5c29297815a2a1de028903f19692"}, {file = "ruff-0.6.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5278d3e095ccc8c30430bcc9bc550f778790acc211865520f3041910a28d0024"},
{file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ccd078c66a8e419475174bfe60a69adb36ce04f8d4e91b006f1329d5cd44bcf"}, {file = "ruff-0.6.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fe6d5f65d6f276ee7a0fc50a0cecaccb362d30ef98a110f99cac1c7872df2f18"},
{file = "ruff-0.5.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e31c9bad4ebf8fdb77b59cae75814440731060a09a0e0077d559a556453acbb"}, {file = "ruff-0.6.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2e0dd11e2ae553ee5c92a81731d88a9883af8db7408db47fc81887c1f8b672e"},
{file = "ruff-0.5.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d796327eed8e168164346b769dd9a27a70e0298d667b4ecee6877ce8095ec8e"}, {file = "ruff-0.6.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d812615525a34ecfc07fd93f906ef5b93656be01dfae9a819e31caa6cfe758a1"},
{file = "ruff-0.5.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a09ea2c3f7778cc635e7f6edf57d566a8ee8f485f3c4454db7771efb692c499"}, {file = "ruff-0.6.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:faaa4060f4064c3b7aaaa27328080c932fa142786f8142aff095b42b6a2eb631"},
{file = "ruff-0.5.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a36d8dcf55b3a3bc353270d544fb170d75d2dff41eba5df57b4e0b67a95bb64e"}, {file = "ruff-0.6.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:99d7ae0df47c62729d58765c593ea54c2546d5de213f2af2a19442d50a10cec9"},
{file = "ruff-0.5.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9369c218f789eefbd1b8d82a8cf25017b523ac47d96b2f531eba73770971c9e5"}, {file = "ruff-0.6.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9eb18dfd7b613eec000e3738b3f0e4398bf0153cb80bfa3e351b3c1c2f6d7b15"},
{file = "ruff-0.5.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b88ca3db7eb377eb24fb7c82840546fb7acef75af4a74bd36e9ceb37a890257e"}, {file = "ruff-0.6.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c62bc04c6723a81e25e71715aa59489f15034d69bf641df88cb38bdc32fd1dbb"},
{file = "ruff-0.5.7-py3-none-win32.whl", hash = "sha256:33d61fc0e902198a3e55719f4be6b375b28f860b09c281e4bdbf783c0566576a"}, {file = "ruff-0.6.1-py3-none-win32.whl", hash = "sha256:9fb4c4e8b83f19c9477a8745e56d2eeef07a7ff50b68a6998f7d9e2e3887bdc4"},
{file = "ruff-0.5.7-py3-none-win_amd64.whl", hash = "sha256:083bbcbe6fadb93cd86709037acc510f86eed5a314203079df174c40bbbca6b3"}, {file = "ruff-0.6.1-py3-none-win_amd64.whl", hash = "sha256:c2ebfc8f51ef4aca05dad4552bbcf6fe8d1f75b2f6af546cc47cc1c1ca916b5b"},
{file = "ruff-0.5.7-py3-none-win_arm64.whl", hash = "sha256:2dca26154ff9571995107221d0aeaad0e75a77b5a682d6236cf89a58c70b76f4"}, {file = "ruff-0.6.1-py3-none-win_arm64.whl", hash = "sha256:3bc81074971b0ffad1bd0c52284b22411f02a11a012082a76ac6da153536e014"},
{file = "ruff-0.5.7.tar.gz", hash = "sha256:8dfc0a458797f5d9fb622dd0efc52d796f23f0a1493a9527f4e49a550ae9a7e5"}, {file = "ruff-0.6.1.tar.gz", hash = "sha256:af3ffd8c6563acb8848d33cd19a69b9bfe943667f0419ca083f8ebe4224a3436"},
] ]
[[package]] [[package]]
@@ -3088,93 +3140,6 @@ tzdata = {version = "*", markers = "platform_system == \"Windows\""}
[package.extras] [package.extras]
devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"]
[[package]]
name = "ujson"
version = "5.10.0"
description = "Ultra fast JSON encoder and decoder for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "ujson-5.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2601aa9ecdbee1118a1c2065323bda35e2c5a2cf0797ef4522d485f9d3ef65bd"},
{file = "ujson-5.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:348898dd702fc1c4f1051bc3aacbf894caa0927fe2c53e68679c073375f732cf"},
{file = "ujson-5.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22cffecf73391e8abd65ef5f4e4dd523162a3399d5e84faa6aebbf9583df86d6"},
{file = "ujson-5.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26b0e2d2366543c1bb4fbd457446f00b0187a2bddf93148ac2da07a53fe51569"},
{file = "ujson-5.10.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:caf270c6dba1be7a41125cd1e4fc7ba384bf564650beef0df2dd21a00b7f5770"},
{file = "ujson-5.10.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a245d59f2ffe750446292b0094244df163c3dc96b3ce152a2c837a44e7cda9d1"},
{file = "ujson-5.10.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:94a87f6e151c5f483d7d54ceef83b45d3a9cca7a9cb453dbdbb3f5a6f64033f5"},
{file = "ujson-5.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:29b443c4c0a113bcbb792c88bea67b675c7ca3ca80c3474784e08bba01c18d51"},
{file = "ujson-5.10.0-cp310-cp310-win32.whl", hash = "sha256:c18610b9ccd2874950faf474692deee4223a994251bc0a083c114671b64e6518"},
{file = "ujson-5.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:924f7318c31874d6bb44d9ee1900167ca32aa9b69389b98ecbde34c1698a250f"},
{file = "ujson-5.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a5b366812c90e69d0f379a53648be10a5db38f9d4ad212b60af00bd4048d0f00"},
{file = "ujson-5.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:502bf475781e8167f0f9d0e41cd32879d120a524b22358e7f205294224c71126"},
{file = "ujson-5.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b91b5d0d9d283e085e821651184a647699430705b15bf274c7896f23fe9c9d8"},
{file = "ujson-5.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:129e39af3a6d85b9c26d5577169c21d53821d8cf68e079060602e861c6e5da1b"},
{file = "ujson-5.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f77b74475c462cb8b88680471193064d3e715c7c6074b1c8c412cb526466efe9"},
{file = "ujson-5.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ec0ca8c415e81aa4123501fee7f761abf4b7f386aad348501a26940beb1860f"},
{file = "ujson-5.10.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab13a2a9e0b2865a6c6db9271f4b46af1c7476bfd51af1f64585e919b7c07fd4"},
{file = "ujson-5.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:57aaf98b92d72fc70886b5a0e1a1ca52c2320377360341715dd3933a18e827b1"},
{file = "ujson-5.10.0-cp311-cp311-win32.whl", hash = "sha256:2987713a490ceb27edff77fb184ed09acdc565db700ee852823c3dc3cffe455f"},
{file = "ujson-5.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:f00ea7e00447918ee0eff2422c4add4c5752b1b60e88fcb3c067d4a21049a720"},
{file = "ujson-5.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98ba15d8cbc481ce55695beee9f063189dce91a4b08bc1d03e7f0152cd4bbdd5"},
{file = "ujson-5.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9d2edbf1556e4f56e50fab7d8ff993dbad7f54bac68eacdd27a8f55f433578e"},
{file = "ujson-5.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6627029ae4f52d0e1a2451768c2c37c0c814ffc04f796eb36244cf16b8e57043"},
{file = "ujson-5.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8ccb77b3e40b151e20519c6ae6d89bfe3f4c14e8e210d910287f778368bb3d1"},
{file = "ujson-5.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3caf9cd64abfeb11a3b661329085c5e167abbe15256b3b68cb5d914ba7396f3"},
{file = "ujson-5.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6e32abdce572e3a8c3d02c886c704a38a1b015a1fb858004e03d20ca7cecbb21"},
{file = "ujson-5.10.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a65b6af4d903103ee7b6f4f5b85f1bfd0c90ba4eeac6421aae436c9988aa64a2"},
{file = "ujson-5.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:604a046d966457b6cdcacc5aa2ec5314f0e8c42bae52842c1e6fa02ea4bda42e"},
{file = "ujson-5.10.0-cp312-cp312-win32.whl", hash = "sha256:6dea1c8b4fc921bf78a8ff00bbd2bfe166345f5536c510671bccececb187c80e"},
{file = "ujson-5.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:38665e7d8290188b1e0d57d584eb8110951a9591363316dd41cf8686ab1d0abc"},
{file = "ujson-5.10.0-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:618efd84dc1acbd6bff8eaa736bb6c074bfa8b8a98f55b61c38d4ca2c1f7f287"},
{file = "ujson-5.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38d5d36b4aedfe81dfe251f76c0467399d575d1395a1755de391e58985ab1c2e"},
{file = "ujson-5.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67079b1f9fb29ed9a2914acf4ef6c02844b3153913eb735d4bf287ee1db6e557"},
{file = "ujson-5.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7d0e0ceeb8fe2468c70ec0c37b439dd554e2aa539a8a56365fd761edb418988"},
{file = "ujson-5.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:59e02cd37bc7c44d587a0ba45347cc815fb7a5fe48de16bf05caa5f7d0d2e816"},
{file = "ujson-5.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2a890b706b64e0065f02577bf6d8ca3b66c11a5e81fb75d757233a38c07a1f20"},
{file = "ujson-5.10.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:621e34b4632c740ecb491efc7f1fcb4f74b48ddb55e65221995e74e2d00bbff0"},
{file = "ujson-5.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9500e61fce0cfc86168b248104e954fead61f9be213087153d272e817ec7b4f"},
{file = "ujson-5.10.0-cp313-cp313-win32.whl", hash = "sha256:4c4fc16f11ac1612f05b6f5781b384716719547e142cfd67b65d035bd85af165"},
{file = "ujson-5.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:4573fd1695932d4f619928fd09d5d03d917274381649ade4328091ceca175539"},
{file = "ujson-5.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a984a3131da7f07563057db1c3020b1350a3e27a8ec46ccbfbf21e5928a43050"},
{file = "ujson-5.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:73814cd1b9db6fc3270e9d8fe3b19f9f89e78ee9d71e8bd6c9a626aeaeaf16bd"},
{file = "ujson-5.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61e1591ed9376e5eddda202ec229eddc56c612b61ac6ad07f96b91460bb6c2fb"},
{file = "ujson-5.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2c75269f8205b2690db4572a4a36fe47cd1338e4368bc73a7a0e48789e2e35a"},
{file = "ujson-5.10.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7223f41e5bf1f919cd8d073e35b229295aa8e0f7b5de07ed1c8fddac63a6bc5d"},
{file = "ujson-5.10.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc2fd6b3067c0782e7002ac3b38cf48608ee6366ff176bbd02cf969c9c20fe"},
{file = "ujson-5.10.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:232cc85f8ee3c454c115455195a205074a56ff42608fd6b942aa4c378ac14dd7"},
{file = "ujson-5.10.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:cc6139531f13148055d691e442e4bc6601f6dba1e6d521b1585d4788ab0bfad4"},
{file = "ujson-5.10.0-cp38-cp38-win32.whl", hash = "sha256:e7ce306a42b6b93ca47ac4a3b96683ca554f6d35dd8adc5acfcd55096c8dfcb8"},
{file = "ujson-5.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:e82d4bb2138ab05e18f089a83b6564fee28048771eb63cdecf4b9b549de8a2cc"},
{file = "ujson-5.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dfef2814c6b3291c3c5f10065f745a1307d86019dbd7ea50e83504950136ed5b"},
{file = "ujson-5.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4734ee0745d5928d0ba3a213647f1c4a74a2a28edc6d27b2d6d5bd9fa4319e27"},
{file = "ujson-5.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47ebb01bd865fdea43da56254a3930a413f0c5590372a1241514abae8aa7c76"},
{file = "ujson-5.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dee5e97c2496874acbf1d3e37b521dd1f307349ed955e62d1d2f05382bc36dd5"},
{file = "ujson-5.10.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7490655a2272a2d0b072ef16b0b58ee462f4973a8f6bbe64917ce5e0a256f9c0"},
{file = "ujson-5.10.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ba17799fcddaddf5c1f75a4ba3fd6441f6a4f1e9173f8a786b42450851bd74f1"},
{file = "ujson-5.10.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2aff2985cef314f21d0fecc56027505804bc78802c0121343874741650a4d3d1"},
{file = "ujson-5.10.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ad88ac75c432674d05b61184178635d44901eb749786c8eb08c102330e6e8996"},
{file = "ujson-5.10.0-cp39-cp39-win32.whl", hash = "sha256:2544912a71da4ff8c4f7ab5606f947d7299971bdd25a45e008e467ca638d13c9"},
{file = "ujson-5.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:3ff201d62b1b177a46f113bb43ad300b424b7847f9c5d38b1b4ad8f75d4a282a"},
{file = "ujson-5.10.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5b6fee72fa77dc172a28f21693f64d93166534c263adb3f96c413ccc85ef6e64"},
{file = "ujson-5.10.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:61d0af13a9af01d9f26d2331ce49bb5ac1fb9c814964018ac8df605b5422dcb3"},
{file = "ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecb24f0bdd899d368b715c9e6664166cf694d1e57be73f17759573a6986dd95a"},
{file = "ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbd8fd427f57a03cff3ad6574b5e299131585d9727c8c366da4624a9069ed746"},
{file = "ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beeaf1c48e32f07d8820c705ff8e645f8afa690cca1544adba4ebfa067efdc88"},
{file = "ujson-5.10.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:baed37ea46d756aca2955e99525cc02d9181de67f25515c468856c38d52b5f3b"},
{file = "ujson-5.10.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7663960f08cd5a2bb152f5ee3992e1af7690a64c0e26d31ba7b3ff5b2ee66337"},
{file = "ujson-5.10.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:d8640fb4072d36b08e95a3a380ba65779d356b2fee8696afeb7794cf0902d0a1"},
{file = "ujson-5.10.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78778a3aa7aafb11e7ddca4e29f46bc5139131037ad628cc10936764282d6753"},
{file = "ujson-5.10.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0111b27f2d5c820e7f2dbad7d48e3338c824e7ac4d2a12da3dc6061cc39c8e6"},
{file = "ujson-5.10.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:c66962ca7565605b355a9ed478292da628b8f18c0f2793021ca4425abf8b01e5"},
{file = "ujson-5.10.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ba43cc34cce49cf2d4bc76401a754a81202d8aa926d0e2b79f0ee258cb15d3a4"},
{file = "ujson-5.10.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ac56eb983edce27e7f51d05bc8dd820586c6e6be1c5216a6809b0c668bb312b8"},
{file = "ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44bd4b23a0e723bf8b10628288c2c7c335161d6840013d4d5de20e48551773b"},
{file = "ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c10f4654e5326ec14a46bcdeb2b685d4ada6911050aa8baaf3501e57024b804"},
{file = "ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0de4971a89a762398006e844ae394bd46991f7c385d7a6a3b93ba229e6dac17e"},
{file = "ujson-5.10.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e1402f0564a97d2a52310ae10a64d25bcef94f8dd643fcf5d310219d915484f7"},
{file = "ujson-5.10.0.tar.gz", hash = "sha256:b3cd8f3c5d8c7738257f1018880444f7b7d9b66232c64649f562d7ba86ad4bc1"},
]
[[package]] [[package]]
name = "uvicorn" name = "uvicorn"
version = "0.30.5" version = "0.30.5"
@@ -3740,4 +3705,4 @@ cffi = ["cffi (>=1.11)"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.10" python-versions = "^3.10"
content-hash = "b153df349812da94c345ea3ea5587c26e1adf12efa9f98706658f20e01d119d4" content-hash = "3126e56a799e58845cb01c95413f34978cd49c250962f6eba9346302471ff67a"

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = 'nonebot-plugin-tetris-stats' name = 'nonebot-plugin-tetris-stats'
version = '1.4.7' version = '1.4.17'
description = '一款基于 NoneBot2 的用于查询 Tetris 相关游戏数据的插件' description = '一款基于 NoneBot2 的用于查询 Tetris 相关游戏数据的插件'
authors = ['scdhh <wallfjjd@gmail.com>'] authors = ['scdhh <wallfjjd@gmail.com>']
readme = 'README.md' readme = 'README.md'
@@ -25,11 +25,12 @@ async-lru = '^2.0.4'
httpx = '^0.27.0' httpx = '^0.27.0'
jinja2 = '^3.1.3' jinja2 = '^3.1.3'
lxml = '^5.1.0' lxml = '^5.1.0'
msgspec = "^0.18.6"
pandas = '>=1.4.3,<3.0.0' pandas = '>=1.4.3,<3.0.0'
pillow = '^10.3.0' pillow = '^10.3.0'
playwright = '^1.41.2' playwright = '^1.41.2'
rich = '^13.7.1' rich = '^13.7.1'
ujson = '^5.9.0' yarl = "^1.9.4"
zstandard = '>=0.22,<0.24' zstandard = '>=0.22,<0.24'
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
@@ -133,4 +134,3 @@ quote-style = 'single'
[tool.nonebot] [tool.nonebot]
plugins = ['nonebot_plugin_tetris_stats'] plugins = ['nonebot_plugin_tetris_stats']
# plugins = ['test_aps']