Compare commits

...

20 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
25 changed files with 358 additions and 317 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,3 +1,4 @@
from nonebot import get_plugin_config
from nonebot_plugin_localstore import get_cache_dir, get_data_dir from nonebot_plugin_localstore import get_cache_dir, get_data_dir
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
@@ -5,10 +6,22 @@ CACHE_PATH = get_cache_dir('nonebot_plugin_tetris_stats')
DATA_PATH = get_data_dir('nonebot_plugin_tetris_stats') DATA_PATH = get_data_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): class ScopedConfig(BaseModel):
request_timeout: float = 30.0 request_timeout: float = 30.0
screenshot_quality: float = 2 screenshot_quality: float = 2
proxy: Proxy = Field(default_factory=Proxy)
class Config(BaseModel): class Config(BaseModel):
tetris: ScopedConfig = Field(default_factory=ScopedConfig) tetris: ScopedConfig = Field(default_factory=ScopedConfig)
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
@@ -88,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:
@@ -117,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}'
@@ -147,7 +141,7 @@ class Player:
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]
@@ -217,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,14 +1,16 @@
from typing import Literal
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from ...typing import Rank, S1Rank, S1ValidRank, ValidRank from ...typing import Rank, S1Rank, S1ValidRank
from ..base import SuccessModel from ..base import SuccessModel
class PastInner(BaseModel): class PastInner(BaseModel):
season: str season: str
username: str username: str
country: str country: str | None = None
placement: int placement: int | None = None
gamesplayed: int gamesplayed: int
gameswon: int gameswon: int
glicko: float glicko: float
@@ -24,10 +26,57 @@ class PastInner(BaseModel):
class Past(BaseModel): class Past(BaseModel):
first: PastInner = Field(..., alias='1') first: PastInner | None = Field(default=None, alias='1')
class Data(BaseModel): 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 gamesplayed: int
gameswon: int gameswon: int
glicko: float glicko: float
@@ -35,21 +84,19 @@ class Data(BaseModel):
gxe: float gxe: float
tr: float tr: float
rank: Rank rank: Rank
bestrank: ValidRank = Field('z') bestrank: Rank
standing: int
apm: float apm: float
pps: float pps: float
vs: float vs: float
decaying: bool
standing: int
standing_local: int standing_local: int
prev_rank: ValidRank | None = None prev_rank: Rank | None = None
prev_at: int prev_at: int
next_rank: ValidRank | None = None next_rank: Rank | None = None
next_at: int next_at: int
percentile: float percentile: float
percentile_rank: Rank percentile_rank: str
past: Past
class LeagueSuccessModel(SuccessModel): class LeagueSuccessModel(SuccessModel):
data: Data data: NeverPlayedData | NeverRatedData | RatedData

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,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,6 +15,7 @@ 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
@@ -39,13 +39,13 @@ 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
if TYPE_CHECKING: if TYPE_CHECKING:
from .api.schemas.summaries import SoloSuccessModel, ZenSuccessModel from .api.schemas.summaries import SoloSuccessModel, ZenSuccessModel
from .api.schemas.summaries.league import LeagueSuccessModel
from .api.schemas.user import User from .api.schemas.user import User
from .api.schemas.user_info import UserInfoSuccess from .api.schemas.user_info import UserInfoSuccess
@@ -199,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',
@@ -229,7 +233,7 @@ async def make_query_image_v2(player: Player) -> bytes:
), ),
tetra_league=TetraLeague( tetra_league=TetraLeague(
rank=league.data.rank, rank=league.data.rank,
highest_rank=league.data.bestrank, highest_rank='z' if isinstance(league.data, NeverRatedData) else league.data.bestrank,
tr=round(league.data.tr, 2), tr=round(league.data.tr, 2),
glicko=round(league.data.glicko, 2), glicko=round(league.data.glicko, 2),
rd=round(league.data.rd, 2), rd=round(league.data.rd, 2),
@@ -243,7 +247,9 @@ async def make_query_image_v2(player: Player) -> bytes:
statistic=TetraLeagueStatistic(total=league.data.gamesplayed, wins=league.data.gameswon), statistic=TetraLeagueStatistic(total=league.data.gamesplayed, wins=league.data.gameswon),
decaying=league.data.decaying, decaying=league.data.decaying,
history=None, 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(

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

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

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

@@ -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.request_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,13 +1,10 @@
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
@@ -27,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

@@ -13,7 +13,7 @@ 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 rich.progress import Progress from rich.progress import Progress
from ..config.config import CACHE_PATH, DATA_PATH from ..config.config import CACHE_PATH, DATA_PATH, config
driver = get_driver() driver = get_driver()
@@ -24,7 +24,7 @@ alc = on_alconna(Alconna('更新模板', Option('--revision', Args['revision', s
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 = (
@@ -105,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.1" 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.1-py3-none-any.whl", hash = "sha256:1d4daba6659774b65ebbc9b679d50c5505a8dcdd76a71a122b09452e0aaa2d11"}, {file = "nonebot_plugin_user-0.4.2-py3-none-any.whl", hash = "sha256:c7312109614cb40e1887e534deb1e81dd213b2304dd34cba738b35580971cc9f"},
{file = "nonebot_plugin_user-0.4.1.tar.gz", hash = "sha256:80ea561a46c22d0e087edefb9a2bce8cfc4395426fbb54506d85382538eb68de"}, {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.6.0" 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.6.0-py3-none-linux_armv6l.whl", hash = "sha256:92dcce923e5df265781e5fc76f9a1edad52201a7aafe56e586b90988d5239013"}, {file = "ruff-0.6.1-py3-none-linux_armv6l.whl", hash = "sha256:b4bb7de6a24169dc023f992718a9417380301b0c2da0fe85919f47264fb8add9"},
{file = "ruff-0.6.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:31b90ff9dc79ed476c04e957ba7e2b95c3fceb76148f2079d0d68a908d2cfae7"}, {file = "ruff-0.6.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:45efaae53b360c81043e311cdec8a7696420b3d3e8935202c2846e7a97d4edae"},
{file = "ruff-0.6.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6d834a9ec9f8287dd6c3297058b3a265ed6b59233db22593379ee38ebc4b9768"}, {file = "ruff-0.6.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:bc60c7d71b732c8fa73cf995efc0c836a2fd8b9810e115be8babb24ae87e0850"},
{file = "ruff-0.6.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2089267692696aba342179471831a085043f218706e642564812145df8b8d0d"}, {file = "ruff-0.6.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c7477c3b9da822e2db0b4e0b59e61b8a23e87886e727b327e7dcaf06213c5cf"},
{file = "ruff-0.6.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aa62b423ee4bbd8765f2c1dbe8f6aac203e0583993a91453dc0a449d465c84da"}, {file = "ruff-0.6.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a0af7ab3f86e3dc9f157a928e08e26c4b40707d0612b01cd577cc84b8905cc9"},
{file = "ruff-0.6.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7344e1a964b16b1137ea361d6516ce4ee61a0403fa94252a1913ecc1311adcae"}, {file = "ruff-0.6.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:392688dbb50fecf1bf7126731c90c11a9df1c3a4cdc3f481b53e851da5634fa5"},
{file = "ruff-0.6.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:487f3a35c3f33bf82be212ce15dc6278ea854e35573a3f809442f73bec8b2760"}, {file = "ruff-0.6.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5278d3e095ccc8c30430bcc9bc550f778790acc211865520f3041910a28d0024"},
{file = "ruff-0.6.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:75db409984077a793cf344d499165298a6f65449e905747ac65983b12e3e64b1"}, {file = "ruff-0.6.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fe6d5f65d6f276ee7a0fc50a0cecaccb362d30ef98a110f99cac1c7872df2f18"},
{file = "ruff-0.6.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84908bd603533ecf1db456d8fc2665d1f4335d722e84bc871d3bbd2d1116c272"}, {file = "ruff-0.6.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2e0dd11e2ae553ee5c92a81731d88a9883af8db7408db47fc81887c1f8b672e"},
{file = "ruff-0.6.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f1749a0aef3ec41ed91a0e2127a6ae97d2e2853af16dbd4f3c00d7a3af726c5"}, {file = "ruff-0.6.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d812615525a34ecfc07fd93f906ef5b93656be01dfae9a819e31caa6cfe758a1"},
{file = "ruff-0.6.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:016fea751e2bcfbbd2f8cb19b97b37b3fd33148e4df45b526e87096f4e17354f"}, {file = "ruff-0.6.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:faaa4060f4064c3b7aaaa27328080c932fa142786f8142aff095b42b6a2eb631"},
{file = "ruff-0.6.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6ae80f141b53b2e36e230017e64f5ea2def18fac14334ffceaae1b780d70c4f7"}, {file = "ruff-0.6.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:99d7ae0df47c62729d58765c593ea54c2546d5de213f2af2a19442d50a10cec9"},
{file = "ruff-0.6.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:eaaaf33ea4b3f63fd264d6a6f4a73fa224bbfda4b438ffea59a5340f4afa2bb5"}, {file = "ruff-0.6.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9eb18dfd7b613eec000e3738b3f0e4398bf0153cb80bfa3e351b3c1c2f6d7b15"},
{file = "ruff-0.6.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7667ddd1fc688150a7ca4137140867584c63309695a30016880caf20831503a0"}, {file = "ruff-0.6.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c62bc04c6723a81e25e71715aa59489f15034d69bf641df88cb38bdc32fd1dbb"},
{file = "ruff-0.6.0-py3-none-win32.whl", hash = "sha256:ae48365aae60d40865a412356f8c6f2c0be1c928591168111eaf07eaefa6bea3"}, {file = "ruff-0.6.1-py3-none-win32.whl", hash = "sha256:9fb4c4e8b83f19c9477a8745e56d2eeef07a7ff50b68a6998f7d9e2e3887bdc4"},
{file = "ruff-0.6.0-py3-none-win_amd64.whl", hash = "sha256:774032b507c96f0c803c8237ce7d2ef3934df208a09c40fa809c2931f957fe5e"}, {file = "ruff-0.6.1-py3-none-win_amd64.whl", hash = "sha256:c2ebfc8f51ef4aca05dad4552bbcf6fe8d1f75b2f6af546cc47cc1c1ca916b5b"},
{file = "ruff-0.6.0-py3-none-win_arm64.whl", hash = "sha256:a5366e8c3ae6b2dc32821749b532606c42e609a99b0ae1472cf601da931a048c"}, {file = "ruff-0.6.1-py3-none-win_arm64.whl", hash = "sha256:3bc81074971b0ffad1bd0c52284b22411f02a11a012082a76ac6da153536e014"},
{file = "ruff-0.6.0.tar.gz", hash = "sha256:272a81830f68f9bd19d49eaf7fa01a5545c5a2e86f32a9935bb0e4bb9a1db5b8"}, {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.10' 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]