Compare commits

...

15 Commits
1.0.0 ... 1.0.1

Author SHA1 Message Date
f39faced7e 🔖 1.0.1 2024-05-04 07:05:47 +08:00
fffa07dc03 IO查数据使用图片回复 2024-05-04 07:04:57 +08:00
0467b3e5df 💄 微调页脚 2024-05-04 07:04:57 +08:00
f6cc0229ba 💄 新增雷达图数据的 tips 2024-05-04 07:04:57 +08:00
e2708b661d 💄 将雷达图的 OR 替换成 DSPS 2024-05-04 07:04:56 +08:00
65d019a6d3 💄 暂时禁用 TR 折线图 2024-05-04 06:58:31 +08:00
be1b07d5dc 查询图支持处理没有签名的情况 2024-05-04 00:43:06 +08:00
c92bc3aaad 添加开发依赖 types-Pillow 2024-05-03 05:48:39 +08:00
d4b887ef83 💄 css 统一使用 class 2024-05-03 02:12:36 +08:00
dependabot[bot]
695ff13aa2 ⬆️ Bump nonebot-plugin-alconna from 0.45.2 to 0.45.3 (#306)
Bumps [nonebot-plugin-alconna](https://github.com/nonebot/plugin-alconna) from 0.45.2 to 0.45.3.
- [Release notes](https://github.com/nonebot/plugin-alconna/releases)
- [Commits](https://github.com/nonebot/plugin-alconna/compare/v0.45.2...v0.45.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-03 00:16:06 +08:00
ec1001b3bb query 也使用 UniMessage 进行发送 2024-05-02 21:29:49 +08:00
b545b12255 将 generate_message 合并到 handle_query 2024-05-02 21:01:50 +08:00
b2505e0979 🔧 更新 ruff 配置 2024-05-02 20:53:27 +08:00
38defe37cd 🚨 删除不需要的 noqa 2024-05-02 20:52:00 +08:00
7a3d7c908c 添加 override 标记 2024-05-02 20:51:27 +08:00
14 changed files with 315 additions and 203 deletions

View File

@@ -8,6 +8,7 @@ from nonebot_plugin_orm import Model
from pydantic import BaseModel, ValidationError from pydantic import BaseModel, ValidationError
from sqlalchemy import JSON, DateTime, Dialect, PickleType, String, TypeDecorator from sqlalchemy import JSON, DateTime, Dialect, PickleType, String, TypeDecorator
from sqlalchemy.orm import Mapped, MappedAsDataclass, mapped_column from sqlalchemy.orm import Mapped, MappedAsDataclass, mapped_column
from typing_extensions import override
from ..game_data_processor.schemas import BaseProcessedData, BaseUser from ..game_data_processor.schemas import BaseProcessedData, BaseUser
from ..utils.typing import CommandType, GameType from ..utils.typing import CommandType, GameType
@@ -16,17 +17,20 @@ from ..utils.typing import CommandType, GameType
class PydanticType(TypeDecorator): class PydanticType(TypeDecorator):
impl = JSON impl = JSON
def __init__(self, get_model: Callable[[], Sequence[type[BaseModel]]], *args: Any, **kwargs: Any): # noqa: ANN401 @override
def __init__(self, get_model: Callable[[], Sequence[type[BaseModel]]], *args: Any, **kwargs: Any):
self.get_model = get_model self.get_model = get_model
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def process_bind_param(self, value: Any | None, dialect: Dialect) -> str: # noqa: ANN401 @override
def process_bind_param(self, value: Any | None, dialect: Dialect) -> str:
# 将 Pydantic 模型实例转换为 JSON # 将 Pydantic 模型实例转换为 JSON
if isinstance(value, tuple(self.get_model())): if isinstance(value, tuple(self.get_model())):
return value.json() # type: ignore[union-attr] return value.json() # type: ignore[union-attr]
raise TypeError raise TypeError
def process_result_value(self, value: Any | None, dialect: Dialect) -> BaseModel: # noqa: ANN401 @override
def process_result_value(self, value: Any | None, dialect: Dialect) -> BaseModel:
# 将 JSON 转换回 Pydantic 模型实例 # 将 JSON 转换回 Pydantic 模型实例
if isinstance(value, str | bytes): if isinstance(value, str | bytes):
for i in self.get_model(): for i in self.get_model():

View File

@@ -55,15 +55,10 @@ class Processor(ABC):
raise NotImplementedError raise NotImplementedError
@abstractmethod @abstractmethod
async def handle_query(self) -> str: async def handle_query(self) -> UniMessage:
"""处理查询消息""" """处理查询消息"""
raise NotImplementedError raise NotImplementedError
@abstractmethod
async def generate_message(self) -> str:
"""生成消息"""
raise NotImplementedError
def __del__(self) -> None: def __del__(self) -> None:
finish_time = datetime.now(tz=UTC) finish_time = datetime.now(tz=UTC)
if Recorder.is_error_event(self.event_id): if Recorder.is_error_event(self.event_id):

View File

@@ -5,6 +5,7 @@ from arclet.alconna import Alconna, AllParam, Arg, ArgFlag, Args, CommandMeta, O
from nonebot.adapters import Bot, Event from nonebot.adapters import Bot, Event
from nonebot.matcher import Matcher from nonebot.matcher import Matcher
from nonebot_plugin_alconna import At, on_alconna from nonebot_plugin_alconna import At, on_alconna
from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_orm import get_session from nonebot_plugin_orm import get_session
from nonebot_plugin_userinfo import BotUserInfo, UserInfo # type: ignore[import-untyped] from nonebot_plugin_userinfo import BotUserInfo, UserInfo # type: ignore[import-untyped]
from sqlalchemy import func, select from sqlalchemy import func, select
@@ -117,10 +118,11 @@ async def _(bot: Bot, event: Event, matcher: Matcher, target: At | Me):
command_args=[], command_args=[],
) )
try: try:
await matcher.finish(message + await proc.handle_query()) await (UniMessage(message) + await proc.handle_query()).send()
except NeedCatchError as e: except NeedCatchError as e:
await matcher.send(str(e)) await matcher.send(str(e))
raise HandleNotFinishedError from e raise HandleNotFinishedError from e
await matcher.finish()
@alc.assign('query') @alc.assign('query')
@@ -131,10 +133,11 @@ async def _(event: Event, matcher: Matcher, account: User):
command_args=[], command_args=[],
) )
try: try:
await matcher.finish(await proc.handle_query()) await (await proc.handle_query()).send()
except NeedCatchError as e: except NeedCatchError as e:
await matcher.send(str(e)) await matcher.send(str(e))
raise HandleNotFinishedError from e raise HandleNotFinishedError from e
await matcher.finish()
@alc.assign('rank') @alc.assign('rank')

View File

@@ -1,3 +1,4 @@
from asyncio import gather
from collections import defaultdict from collections import defaultdict
from collections.abc import Callable from collections.abc import Callable
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
@@ -18,6 +19,7 @@ from nonebot_plugin_localstore import get_data_file # type: ignore[import-untyp
from nonebot_plugin_orm import get_session from nonebot_plugin_orm import get_session
from nonebot_plugin_userinfo import UserInfo as NBUserInfo # type: ignore[import-untyped] from nonebot_plugin_userinfo import UserInfo as NBUserInfo # type: ignore[import-untyped]
from sqlalchemy import select from sqlalchemy import select
from typing_extensions import override
from zstandard import ZstdCompressor from zstandard import ZstdCompressor
from ...db import BindStatus, create_or_update_bind from ...db import BindStatus, create_or_update_bind
@@ -32,15 +34,13 @@ from .. import Processor as ProcessorMeta
from .cache import Cache from .cache import Cache
from .constant import BASE_URL, GAME_TYPE, RANK_PERCENTILE from .constant import BASE_URL, GAME_TYPE, RANK_PERCENTILE
from .model import IORank from .model import IORank
from .schemas.league_all import FailedModel as LeagueAllFailed from .schemas.base import FailedModel
from .schemas.league_all import LeagueAll from .schemas.league_all import LeagueAll
from .schemas.league_all import ValidUser as LeagueAllUser from .schemas.league_all import ValidUser as LeagueAllUser
from .schemas.response import ProcessedData, RawResponse from .schemas.response import ProcessedData, RawResponse
from .schemas.user import User from .schemas.user import User
from .schemas.user_info import FailedModel as InfoFailed from .schemas.user_info import NeverPlayedLeague, NeverRatedLeague, RatedLeague, UserInfo
from .schemas.user_info import NeverPlayedLeague, NeverRatedLeague, UserInfo
from .schemas.user_info import SuccessModel as InfoSuccess from .schemas.user_info import SuccessModel as InfoSuccess
from .schemas.user_records import FailedModel as RecordsFailed
from .schemas.user_records import SoloRecord, UserRecords from .schemas.user_records import SoloRecord, UserRecords
from .schemas.user_records import SuccessModel as RecordsSuccess from .schemas.user_records import SuccessModel as RecordsSuccess
from .typing import Rank from .typing import Rank
@@ -63,15 +63,18 @@ class Processor(ProcessorMeta):
raw_response: RawResponse raw_response: RawResponse
processed_data: ProcessedData processed_data: ProcessedData
@override
def __init__(self, event_id: int, user: User, command_args: list[str]) -> None: def __init__(self, event_id: int, user: User, command_args: list[str]) -> None:
super().__init__(event_id, user, command_args) super().__init__(event_id, user, command_args)
self.raw_response = RawResponse() self.raw_response = RawResponse()
self.processed_data = ProcessedData() self.processed_data = ProcessedData()
@property @property
@override
def game_platform(self) -> Literal['IO']: def game_platform(self) -> Literal['IO']:
return GAME_TYPE return GAME_TYPE
@override
async def handle_bind(self, platform: str, account: str, bot_info: NBUserInfo) -> UniMessage: async def handle_bind(self, platform: str, account: str, bot_info: NBUserInfo) -> UniMessage:
"""处理绑定消息""" """处理绑定消息"""
self.command_type = 'bind' self.command_type = 'bind'
@@ -110,11 +113,105 @@ class Processor(ProcessorMeta):
) )
return message return message
async def handle_query(self) -> str: @override
async def handle_query(self) -> UniMessage:
"""处理查询消息""" """处理查询消息"""
self.command_type = 'query' self.command_type = 'query'
await self.get_user() await self.get_user()
return await self.generate_message() user_info, user_records = await gather(self.get_user_info(), self.get_user_records())
user_name = user_info.data.user.username.upper()
league = user_info.data.user.league
sprint = user_records.data.records.sprint
blitz = user_records.data.records.blitz
if isinstance(league, RatedLeague) and league.vs is not None:
if sprint.record is None:
sprint_value = 'N/A'
else:
if not isinstance(sprint.record, SoloRecord):
raise WhatTheFuckError('40L记录不是单人记录')
duration = timedelta(milliseconds=sprint.record.endcontext.final_time).total_seconds()
sprint_value = f'{duration:.1f}s' if duration < 60 else f'{duration // 60:.0f}m {duration % 60:.1f}s' # noqa: PLR2004
if blitz.record is None:
blitz_value = 'N/A'
else:
if not isinstance(blitz.record, SoloRecord):
raise WhatTheFuckError('Blitz记录不是单人记录')
blitz_value = f'{blitz.record.endcontext.score:,}'
async with HostPage(
await render(
'data.j2.html',
user_avatar=f'https://tetr.io/user-content/avatars/{user_info.data.user.id}.jpg?rv={user_info.data.user.avatar_revision}'
if user_info.data.user.avatar_revision is not None
else f'../../identicon?md5={md5(user_info.data.user.id.encode()).hexdigest()}', # noqa: S324
user_name=user_name,
user_sign=user_info.data.user.bio,
game_type='TETR.IO',
ranking=round(league.glicko, 2),
rd=round(league.rd, 2),
rank=league.rank,
TR=round(league.rating, 2),
global_rank=league.standing,
lpm=round(lpm := (league.pps * 24), 2),
pps=league.pps,
apm=league.apm,
apl=round(league.apm / lpm, 2),
adpm=round(adpm := (league.vs * 0.6), 2),
adpl=round(adpm / lpm, 2),
vs=league.vs,
sprint=sprint_value,
blitz=blitz_value,
data=[[0, 0]],
split_value=0,
value_max=0,
value_min=0,
offset=0,
app=(app := (league.apm / (60 * league.pps))),
dsps=(dsps := ((league.vs / 100) - (league.apm / 60))),
dspp=(dspp := (dsps / league.pps)),
ci=150 * dspp - 125 * app + 50 * (league.vs / league.apm) - 25,
ge=2 * ((app * dsps) / league.pps),
)
) as page_hash:
return UniMessage.image(
raw=await screenshot(
urlunparse(('http', get_self_netloc(), f'/host/page/{page_hash}.html', '', '', ''))
)
)
# call back
ret_message = ''
if isinstance(league, NeverPlayedLeague):
ret_message += f'用户 {user_name} 没有排位统计数据'
else:
if isinstance(league, NeverRatedLeague):
ret_message += f'用户 {user_name} 暂未完成定级赛, 最近十场的数据:'
else:
if league.rank == 'z':
ret_message += f'用户 {user_name} 暂无段位, {round(league.rating,2)} TR'
else:
ret_message += (
f'{league.rank.upper()} 段用户 {user_name} {round(league.rating,2)} TR (#{league.standing})'
)
ret_message += f', 段位分 {round(league.glicko,2)}±{round(league.rd,2)}, 最近十场的数据:'
lpm = league.pps * 24
ret_message += f"\nL'PM: {round(lpm, 2)} ( {league.pps} pps )"
ret_message += f'\nAPM: {league.apm} ( x{round(league.apm/lpm,2)} )'
if league.vs is not None:
adpm = league.vs * 0.6
ret_message += f'\nADPM: {round(adpm,2)} ( x{round(adpm/lpm,2)} ) ( {league.vs}vs )'
user_records = await self.get_user_records()
sprint = user_records.data.records.sprint
if sprint.record is not None:
if not isinstance(sprint.record, SoloRecord):
raise WhatTheFuckError('40L记录不是单人记录')
ret_message += f'\n40L: {round(sprint.record.endcontext.final_time/1000,2)}s'
ret_message += f' ( #{sprint.rank} )' if sprint.rank is not None else ''
blitz = user_records.data.records.blitz
if blitz.record is not None:
if not isinstance(blitz.record, SoloRecord):
raise WhatTheFuckError('Blitz记录不是单人记录')
ret_message += f'\nBlitz: {blitz.record.endcontext.score}'
ret_message += f' ( #{blitz.rank} )' if blitz.rank is not None else ''
return UniMessage(ret_message)
async def get_user(self) -> None: async def get_user(self) -> None:
""" """
@@ -132,7 +229,7 @@ class Processor(ProcessorMeta):
splice_url([BASE_URL, 'users/', f'{self.user.ID or self.user.name}']) splice_url([BASE_URL, 'users/', f'{self.user.ID or self.user.name}'])
) )
user_info: UserInfo = type_validate_json(UserInfo, self.raw_response.user_info) # type: ignore[arg-type] user_info: UserInfo = type_validate_json(UserInfo, self.raw_response.user_info) # type: ignore[arg-type]
if isinstance(user_info, InfoFailed): if isinstance(user_info, FailedModel):
raise RequestError(f'用户信息请求错误:\n{user_info.error}') raise RequestError(f'用户信息请求错误:\n{user_info.error}')
self.processed_data.user_info = user_info self.processed_data.user_info = user_info
return self.processed_data.user_info return self.processed_data.user_info
@@ -144,51 +241,11 @@ class Processor(ProcessorMeta):
splice_url([BASE_URL, 'users/', f'{self.user.ID or self.user.name}/', 'records']) splice_url([BASE_URL, 'users/', f'{self.user.ID or self.user.name}/', 'records'])
) )
user_records: UserRecords = type_validate_json(UserRecords, self.raw_response.user_records) # type: ignore[arg-type] user_records: UserRecords = type_validate_json(UserRecords, self.raw_response.user_records) # type: ignore[arg-type]
if isinstance(user_records, RecordsFailed): if isinstance(user_records, FailedModel):
raise RequestError(f'用户Solo数据请求错误:\n{user_records.error}') raise RequestError(f'用户Solo数据请求错误:\n{user_records.error}')
self.processed_data.user_records = user_records self.processed_data.user_records = user_records
return self.processed_data.user_records return self.processed_data.user_records
async def generate_message(self) -> str:
"""生成消息"""
user_info = await self.get_user_info()
user_name = user_info.data.user.username.upper()
league = user_info.data.user.league
ret_message = ''
if isinstance(league, NeverPlayedLeague):
ret_message += f'用户 {user_name} 没有排位统计数据'
else:
if isinstance(league, NeverRatedLeague):
ret_message += f'用户 {user_name} 暂未完成定级赛, 最近十场的数据:'
else:
if league.rank == 'z':
ret_message += f'用户 {user_name} 暂无段位, {round(league.rating,2)} TR'
else:
ret_message += (
f'{league.rank.upper()} 段用户 {user_name} {round(league.rating,2)} TR (#{league.standing})'
)
ret_message += f', 段位分 {round(league.glicko,2)}±{round(league.rd,2)}, 最近十场的数据:'
lpm = league.pps * 24
ret_message += f"\nL'PM: {round(lpm, 2)} ( {league.pps} pps )"
ret_message += f'\nAPM: {league.apm} ( x{round(league.apm/(league.pps*24),2)} )'
if league.vs is not None:
adpm = league.vs * 0.6
ret_message += f'\nADPM: {round(adpm,2)} ( x{round(adpm/lpm,2)} ) ( {league.vs}vs )'
user_records = await self.get_user_records()
sprint = user_records.data.records.sprint
if sprint.record is not None:
if not isinstance(sprint.record, SoloRecord):
raise WhatTheFuckError('40L记录不是单人记录')
ret_message += f'\n40L: {round(sprint.record.endcontext.final_time/1000,2)}s'
ret_message += f' ( #{sprint.rank} )' if sprint.rank is not None else ''
blitz = user_records.data.records.blitz
if blitz.record is not None:
if not isinstance(blitz.record, SoloRecord):
raise WhatTheFuckError('Blitz记录不是单人记录')
ret_message += f'\nBlitz: {blitz.record.endcontext.score}'
ret_message += f' ( #{blitz.rank} )' if blitz.rank is not None else ''
return ret_message
@scheduler.scheduled_job('cron', hour='0,6,12,18', minute=0) @scheduler.scheduled_job('cron', hour='0,6,12,18', minute=0)
@retry(exception_type=RequestError, delay=timedelta(minutes=15)) @retry(exception_type=RequestError, delay=timedelta(minutes=15))
@@ -197,7 +254,7 @@ async def get_io_rank_data() -> None:
LeagueAll, # type: ignore[arg-type] LeagueAll, # type: ignore[arg-type]
(data := await Cache.get(splice_url([BASE_URL, 'users/lists/league/all']))), (data := await Cache.get(splice_url([BASE_URL, 'users/lists/league/all']))),
) )
if isinstance(league_all, LeagueAllFailed): if isinstance(league_all, FailedModel):
raise RequestError(f'排行榜数据请求错误:\n{league_all.error}') raise RequestError(f'排行榜数据请求错误:\n{league_all.error}')
def pps(user: LeagueAllUser) -> float: def pps(user: LeagueAllUser) -> float:

View File

@@ -2,6 +2,7 @@ from arclet.alconna import Alconna, AllParam, Arg, ArgFlag, Args, CommandMeta, O
from nonebot.adapters import Bot, Event from nonebot.adapters import Bot, Event
from nonebot.matcher import Matcher from nonebot.matcher import Matcher
from nonebot_plugin_alconna import At, on_alconna from nonebot_plugin_alconna import At, on_alconna
from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_orm import get_session from nonebot_plugin_orm import get_session
from nonebot_plugin_userinfo import BotUserInfo, EventUserInfo, UserInfo # type: ignore[import-untyped] from nonebot_plugin_userinfo import BotUserInfo, EventUserInfo, UserInfo # type: ignore[import-untyped]
@@ -111,10 +112,11 @@ async def _(bot: Bot, event: Event, matcher: Matcher, target: At | Me):
command_args=[], command_args=[],
) )
try: try:
await matcher.finish(message + await proc.handle_query()) await (UniMessage(message) + await proc.handle_query()).send()
except NeedCatchError as e: except NeedCatchError as e:
await matcher.send(str(e)) await matcher.send(str(e))
raise HandleNotFinishedError from e raise HandleNotFinishedError from e
await matcher.finish()
@alc.assign('query') @alc.assign('query')
@@ -125,10 +127,11 @@ async def _(event: Event, matcher: Matcher, account: User):
command_args=[], command_args=[],
) )
try: try:
await matcher.finish(await proc.handle_query()) await (await proc.handle_query()).send()
except NeedCatchError as e: except NeedCatchError as e:
await matcher.send(str(e)) await matcher.send(str(e))
raise HandleNotFinishedError from e raise HandleNotFinishedError from e
await matcher.finish()
add_default_handlers(alc) add_default_handlers(alc)

View File

@@ -10,6 +10,7 @@ from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_orm import get_session from nonebot_plugin_orm import get_session
from nonebot_plugin_userinfo import UserInfo # type: ignore[import-untyped] from nonebot_plugin_userinfo import UserInfo # type: ignore[import-untyped]
from pandas import read_html from pandas import read_html
from typing_extensions import override
from ...db import BindStatus, create_or_update_bind from ...db import BindStatus, create_or_update_bind
from ...utils.avatar import get_avatar from ...utils.avatar import get_avatar
@@ -30,6 +31,7 @@ class User(BaseUser):
name: str name: str
@property @property
@override
def unique_identifier(self) -> str: def unique_identifier(self) -> str:
return self.name return self.name
@@ -57,15 +59,18 @@ class Processor(ProcessorMeta):
raw_response: RawResponse raw_response: RawResponse
processed_data: ProcessedData processed_data: ProcessedData
@override
def __init__(self, event_id: int, user: User, command_args: list[str]) -> None: def __init__(self, event_id: int, user: User, command_args: list[str]) -> None:
super().__init__(event_id, user, command_args) super().__init__(event_id, user, command_args)
self.raw_response = RawResponse() self.raw_response = RawResponse()
self.processed_data = ProcessedData() self.processed_data = ProcessedData()
@property @property
@override
def game_platform(self) -> Literal['TOP']: def game_platform(self) -> Literal['TOP']:
return GAME_TYPE return GAME_TYPE
@override
async def handle_bind(self, platform: str, account: str, bot_info: UserInfo, user_info: UserInfo) -> UniMessage: async def handle_bind(self, platform: str, account: str, bot_info: UserInfo, user_info: UserInfo) -> UniMessage:
"""处理绑定消息""" """处理绑定消息"""
self.command_type = 'bind' self.command_type = 'bind'
@@ -99,11 +104,26 @@ class Processor(ProcessorMeta):
) )
return message return message
async def handle_query(self) -> str: @override
async def handle_query(self) -> UniMessage:
"""处理查询消息""" """处理查询消息"""
self.command_type = 'query' self.command_type = 'query'
await self.check_user() await self.check_user()
return await self.generate_message() game_data = await self.get_game_data()
message = ''
if game_data.day is not None:
message += f'用户 {self.user.name} 24小时内统计数据为: '
message += f"\nL'PM: {round(game_data.day.lpm,2)} ( {round(game_data.day.lpm/24,2)} pps )"
message += f'\nAPM: {round(game_data.day.apm,2)} ( x{round(game_data.day.apm/game_data.day.lpm,2)} )'
else:
message += f'用户 {self.user.name} 暂无24小时内统计数据'
if game_data.total is not None:
message += '\n历史统计数据为: '
message += f"\nL'PM: {round(game_data.total.lpm,2)} ( {round(game_data.total.lpm/24,2)} pps )"
message += f'\nAPM: {round(game_data.total.apm,2)} ( x{round(game_data.total.apm/game_data.total.lpm,2)} )'
else:
message += '\n暂无历史统计数据'
return UniMessage(message)
async def get_user_profile(self) -> str: async def get_user_profile(self) -> str:
"""获取用户信息""" """获取用户信息"""
@@ -140,21 +160,3 @@ class Processor(ProcessorMeta):
dataframe = read_html(table, encoding='utf-8', header=0)[0] dataframe = read_html(table, encoding='utf-8', header=0)[0]
total = Data(lpm=dataframe['lpm'].mean(), apm=dataframe['apm'].mean()) if len(dataframe) != 0 else None total = Data(lpm=dataframe['lpm'].mean(), apm=dataframe['apm'].mean()) if len(dataframe) != 0 else None
return GameData(day=day, total=total) return GameData(day=day, total=total)
async def generate_message(self) -> str:
"""生成消息"""
game_data = await self.get_game_data()
message = ''
if game_data.day is not None:
message += f'用户 {self.user.name} 24小时内统计数据为: '
message += f"\nL'PM: {round(game_data.day.lpm,2)} ( {round(game_data.day.lpm/24,2)} pps )"
message += f'\nAPM: {round(game_data.day.apm,2)} ( x{round(game_data.day.apm/game_data.day.lpm,2)} )'
else:
message += f'用户 {self.user.name} 暂无24小时内统计数据'
if game_data.total is not None:
message += '\n历史统计数据为: '
message += f"\nL'PM: {round(game_data.total.lpm,2)} ( {round(game_data.total.lpm/24,2)} pps )"
message += f'\nAPM: {round(game_data.total.apm,2)} ( x{round(game_data.total.apm/game_data.total.lpm,2)} )'
else:
message += '\n暂无历史统计数据'
return message

View File

@@ -4,6 +4,7 @@ from arclet.alconna import Alconna, AllParam, Arg, ArgFlag, Args, CommandMeta, O
from nonebot.adapters import Bot, Event from nonebot.adapters import Bot, Event
from nonebot.matcher import Matcher from nonebot.matcher import Matcher
from nonebot_plugin_alconna import At, on_alconna from nonebot_plugin_alconna import At, on_alconna
from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_orm import get_session from nonebot_plugin_orm import get_session
from nonebot_plugin_userinfo import BotUserInfo, UserInfo # type: ignore[import-untyped] from nonebot_plugin_userinfo import BotUserInfo, UserInfo # type: ignore[import-untyped]
@@ -72,12 +73,13 @@ alc = on_alconna(
async def finish_special_query(matcher: Matcher, proc: Processor) -> NoReturn: async def finish_special_query(matcher: Matcher, proc: Processor) -> NoReturn:
try: try:
await matcher.finish(await proc.handle_query()) await (await proc.handle_query()).send()
except NeedCatchError as e: except NeedCatchError as e:
if isinstance(e, RequestError) and '未找到此用户' in e.message: if isinstance(e, RequestError) and '未找到此用户' in e.message:
matcher.skip() matcher.skip()
await matcher.send(str(e)) await matcher.send(str(e))
raise HandleNotFinishedError from e raise HandleNotFinishedError from e
await matcher.finish()
try: try:
@@ -165,10 +167,11 @@ async def _(bot: Bot, event: Event, matcher: Matcher, target: At | Me):
command_args=[], command_args=[],
) )
try: try:
await matcher.finish(message + await proc.handle_query()) await (UniMessage(message) + await proc.handle_query()).send()
except NeedCatchError as e: except NeedCatchError as e:
await matcher.send(str(e)) await matcher.send(str(e))
raise HandleNotFinishedError from e raise HandleNotFinishedError from e
await matcher.finish()
@alc.assign('query') @alc.assign('query')
@@ -179,10 +182,11 @@ async def _(event: Event, matcher: Matcher, account: User):
command_args=[], command_args=[],
) )
try: try:
await matcher.finish(await proc.handle_query()) await (await proc.handle_query()).send()
except NeedCatchError as e: except NeedCatchError as e:
await matcher.send(str(e)) await matcher.send(str(e))
raise HandleNotFinishedError from e raise HandleNotFinishedError from e
await matcher.finish()
add_default_handlers(alc) add_default_handlers(alc)

View File

@@ -8,6 +8,7 @@ from nonebot.compat import type_validate_json
from nonebot_plugin_alconna.uniseg import UniMessage from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_orm import get_session from nonebot_plugin_orm import get_session
from nonebot_plugin_userinfo import UserInfo as NBUserInfo # type: ignore[import-untyped] from nonebot_plugin_userinfo import UserInfo as NBUserInfo # type: ignore[import-untyped]
from typing_extensions import override
from ...db import BindStatus, create_or_update_bind from ...db import BindStatus, create_or_update_bind
from ...utils.avatar import get_avatar from ...utils.avatar import get_avatar
@@ -32,6 +33,7 @@ class User(BaseUser):
name: str | None = None name: str | None = None
@property @property
@override
def unique_identifier(self) -> str: def unique_identifier(self) -> str:
if self.teaid is None: if self.teaid is None:
raise ValueError('不完整的User!') raise ValueError('不完整的User!')
@@ -70,15 +72,18 @@ class Processor(ProcessorMeta):
raw_response: RawResponse raw_response: RawResponse
processed_data: ProcessedData processed_data: ProcessedData
@override
def __init__(self, event_id: int, user: User, command_args: list[str]) -> None: def __init__(self, event_id: int, user: User, command_args: list[str]) -> None:
super().__init__(event_id, user, command_args) super().__init__(event_id, user, command_args)
self.raw_response = RawResponse(user_profile={}) self.raw_response = RawResponse(user_profile={})
self.processed_data = ProcessedData(user_profile={}) self.processed_data = ProcessedData(user_profile={})
@property @property
@override
def game_platform(self) -> Literal['TOS']: def game_platform(self) -> Literal['TOS']:
return GAME_TYPE return GAME_TYPE
@override
async def handle_bind(self, platform: str, account: str, bot_info: NBUserInfo) -> UniMessage: async def handle_bind(self, platform: str, account: str, bot_info: NBUserInfo) -> UniMessage:
"""处理绑定消息""" """处理绑定消息"""
self.command_type = 'bind' self.command_type = 'bind'
@@ -113,11 +118,29 @@ class Processor(ProcessorMeta):
) )
return message return message
async def handle_query(self) -> str: @override
async def handle_query(self) -> UniMessage:
"""处理查询消息""" """处理查询消息"""
self.command_type = 'query' self.command_type = 'query'
await self.get_user() await self.get_user()
return await self.generate_message() user_info = (await self.get_user_info()).data
message = f'用户 {user_info.name} ({user_info.teaid}) '
if user_info.ranked_games == '0':
message += '暂无段位统计数据'
else:
message += f', 段位分 {round(float(user_info.rating_now),2)}±{round(float(user_info.rd_now),2)} ({round(float(user_info.vol_now),2)}) '
game_data = await self.get_game_data()
if game_data is None:
message += ', 暂无游戏数据'
else:
message += f', 最近 {game_data.num} 局数据'
message += f"\nL'PM: {game_data.lpm} ( {game_data.pps} pps )"
message += f'\nAPM: {game_data.apm} ( x{game_data.apl} )'
message += f'\nADPM: {game_data.adpm} ( x{game_data.adpl} ) ( {game_data.vs}vs )'
message += f'\n40L: {float(user_info.pb_sprint)/1000:.2f}s' if user_info.pb_sprint != '2147483647' else ''
message += f'\nMarathon: {user_info.pb_marathon}' if user_info.pb_marathon != '0' else ''
message += f'\nChallenge: {user_info.pb_challenge}' if user_info.pb_challenge != '0' else ''
return UniMessage(message)
async def get_user(self) -> None: async def get_user(self) -> None:
""" """
@@ -228,24 +251,3 @@ class Processor(ProcessorMeta):
adpl=round((adpm / lpm), 2), adpl=round((adpm / lpm), 2),
vs=round((adpm / 60 * 100), 2), vs=round((adpm / 60 * 100), 2),
) )
async def generate_message(self) -> str:
"""生成消息"""
user_info = (await self.get_user_info()).data
message = f'用户 {user_info.name} ({user_info.teaid}) '
if user_info.ranked_games == '0':
message += '暂无段位统计数据'
else:
message += f', 段位分 {round(float(user_info.rating_now),2)}±{round(float(user_info.rd_now),2)} ({round(float(user_info.vol_now),2)}) '
game_data = await self.get_game_data()
if game_data is None:
message += ', 暂无游戏数据'
else:
message += f', 最近 {game_data.num} 局数据'
message += f"\nL'PM: {game_data.lpm} ( {game_data.pps} pps )"
message += f'\nAPM: {game_data.apm} ( x{game_data.apl} )'
message += f'\nADPM: {game_data.adpm} ( x{game_data.adpl} ) ( {game_data.vs}vs )'
message += f'\n40L: {float(user_info.pb_sprint)/1000:.2f}s' if user_info.pb_sprint != '2147483647' else ''
message += f'\nMarathon: {user_info.pb_marathon}' if user_info.pb_marathon != '0' else ''
message += f'\nChallenge: {user_info.pb_challenge}' if user_info.pb_challenge != '0' else ''
return message

View File

@@ -67,7 +67,7 @@
text-align: right; text-align: right;
} }
#main-content { .main-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -77,21 +77,22 @@
font-family: 'CabinetGrotesk-Variable'; font-family: 'CabinetGrotesk-Variable';
} }
#account-box { .account-box {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
#info-box { .info-box {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
margin-top: 10px; margin-top: 10px;
} }
#user-info-box { .user-info-box {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: space-between;
padding: 25px; padding: 25px;
gap: 10px; gap: 10px;
@@ -103,14 +104,14 @@
box-sizing: border-box; box-sizing: border-box;
} }
#user-avatar { .user-avatar {
width: 125px; width: 125px;
height: 125px; height: 125px;
filter: drop-shadow(0px 11px 23px rgba(0, 0, 0, 0.22)); filter: drop-shadow(0px 11px 23px rgba(0, 0, 0, 0.22));
border-radius: 65px; border-radius: 65px;
} }
#user-name { .user-name {
font-weight: 800; font-weight: 800;
font-size: 25px; font-size: 25px;
line-height: 31px; line-height: 31px;
@@ -118,7 +119,7 @@
color: #000000; color: #000000;
} }
#user-sign { .user-sign {
width: 225px; width: 225px;
height: 66px; height: 66px;
@@ -132,7 +133,7 @@
color: #000000; color: #000000;
} }
#game-info-box { .game-info-box {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
@@ -147,19 +148,19 @@
box-sizing: border-box; box-sizing: border-box;
} }
#game-type-box { .game-type-box {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
#game-logo { .game-logo {
width: 60px; width: 60px;
height: 60px; height: 60px;
border-radius: 10px; border-radius: 10px;
} }
#game-name { .game-name {
font-weight: 800; font-weight: 800;
font-size: 30px; font-size: 30px;
line-height: 37px; line-height: 37px;
@@ -167,18 +168,18 @@
color: #000000; color: #000000;
} }
#game-info-dividing-line { .game-info-dividing-line {
width: 225px; width: 225px;
border: 1px solid #bababa; border: 1px solid #bababa;
transform: rotate(0.25deg); transform: rotate(0.25deg);
} }
#ranking-info-box { .ranking-info-box {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
#ranking-title { .ranking-title {
font-weight: 800; font-weight: 800;
font-size: 25px; font-size: 25px;
line-height: 31px; line-height: 31px;
@@ -186,7 +187,7 @@
color: #000000; color: #000000;
} }
#ranking { .ranking {
font-weight: 400; font-weight: 400;
font-size: 50px; font-size: 50px;
line-height: 120%; line-height: 120%;
@@ -194,7 +195,7 @@
color: #000000; color: #000000;
} }
#rd { .rd {
margin-top: -16px; margin-top: -16px;
font-weight: 300; font-weight: 300;
@@ -214,7 +215,7 @@
background: linear-gradient(222.34deg, #525252 11.97%, #1d1916 89.73%); background: linear-gradient(222.34deg, #525252 11.97%, #1d1916 89.73%);
} }
#TR-title { .TR-title {
position: absolute; position: absolute;
margin-left: 24px; margin-left: 24px;
margin-top: 19px; margin-top: 19px;
@@ -227,7 +228,7 @@
color: #fafafa; color: #fafafa;
} }
#rank-icon { .rank-icon {
position: absolute; position: absolute;
margin-left: 27px; margin-left: 27px;
margin-top: 90px; margin-top: 90px;
@@ -236,7 +237,7 @@
height: 50px; height: 50px;
} }
#TR { .TR {
position: absolute; position: absolute;
margin-left: 24px; margin-left: 24px;
margin-top: 143px; margin-top: 143px;
@@ -248,14 +249,14 @@
color: #fafafa; color: #fafafa;
} }
#multiplayer-box { .multiplayer-box {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
margin-top: 14px; margin-top: 14px;
} }
#multiplayer-data-box { .multiplayer-data-box {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
@@ -268,84 +269,102 @@
margin-top: 0px; margin-top: 0px;
} }
#lpm-box { .lpm-box {
background-image: url('../static/data/LPM.svg'); background-image: url('../static/data/LPM.svg');
} }
#lpm-value { .lpm-value {
color: #4d7d0f; color: #4d7d0f;
} }
#pps-value { .pps-value {
color: #4d7d0f; color: #4d7d0f;
} }
#apm-box { .apm-box {
background-image: url('../static/data/APM.svg'); background-image: url('../static/data/APM.svg');
} }
#apm-value { .apm-value {
color: #b5530a; color: #b5530a;
} }
#apl-value { .apl-value {
color: #b5530a; color: #b5530a;
} }
#adpm-box { .adpm-box {
background-image: url('../static/data/ADPM.svg'); background-image: url('../static/data/ADPM.svg');
} }
#adpm-value { .adpm-value {
color: #235db4; color: #235db4;
} }
#vs-value { .vs-value {
top: 62px; top: 62px;
color: #4779c6; color: #4779c6;
} }
#adpl-value { .adpl-value {
color: #4779c6; color: #4779c6;
} }
.radar-chart-box {
display: flex;
flex-direction: column;
}
.radar-background {
background: linear-gradient(222.34deg, #525252 11.97%, #1d1916 89.73%),
linear-gradient(222.34deg, #4f9dff 11.97%, #2563ea 89.73%);
}
#radar-chart { #radar-chart {
width: 275px; width: 275px;
height: 275px; height: 275px;
background: linear-gradient(222.34deg, #525252 11.97%, #1d1916 89.73%),
linear-gradient(222.34deg, #4f9dff 11.97%, #2563ea 89.73%);
} }
#singleplayer-box { .radar-description {
text-align: left;
padding-left: 20px;
padding-top: 15px;
font-size: 12px;
color: #fafafa;
box-sizing: border-box;
}
.singleplayer-box {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-content: space-between; align-content: space-between;
margin-top: 14px; margin-top: 14px;
} }
#sprint-box { .sprint-box {
background-image: url('../static/data/40L.svg'); background-image: url('../static/data/40L.svg');
} }
#blitz-box { .blitz-box {
background-image: url('../static/data/Blitz.svg'); background-image: url('../static/data/Blitz.svg');
} }
#sprint-value { .sprint-value {
color: #b42323; color: #b42323;
} }
#blitz-value { .blitz-value {
color: #8e23b4; color: #8e23b4;
} }
#footer { .footer {
display: flex; display: flex;
justify-content: center; justify-content: center;
margin-top: 20px; margin-top: 20px;
margin-bottom: 20px; margin-bottom: 20px;
font-family: '26FGalaxySans-ObliqueVF'; font-family: '26FGalaxySans-ObliqueVF';
font-size: 32px; font-size: 30px;
font-weight: 257; font-weight: 257;
line-height: 120%;
text-align: center; text-align: center;
} }

View File

@@ -8,75 +8,84 @@
</head> </head>
<body> <body>
<div id="main-content"> <div class="main-content">
<span class="big-title">Account&Rankings</span> <span class="big-title">Account&Rankings</span>
<div id="account-box"> <div class="account-box">
<div id="info-box"> <div class="info-box">
<div class="flex-gap"></div> <div class="flex-gap"></div>
<div class="box-shadow box-rounded-corners" id="user-info-box"> <div class="box-shadow box-rounded-corners user-info-box">
<img id="user-avatar" src="{{user_avatar}}" /> <div class="flex-gap"></div>
<div id="user-name">{{user_name}}</div> <img class="user-avatar" src="{{user_avatar}}" />
<div id="user-sign">“{{user_sign}}”</div> <div class="flex-gap"></div>
<div class="user-name">{{user_name}}</div>
<div class="flex-gap"></div>
{% if user_sign is not none %}
<div class="user-sign">“{{user_sign}}”</div>
{% endif %}
</div> </div>
<div class="flex-gap"></div> <div class="flex-gap"></div>
<div class="box-shadow box-rounded-corners" id="game-info-box"> <div class="box-shadow box-rounded-corners game-info-box">
<div id="game-type-box"> <div class="game-type-box">
<img id="game-logo" src="../../static/static/logo/{{game_type}}.svg" /> <img class="game-logo" src="../../static/static/logo/{{game_type}}.svg" />
<span id="game-name">{{game_type}}</span> <span class="game-name">{{game_type}}</span>
</div> </div>
<div id="game-info-dividing-line"></div> <div class="game-info-dividing-line"></div>
<div id="ranking-info-box"> <div class="ranking-info-box">
<span id="ranking-title">Ranking</span> <span class="ranking-title">Ranking</span>
<span id="ranking">{{ranking}}</span> <span class="ranking">{{ranking}}</span>
<span id="rd">±{{rd}}</span> <span class="rd">±{{rd}}</span>
</div> </div>
</div> </div>
<div class="flex-gap"></div> <div class="flex-gap"></div>
</div> </div>
<div class="chart-shadow box-rounded-corners" id="TR-curve-chart"> {# <div class="chart-shadow box-rounded-corners" id="TR-curve-chart">
<span id="TR-title">Tetra Rating (TR)</span> <span class="TR-title">Tetra Rating (TR)</span>
<img id="rank-icon" src="../../static/static/rank/{{rank}}.svg" /> <img class="rank-icon" src="../../static/static/rank/{{rank}}.svg" />
<span id="TR" style="display: flex; align-items: flex-end" <span class="TR" style="display: flex; align-items: flex-end"
>{{TR}}&nbsp; >{{TR}}&nbsp;
<p style="font-size: 30px; font-weight: 400; line-height: 47px">(#{{global_rank}})</p> <p style="font-size: 30px; font-weight: 400; line-height: 47px">(#{{global_rank}})</p>
</span> </span>
</div> </div> #}
</div> </div>
<span class="big-title">Multiplayer Stats</span> <span class="big-title">Multiplayer Stats</span>
<div id="multiplayer-box"> <div class="multiplayer-box">
<div class="flex-gap"></div> <div class="flex-gap"></div>
<div id="multiplayer-data-box"> <div class="multiplayer-data-box">
<div class="multiplayer-data small-data-box box-shadow box-rounded-corners" id="lpm-box"> <div class="multiplayer-data small-data-box box-shadow box-rounded-corners lpm-box">
<span class="big-data-value" id="lpm-value">{{lpm}}</span> <span class="big-data-value lpm-value">{{lpm}}</span>
<span class="small-data-value" id="pps-value">{{pps}} pps</span> <span class="small-data-value pps-value">{{pps}} pps</span>
</div> </div>
<div class="multiplayer-data small-data-box box-shadow box-rounded-corners" id="apm-box"> <div class="multiplayer-data small-data-box box-shadow box-rounded-corners apm-box">
<span class="big-data-value" id="apm-value">{{apm}}</span> <span class="big-data-value apm-value">{{apm}}</span>
<span class="small-data-value" id="apl-value">x{{apl}}</span> <span class="small-data-value apl-value">x{{apl}}</span>
</div> </div>
<div class="multiplayer-data small-data-box box-shadow box-rounded-corners" id="adpm-box"> <div class="multiplayer-data small-data-box box-shadow box-rounded-corners adpm-box">
<span class="big-data-value" id="adpm-value">{{adpm}}</span> <span class="big-data-value adpm-value">{{adpm}}</span>
<span class="small-data-value" id="adpl-value">x{{adpl}}</span> <span class="small-data-value adpl-value">x{{adpl}}</span>
<span class="small-data-value" id="vs-value">{{vs}} vs</span> <span class="small-data-value vs-value">{{vs}} vs</span>
</div> </div>
</div> </div>
<div class="flex-gap"></div> <div class="flex-gap"></div>
<div class="chart-shadow box-rounded-corners" id="radar-chart"></div> <div class="radar-chart-box">
<div class="chart-shadow box-rounded-corners radar-background" id="radar-chart"></div>
<div class="flex-gap"></div>
<div class="chart-shadow box-rounded-corners small-data-box radar-background radar-description"><p style="font-size: 18px;display: inline;">tips: </p><br />DSPS 每秒挖掘<br />DSPP 每块挖掘<br />CI 奶酪指数<br />GE 垃圾利用率</div>
</div>
<div class="flex-gap"></div> <div class="flex-gap"></div>
</div> </div>
<span class="big-title">Singleplayer Stats</span> <span class="big-title">Singleplayer Stats</span>
<div id="singleplayer-box"> <div class="singleplayer-box">
<div class="flex-gap"></div> <div class="flex-gap"></div>
<div class="small-data-box box-shadow box-rounded-corners" id="sprint-box"> <div class="small-data-box box-shadow box-rounded-corners sprint-box">
<span class="big-data-value" id="sprint-value">{{sprint}}</span> <span class="big-data-value sprint-value">{{sprint}}</span>
</div> </div>
<div class="flex-gap"></div> <div class="flex-gap"></div>
<div class="small-data-box box-shadow box-rounded-corners" id="blitz-box"> <div class="small-data-box box-shadow box-rounded-corners blitz-box">
<span class="big-data-value" id="blitz-value">{{blitz}}</span> <span class="big-data-value blitz-value">{{blitz}}</span>
</div> </div>
<div class="flex-gap"></div> <div class="flex-gap"></div>
</div> </div>
<div id="footer">Powered by<br />Nonebot2 x nonebot-plugin-tetris-stats</div> <div class="footer">Powered by<br />Nonebot2 x nonebot-plugin-tetris-stats</div>
</div> </div>
</body> </body>
@@ -279,8 +288,8 @@
indicator: [ indicator: [
{ name: 'PPS' }, { name: 'PPS' },
{ name: 'APP', nameRotate: 60 }, { name: 'APP', nameRotate: 60 },
{ name: 'DSPP', nameRotate: -60 }, { name: 'DSPS', nameRotate: -60 },
{ name: 'OR' }, { name: 'DSPP' },
{ name: 'CI', nameRotate: 60 }, { name: 'CI', nameRotate: 60 },
{ name: 'GE', nameRotate: -60 }, { name: 'GE', nameRotate: -60 },
], ],
@@ -341,7 +350,7 @@
}, },
data: [ data: [
{ {
value: [{{pps}}, {{app}}, {{dspp}}, {{OR}}, {{ci}}, {{ge}}], value: [{{pps}}, {{app}}, {{dsps}}, {{dspp}}, {{ci}}, {{ge}}],
}, },
], ],
}, },

View File

@@ -45,7 +45,7 @@ class BrowserManager:
logger.error('安装/更新 playwright 浏览器失败') logger.error('安装/更新 playwright 浏览器失败')
try: try:
await cls._start_browser() await cls._start_browser()
except BaseException as e: # noqa: BLE001 不知道会有什么异常, 交给用户解决 except BaseException as e: # 不知道会有什么异常, 交给用户解决
raise ImportError( raise ImportError(
'playwright 启动失败, 请尝试在命令行运行 playwright install-deps firefox, 如果仍然启动失败, 请参考上面的报错👆' 'playwright 启动失败, 请尝试在命令行运行 playwright install-deps firefox, 如果仍然启动失败, 请参考上面的报错👆'
) from e ) from e

View File

@@ -34,7 +34,7 @@ async def render(
*, *,
user_avatar: str, user_avatar: str,
user_name: str, user_name: str,
user_sign: str, user_sign: str | None,
game_type: Literal['TETR.IO'], game_type: Literal['TETR.IO'],
ranking: str | float, ranking: str | float,
rd: str | float, rd: str | float,
@@ -56,8 +56,8 @@ async def render(
value_min: int, value_min: int,
offset: int, offset: int,
app: str | float, app: str | float,
dsps: str | float,
dspp: str | float, dspp: str | float,
OR: str | float, # noqa: N803
ci: str | float, ci: str | float,
ge: str | float, ge: str | float,
) -> str: ... ) -> str: ...

19
poetry.lock generated
View File

@@ -1141,13 +1141,13 @@ nonebot2 = ">=2.2.0"
[[package]] [[package]]
name = "nonebot-plugin-alconna" name = "nonebot-plugin-alconna"
version = "0.45.2" version = "0.45.3"
description = "Alconna Adapter for Nonebot" description = "Alconna Adapter for Nonebot"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
files = [ files = [
{file = "nonebot_plugin_alconna-0.45.2-py3-none-any.whl", hash = "sha256:38461346f52479c3133d9a789cca1d280a05834790dbadeecfc88639a1e326a2"}, {file = "nonebot_plugin_alconna-0.45.3-py3-none-any.whl", hash = "sha256:1e6ff5e99464ea2acad03df8b9fc48949b7d40a7c8d1976c53a934eafb8d3bf0"},
{file = "nonebot_plugin_alconna-0.45.2.tar.gz", hash = "sha256:a54bd294c0a829fcd344a3d3f5b9b040c1dcdbe739245557037cca019187d031"}, {file = "nonebot_plugin_alconna-0.45.3.tar.gz", hash = "sha256:7667df82fdae02842b0fa28b39d61daf501f1af41d6fecf288fb8bb38a35ff9d"},
] ]
[package.dependencies] [package.dependencies]
@@ -2061,6 +2061,17 @@ typing-extensions = ">=4.5,<5.0"
[package.extras] [package.extras]
test = ["beautifulsoup4 (>=4.8,<5.0)", "html5lib (==1.1)", "lxml (>=4.9)", "mypy (==1.9.*)", "pyright (>=1.1.289)", "pytest (>=7.0,<9)", "pytest-mypy-plugins (==1.11.1)", "tox (>=4.0,<5.0)", "typeguard (>=3.0,<5)"] test = ["beautifulsoup4 (>=4.8,<5.0)", "html5lib (==1.1)", "lxml (>=4.9)", "mypy (==1.9.*)", "pyright (>=1.1.289)", "pytest (>=7.0,<9)", "pytest-mypy-plugins (==1.11.1)", "tox (>=4.0,<5.0)", "typeguard (>=3.0,<5)"]
[[package]]
name = "types-pillow"
version = "10.2.0.20240423"
description = "Typing stubs for Pillow"
optional = false
python-versions = ">=3.8"
files = [
{file = "types-Pillow-10.2.0.20240423.tar.gz", hash = "sha256:696e68b9b6a58548fc307a8669830469237c5b11809ddf978ac77fafa79251cd"},
{file = "types_Pillow-10.2.0.20240423-py3-none-any.whl", hash = "sha256:bd12923093b96c91d523efcdb66967a307f1a843bcfaf2d5a529146c10a9ced3"},
]
[[package]] [[package]]
name = "types-pytz" name = "types-pytz"
version = "2024.1.0.20240417" version = "2024.1.0.20240417"
@@ -2679,4 +2690,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 = "c568aeaa3fa7dc9049bb95fb3f682754aff0be497fa7e097c51d1becb90a84a5" content-hash = "b850c0a22d448d72056fadd2f5c0a5b3c72c44a081e6a4b6a4c70c90177b83e3"

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = 'nonebot-plugin-tetris-stats' name = 'nonebot-plugin-tetris-stats'
version = '1.0.0' version = '1.0.1'
description = '一款基于 NoneBot2 的用于查询 Tetris 相关游戏数据的插件' description = '一款基于 NoneBot2 的用于查询 Tetris 相关游戏数据的插件'
authors = ['scdhh <wallfjjd@gmail.com>'] authors = ['scdhh <wallfjjd@gmail.com>']
readme = 'README.md' readme = 'README.md'
@@ -39,6 +39,7 @@ nonebot-adapter-onebot = "^2.4.1"
nonebot-adapter-satori = "^0.11.4" nonebot-adapter-satori = "^0.11.4"
nonebot-adapter-kaiheila = "^0.3.4" nonebot-adapter-kaiheila = "^0.3.4"
nonebot-adapter-discord = "^0.1.3" nonebot-adapter-discord = "^0.1.3"
types-pillow = "^10.2.0.20240423"
[tool.poetry.group.debug.dependencies] [tool.poetry.group.debug.dependencies]
objprint = '^0.2.2' objprint = '^0.2.2'
@@ -49,6 +50,10 @@ requires = ['poetry-core>=1.0.0']
build-backend = 'poetry.core.masonry.api' build-backend = 'poetry.core.masonry.api'
[tool.ruff] [tool.ruff]
line-length = 120
target-version = "py310"
[tool.ruff.lint]
select = [ select = [
'F', # pyflakes 'F', # pyflakes
'E', # pycodestyle errors 'E', # pycodestyle errors
@@ -89,14 +94,12 @@ ignore = [
'ANN202', # 向 NoneBot 注册的函数 'ANN202', # 向 NoneBot 注册的函数
'TRY003', 'TRY003',
] ]
line-length = 120
target-version = "py310"
flake8-quotes = { inline-quotes = 'single', multiline-quotes = 'double' } flake8-quotes = { inline-quotes = 'single', multiline-quotes = 'double' }
[tool.ruff.flake8-annotations] [tool.ruff.lint.flake8-annotations]
mypy-init-return = true mypy-init-return = true
[tool.ruff.flake8-builtins] [tool.ruff.lint.flake8-builtins]
builtins-ignorelist = ["id"] builtins-ignorelist = ["id"]
[tool.ruff.format] [tool.ruff.format]