Compare commits

...

29 Commits

Author SHA1 Message Date
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
d35469cdef 🔖 1.4.7 2024-08-15 17:01:27 +08:00
0cbae117aa ️ 将 _parse_profile 改成静态方法 2024-08-15 16:56:10 +08:00
25dc57d911 top query 使用图片回复 close #61 2024-08-15 16:55:13 +08:00
6042417b65 ️ 删除不需要的 async 2024-08-15 16:50:11 +08:00
22 changed files with 266 additions and 81 deletions

View File

@@ -1,13 +1,14 @@
from pathlib import Path 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 ScopedConfig(BaseModel):
request_timeout: float = 30.0
screenshot_quality: float = 2
class Config(BaseModel): class Config(BaseModel):
"""配置类""" tetris: ScopedConfig = Field(default_factory=ScopedConfig)
tetris_req_timeout: float = 30.0
tetris_screenshot_quality: float = 2

View File

@@ -24,6 +24,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 +56,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,
} }
@@ -138,6 +140,8 @@ 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:
@@ -164,20 +168,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',

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

@@ -19,9 +19,18 @@ from sqlalchemy import select
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,
@@ -211,11 +223,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

@@ -48,7 +48,8 @@ class Player:
) )
return self._user_profile return self._user_profile
def _parse_profile(self, original_user_profile: bytes) -> UserProfile: @staticmethod
def _parse_profile(original_user_profile: bytes) -> UserProfile:
html = etree.HTML(original_user_profile) html = etree.HTML(original_user_profile)
user_name = html.xpath('//div[@class="mycontent"]/h1/text()')[0].replace("'s profile", '') user_name = html.xpath('//div[@class="mycontent"]/h1/text()')[0].replace("'s profile", '')
today = None today = None
@@ -67,4 +68,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

@@ -8,12 +8,20 @@ from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[im
from nonebot_plugin_user import get_user from nonebot_plugin_user import get_user
from ...db import query_bind_info, trigger from ...db import query_bind_info, trigger
from ...utils.metrics import get_metrics from ...utils.exception import FallbackError
from ...utils.host import HostPage, get_self_netloc
from ...utils.metrics import TetrisMetricsBasicWithLPM, get_metrics
from ...utils.render import render
from ...utils.render.avatar import get_avatar
from ...utils.render.schemas.base import People
from ...utils.render.schemas.top_info import Data as InfoData
from ...utils.render.schemas.top_info import Info
from ...utils.screenshot import screenshot
from ...utils.typing import Me from ...utils.typing import Me
from ..constant import CANT_VERIFY_MESSAGE from ..constant import CANT_VERIFY_MESSAGE
from . import alc from . import alc
from .api import Player from .api import Player
from .api.schemas.user_profile import UserProfile from .api.schemas.user_profile import Data, UserProfile
from .constant import GAME_TYPE from .constant import GAME_TYPE
@@ -35,8 +43,10 @@ async def _(event: Event, matcher: Matcher, target: At | Me, event_session: Even
) )
if bind is None: if bind is None:
await matcher.finish('未查询到绑定信息') await matcher.finish('未查询到绑定信息')
message = CANT_VERIFY_MESSAGE await (
await (message + make_query_text(await Player(user_name=bind.game_account, trust=True).get_profile())).finish() UniMessage(CANT_VERIFY_MESSAGE)
+ await make_query_result(await Player(user_name=bind.game_account, trust=True).get_profile())
).finish()
@alc.assign('TOP.query') @alc.assign('TOP.query')
@@ -47,7 +57,34 @@ async def _(account: Player, event_session: EventSession):
command_type='query', command_type='query',
command_args=[], command_args=[],
): ):
await (make_query_text(await account.get_profile())).finish() await (await make_query_result(await account.get_profile())).finish()
def get_avg_metrics(data: list[Data]) -> TetrisMetricsBasicWithLPM:
total_lpm = total_apm = 0.0
for value in data:
total_lpm += value.lpm
total_apm += value.apm
num = len(data)
return get_metrics(lpm=total_lpm / num, apm=total_apm / num)
async def make_query_image(profile: UserProfile) -> bytes:
if profile.today is None or profile.total is None:
raise FallbackError
today = get_metrics(lpm=profile.today.lpm, apm=profile.today.apm)
history = get_avg_metrics(profile.total)
async with HostPage(
await render(
'v1/top/info',
Info(
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),
history=InfoData(pps=history.pps, lpm=history.lpm, apm=history.apm, apl=history.apl),
),
)
) as page_hash:
return await screenshot(f'http://{get_self_netloc()}/host/{page_hash}.html')
def make_query_text(profile: UserProfile) -> UniMessage: def make_query_text(profile: UserProfile) -> UniMessage:
@@ -60,15 +97,18 @@ def make_query_text(profile: UserProfile) -> UniMessage:
else: else:
message += f'用户 {profile.user_name} 暂无24小时内统计数据' message += f'用户 {profile.user_name} 暂无24小时内统计数据'
if profile.total is not None: if profile.total is not None:
total_lpm = total_apm = 0.0 total = get_avg_metrics(profile.total)
for value in profile.total:
total_lpm += value.lpm
total_apm += value.apm
num = len(profile.total)
total = get_metrics(lpm=total_lpm / num, apm=total_apm / num)
message += '\n历史统计数据为: ' message += '\n历史统计数据为: '
message += f"\nL'PM: {total.lpm} ( {total.pps} pps )" message += f"\nL'PM: {total.lpm} ( {total.pps} pps )"
message += f'\nAPM: {total.apm} ( x{total.apl} )' message += f'\nAPM: {total.apm} ( x{total.apl} )'
else: else:
message += '\n暂无历史统计数据' message += '\n暂无历史统计数据'
return UniMessage(message) return UniMessage(message)
async def make_query_result(profile: UserProfile) -> UniMessage:
try:
return UniMessage.image(raw=await make_query_image(profile))
except FallbackError:
...
return make_query_text(profile)

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

@@ -55,7 +55,7 @@ def _():
@app.get('/host/{page_hash}.html', status_code=status.HTTP_200_OK) @app.get('/host/{page_hash}.html', status_code=status.HTTP_200_OK)
async def _(page_hash: str) -> HTMLResponse: def _(page_hash: str) -> HTMLResponse:
if page_hash in HostPage.pages: if page_hash in HostPage.pages:
return HTMLResponse(HostPage.pages[page_hash]) return HTMLResponse(HostPage.pages[page_hash])
return NOT_FOUND return NOT_FOUND

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

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

@@ -119,7 +119,7 @@ class Request:
async def request(cls, url: str, *, is_json: bool = True) -> bytes: async def request(cls, url: str, *, is_json: bool = True) -> bytes:
"""请求api""" """请求api"""
try: try:
async with AsyncClient(cookies=cls._cookies, timeout=config.tetris_req_timeout) as session: async with AsyncClient(cookies=cls._cookies, timeout=config.tetris.request_timeout) as session:
response = await session.get(url, headers=cls._headers) response = await session.get(url, headers=cls._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}'

View File

@@ -14,10 +14,9 @@ config = get_plugin_config(Config)
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 +27,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,12 +11,13 @@ 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
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)
@@ -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 (
@@ -56,7 +57,7 @@ async def download_templates(tag: str) -> Path:
return path return path
async def unzip_templates(zip_path: Path) -> Path: def unzip_templates(zip_path: Path) -> Path:
logger.info('开始解压模板') logger.info('开始解压模板')
temp_path = TEMPLATES_DIR.parent / f'temp_{time_ns()}' temp_path = TEMPLATES_DIR.parent / f'temp_{time_ns()}'
with ZipFile(zip_path) as zip_file: with ZipFile(zip_path) as zip_file:
@@ -91,7 +92,7 @@ async def check_hash(hash_file_path: Path) -> bool:
async def init_templates(tag: str) -> bool: async def init_templates(tag: str) -> bool:
logger.info(f'开始初始化模板 {tag}') logger.info(f'开始初始化模板 {tag}')
temp_path = await unzip_templates(await download_templates(tag)) temp_path = unzip_templates(await download_templates(tag))
if not await check_hash(temp_path / 'hash.sha256'): if not await check_hash(temp_path / 'hash.sha256'):
rmtree(temp_path) rmtree(temp_path)
return False return False

44
poetry.lock generated
View File

@@ -1927,13 +1927,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.1"
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.1-py3-none-any.whl", hash = "sha256:1d4daba6659774b65ebbc9b679d50c5505a8dcdd76a71a122b09452e0aaa2d11"},
{file = "nonebot_plugin_user-0.4.0.tar.gz", hash = "sha256:d75b87b9f4ebc301106ede6da106ea3e352e186899fb03221030476133cd915b"}, {file = "nonebot_plugin_user-0.4.1.tar.gz", hash = "sha256:80ea561a46c22d0e087edefb9a2bce8cfc4395426fbb54506d85382538eb68de"},
] ]
[package.dependencies] [package.dependencies]
@@ -2706,29 +2706,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.5.7" version = "0.6.0"
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.0-py3-none-linux_armv6l.whl", hash = "sha256:92dcce923e5df265781e5fc76f9a1edad52201a7aafe56e586b90988d5239013"},
{file = "ruff-0.5.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00cc8872331055ee017c4f1071a8a31ca0809ccc0657da1d154a1d2abac5c0be"}, {file = "ruff-0.6.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:31b90ff9dc79ed476c04e957ba7e2b95c3fceb76148f2079d0d68a908d2cfae7"},
{file = "ruff-0.5.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf3d86a1fdac1aec8a3417a63587d93f906c678bb9ed0b796da7b59c1114a1e"}, {file = "ruff-0.6.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6d834a9ec9f8287dd6c3297058b3a265ed6b59233db22593379ee38ebc4b9768"},
{file = "ruff-0.5.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a01c34400097b06cf8a6e61b35d6d456d5bd1ae6961542de18ec81eaf33b4cb8"}, {file = "ruff-0.6.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2089267692696aba342179471831a085043f218706e642564812145df8b8d0d"},
{file = "ruff-0.5.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcc8054f1a717e2213500edaddcf1dbb0abad40d98e1bd9d0ad364f75c763eea"}, {file = "ruff-0.6.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aa62b423ee4bbd8765f2c1dbe8f6aac203e0583993a91453dc0a449d465c84da"},
{file = "ruff-0.5.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f70284e73f36558ef51602254451e50dd6cc479f8b6f8413a95fcb5db4a55fc"}, {file = "ruff-0.6.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7344e1a964b16b1137ea361d6516ce4ee61a0403fa94252a1913ecc1311adcae"},
{file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a78ad870ae3c460394fc95437d43deb5c04b5c29297815a2a1de028903f19692"}, {file = "ruff-0.6.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:487f3a35c3f33bf82be212ce15dc6278ea854e35573a3f809442f73bec8b2760"},
{file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ccd078c66a8e419475174bfe60a69adb36ce04f8d4e91b006f1329d5cd44bcf"}, {file = "ruff-0.6.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:75db409984077a793cf344d499165298a6f65449e905747ac65983b12e3e64b1"},
{file = "ruff-0.5.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e31c9bad4ebf8fdb77b59cae75814440731060a09a0e0077d559a556453acbb"}, {file = "ruff-0.6.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84908bd603533ecf1db456d8fc2665d1f4335d722e84bc871d3bbd2d1116c272"},
{file = "ruff-0.5.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d796327eed8e168164346b769dd9a27a70e0298d667b4ecee6877ce8095ec8e"}, {file = "ruff-0.6.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f1749a0aef3ec41ed91a0e2127a6ae97d2e2853af16dbd4f3c00d7a3af726c5"},
{file = "ruff-0.5.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a09ea2c3f7778cc635e7f6edf57d566a8ee8f485f3c4454db7771efb692c499"}, {file = "ruff-0.6.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:016fea751e2bcfbbd2f8cb19b97b37b3fd33148e4df45b526e87096f4e17354f"},
{file = "ruff-0.5.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a36d8dcf55b3a3bc353270d544fb170d75d2dff41eba5df57b4e0b67a95bb64e"}, {file = "ruff-0.6.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6ae80f141b53b2e36e230017e64f5ea2def18fac14334ffceaae1b780d70c4f7"},
{file = "ruff-0.5.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9369c218f789eefbd1b8d82a8cf25017b523ac47d96b2f531eba73770971c9e5"}, {file = "ruff-0.6.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:eaaaf33ea4b3f63fd264d6a6f4a73fa224bbfda4b438ffea59a5340f4afa2bb5"},
{file = "ruff-0.5.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b88ca3db7eb377eb24fb7c82840546fb7acef75af4a74bd36e9ceb37a890257e"}, {file = "ruff-0.6.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7667ddd1fc688150a7ca4137140867584c63309695a30016880caf20831503a0"},
{file = "ruff-0.5.7-py3-none-win32.whl", hash = "sha256:33d61fc0e902198a3e55719f4be6b375b28f860b09c281e4bdbf783c0566576a"}, {file = "ruff-0.6.0-py3-none-win32.whl", hash = "sha256:ae48365aae60d40865a412356f8c6f2c0be1c928591168111eaf07eaefa6bea3"},
{file = "ruff-0.5.7-py3-none-win_amd64.whl", hash = "sha256:083bbcbe6fadb93cd86709037acc510f86eed5a314203079df174c40bbbca6b3"}, {file = "ruff-0.6.0-py3-none-win_amd64.whl", hash = "sha256:774032b507c96f0c803c8237ce7d2ef3934df208a09c40fa809c2931f957fe5e"},
{file = "ruff-0.5.7-py3-none-win_arm64.whl", hash = "sha256:2dca26154ff9571995107221d0aeaad0e75a77b5a682d6236cf89a58c70b76f4"}, {file = "ruff-0.6.0-py3-none-win_arm64.whl", hash = "sha256:a5366e8c3ae6b2dc32821749b532606c42e609a99b0ae1472cf601da931a048c"},
{file = "ruff-0.5.7.tar.gz", hash = "sha256:8dfc0a458797f5d9fb622dd0efc52d796f23f0a1493a9527f4e49a550ae9a7e5"}, {file = "ruff-0.6.0.tar.gz", hash = "sha256:272a81830f68f9bd19d49eaf7fa01a5545c5a2e86f32a9935bb0e4bb9a1db5b8"},
] ]
[[package]] [[package]]

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = 'nonebot-plugin-tetris-stats' name = 'nonebot-plugin-tetris-stats'
version = '1.4.6' version = '1.4.16'
description = '一款基于 NoneBot2 的用于查询 Tetris 相关游戏数据的插件' description = '一款基于 NoneBot2 的用于查询 Tetris 相关游戏数据的插件'
authors = ['scdhh <wallfjjd@gmail.com>'] authors = ['scdhh <wallfjjd@gmail.com>']
readme = 'README.md' readme = 'README.md'
@@ -133,4 +133,3 @@ quote-style = 'single'
[tool.nonebot] [tool.nonebot]
plugins = ['nonebot_plugin_tetris_stats'] plugins = ['nonebot_plugin_tetris_stats']
# plugins = ['test_aps']