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 sqlalchemy import JSON, DateTime, Dialect, PickleType, String, TypeDecorator
from sqlalchemy.orm import Mapped, MappedAsDataclass, mapped_column
from typing_extensions import override
from ..game_data_processor.schemas import BaseProcessedData, BaseUser
from ..utils.typing import CommandType, GameType
@@ -16,17 +17,20 @@ from ..utils.typing import CommandType, GameType
class PydanticType(TypeDecorator):
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
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
if isinstance(value, tuple(self.get_model())):
return value.json() # type: ignore[union-attr]
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 模型实例
if isinstance(value, str | bytes):
for i in self.get_model():

View File

@@ -55,15 +55,10 @@ class Processor(ABC):
raise NotImplementedError
@abstractmethod
async def handle_query(self) -> str:
async def handle_query(self) -> UniMessage:
"""处理查询消息"""
raise NotImplementedError
@abstractmethod
async def generate_message(self) -> str:
"""生成消息"""
raise NotImplementedError
def __del__(self) -> None:
finish_time = datetime.now(tz=UTC)
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.matcher import Matcher
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_userinfo import BotUserInfo, UserInfo # type: ignore[import-untyped]
from sqlalchemy import func, select
@@ -117,10 +118,11 @@ async def _(bot: Bot, event: Event, matcher: Matcher, target: At | Me):
command_args=[],
)
try:
await matcher.finish(message + await proc.handle_query())
await (UniMessage(message) + await proc.handle_query()).send()
except NeedCatchError as e:
await matcher.send(str(e))
raise HandleNotFinishedError from e
await matcher.finish()
@alc.assign('query')
@@ -131,10 +133,11 @@ async def _(event: Event, matcher: Matcher, account: User):
command_args=[],
)
try:
await matcher.finish(await proc.handle_query())
await (await proc.handle_query()).send()
except NeedCatchError as e:
await matcher.send(str(e))
raise HandleNotFinishedError from e
await matcher.finish()
@alc.assign('rank')

View File

@@ -1,3 +1,4 @@
from asyncio import gather
from collections import defaultdict
from collections.abc import Callable
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_userinfo import UserInfo as NBUserInfo # type: ignore[import-untyped]
from sqlalchemy import select
from typing_extensions import override
from zstandard import ZstdCompressor
from ...db import BindStatus, create_or_update_bind
@@ -32,15 +34,13 @@ from .. import Processor as ProcessorMeta
from .cache import Cache
from .constant import BASE_URL, GAME_TYPE, RANK_PERCENTILE
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 ValidUser as LeagueAllUser
from .schemas.response import ProcessedData, RawResponse
from .schemas.user import User
from .schemas.user_info import FailedModel as InfoFailed
from .schemas.user_info import NeverPlayedLeague, NeverRatedLeague, UserInfo
from .schemas.user_info import NeverPlayedLeague, NeverRatedLeague, RatedLeague, UserInfo
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 SuccessModel as RecordsSuccess
from .typing import Rank
@@ -63,15 +63,18 @@ class Processor(ProcessorMeta):
raw_response: RawResponse
processed_data: ProcessedData
@override
def __init__(self, event_id: int, user: User, command_args: list[str]) -> None:
super().__init__(event_id, user, command_args)
self.raw_response = RawResponse()
self.processed_data = ProcessedData()
@property
@override
def game_platform(self) -> Literal['IO']:
return GAME_TYPE
@override
async def handle_bind(self, platform: str, account: str, bot_info: NBUserInfo) -> UniMessage:
"""处理绑定消息"""
self.command_type = 'bind'
@@ -110,11 +113,105 @@ class Processor(ProcessorMeta):
)
return message
async def handle_query(self) -> str:
@override
async def handle_query(self) -> UniMessage:
"""处理查询消息"""
self.command_type = 'query'
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:
"""
@@ -132,7 +229,7 @@ class Processor(ProcessorMeta):
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]
if isinstance(user_info, InfoFailed):
if isinstance(user_info, FailedModel):
raise RequestError(f'用户信息请求错误:\n{user_info.error}')
self.processed_data.user_info = 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'])
)
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}')
self.processed_data.user_records = 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)
@retry(exception_type=RequestError, delay=timedelta(minutes=15))
@@ -197,7 +254,7 @@ async def get_io_rank_data() -> None:
LeagueAll, # type: ignore[arg-type]
(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}')
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.matcher import Matcher
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_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=[],
)
try:
await matcher.finish(message + await proc.handle_query())
await (UniMessage(message) + await proc.handle_query()).send()
except NeedCatchError as e:
await matcher.send(str(e))
raise HandleNotFinishedError from e
await matcher.finish()
@alc.assign('query')
@@ -125,10 +127,11 @@ async def _(event: Event, matcher: Matcher, account: User):
command_args=[],
)
try:
await matcher.finish(await proc.handle_query())
await (await proc.handle_query()).send()
except NeedCatchError as e:
await matcher.send(str(e))
raise HandleNotFinishedError from e
await matcher.finish()
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_userinfo import UserInfo # type: ignore[import-untyped]
from pandas import read_html
from typing_extensions import override
from ...db import BindStatus, create_or_update_bind
from ...utils.avatar import get_avatar
@@ -30,6 +31,7 @@ class User(BaseUser):
name: str
@property
@override
def unique_identifier(self) -> str:
return self.name
@@ -57,15 +59,18 @@ class Processor(ProcessorMeta):
raw_response: RawResponse
processed_data: ProcessedData
@override
def __init__(self, event_id: int, user: User, command_args: list[str]) -> None:
super().__init__(event_id, user, command_args)
self.raw_response = RawResponse()
self.processed_data = ProcessedData()
@property
@override
def game_platform(self) -> Literal['TOP']:
return GAME_TYPE
@override
async def handle_bind(self, platform: str, account: str, bot_info: UserInfo, user_info: UserInfo) -> UniMessage:
"""处理绑定消息"""
self.command_type = 'bind'
@@ -99,11 +104,26 @@ class Processor(ProcessorMeta):
)
return message
async def handle_query(self) -> str:
@override
async def handle_query(self) -> UniMessage:
"""处理查询消息"""
self.command_type = 'query'
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:
"""获取用户信息"""
@@ -140,21 +160,3 @@ class Processor(ProcessorMeta):
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
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.matcher import Matcher
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_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:
try:
await matcher.finish(await proc.handle_query())
await (await proc.handle_query()).send()
except NeedCatchError as e:
if isinstance(e, RequestError) and '未找到此用户' in e.message:
matcher.skip()
await matcher.send(str(e))
raise HandleNotFinishedError from e
await matcher.finish()
try:
@@ -165,10 +167,11 @@ async def _(bot: Bot, event: Event, matcher: Matcher, target: At | Me):
command_args=[],
)
try:
await matcher.finish(message + await proc.handle_query())
await (UniMessage(message) + await proc.handle_query()).send()
except NeedCatchError as e:
await matcher.send(str(e))
raise HandleNotFinishedError from e
await matcher.finish()
@alc.assign('query')
@@ -179,10 +182,11 @@ async def _(event: Event, matcher: Matcher, account: User):
command_args=[],
)
try:
await matcher.finish(await proc.handle_query())
await (await proc.handle_query()).send()
except NeedCatchError as e:
await matcher.send(str(e))
raise HandleNotFinishedError from e
await matcher.finish()
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_orm import get_session
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 ...utils.avatar import get_avatar
@@ -32,6 +33,7 @@ class User(BaseUser):
name: str | None = None
@property
@override
def unique_identifier(self) -> str:
if self.teaid is None:
raise ValueError('不完整的User!')
@@ -70,15 +72,18 @@ class Processor(ProcessorMeta):
raw_response: RawResponse
processed_data: ProcessedData
@override
def __init__(self, event_id: int, user: User, command_args: list[str]) -> None:
super().__init__(event_id, user, command_args)
self.raw_response = RawResponse(user_profile={})
self.processed_data = ProcessedData(user_profile={})
@property
@override
def game_platform(self) -> Literal['TOS']:
return GAME_TYPE
@override
async def handle_bind(self, platform: str, account: str, bot_info: NBUserInfo) -> UniMessage:
"""处理绑定消息"""
self.command_type = 'bind'
@@ -113,11 +118,29 @@ class Processor(ProcessorMeta):
)
return message
async def handle_query(self) -> str:
@override
async def handle_query(self) -> UniMessage:
"""处理查询消息"""
self.command_type = 'query'
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:
"""
@@ -228,24 +251,3 @@ class Processor(ProcessorMeta):
adpl=round((adpm / lpm), 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;
}
#main-content {
.main-content {
display: flex;
flex-direction: column;
@@ -77,21 +77,22 @@
font-family: 'CabinetGrotesk-Variable';
}
#account-box {
.account-box {
display: flex;
flex-direction: column;
}
#info-box {
.info-box {
display: flex;
justify-content: space-between;
margin-top: 10px;
}
#user-info-box {
.user-info-box {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
padding: 25px;
gap: 10px;
@@ -103,14 +104,14 @@
box-sizing: border-box;
}
#user-avatar {
.user-avatar {
width: 125px;
height: 125px;
filter: drop-shadow(0px 11px 23px rgba(0, 0, 0, 0.22));
border-radius: 65px;
}
#user-name {
.user-name {
font-weight: 800;
font-size: 25px;
line-height: 31px;
@@ -118,7 +119,7 @@
color: #000000;
}
#user-sign {
.user-sign {
width: 225px;
height: 66px;
@@ -132,7 +133,7 @@
color: #000000;
}
#game-info-box {
.game-info-box {
display: flex;
flex-direction: column;
align-items: flex-start;
@@ -147,19 +148,19 @@
box-sizing: border-box;
}
#game-type-box {
.game-type-box {
display: flex;
flex-direction: column;
}
#game-logo {
.game-logo {
width: 60px;
height: 60px;
border-radius: 10px;
}
#game-name {
.game-name {
font-weight: 800;
font-size: 30px;
line-height: 37px;
@@ -167,18 +168,18 @@
color: #000000;
}
#game-info-dividing-line {
.game-info-dividing-line {
width: 225px;
border: 1px solid #bababa;
transform: rotate(0.25deg);
}
#ranking-info-box {
.ranking-info-box {
display: flex;
flex-direction: column;
}
#ranking-title {
.ranking-title {
font-weight: 800;
font-size: 25px;
line-height: 31px;
@@ -186,7 +187,7 @@
color: #000000;
}
#ranking {
.ranking {
font-weight: 400;
font-size: 50px;
line-height: 120%;
@@ -194,7 +195,7 @@
color: #000000;
}
#rd {
.rd {
margin-top: -16px;
font-weight: 300;
@@ -214,7 +215,7 @@
background: linear-gradient(222.34deg, #525252 11.97%, #1d1916 89.73%);
}
#TR-title {
.TR-title {
position: absolute;
margin-left: 24px;
margin-top: 19px;
@@ -227,7 +228,7 @@
color: #fafafa;
}
#rank-icon {
.rank-icon {
position: absolute;
margin-left: 27px;
margin-top: 90px;
@@ -236,7 +237,7 @@
height: 50px;
}
#TR {
.TR {
position: absolute;
margin-left: 24px;
margin-top: 143px;
@@ -248,14 +249,14 @@
color: #fafafa;
}
#multiplayer-box {
.multiplayer-box {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-top: 14px;
}
#multiplayer-data-box {
.multiplayer-data-box {
display: flex;
flex-direction: column;
}
@@ -268,84 +269,102 @@
margin-top: 0px;
}
#lpm-box {
.lpm-box {
background-image: url('../static/data/LPM.svg');
}
#lpm-value {
.lpm-value {
color: #4d7d0f;
}
#pps-value {
.pps-value {
color: #4d7d0f;
}
#apm-box {
.apm-box {
background-image: url('../static/data/APM.svg');
}
#apm-value {
.apm-value {
color: #b5530a;
}
#apl-value {
.apl-value {
color: #b5530a;
}
#adpm-box {
.adpm-box {
background-image: url('../static/data/ADPM.svg');
}
#adpm-value {
.adpm-value {
color: #235db4;
}
#vs-value {
.vs-value {
top: 62px;
color: #4779c6;
}
#adpl-value {
.adpl-value {
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 {
width: 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;
flex-direction: row;
align-content: space-between;
margin-top: 14px;
}
#sprint-box {
.sprint-box {
background-image: url('../static/data/40L.svg');
}
#blitz-box {
.blitz-box {
background-image: url('../static/data/Blitz.svg');
}
#sprint-value {
.sprint-value {
color: #b42323;
}
#blitz-value {
.blitz-value {
color: #8e23b4;
}
#footer {
.footer {
display: flex;
justify-content: center;
margin-top: 20px;
margin-bottom: 20px;
font-family: '26FGalaxySans-ObliqueVF';
font-size: 32px;
font-size: 30px;
font-weight: 257;
line-height: 120%;
text-align: center;
}

View File

@@ -8,75 +8,84 @@
</head>
<body>
<div id="main-content">
<div class="main-content">
<span class="big-title">Account&Rankings</span>
<div id="account-box">
<div id="info-box">
<div class="account-box">
<div class="info-box">
<div class="flex-gap"></div>
<div class="box-shadow box-rounded-corners" id="user-info-box">
<img id="user-avatar" src="{{user_avatar}}" />
<div id="user-name">{{user_name}}</div>
<div id="user-sign">“{{user_sign}}”</div>
<div class="box-shadow box-rounded-corners user-info-box">
<div class="flex-gap"></div>
<img class="user-avatar" src="{{user_avatar}}" />
<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 class="flex-gap"></div>
<div class="box-shadow box-rounded-corners" id="game-info-box">
<div id="game-type-box">
<img id="game-logo" src="../../static/static/logo/{{game_type}}.svg" />
<span id="game-name">{{game_type}}</span>
<div class="box-shadow box-rounded-corners game-info-box">
<div class="game-type-box">
<img class="game-logo" src="../../static/static/logo/{{game_type}}.svg" />
<span class="game-name">{{game_type}}</span>
</div>
<div id="game-info-dividing-line"></div>
<div id="ranking-info-box">
<span id="ranking-title">Ranking</span>
<span id="ranking">{{ranking}}</span>
<span id="rd">±{{rd}}</span>
<div class="game-info-dividing-line"></div>
<div class="ranking-info-box">
<span class="ranking-title">Ranking</span>
<span class="ranking">{{ranking}}</span>
<span class="rd">±{{rd}}</span>
</div>
</div>
<div class="flex-gap"></div>
</div>
<div class="chart-shadow box-rounded-corners" id="TR-curve-chart">
<span id="TR-title">Tetra Rating (TR)</span>
<img id="rank-icon" src="../../static/static/rank/{{rank}}.svg" />
<span id="TR" style="display: flex; align-items: flex-end"
{# <div class="chart-shadow box-rounded-corners" id="TR-curve-chart">
<span class="TR-title">Tetra Rating (TR)</span>
<img class="rank-icon" src="../../static/static/rank/{{rank}}.svg" />
<span class="TR" style="display: flex; align-items: flex-end"
>{{TR}}&nbsp;
<p style="font-size: 30px; font-weight: 400; line-height: 47px">(#{{global_rank}})</p>
</span>
</div>
</div> #}
</div>
<span class="big-title">Multiplayer Stats</span>
<div id="multiplayer-box">
<div class="multiplayer-box">
<div class="flex-gap"></div>
<div id="multiplayer-data-box">
<div class="multiplayer-data small-data-box box-shadow box-rounded-corners" id="lpm-box">
<span class="big-data-value" id="lpm-value">{{lpm}}</span>
<span class="small-data-value" id="pps-value">{{pps}} pps</span>
<div class="multiplayer-data-box">
<div class="multiplayer-data small-data-box box-shadow box-rounded-corners lpm-box">
<span class="big-data-value lpm-value">{{lpm}}</span>
<span class="small-data-value pps-value">{{pps}} pps</span>
</div>
<div class="multiplayer-data small-data-box box-shadow box-rounded-corners" id="apm-box">
<span class="big-data-value" id="apm-value">{{apm}}</span>
<span class="small-data-value" id="apl-value">x{{apl}}</span>
<div class="multiplayer-data small-data-box box-shadow box-rounded-corners apm-box">
<span class="big-data-value apm-value">{{apm}}</span>
<span class="small-data-value apl-value">x{{apl}}</span>
</div>
<div class="multiplayer-data small-data-box box-shadow box-rounded-corners" id="adpm-box">
<span class="big-data-value" id="adpm-value">{{adpm}}</span>
<span class="small-data-value" id="adpl-value">x{{adpl}}</span>
<span class="small-data-value" id="vs-value">{{vs}} vs</span>
<div class="multiplayer-data small-data-box box-shadow box-rounded-corners adpm-box">
<span class="big-data-value adpm-value">{{adpm}}</span>
<span class="small-data-value adpl-value">x{{adpl}}</span>
<span class="small-data-value vs-value">{{vs}} vs</span>
</div>
</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>
<span class="big-title">Singleplayer Stats</span>
<div id="singleplayer-box">
<div class="singleplayer-box">
<div class="flex-gap"></div>
<div class="small-data-box box-shadow box-rounded-corners" id="sprint-box">
<span class="big-data-value" id="sprint-value">{{sprint}}</span>
<div class="small-data-box box-shadow box-rounded-corners sprint-box">
<span class="big-data-value sprint-value">{{sprint}}</span>
</div>
<div class="flex-gap"></div>
<div class="small-data-box box-shadow box-rounded-corners" id="blitz-box">
<span class="big-data-value" id="blitz-value">{{blitz}}</span>
<div class="small-data-box box-shadow box-rounded-corners blitz-box">
<span class="big-data-value blitz-value">{{blitz}}</span>
</div>
<div class="flex-gap"></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>
</body>
@@ -279,8 +288,8 @@
indicator: [
{ name: 'PPS' },
{ name: 'APP', nameRotate: 60 },
{ name: 'DSPP', nameRotate: -60 },
{ name: 'OR' },
{ name: 'DSPS', nameRotate: -60 },
{ name: 'DSPP' },
{ name: 'CI', nameRotate: 60 },
{ name: 'GE', nameRotate: -60 },
],
@@ -341,7 +350,7 @@
},
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 浏览器失败')
try:
await cls._start_browser()
except BaseException as e: # noqa: BLE001 不知道会有什么异常, 交给用户解决
except BaseException as e: # 不知道会有什么异常, 交给用户解决
raise ImportError(
'playwright 启动失败, 请尝试在命令行运行 playwright install-deps firefox, 如果仍然启动失败, 请参考上面的报错👆'
) from e

View File

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

19
poetry.lock generated
View File

@@ -1141,13 +1141,13 @@ nonebot2 = ">=2.2.0"
[[package]]
name = "nonebot-plugin-alconna"
version = "0.45.2"
version = "0.45.3"
description = "Alconna Adapter for Nonebot"
optional = false
python-versions = ">=3.9"
files = [
{file = "nonebot_plugin_alconna-0.45.2-py3-none-any.whl", hash = "sha256:38461346f52479c3133d9a789cca1d280a05834790dbadeecfc88639a1e326a2"},
{file = "nonebot_plugin_alconna-0.45.2.tar.gz", hash = "sha256:a54bd294c0a829fcd344a3d3f5b9b040c1dcdbe739245557037cca019187d031"},
{file = "nonebot_plugin_alconna-0.45.3-py3-none-any.whl", hash = "sha256:1e6ff5e99464ea2acad03df8b9fc48949b7d40a7c8d1976c53a934eafb8d3bf0"},
{file = "nonebot_plugin_alconna-0.45.3.tar.gz", hash = "sha256:7667df82fdae02842b0fa28b39d61daf501f1af41d6fecf288fb8bb38a35ff9d"},
]
[package.dependencies]
@@ -2061,6 +2061,17 @@ typing-extensions = ">=4.5,<5.0"
[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)"]
[[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]]
name = "types-pytz"
version = "2024.1.0.20240417"
@@ -2679,4 +2690,4 @@ cffi = ["cffi (>=1.11)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "c568aeaa3fa7dc9049bb95fb3f682754aff0be497fa7e097c51d1becb90a84a5"
content-hash = "b850c0a22d448d72056fadd2f5c0a5b3c72c44a081e6a4b6a4c70c90177b83e3"

View File

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