Compare commits

...

50 Commits

Author SHA1 Message Date
aff2fa120a 🔖 1.1.2 2024-05-10 11:49:05 +08:00
1c057661c2 🐛 防止历史记录一条都没有的数组越界 2024-05-10 11:48:47 +08:00
83bcd14012 🔖 1.1.1 2024-05-10 11:30:08 +08:00
70f53a2c76 🐛 修复 init_templates 和 mount assets 的运行顺序问题 2024-05-10 11:29:46 +08:00
6df70f621e 🔖 1.1.0 2024-05-10 11:14:38 +08:00
8ba3f3c3f4 🎨 拆分函数 2024-05-10 11:13:39 +08:00
a5c4e7df5c 🐛 修正 TETR.IO Badge 模型定义 2024-05-10 09:46:09 +08:00
66db7a8a28 🚨 判断 cookie 是否拥有对应字段 2024-05-10 09:42:15 +08:00
呵呵です
716e392a3a 使用新版模板 (#313)
* 🔥 删除现有模板

*  自动克隆模板仓库

* 🔥 删除 identicon 相关代码

* 🚚 修改静态文件路径

*  使用新模板进行渲染

*  每次渲染都获取一次模板, 以应对实时更新

*  TETR.IO 绑定图使用新模板

* 🚚 修改网络路径

*  TOP 绑定图使用新模板

*  TOS 绑定图使用新模板

* 🐛 防止截图超时

* 🐛 Pydantic V1 会把 float 转换成 int

* ✏️ 模板字段名写错了

*  兼容 Pydantic V1

*  TETR.IO 查询图使用新模板

* 🐛 在查询的用户没有历史记录时不去查询更多记录
2024-05-10 09:41:05 +08:00
呵呵です
e47f1bb6f9 渲染 历史tr 曲线图 (#312) 2024-05-08 18:26:08 +08:00
03d34c5572 🔖 1.0.4 2024-05-07 17:22:46 +08:00
04b480ef52 🗃️ HistoricalData 添加 user_unique_identifier 字段 2024-05-07 17:21:52 +08:00
5563b01937 Revert " 为使用了 alias 的 pydantic model 设置 populate_by_name"
This reverts commit 17690e673f.
2024-05-07 08:51:55 +08:00
504edb08de 🔖 1.0.3 2024-05-07 08:50:53 +08:00
c283f1ca49 🗃️ 更正 HistoricalData 中的数据 2024-05-07 08:50:29 +08:00
0171953264 修改 PydanticType raise 的 Error 类型 2024-05-07 08:48:33 +08:00
7515daccc7 添加依赖 rich 2024-05-07 08:33:03 +08:00
17690e673f 为使用了 alias 的 pydantic model 设置 populate_by_name 2024-05-07 08:32:39 +08:00
e9b3c30a13 🎨 优化模型定义 2024-05-07 06:47:34 +08:00
42484b9c2c 适配 Pydantic V2 2024-05-07 06:47:34 +08:00
42828f23f6 PydanticType dump model 的时候会使用 by_alias=True 2024-05-07 06:47:34 +08:00
d0af2e83c4 更新 Metadata 的 homepage 2024-05-07 06:47:33 +08:00
5534456b22 ⬇️ 错误的使用了 Python3.11 的新特性 2024-05-07 06:47:25 +08:00
dependabot[bot]
1928506021 ⬆️ Bump nonebot-adapter-satori from 0.11.4 to 0.11.5 (#309)
Bumps [nonebot-adapter-satori](https://github.com/nonebot/adapter-satori) from 0.11.4 to 0.11.5.
- [Release notes](https://github.com/nonebot/adapter-satori/releases)
- [Commits](https://github.com/nonebot/adapter-satori/compare/v0.11.4...v0.11.5)

---
updated-dependencies:
- dependency-name: nonebot-adapter-satori
  dependency-type: direct:development
  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-07 06:20:54 +08:00
dependabot[bot]
67da935849 ⬆️ Bump ruff from 0.4.2 to 0.4.3 (#308)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.4.2 to 0.4.3.
- [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/v0.4.2...v0.4.3)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:development
  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-07 06:19:33 +08:00
dependabot[bot]
e1e8743c48 ⬆️ Bump jinja2 from 3.1.3 to 3.1.4 (#307)
Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.3 to 3.1.4.
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/3.1.3...3.1.4)

---
updated-dependencies:
- dependency-name: jinja2
  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-07 06:17:34 +08:00
e5556bad1d 🔖 1.0.2 2024-05-04 22:10:35 +08:00
889405ea6b 💄 更新数据图样式 2024-05-04 22:06:58 +08:00
66e1850297 使用 UniMessage.finish 2024-05-04 07:54:35 +08:00
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
bc37a015d6 🔖 1.0.0 2024-05-02 01:24:29 +08:00
呵呵です
fd85140c99 绑定使用图片回复 #61 (#305)
* 🚧 查数据图初版测试

Co-authored-by: C1ystal <m17687496044@163.com>
Co-authored-by: C29H25N3O5 <michaelgu495@gmail.com>

* 🙈 添加一些 ignore 文件

* 🎨 格式化代码

* 🐛 修复格式化导致的样式爆炸

* 💄 优化曲线图观感

* 💄 将雷达图的指示器名称旋转显示

* 💄 查数据图第二版

Co-authored-by: C29H25N3O5 <michaelgu495@gmail.com>

* ✏️ 修复 typo

* 💄 把用户头像文件的引用放到 html 里

* 💄 账户绑定图第一版

Co-authored-by: C1ystal <m17687496044@163.com>
Co-authored-by: C29H25N3O5 <michaelgu495@gmail.com>

* 🚧 模板化测试

*  添加依赖 fastapi

*  通过 FastAPI 提供静态文件

*  添加依赖 jinja2

* 💄 更新数据图模板 (#291)

* feat(template): show actual value

* feat(template): add user avatar

* feat(template): fix radar

* feat(style): fix name container width fixed caused display misplacement

* feat(style): fix vs value wrap display

* feat(template): make check data length in template

* feat(template): update radar data

* feat(jinja): update data

* fix(template): fix typo

* feat(style): prevent sign too long

* feat(template): turn off echarts animation

* chore(deps): add identicon.js

* fix(template): fix typo

* 🙈 更新.gitignore

* 🏗️ 大部分重构为 flex 布局

---------

Co-authored-by: shoucandanghehe <wallfjjd@gmail.com>
Co-authored-by: 呵呵です <51957264+shoucandanghehe@users.noreply.github.com>

*  添加依赖 nonebot_plugin_userinfo

*  通过 FastAPI 托管渲染后的模板

*  新增头像 api 使用 playwright 生成

*  修正模板资源文件引用路径
被托管后的正确路径

* 💄 将绑定图模板化

* 💄 重命名变量

* 🚚 重命名资源

*  使用 jinja2 渲染模板

*  使用 playwright 渲染网页

* 🩹 渲染模板时对 IO 进行一些额外处理

*  添加依赖 pillow

* 🚚 修改托管页面的路由路径

* 💬 优化绑定图文案

*  新增获取自身网络位置的方法

* 🍱 更新 unknown.svg

*  新增获取用户头像的方法

*  绑定消息使用图片回复

*  为 identicon api 添加缓存

* 🔥 删除旧文件

* 🚚 重命名模板

* 📄 添加字体 License

* 🙈 更新.gitignore

---------

Co-authored-by: C1ystal <m17687496044@163.com>
Co-authored-by: C29H25N3O5 <michaelgu495@gmail.com>
Co-authored-by: 渣渣120 <WOSHIZHAZHA120@qq.com>
2024-05-02 01:22:33 +08:00
dependabot[bot]
80f4316564 ⬆️ Bump nonebot2 from 2.2.1 to 2.3.0 (#304)
Bumps [nonebot2](https://github.com/nonebot/nonebot2) from 2.2.1 to 2.3.0.
- [Release notes](https://github.com/nonebot/nonebot2/releases)
- [Changelog](https://github.com/nonebot/nonebot2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nonebot/nonebot2/compare/v2.2.1...v2.3.0)

---
updated-dependencies:
- dependency-name: nonebot2
  dependency-type: direct:production
  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-05-02 00:31:44 +08:00
dependabot[bot]
3b9c0c89b1 ⬆️ Bump nonebot-adapter-satori from 0.11.3 to 0.11.4 (#302)
Bumps [nonebot-adapter-satori](https://github.com/nonebot/adapter-satori) from 0.11.3 to 0.11.4.
- [Release notes](https://github.com/nonebot/adapter-satori/releases)
- [Commits](https://github.com/nonebot/adapter-satori/compare/v0.11.3...v0.11.4)

---
updated-dependencies:
- dependency-name: nonebot-adapter-satori
  dependency-type: direct:development
  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-04-30 23:25:33 +08:00
c02fdfc47f 🔖 1.0.0.a17 2024-04-30 02:27:31 +08:00
93b169fa40 🐛 修复对 Pydantic V1 的适配 2024-04-30 02:27:08 +08:00
27 changed files with 1366 additions and 241 deletions

11
.gitignore vendored
View File

@@ -8,3 +8,14 @@ Untitled*
*_cache*
*backup*
*.pyc
node_modules
.prettier*
package.json
pnpm-lock.yaml
*.drawio.svg
package-lock.json
*Zone.Identifier
.env*
bot.py
TODO
*.fish

View File

@@ -13,10 +13,11 @@ __plugin_meta__ = PluginMetadata(
description='一个用于查询 Tetris 相关游戏玩家数据的插件',
usage='发送 {游戏名} --help 查询使用方法',
type='application',
homepage='https://github.com/shoucandanghehe/nonebot-plugin-tetris-stats',
homepage='https://github.com/A-minos/nonebot-plugin-tetris-stats',
extra={
'orm_version_location': migrations,
},
)
from . import game_data_processor # noqa: F401, E402
from .utils import host # noqa: F401, E402

View File

@@ -0,0 +1,92 @@
"""Correct the data in HistoricalData
迁移 ID: 8a91210ce14d
父迁移: 0d50142b780f
创建时间: 2024-05-06 08:16:38.487214
"""
from __future__ import annotations
from collections.abc import Sequence
from alembic import op
from nonebot.log import logger
from sqlalchemy import select
from sqlalchemy.ext.automap import automap_base
from sqlalchemy.orm import Session
revision: str = '8a91210ce14d'
down_revision: str | Sequence[str] | None = '0d50142b780f'
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade(name: str = '') -> None:
if name:
return
from nonebot_plugin_tetris_stats.version import __version__
if __version__ != '1.0.3':
logger.critical('本迁移需要1.0.3版本, 请先锁定版本至1.0.3版本再执行本迁移')
raise RuntimeError('本迁移需要1.0.3版本, 请先锁定版本至1.0.3版本再执行本迁移')
from nonebot.compat import PYDANTIC_V2, type_validate_json
from pydantic import BaseModel, ValidationError
from rich.progress import (
BarColumn,
MofNCompleteColumn,
Progress,
TaskProgressColumn,
TextColumn,
TimeRemainingColumn,
)
from nonebot_plugin_tetris_stats.game_data_processor.schemas import BaseProcessedData
Base = automap_base() # noqa: N806
Base.prepare(autoload_with=op.get_bind())
HistoricalData = Base.classes.nonebot_plugin_tetris_stats_historicaldata # noqa: N806
if PYDANTIC_V2:
def model_to_json(value: BaseModel) -> str:
return value.model_dump_json(by_alias=True)
else:
def model_to_json(value: BaseModel) -> str:
return value.json(by_alias=True)
models = BaseProcessedData.__subclasses__()
def json_to_model(value: str) -> BaseModel:
for i in models:
try:
return type_validate_json(i, value)
except ValidationError: # noqa: PERF203
...
raise ValueError
with Session(op.get_bind()) as session:
count = session.query(HistoricalData).count()
with Progress(
TextColumn('[progress.description]{task.description}'),
BarColumn(),
MofNCompleteColumn(),
TaskProgressColumn(),
TimeRemainingColumn(),
) as progress:
task_id = progress.add_task('[cyan]Updateing:', total=count)
for i in range(0, count, 100):
for j in session.scalars(
select(HistoricalData).where(HistoricalData.id > i).order_by(HistoricalData.id).limit(100)
):
model = json_to_model(j.processed_data)
j.processed_data = model_to_json(model)
progress.update(task_id, advance=1)
session.commit()
logger.success('Corrected HistoricalData')
def downgrade(name: str = '') -> None:
if name:
return

View File

@@ -0,0 +1,103 @@
"""Add user_unique_identifier field to HistoricalData
迁移 ID: b7fbdafc339a
父迁移: 8a91210ce14d
创建时间: 2024-05-07 16:55:29.527215
"""
from __future__ import annotations
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
from nonebot.log import logger
revision: str = 'b7fbdafc339a'
down_revision: str | Sequence[str] | None = '8a91210ce14d'
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade(name: str = '') -> None:
if name:
return
from nonebot_plugin_tetris_stats.version import __version__
if __version__ != '1.0.4':
logger.critical('本迁移需要1.0.4版本, 请先锁定版本至1.0.4版本再执行本迁移')
raise RuntimeError('本迁移需要1.0.4版本, 请先锁定版本至1.0.4版本再执行本迁移')
from nonebot.compat import type_validate_json
from pydantic import ValidationError
from rich.progress import (
BarColumn,
MofNCompleteColumn,
Progress,
TaskProgressColumn,
TextColumn,
TimeRemainingColumn,
)
from sqlalchemy import select
from sqlalchemy.ext.automap import automap_base
from sqlalchemy.orm import Session
from nonebot_plugin_tetris_stats.game_data_processor.schemas import BaseUser
with op.batch_alter_table('nonebot_plugin_tetris_stats_historicaldata', schema=None) as batch_op:
batch_op.add_column(sa.Column('user_unique_identifier', sa.String(length=32), nullable=True))
batch_op.create_index(
batch_op.f('ix_nonebot_plugin_tetris_stats_historicaldata_user_unique_identifier'),
['user_unique_identifier'],
unique=False,
)
Base = automap_base() # noqa: N806
connection = op.get_bind()
Base.prepare(autoload_with=connection)
HistoricalData = Base.classes.nonebot_plugin_tetris_stats_historicaldata # noqa: N806
models: list[type[BaseUser]] = BaseUser.__subclasses__()
def json_to_model(value: str) -> BaseUser:
for i in models:
try:
return type_validate_json(i, value)
except ValidationError: # noqa: PERF203
...
raise ValueError
with Session(op.get_bind()) as session:
count = session.query(HistoricalData).count()
with Progress(
TextColumn('[progress.description]{task.description}'),
BarColumn(),
MofNCompleteColumn(),
TaskProgressColumn(),
TimeRemainingColumn(),
) as progress:
task_id = progress.add_task('[cyan]Updateing:', total=count)
for i in range(0, count, 100):
for j in session.scalars(
select(HistoricalData).where(HistoricalData.id > i).order_by(HistoricalData.id).limit(100)
):
model = json_to_model(j.game_user)
try:
j.user_unique_identifier = model.unique_identifier
except ValueError:
session.delete(j)
progress.update(task_id, advance=1)
session.commit()
with op.batch_alter_table('nonebot_plugin_tetris_stats_historicaldata', schema=None) as batch_op:
batch_op.alter_column('user_unique_identifier', existing_type=sa.VARCHAR(length=32), nullable=False)
logger.success('database upgrade success')
def downgrade(name: str = '') -> None:
if name:
return
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('nonebot_plugin_tetris_stats_historicaldata', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_nonebot_plugin_tetris_stats_historicaldata_user_unique_identifier'))
batch_op.drop_column('user_unique_identifier')
# ### end Alembic commands ###

View File

@@ -1,3 +1,5 @@
from enum import Enum, auto
from nonebot_plugin_orm import AsyncSession
from sqlalchemy import select
@@ -5,6 +7,11 @@ from ..utils.typing import GameType
from .models import Bind
class BindStatus(Enum):
SUCCESS = auto()
UPDATE = auto()
async def query_bind_info(
session: AsyncSession,
chat_platform: str,
@@ -27,7 +34,7 @@ async def create_or_update_bind(
chat_account: str,
game_platform: GameType,
game_account: str,
) -> str:
) -> BindStatus:
bind = await query_bind_info(
session=session,
chat_platform=chat_platform,
@@ -42,9 +49,9 @@ async def create_or_update_bind(
game_account=game_account,
)
session.add(bind)
message = '绑定成功'
message = BindStatus.SUCCESS
else:
bind.game_account = game_account
message = '更新绑定成功'
message = BindStatus.UPDATE
await session.commit()
return message

View File

@@ -3,10 +3,12 @@ from datetime import datetime
from typing import Any
from nonebot.adapters import Message
from nonebot.compat import PYDANTIC_V2, type_validate_json
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
@@ -15,25 +17,38 @@ 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
# 将 Pydantic 模型实例转换为 JSON
if isinstance(value, tuple(self.get_model())):
return value.json() # type: ignore[union-attr]
raise TypeError
if PYDANTIC_V2:
def process_result_value(self, value: Any | None, dialect: Dialect) -> BaseModel: # 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.model_dump_json(by_alias=True) # type: ignore[union-attr]
raise TypeError
else:
@override
def process_bind_param(self, value: Any | None, dialect: Dialect) -> str:
# 将 Pydantic 模型实例转换为 JSON
if isinstance(value, tuple(self.get_model())):
return value.json(by_alias=True) # type: ignore[union-attr]
raise TypeError
@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():
try:
return i.model_validate_json(value)
return type_validate_json(i, value)
except ValidationError: # noqa: PERF203
...
raise TypeError
raise ValueError
class Bind(MappedAsDataclass, Model):
@@ -55,6 +70,7 @@ class HistoricalData(MappedAsDataclass, Model):
game_platform: Mapped[GameType] = mapped_column(String(32), index=True, init=False)
command_type: Mapped[CommandType] = mapped_column(String(16), index=True, init=False)
command_args: Mapped[list[str]] = mapped_column(JSON, init=False)
user_unique_identifier: Mapped[str] = mapped_column(String(32), index=True, init=False)
game_user: Mapped[BaseUser] = mapped_column(PydanticType(get_model=BaseUser.__subclasses__), init=False)
processed_data: Mapped[BaseProcessedData] = mapped_column(
PydanticType(get_model=BaseProcessedData.__subclasses__), init=False

View File

@@ -4,6 +4,8 @@ from typing import Any
from nonebot.matcher import Matcher
from nonebot_plugin_alconna import AlcMatches, AlconnaMatcher
from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_userinfo import UserInfo # type: ignore[import-untyped]
from ..utils.exception import MessageFormatError
from ..utils.recorder import Recorder
@@ -41,20 +43,22 @@ class Processor(ABC):
raise NotImplementedError
@abstractmethod
async def handle_bind(self, platform: str, account: str) -> str:
async def handle_bind(
self,
platform: str,
account: str,
bot_info: UserInfo,
*args: Any, # noqa: ANN401
**kwargs: Any, # noqa: ANN401
) -> UniMessage:
"""处理绑定消息"""
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):
@@ -64,6 +68,7 @@ class Processor(ABC):
historical_data.game_platform = self.game_platform
historical_data.command_type = self.command_type
historical_data.command_args = self.command_args
historical_data.user_unique_identifier = self.user.unique_identifier
historical_data.game_user = self.user
historical_data.processed_data = self.processed_data
historical_data.finish_time = finish_time

View File

@@ -5,7 +5,9 @@ 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
from ...db import query_bind_info
@@ -86,14 +88,12 @@ alc.shortcut('fkosk', {'command': 'io查', 'args': ['我']})
@alc.assign('bind')
async def _(bot: Bot, event: Event, matcher: Matcher, account: User):
proc = Processor(
event_id=id(event),
user=account,
command_args=[],
)
async def _(bot: Bot, event: Event, matcher: Matcher, account: User, bot_info: UserInfo = BotUserInfo()): # noqa: B008
proc = Processor(event_id=id(event), user=account, command_args=[])
try:
await matcher.finish(await proc.handle_bind(platform=get_platform(bot), account=event.get_user_id()))
await (
await proc.handle_bind(platform=get_platform(bot), account=event.get_user_id(), bot_info=bot_info)
).finish()
except NeedCatchError as e:
await matcher.send(str(e))
raise HandleNotFinishedError from e
@@ -117,7 +117,7 @@ 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()).finish()
except NeedCatchError as e:
await matcher.send(str(e))
raise HandleNotFinishedError from e
@@ -131,7 +131,7 @@ async def _(event: Event, matcher: Matcher, account: User):
command_args=[],
)
try:
await matcher.finish(await proc.handle_query())
await (await proc.handle_query()).finish()
except NeedCatchError as e:
await matcher.send(str(e))
raise HandleNotFinishedError from e

View File

@@ -23,3 +23,5 @@ RANK_PERCENTILE: dict[Rank, float] = {
'd+': 97.5,
'd': 100,
}
TR_MIN = 0
TR_MAX = 25000

View File

@@ -1,40 +1,49 @@
from asyncio import gather
from collections import defaultdict
from collections.abc import Callable
from datetime import datetime, timedelta, timezone
from hashlib import sha512
from math import floor
from hashlib import md5, sha512
from math import ceil, floor
from re import match
from statistics import mean
from typing import Literal
from urllib.parse import urlunparse
from zoneinfo import ZoneInfo
from aiofiles import open
from nonebot import get_driver
from nonebot.compat import type_validate_json
from nonebot.utils import run_sync
from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_apscheduler import scheduler # type: ignore[import-untyped]
from nonebot_plugin_localstore import get_data_file # type: ignore[import-untyped]
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 create_or_update_bind
from ...db import BindStatus, create_or_update_bind
from ...db.models import HistoricalData
from ...utils.avatar import get_avatar
from ...utils.exception import MessageFormatError, RequestError, WhatTheFuckError
from ...utils.host import HostPage, get_self_netloc
from ...utils.render import Bind, TETRIOInfo, render
from ...utils.request import splice_url
from ...utils.retry import retry
from ...utils.screenshot import screenshot
from .. import Processor as ProcessorMeta
from .cache import Cache
from .constant import BASE_URL, GAME_TYPE, RANK_PERCENTILE
from .constant import BASE_URL, GAME_TYPE, RANK_PERCENTILE, TR_MAX, TR_MIN
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 MultiRecord, SoloRecord, UserRecords
from .schemas.user_records import SuccessModel as RecordsSuccess
from .typing import Rank
@@ -51,40 +60,298 @@ def identify_user_info(info: str) -> User | MessageFormatError:
return MessageFormatError('用户名/ID不合法')
def get_value_bounds(values: list[int | float]) -> tuple[int, int]:
value_max = 10 * ceil(max(values) / 10)
value_min = 10 * floor(min(values) / 10)
return value_max, value_min
def get_split(value_max: int, value_min: int) -> tuple[int, int]:
offset = 0
overflow = 0
while True:
if (new_max_value := value_max + offset + overflow) > TR_MAX:
overflow -= 1
continue
if (new_min_value := value_min - offset + overflow) < TR_MIN:
overflow += 1
continue
if ((new_max_value - new_min_value) / 40).is_integer():
split_value = int((value_max + offset - (value_min - offset)) / 4)
break
offset += 1
return split_value, offset + overflow
def get_specified_point(
previous_point: TETRIOInfo.TetraLeagueHistory.Data,
behind_point: TETRIOInfo.TetraLeagueHistory.Data,
point_time: datetime,
) -> TETRIOInfo.TetraLeagueHistory.Data:
"""根据给出的 previous_point 和 behind_point, 推算 point_time 点处的数据
Args:
previous_point (TETRIOInfo.TetraLeagueHistory.Data): 前面的数据点
behind_point (TETRIOInfo.TetraLeagueHistory.Data): 后面的数据点
point_time (datetime): 要推算的点的位置
Returns:
TETRIOInfo.TetraLeagueHistory.Data: 要推算的点的数据
"""
# 求两个点的斜率
slope = (behind_point.tr - previous_point.tr) / (
datetime.timestamp(behind_point.record_at) - datetime.timestamp(previous_point.record_at)
)
return TETRIOInfo.TetraLeagueHistory.Data(
record_at=point_time,
tr=previous_point.tr + slope * (datetime.timestamp(point_time) - datetime.timestamp(previous_point.record_at)),
)
class Processor(ProcessorMeta):
user: User
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
async def handle_bind(self, platform: str, account: str) -> str:
@override
async def handle_bind(self, platform: str, account: str, bot_info: NBUserInfo) -> UniMessage:
"""处理绑定消息"""
self.command_type = 'bind'
await self.get_user()
if self.user.ID is None:
raise # FIXME: 不知道怎么才能把这类型给变过来了
async with get_session() as session:
return await create_or_update_bind(
bind_status = await create_or_update_bind(
session=session,
chat_platform=platform,
chat_account=account,
game_platform=GAME_TYPE,
game_account=self.user.ID,
)
bot_avatar = await get_avatar(bot_info, 'Data URI', '../../static/logo/logo.svg')
user_info = await self.get_user_info()
if bind_status in (BindStatus.SUCCESS, BindStatus.UPDATE):
async with HostPage(
await render(
'binding',
Bind(
platform='TETR.IO',
status='unknown',
user=Bind.People(
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'{{"type":"identicon","hash":"{md5(user_info.data.user.id.encode()).hexdigest()}"}}', # noqa: S324
name=user_info.data.user.username.upper(),
),
bot=Bind.People(
avatar=bot_avatar,
name=bot_info.user_name,
),
command='io查我',
),
)
) as page_hash:
message = UniMessage.image(
raw=await screenshot(urlunparse(('http', get_self_netloc(), f'/host/{page_hash}.html', '', '', '')))
)
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())
sprint = user_records.data.records.sprint
blitz = user_records.data.records.blitz
if isinstance(sprint.record, MultiRecord) or isinstance(blitz.record, MultiRecord):
raise WhatTheFuckError('单人游戏记录是多人游戏记录')
try:
return UniMessage.image(raw=await self.make_query_image(self.user, user_info, sprint.record, blitz.record))
except TypeError:
...
# fallback
league = user_info.data.user.league
user_name = user_info.data.user.username.upper()
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 )'
if sprint.record is not None:
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 ''
if blitz.record is not None:
ret_message += f'\nBlitz: {blitz.record.endcontext.score}'
ret_message += f' ( #{blitz.rank} )' if blitz.rank is not None else ''
return UniMessage(ret_message)
@staticmethod
async def query_historical_data(user: User, user_info: InfoSuccess) -> list[TETRIOInfo.TetraLeagueHistory.Data]:
today = datetime.now(ZoneInfo('Asia/Shanghai')).replace(hour=0, minute=0, second=0, microsecond=0)
forward = timedelta(days=9)
start_time = (today - forward).astimezone(UTC)
async with get_session() as session:
historical_data = (
await session.scalars(
select(HistoricalData)
.where(HistoricalData.trigger_time >= start_time)
.where(HistoricalData.game_platform == GAME_TYPE)
.where(HistoricalData.user_unique_identifier == user.unique_identifier)
)
).all()
if historical_data:
extra = (
await session.scalars(
select(HistoricalData)
.where(HistoricalData.game_platform == GAME_TYPE)
.where(HistoricalData.user_unique_identifier == user.unique_identifier)
.order_by(HistoricalData.id.desc())
.where(HistoricalData.id < min([i.id for i in historical_data]))
.limit(1)
)
).one_or_none()
if extra is not None:
historical_data = list(historical_data)
historical_data.append(extra)
if not historical_data:
return [
TETRIOInfo.TetraLeagueHistory.Data(record_at=today - forward, tr=user_info.data.user.league.rating),
TETRIOInfo.TetraLeagueHistory.Data(
record_at=today.replace(microsecond=1000), tr=user_info.data.user.league.rating
),
]
histories = [
TETRIOInfo.TetraLeagueHistory.Data(
record_at=i.processed_data.user_info.cache.cached_at.astimezone(ZoneInfo('Asia/Shanghai')),
tr=i.processed_data.user_info.data.user.league.rating,
)
for i in historical_data
if isinstance(i.processed_data, ProcessedData)
and i.processed_data.user_info is not None
and isinstance(i.processed_data.user_info.data.user.league, RatedLeague)
]
# 按照时间排序
histories = sorted(histories, key=lambda x: x.record_at)
for index, value in enumerate(histories):
# 在历史记录里找有没有今天0点后的数据
if value.record_at > today:
histories = histories[:index] + [
get_specified_point(histories[index - 1], histories[index], today.replace(microsecond=1000))
]
break
else:
histories.append(
get_specified_point(
histories[-1],
TETRIOInfo.TetraLeagueHistory.Data(
record_at=user_info.cache.cached_at, tr=user_info.data.user.league.rating
),
today.replace(microsecond=1000),
)
)
if histories[0].record_at < (today - forward):
histories[0] = get_specified_point(
histories[0],
histories[1],
today - forward,
)
else:
histories.insert(0, TETRIOInfo.TetraLeagueHistory.Data(record_at=today - forward, tr=histories[0].tr))
return histories
@staticmethod
async def make_query_image(
user: User, user_info: InfoSuccess, sprint: SoloRecord | None, blitz: SoloRecord | None
) -> bytes:
league = user_info.data.user.league
if not isinstance(league, RatedLeague) or league.vs is None:
raise TypeError
user_name = user_info.data.user.username.upper()
histories = await Processor.query_historical_data(user, user_info)
value_max, value_min = get_value_bounds([i.tr for i in histories])
split_value, offset = get_split(value_max, value_min)
if sprint is not None:
duration = timedelta(milliseconds=sprint.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
else:
sprint_value = 'N/A'
blitz_value = f'{blitz.endcontext.score:,}' if blitz is not None else 'N/A'
async with HostPage(
await render(
'tetrio/info',
TETRIOInfo(
user=TETRIOInfo.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'{{"type":"identicon","hash":"{md5(user_info.data.user.id.encode()).hexdigest()}"}}', # noqa: S324
name=user_name,
bio=user_info.data.user.bio,
),
ranking=TETRIOInfo.Ranking(
rating=round(league.glicko, 2),
rd=round(league.rd, 2),
),
tetra_league=TETRIOInfo.TetraLeague(
rank=league.rank,
tr=round(league.rating, 2),
global_rank=league.standing,
pps=league.pps,
lpm=round(lpm := (league.pps * 24), 2),
apm=league.apm,
apl=round(league.apm / lpm, 2),
vs=league.vs,
adpm=round(adpm := (league.vs * 0.6), 2),
adpl=round(adpm / lpm, 2),
),
tetra_league_history=TETRIOInfo.TetraLeagueHistory(
data=histories,
split_interval=split_value,
min_tr=value_min,
max_tr=value_max,
offset=offset,
),
radar=TETRIOInfo.Radar(
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),
),
sprint=sprint_value,
blitz=blitz_value,
),
)
) as page_hash:
return await screenshot(urlunparse(('http', get_self_netloc(), f'/host/{page_hash}.html', '', '', '')))
async def get_user(self) -> None:
"""
@@ -102,7 +369,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
@@ -114,51 +381,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))
@@ -167,7 +394,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

@@ -5,9 +5,19 @@ from .base import FailedModel
from .base import SuccessModel as BaseSuccessModel
class _User(BaseModel):
id: str = Field(..., alias='_id')
username: str
role: str
xp: float
supporter: bool
verified: bool
country: str | None = None
class SuccessModel(BaseSuccessModel):
class Data(BaseModel):
class ValidUser(BaseModel):
class ValidUser(_User):
class League(BaseModel):
gamesplayed: int
gameswon: int
@@ -21,16 +31,9 @@ class SuccessModel(BaseSuccessModel):
vs: float
decaying: bool
id: str = Field(..., alias='_id')
username: str
role: str
xp: float
league: League
supporter: bool
verified: bool
country: str | None = None
class InvalidUser(BaseModel):
class InvalidUser(_User):
class League(BaseModel):
gamesplayed: int
gameswon: int
@@ -44,14 +47,7 @@ class SuccessModel(BaseSuccessModel):
vs: float | None = None
decaying: bool
id: str = Field(..., alias='_id')
username: str
role: str
xp: float
league: League
supporter: bool
verified: bool
country: str | None
users: list[ValidUser | InvalidUser]

View File

@@ -14,7 +14,8 @@ class SuccessModel(BaseSuccessModel):
class Badge(BaseModel):
id: str
label: str
ts: datetime | None = None
group: str | None = None
ts: datetime | Literal[False] | None = None
class NeverPlayedLeague(BaseModel):
gamesplayed: Literal[0]

View File

@@ -68,31 +68,25 @@ class EndContext(BaseModel):
gametype: str
class BaseModeRecord(BaseModel):
class SoloRecord(BaseModel):
class User(BaseModel):
id: str = Field(..., alias='_id')
username: str
class _User(BaseModel):
id: str = Field(..., alias='_id')
username: str
id: str = Field(..., alias='_id')
stream: str
replayid: str
user: User
ts: datetime
ismulti: bool | None = None
class _Record(BaseModel):
id: str = Field(..., alias='_id')
stream: str
replayid: str
user: _User
ts: datetime
ismulti: bool | None = None
class BaseModeRecord(BaseModel):
class SoloRecord(_Record):
endcontext: EndContext
class MultiRecord(BaseModel):
class User(BaseModel):
id: str = Field(..., alias='_id')
username: str
id: str = Field(..., alias='_id')
stream: str
replayid: str
user: User
ts: datetime
ismulti: bool | None = None
class MultiRecord(_Record):
endcontext: list[EndContext]
record: SoloRecord | MultiRecord | None = None

View File

@@ -2,7 +2,9 @@ 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]
from ...db import query_bind_info
from ...utils.exception import HandleNotFinishedError, NeedCatchError
@@ -67,14 +69,25 @@ alc = on_alconna(
@alc.assign('bind')
async def _(bot: Bot, event: Event, matcher: Matcher, account: User):
async def _( # noqa: PLR0913
bot: Bot,
event: Event,
matcher: Matcher,
account: User,
bot_info: UserInfo = BotUserInfo(), # noqa: B008
user_info: UserInfo = EventUserInfo(), # noqa: B008
):
proc = Processor(
event_id=id(event),
user=account,
command_args=[],
)
try:
await matcher.finish(await proc.handle_bind(platform=get_platform(bot), account=event.get_user_id()))
await (
await proc.handle_bind(
platform=get_platform(bot), account=event.get_user_id(), bot_info=bot_info, user_info=user_info
)
).finish()
except NeedCatchError as e:
await matcher.send(str(e))
raise HandleNotFinishedError from e
@@ -98,7 +111,7 @@ 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()).finish()
except NeedCatchError as e:
await matcher.send(str(e))
raise HandleNotFinishedError from e
@@ -112,7 +125,7 @@ async def _(event: Event, matcher: Matcher, account: User):
command_args=[],
)
try:
await matcher.finish(await proc.handle_query())
await (await proc.handle_query()).finish()
except NeedCatchError as e:
await matcher.send(str(e))
raise HandleNotFinishedError from e

View File

@@ -3,15 +3,22 @@ from dataclasses import dataclass
from io import StringIO
from re import match
from typing import Literal
from urllib.parse import urlencode
from urllib.parse import urlencode, urlunparse
from lxml import etree
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 create_or_update_bind
from ...db import BindStatus, create_or_update_bind
from ...utils.avatar import get_avatar
from ...utils.exception import MessageFormatError, RequestError
from ...utils.host import HostPage, get_self_netloc
from ...utils.render import Bind, render
from ...utils.request import Request, splice_url
from ...utils.screenshot import screenshot
from .. import Processor as ProcessorMeta
from ..schemas import BaseUser
from .constant import BASE_URL, GAME_TYPE
@@ -24,6 +31,7 @@ class User(BaseUser):
name: str
@property
@override
def unique_identifier(self) -> str:
return self.name
@@ -51,33 +59,74 @@ 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
async def handle_bind(self, platform: str, account: str) -> str:
@override
async def handle_bind(self, platform: str, account: str, bot_info: UserInfo, user_info: UserInfo) -> UniMessage:
"""处理绑定消息"""
self.command_type = 'bind'
await self.check_user()
async with get_session() as session:
return await create_or_update_bind(
bind_status = await create_or_update_bind(
session=session,
chat_platform=platform,
chat_account=account,
game_platform=GAME_TYPE,
game_account=self.user.name,
)
if bind_status in (BindStatus.SUCCESS, BindStatus.UPDATE):
async with HostPage(
await render(
'binding',
Bind(
platform=self.game_platform,
status='unknown',
user=Bind.People(
avatar=await get_avatar(user_info, 'Data URI', None),
name=(await self.get_user_name()).upper(),
),
bot=Bind.People(
avatar=await get_avatar(bot_info, 'Data URI', '../../static/logo/logo.svg'),
name=bot_info.user_name,
),
command='top查我',
),
)
) as page_hash:
message = UniMessage.image(
raw=await screenshot(urlunparse(('http', get_self_netloc(), f'/host/{page_hash}.html', '', '', '')))
)
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:
"""获取用户信息"""
@@ -114,21 +163,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,7 +4,9 @@ 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]
from ...db import query_bind_info
from ...utils.exception import HandleNotFinishedError, NeedCatchError, RequestError
@@ -71,7 +73,7 @@ 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()).finish()
except NeedCatchError as e:
if isinstance(e, RequestError) and '未找到此用户' in e.message:
matcher.skip()
@@ -130,14 +132,25 @@ except ImportError:
@alc.assign('bind')
async def _(bot: Bot, event: Event, matcher: Matcher, account: User):
async def _( # noqa: PLR0913
bot: Bot,
event: Event,
matcher: Matcher,
account: User,
bot_info: UserInfo = BotUserInfo(), # noqa: B008
user_info: UserInfo = EventUserInfo(), # noqa: B008
):
proc = Processor(
event_id=id(event),
user=account,
command_args=[],
)
try:
await matcher.finish(await proc.handle_bind(platform=get_platform(bot), account=event.get_user_id()))
await (
await proc.handle_bind(
platform=get_platform(bot), account=event.get_user_id(), bot_info=bot_info, nb_user_info=user_info
)
).finish()
except NeedCatchError as e:
await matcher.send(str(e))
raise HandleNotFinishedError from e
@@ -161,7 +174,7 @@ 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()).finish()
except NeedCatchError as e:
await matcher.send(str(e))
raise HandleNotFinishedError from e
@@ -175,7 +188,7 @@ async def _(event: Event, matcher: Matcher, account: User):
command_args=[],
)
try:
await matcher.finish(await proc.handle_query())
await (await proc.handle_query()).finish()
except NeedCatchError as e:
await matcher.send(str(e))
raise HandleNotFinishedError from e

View File

@@ -1,15 +1,22 @@
from dataclasses import dataclass
from re import match
from typing import Literal
from urllib.parse import urlencode
from urllib.parse import urlencode, urlunparse
from httpx import TimeoutException
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 create_or_update_bind
from ...db import BindStatus, create_or_update_bind
from ...utils.avatar import get_avatar
from ...utils.exception import MessageFormatError, RequestError
from ...utils.host import HostPage, get_self_netloc
from ...utils.render import Bind, render
from ...utils.request import Request, splice_url
from ...utils.screenshot import screenshot
from .. import Processor as ProcessorMeta
from ..schemas import BaseUser
from .constant import BASE_URL, GAME_TYPE
@@ -26,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!')
@@ -64,33 +72,79 @@ 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
async def handle_bind(self, platform: str, account: str) -> str:
@override
async def handle_bind(
self, platform: str, account: str, bot_info: NBUserInfo, nb_user_info: NBUserInfo
) -> UniMessage:
"""处理绑定消息"""
self.command_type = 'bind'
await self.get_user()
async with get_session() as session:
return await create_or_update_bind(
bind_status = await create_or_update_bind(
session=session,
chat_platform=platform,
chat_account=account,
game_platform=GAME_TYPE,
game_account=self.user.unique_identifier,
)
user_info = await self.get_user_info()
if bind_status in (BindStatus.SUCCESS, BindStatus.UPDATE):
async with HostPage(
await render(
'binding',
Bind(
platform=self.game_platform,
status='unknown',
user=Bind.People(
avatar=await get_avatar(nb_user_info, 'Data URI', None), name=user_info.data.name
),
bot=Bind.People(
avatar=await get_avatar(bot_info, 'Data URI', '../../static/logo/logo.svg'),
name=bot_info.user_name,
),
command='茶服查我',
),
)
) as page_hash:
message = UniMessage.image(
raw=await screenshot(urlunparse(('http', get_self_netloc(), f'/host/{page_hash}.html', '', '', '')))
)
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:
"""
@@ -155,8 +209,8 @@ class Processor(ProcessorMeta):
failover_code=[502],
failover_exc=(TimeoutException,),
)
self.processed_data.user_profile[params] = UserProfile.model_validate_json(
self.raw_response.user_profile[params]
self.processed_data.user_profile[params] = type_validate_json(
UserProfile, self.raw_response.user_profile[params]
)
return self.processed_data.user_profile[params]
@@ -201,24 +255,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

@@ -0,0 +1,52 @@
from base64 import b64encode
from io import BytesIO
from typing import Literal, overload
from nonebot_plugin_userinfo import UserInfo # type: ignore[import-untyped]
from PIL import Image
@overload
async def get_avatar(user: UserInfo, scheme: Literal['Data URI'], default: str | None) -> str:
"""获取用户头像的指定格式
Args:
user (UserInfo): 要获取的用户
scheme (Literal[&#39;Data URI&#39;]): 格式
default (str | None): 获取不到时的默认值
Raises:
TypeError: Can't get avatar: 当获取不到头像并且没有设置默认值时抛出
TypeError: Can't get avatar format: 当获取到的头像无法识别格式时抛出
Returns:
str: Data URI 格式的头像
"""
@overload
async def get_avatar(user: UserInfo, scheme: Literal['bytes'], default: str | None) -> bytes:
"""获取用户头像的指定格式
Args:
user (UserInfo): 要获取的用户
scheme (Literal[&#39;bytes&#39;]): 格式
default (str | None): 获取不到时的默认值
Returns:
bytes: bytes 格式的头像
"""
async def get_avatar(user: UserInfo, scheme: Literal['Data URI', 'bytes'], default: str | None) -> str | bytes:
if user.user_avatar is None:
if default is None:
raise TypeError("Can't get avatar")
return default
bot_avatar = await user.user_avatar.get_image()
if scheme == 'Data URI':
avatar_format = Image.open(BytesIO(bot_avatar)).format
if avatar_format is None:
raise TypeError("Can't get avatar format")
return f'data:{Image.MIME[avatar_format]};base64,{b64encode(bot_avatar).decode()}'
return bot_avatar

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

@@ -0,0 +1,70 @@
from hashlib import sha256
from ipaddress import IPv4Address, IPv6Address
from typing import ClassVar
from fastapi import FastAPI, status
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from nonebot import get_app, get_driver
from nonebot.log import logger
from nonebot_plugin_localstore import get_cache_dir # type: ignore[import-untyped]
from pydantic import IPvAnyAddress
from .templates import templates_dir
app = get_app()
driver = get_driver()
global_config = driver.config
cache_dir = get_cache_dir('nonebot_plugin_tetris_stats')
if not isinstance(app, FastAPI):
raise RuntimeError('本插件需要 FastAPI 驱动器才能运行') # noqa: TRY004
NOT_FOUND = HTMLResponse('404 Not Found', status_code=status.HTTP_404_NOT_FOUND)
class HostPage:
pages: ClassVar[dict[str, str]] = {}
def __init__(self, page: str) -> None:
self.page_hash = sha256(page.encode()).hexdigest()
self.pages[self.page_hash] = page
async def __aenter__(self) -> str:
return self.page_hash
async def __aexit__(self, exc_type, exc, tb) -> None: # noqa: ANN001
self.pages.pop(self.page_hash, None)
@driver.on_startup
def _():
app.mount(
'/host/assets',
StaticFiles(directory=templates_dir / 'assets'),
name='assets',
)
logger.success('assets mounted')
@app.get('/host/{page_hash}.html', status_code=status.HTTP_200_OK)
async def _(page_hash: str) -> HTMLResponse:
if page_hash in HostPage.pages:
return HTMLResponse(HostPage.pages[page_hash])
return NOT_FOUND
def get_self_netloc() -> str:
host: IPv4Address | IPv6Address | IPvAnyAddress = global_config.host
if isinstance(host, IPv4Address):
if host == IPv4Address('0.0.0.0'): # noqa: S104
host = IPv4Address('127.0.0.1')
netloc = f'{host}:{global_config.port}'
else:
if host == IPv6Address('::'):
host = IPv6Address('::1')
netloc = f'[{host}]:{global_config.port}'
return netloc

View File

@@ -0,0 +1,104 @@
from datetime import datetime
from typing import Annotated, ClassVar, Literal, overload
from jinja2 import Environment, FileSystemLoader
from nonebot.compat import PYDANTIC_V2
from pydantic import BaseModel
from ..game_data_processor.io_data_processor.typing import Rank
from .templates import templates_dir
from .typing import Number
if PYDANTIC_V2:
from pydantic import PlainSerializer
env = Environment(
loader=FileSystemLoader(templates_dir), autoescape=True, trim_blocks=True, lstrip_blocks=True, enable_async=True
)
def format_datetime_to_timestamp(dt: datetime) -> int:
return int(dt.timestamp() * 1000)
class Bind(BaseModel):
class People(BaseModel):
avatar: str
name: str
platform: Literal['TETR.IO', 'TOP', 'TOS']
status: Literal['error', 'success', 'unknown', 'unlink', 'unverified']
user: People
bot: People
command: str
class TETRIOInfo(BaseModel):
class User(BaseModel):
avatar: str
name: str
bio: str | None
class Ranking(BaseModel):
rating: Number
rd: Number
class TetraLeague(BaseModel):
rank: Rank
tr: Number
global_rank: Number
pps: Number
lpm: Number
apm: Number
apl: Number
vs: Number
adpm: Number
adpl: Number
class TetraLeagueHistory(BaseModel):
class Data(BaseModel):
if PYDANTIC_V2:
record_at: Annotated[datetime, PlainSerializer(format_datetime_to_timestamp, return_type=int)]
else:
record_at: datetime # type: ignore[no-redef]
tr: Number
data: list[Data]
split_interval: Number
min_tr: Number
max_tr: Number
offset: Number
class Radar(BaseModel):
app: Number
dsps: Number
dspp: Number
ci: Number
ge: Number
user: User
ranking: Ranking
tetra_league: TetraLeague
tetra_league_history: TetraLeagueHistory
radar: Radar
sprint: str
blitz: str
if not PYDANTIC_V2:
class Config:
json_encoders: ClassVar[dict] = {datetime: format_datetime_to_timestamp}
@overload
async def render(render_type: Literal['binding'], data: Bind) -> str: ...
@overload
async def render(render_type: Literal['tetrio/info'], data: TETRIOInfo) -> str: ...
async def render(render_type: Literal['binding', 'tetrio/info'], data: Bind | TETRIOInfo) -> str:
if PYDANTIC_V2:
return await env.get_template('index.html').render_async(path=render_type, data=data.model_dump_json())
return await env.get_template('index.html').render_async(path=render_type, data=data.json())

View File

@@ -69,7 +69,11 @@ class Request:
raise RequestError('api请求失败')
cls._headers = await response.request.all_headers()
try:
cls._cookies = {i['name']: i['value'] for i in await context.cookies()}
cls._cookies = {
name: value
for i in await context.cookies()
if (name := i.get('name')) is not None and (value := i.get('value')) is not None
}
except KeyError:
cls._cookies = None
return await response.body()

View File

@@ -0,0 +1,11 @@
from .browser import BrowserManager
async def screenshot(url: str) -> bytes:
browser = await BrowserManager.get_browser()
async with (
await browser.new_page(no_viewport=True, viewport={'width': 0, 'height': 0}) as page,
):
await page.goto(url)
await page.wait_for_load_state('networkidle')
return await page.screenshot(full_page=True, type='png')

View File

@@ -0,0 +1,60 @@
from asyncio.subprocess import PIPE, create_subprocess_exec
from shutil import rmtree
from nonebot import get_driver
from nonebot.log import logger
from nonebot_plugin_localstore import get_data_dir # type: ignore[import-untyped]
driver = get_driver()
templates_dir = get_data_dir('nonebot_plugin_tetris_stats') / 'templates'
@driver.on_startup
async def init_templates() -> None:
try:
await create_subprocess_exec('git', '--version', stdout=PIPE)
except FileNotFoundError as e:
raise RuntimeError(
'未找到 git, 请确保 git 已安装并在环境变量中\n安装步骤请参阅: https://git-scm.com/book/zh/v2/%E8%B5%B7%E6%AD%A5-%E5%AE%89%E8%A3%85-Git'
) from e
if not templates_dir.exists():
logger.info('模板仓库不存在, 正在尝试初始化...')
proc = await create_subprocess_exec(
'git',
'clone',
'-b',
'gh-pages',
'https://github.com/A-Minos/tetris-stats-templates',
templates_dir,
'--depth=1',
stdout=PIPE,
stderr=PIPE,
)
stdout, stderr = await proc.communicate()
if proc.returncode != 0:
for i in stderr.decode().splitlines():
logger.error(i)
raise RuntimeError('初始化模板仓库失败')
logger.success('模板仓库初始化成功')
return
proc = await create_subprocess_exec(
'git', 'rev-parse', '--is-inside-work-tree', stdout=PIPE, stderr=PIPE, cwd=templates_dir
)
stdout, stderr = await proc.communicate()
if proc.returncode != 0:
for i in stderr.decode().splitlines():
logger.error(i)
logger.warning('模板仓库状态异常, 尝试重新初始化')
rmtree(templates_dir)
await init_templates()
return
logger.info('正在更新模板仓库...')
proc = await create_subprocess_exec('git', 'pull', stdout=PIPE, stderr=PIPE, cwd=templates_dir)
stdout, stderr = await proc.communicate()
logger.info(stdout.decode().strip())
if proc.returncode != 0:
for i in stderr.decode().splitlines():
logger.error(i)
raise RuntimeError('更新模板仓库失败')
logger.success('模板仓库更新成功')

View File

@@ -1,7 +1,7 @@
from collections.abc import Awaitable, Callable
from typing import Any, Literal
Number = int | float
Number = float | int
GameType = Literal['IO', 'TOP', 'TOS']
CommandType = Literal['bind', 'query']
AsyncCallable = Callable[..., Awaitable[Any]]

361
poetry.lock generated
View File

@@ -127,13 +127,13 @@ zookeeper = ["kazoo"]
[[package]]
name = "arclet-alconna"
version = "1.8.10"
version = "1.8.11"
description = "A High-performance, Generality, Humane Command Line Arguments Parser Library."
optional = false
python-versions = ">=3.8"
files = [
{file = "arclet_alconna-1.8.10-py3-none-any.whl", hash = "sha256:6c4d464e3c1b3c03bce301f51370648fd006df2b820e6f926cb410ced97f0c2f"},
{file = "arclet_alconna-1.8.10.tar.gz", hash = "sha256:379e824919a2750f43a2b87a4a523bf06386b4469c5ace859d47213857606c3e"},
{file = "arclet_alconna-1.8.11-py3-none-any.whl", hash = "sha256:8a0c188400214abe2b72b2e6f4f23ed8d3021270654c15bc46cad62b725b7a44"},
{file = "arclet_alconna-1.8.11.tar.gz", hash = "sha256:3fed5e946709d63b9d7ee7cddf32b145e1f931b8ea9d0b8174cac753d965e9c8"},
]
[package.dependencies]
@@ -159,6 +159,17 @@ files = [
arclet-alconna = ">=1.8.10"
nepattern = ">=0.7.0,<1.0.0"
[[package]]
name = "cachetools"
version = "5.3.3"
description = "Extensible memoizing collections and decorators"
optional = false
python-versions = ">=3.7"
files = [
{file = "cachetools-5.3.3-py3-none-any.whl", hash = "sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945"},
{file = "cachetools-5.3.3.tar.gz", hash = "sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105"},
]
[[package]]
name = "certifi"
version = "2024.2.2"
@@ -270,6 +281,20 @@ files = [
{file = "cssselect-1.2.0.tar.gz", hash = "sha256:666b19839cfaddb9ce9d36bfe4c969132c647b92fc9088c4e23f786b30f1b3dc"},
]
[[package]]
name = "emoji"
version = "2.11.1"
description = "Emoji for Python"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
files = [
{file = "emoji-2.11.1-py2.py3-none-any.whl", hash = "sha256:b7ba25299bbf520cc8727848ae66b986da32aee27dc2887eaea2bff07226ce49"},
{file = "emoji-2.11.1.tar.gz", hash = "sha256:062ff0b3154b6219143f8b9f4b3e5c64c35bc2b146e6e2349ab5f29e218ce1ee"},
]
[package.extras]
dev = ["coverage", "coveralls", "pytest"]
[[package]]
name = "exceptiongroup"
version = "1.2.1"
@@ -286,13 +311,13 @@ test = ["pytest (>=6)"]
[[package]]
name = "fastapi"
version = "0.110.2"
version = "0.110.3"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
optional = false
python-versions = ">=3.8"
files = [
{file = "fastapi-0.110.2-py3-none-any.whl", hash = "sha256:239403f2c0a3dda07a9420f95157a7f014ddb2b770acdbc984f9bdf3ead7afdb"},
{file = "fastapi-0.110.2.tar.gz", hash = "sha256:b53d673652da3b65e8cd787ad214ec0fe303cad00d2b529b86ce7db13f17518d"},
{file = "fastapi-0.110.3-py3-none-any.whl", hash = "sha256:fd7600612f755e4050beb74001310b5a7e1796d149c2ee363124abdfa0289d32"},
{file = "fastapi-0.110.3.tar.gz", hash = "sha256:555700b0159379e94fdbfc6bb66a0f1c43f4cf7060f25239af3d84b63a656626"},
]
[package.dependencies]
@@ -301,7 +326,7 @@ starlette = ">=0.37.2,<0.38.0"
typing-extensions = ">=4.8.0"
[package.extras]
all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
all = ["email_validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
[[package]]
name = "greenlet"
@@ -489,6 +514,25 @@ files = [
{file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
]
[[package]]
name = "importlib-metadata"
version = "7.1.0"
description = "Read metadata from Python packages"
optional = false
python-versions = ">=3.8"
files = [
{file = "importlib_metadata-7.1.0-py3-none-any.whl", hash = "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570"},
{file = "importlib_metadata-7.1.0.tar.gz", hash = "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2"},
]
[package.dependencies]
zipp = ">=0.5"
[package.extras]
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
perf = ["ipython"]
testing = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"]
[[package]]
name = "importlib-resources"
version = "6.4.0"
@@ -504,6 +548,23 @@ files = [
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
testing = ["jaraco.test (>=5.4)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)", "zipp (>=3.17)"]
[[package]]
name = "jinja2"
version = "3.1.4"
description = "A very fast and expressive template engine."
optional = false
python-versions = ">=3.7"
files = [
{file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"},
{file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"},
]
[package.dependencies]
MarkupSafe = ">=2.0"
[package.extras]
i18n = ["Babel (>=2.7)"]
[[package]]
name = "loguru"
version = "0.7.2"
@@ -712,6 +773,30 @@ babel = ["Babel"]
lingua = ["lingua"]
testing = ["pytest"]
[[package]]
name = "markdown-it-py"
version = "3.0.0"
description = "Python port of markdown-it. Markdown parsing, done right!"
optional = false
python-versions = ">=3.8"
files = [
{file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"},
{file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"},
]
[package.dependencies]
mdurl = ">=0.1,<1.0"
[package.extras]
benchmarking = ["psutil", "pytest", "pytest-benchmark"]
code-style = ["pre-commit (>=3.0,<4.0)"]
compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"]
linkify = ["linkify-it-py (>=1,<3)"]
plugins = ["mdit-py-plugins"]
profiling = ["gprof2dot"]
rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"]
testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
[[package]]
name = "markupsafe"
version = "2.1.5"
@@ -781,6 +866,17 @@ files = [
{file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"},
]
[[package]]
name = "mdurl"
version = "0.1.2"
description = "Markdown URL utilities"
optional = false
python-versions = ">=3.7"
files = [
{file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
]
[[package]]
name = "msgpack"
version = "1.0.8"
@@ -1005,13 +1101,13 @@ files = [
[[package]]
name = "nepattern"
version = "0.7.1"
version = "0.7.2"
description = "a complex pattern, support typing"
optional = false
python-versions = ">=3.8"
files = [
{file = "nepattern-0.7.1-py3-none-any.whl", hash = "sha256:e5190a79d398dce72f972ffafc413653414cca12a1dac56d38fcf070d2f034bc"},
{file = "nepattern-0.7.1.tar.gz", hash = "sha256:14bae0dfa93241f3c2d8371cfc5e51072a186a295a7d35fbd7a60b08e83b2605"},
{file = "nepattern-0.7.2-py3-none-any.whl", hash = "sha256:75406623a4a533628a6fb1996a7cc79d7b535a268e2a8bf4b405197175baecca"},
{file = "nepattern-0.7.2.tar.gz", hash = "sha256:7869e7998d870c920611b7a25649fb451c57bf2de450fb5d2e87408ff2917885"},
]
[package.dependencies]
@@ -1066,13 +1162,13 @@ typing-extensions = ">=4.0.0,<5.0.0"
[[package]]
name = "nonebot-adapter-satori"
version = "0.11.3"
version = "0.11.5"
description = "Satori Protocol Adapter for Nonebot2"
optional = false
python-versions = ">=3.8"
files = [
{file = "nonebot_adapter_satori-0.11.3-py3-none-any.whl", hash = "sha256:3df28fe6248f6c3db5dd6f4680253b6c55e6acdbfe4b004c55982dadb8fe093f"},
{file = "nonebot_adapter_satori-0.11.3.tar.gz", hash = "sha256:9724589214438fc508a6fc4e9c12cc2fa6956ecf92d4f255807a737441c88c5d"},
{file = "nonebot_adapter_satori-0.11.5-py3-none-any.whl", hash = "sha256:8c7a94ac6897a0001878f259880efe4df1c5232fe14a2d03bab742f28b279498"},
{file = "nonebot_adapter_satori-0.11.5.tar.gz", hash = "sha256:ab134f0e70302958807b7f91b5c6d0e462c5d39728c93066076699d00c2d067b"},
]
[package.dependencies]
@@ -1080,20 +1176,21 @@ nonebot2 = ">=2.2.0"
[[package]]
name = "nonebot-plugin-alconna"
version = "0.45.0"
version = "0.45.3"
description = "Alconna Adapter for Nonebot"
optional = false
python-versions = ">=3.9"
files = [
{file = "nonebot_plugin_alconna-0.45.0-py3-none-any.whl", hash = "sha256:3394961dc27f2322e8ebb8e5f700b567b3749262689c3a0f85fab55112bc050e"},
{file = "nonebot_plugin_alconna-0.45.0.tar.gz", hash = "sha256:6fa530ef6df02c3b8c2a67979ac55d6978cc647df2ce5a393c87f42e919be641"},
{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]
arclet-alconna = ">=1.8.10"
arclet-alconna = ">=1.8.11"
arclet-alconna-tools = ">=0.7.3"
nepattern = ">=0.7.0"
nonebot2 = ">=2.2.1"
importlib-metadata = ">=4.13.0"
nepattern = ">=0.7.2"
nonebot2 = ">=2.3.0"
[[package]]
name = "nonebot-plugin-apscheduler"
@@ -1161,15 +1258,33 @@ postgresql = ["sqlalchemy[postgresql-psycopgbinary]"]
psycopg = ["sqlalchemy[postgresql-psycopgbinary]"]
sqlite = ["sqlalchemy[aiosqlite]"]
[[package]]
name = "nonebot-plugin-userinfo"
version = "0.2.4"
description = "Nonebot2 用户信息获取插件"
optional = false
python-versions = "<4.0,>=3.8"
files = [
{file = "nonebot_plugin_userinfo-0.2.4-py3-none-any.whl", hash = "sha256:f08dac58759b859f8bf1d1c17e96cdcee92b10613631a430405364a40181f9e4"},
{file = "nonebot_plugin_userinfo-0.2.4.tar.gz", hash = "sha256:1d49ff00ce38c856be4388fc2a954656f07cc529ce38ef9593e3a0ea40f26b6a"},
]
[package.dependencies]
cachetools = ">=5.0.0,<6.0.0"
emoji = ">=2.0.0,<3.0.0"
httpx = ">=0.20.0,<1.0.0"
nonebot2 = ">=2.0.0,<3.0.0"
strenum = ">=0.4.8,<0.5.0"
[[package]]
name = "nonebot2"
version = "2.2.1"
version = "2.3.0"
description = "An asynchronous python bot framework."
optional = false
python-versions = ">=3.8,<4.0"
python-versions = "<4.0,>=3.9"
files = [
{file = "nonebot2-2.2.1-py3-none-any.whl", hash = "sha256:88f2bb456bf90922925bbe489a9effe3b09300f3aa50bfa75ee50d8a83d7330f"},
{file = "nonebot2-2.2.1.tar.gz", hash = "sha256:fe57692300571b00724999238545d8d894523460e6835a11b326a2e1cdf98fc4"},
{file = "nonebot2-2.3.0-py3-none-any.whl", hash = "sha256:ecde620a96197189f9f9b719ab8493b5115e5973cba0bd07ed3d737ce7418462"},
{file = "nonebot2-2.3.0.tar.gz", hash = "sha256:0e4ebdb0eaab96008619aa4890171ea6e7e7af25eb9d1a4e753e58544c46681e"},
]
[package.dependencies]
@@ -1255,7 +1370,6 @@ optional = false
python-versions = ">=3.9"
files = [
{file = "pandas-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90c6fca2acf139569e74e8781709dccb6fe25940488755716d1d354d6bc58bce"},
{file = "pandas-2.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7adfc142dac335d8c1e0dcbd37eb8617eac386596eb9e1a1b77791cf2498238"},
{file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4abfe0be0d7221be4f12552995e58723c7422c80a659da13ca382697de830c08"},
{file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8635c16bf3d99040fdf3ca3db669a7250ddf49c55dc4aa8fe0ae0fa8d6dcc1f0"},
{file = "pandas-2.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:40ae1dffb3967a52203105a077415a86044a2bea011b5f321c6aa64b379a3f51"},
@@ -1276,7 +1390,6 @@ files = [
{file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:43498c0bdb43d55cb162cdc8c06fac328ccb5d2eabe3cadeb3529ae6f0517c32"},
{file = "pandas-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:d187d355ecec3629624fccb01d104da7d7f391db0311145817525281e2804d23"},
{file = "pandas-2.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0ca6377b8fca51815f382bd0b697a0814c8bda55115678cbc94c30aacbb6eff2"},
{file = "pandas-2.2.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9057e6aa78a584bc93a13f0a9bf7e753a5e9770a30b4d758b8d5f2a62a9433cd"},
{file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:001910ad31abc7bf06f49dcc903755d2f7f3a9186c0c040b827e522e9cef0863"},
{file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66b479b0bd07204e37583c191535505410daa8df638fd8e75ae1b383851fe921"},
{file = "pandas-2.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a77e9d1c386196879aa5eb712e77461aaee433e54c68cf253053a73b7e49c33a"},
@@ -1335,6 +1448,92 @@ files = [
numpy = {version = ">=1.26.0", markers = "python_version < \"3.13\""}
types-pytz = ">=2022.1.1"
[[package]]
name = "pillow"
version = "10.3.0"
description = "Python Imaging Library (Fork)"
optional = false
python-versions = ">=3.8"
files = [
{file = "pillow-10.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:90b9e29824800e90c84e4022dd5cc16eb2d9605ee13f05d47641eb183cd73d45"},
{file = "pillow-10.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2c405445c79c3f5a124573a051062300936b0281fee57637e706453e452746c"},
{file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78618cdbccaa74d3f88d0ad6cb8ac3007f1a6fa5c6f19af64b55ca170bfa1edf"},
{file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261ddb7ca91fcf71757979534fb4c128448b5b4c55cb6152d280312062f69599"},
{file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ce49c67f4ea0609933d01c0731b34b8695a7a748d6c8d186f95e7d085d2fe475"},
{file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b14f16f94cbc61215115b9b1236f9c18403c15dd3c52cf629072afa9d54c1cbf"},
{file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d33891be6df59d93df4d846640f0e46f1a807339f09e79a8040bc887bdcd7ed3"},
{file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b50811d664d392f02f7761621303eba9d1b056fb1868c8cdf4231279645c25f5"},
{file = "pillow-10.3.0-cp310-cp310-win32.whl", hash = "sha256:ca2870d5d10d8726a27396d3ca4cf7976cec0f3cb706debe88e3a5bd4610f7d2"},
{file = "pillow-10.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f0d0591a0aeaefdaf9a5e545e7485f89910c977087e7de2b6c388aec32011e9f"},
{file = "pillow-10.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:ccce24b7ad89adb5a1e34a6ba96ac2530046763912806ad4c247356a8f33a67b"},
{file = "pillow-10.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795"},
{file = "pillow-10.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57"},
{file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27"},
{file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994"},
{file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451"},
{file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd"},
{file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad"},
{file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c"},
{file = "pillow-10.3.0-cp311-cp311-win32.whl", hash = "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09"},
{file = "pillow-10.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d"},
{file = "pillow-10.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f"},
{file = "pillow-10.3.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:e46f38133e5a060d46bd630faa4d9fa0202377495df1f068a8299fd78c84de84"},
{file = "pillow-10.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:50b8eae8f7334ec826d6eeffaeeb00e36b5e24aa0b9df322c247539714c6df19"},
{file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d3bea1c75f8c53ee4d505c3e67d8c158ad4df0d83170605b50b64025917f338"},
{file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19aeb96d43902f0a783946a0a87dbdad5c84c936025b8419da0a0cd7724356b1"},
{file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74d28c17412d9caa1066f7a31df8403ec23d5268ba46cd0ad2c50fb82ae40462"},
{file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a"},
{file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d886f5d353333b4771d21267c7ecc75b710f1a73d72d03ca06df49b09015a9ef"},
{file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b5ec25d8b17217d635f8935dbc1b9aa5907962fae29dff220f2659487891cd3"},
{file = "pillow-10.3.0-cp312-cp312-win32.whl", hash = "sha256:51243f1ed5161b9945011a7360e997729776f6e5d7005ba0c6879267d4c5139d"},
{file = "pillow-10.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:412444afb8c4c7a6cc11a47dade32982439925537e483be7c0ae0cf96c4f6a0b"},
{file = "pillow-10.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:798232c92e7665fe82ac085f9d8e8ca98826f8e27859d9a96b41d519ecd2e49a"},
{file = "pillow-10.3.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4eaa22f0d22b1a7e93ff0a596d57fdede2e550aecffb5a1ef1106aaece48e96b"},
{file = "pillow-10.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cd5e14fbf22a87321b24c88669aad3a51ec052eb145315b3da3b7e3cc105b9a2"},
{file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1530e8f3a4b965eb6a7785cf17a426c779333eb62c9a7d1bbcf3ffd5bf77a4aa"},
{file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d512aafa1d32efa014fa041d38868fda85028e3f930a96f85d49c7d8ddc0383"},
{file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:339894035d0ede518b16073bdc2feef4c991ee991a29774b33e515f1d308e08d"},
{file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:aa7e402ce11f0885305bfb6afb3434b3cd8f53b563ac065452d9d5654c7b86fd"},
{file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0ea2a783a2bdf2a561808fe4a7a12e9aa3799b701ba305de596bc48b8bdfce9d"},
{file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c78e1b00a87ce43bb37642c0812315b411e856a905d58d597750eb79802aaaa3"},
{file = "pillow-10.3.0-cp38-cp38-win32.whl", hash = "sha256:72d622d262e463dfb7595202d229f5f3ab4b852289a1cd09650362db23b9eb0b"},
{file = "pillow-10.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:2034f6759a722da3a3dbd91a81148cf884e91d1b747992ca288ab88c1de15999"},
{file = "pillow-10.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2ed854e716a89b1afcedea551cd85f2eb2a807613752ab997b9974aaa0d56936"},
{file = "pillow-10.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc1a390a82755a8c26c9964d457d4c9cbec5405896cba94cf51f36ea0d855002"},
{file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4203efca580f0dd6f882ca211f923168548f7ba334c189e9eab1178ab840bf60"},
{file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3102045a10945173d38336f6e71a8dc71bcaeed55c3123ad4af82c52807b9375"},
{file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6fb1b30043271ec92dc65f6d9f0b7a830c210b8a96423074b15c7bc999975f57"},
{file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8"},
{file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b09b86b27a064c9624d0a6c54da01c1beaf5b6cadfa609cf63789b1d08a797b9"},
{file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d3b2348a78bc939b4fed6552abfd2e7988e0f81443ef3911a4b8498ca084f6eb"},
{file = "pillow-10.3.0-cp39-cp39-win32.whl", hash = "sha256:45ebc7b45406febf07fef35d856f0293a92e7417ae7933207e90bf9090b70572"},
{file = "pillow-10.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:0ba26351b137ca4e0db0342d5d00d2e355eb29372c05afd544ebf47c0956ffeb"},
{file = "pillow-10.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:50fd3f6b26e3441ae07b7c979309638b72abc1a25da31a81a7fbd9495713ef4f"},
{file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6b02471b72526ab8a18c39cb7967b72d194ec53c1fd0a70b050565a0f366d355"},
{file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8ab74c06ffdab957d7670c2a5a6e1a70181cd10b727cd788c4dd9005b6a8acd9"},
{file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:048eeade4c33fdf7e08da40ef402e748df113fd0b4584e32c4af74fe78baaeb2"},
{file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2ec1e921fd07c7cda7962bad283acc2f2a9ccc1b971ee4b216b75fad6f0463"},
{file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c8e73e99da7db1b4cad7f8d682cf6abad7844da39834c288fbfa394a47bbced"},
{file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:16563993329b79513f59142a6b02055e10514c1a8e86dca8b48a893e33cf91e3"},
{file = "pillow-10.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dd78700f5788ae180b5ee8902c6aea5a5726bac7c364b202b4b3e3ba2d293170"},
{file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aff76a55a8aa8364d25400a210a65ff59d0168e0b4285ba6bf2bd83cf675ba32"},
{file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b7bc2176354defba3edc2b9a777744462da2f8e921fbaf61e52acb95bafa9828"},
{file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:793b4e24db2e8742ca6423d3fde8396db336698c55cd34b660663ee9e45ed37f"},
{file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93480005693d247f8346bc8ee28c72a2191bdf1f6b5db469c096c0c867ac015"},
{file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83341b89884e2b2e55886e8fbbf37c3fa5efd6c8907124aeb72f285ae5696e5"},
{file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1a1d1915db1a4fdb2754b9de292642a39a7fb28f1736699527bb649484fb966a"},
{file = "pillow-10.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a0eaa93d054751ee9964afa21c06247779b90440ca41d184aeb5d410f20ff591"},
{file = "pillow-10.3.0.tar.gz", hash = "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d"},
]
[package.extras]
docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"]
fpx = ["olefile"]
mic = ["olefile"]
tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
typing = ["typing-extensions"]
xmp = ["defusedxml"]
[[package]]
name = "playwright"
version = "1.43.0"
@@ -1493,6 +1692,20 @@ typing-extensions = "*"
[package.extras]
dev = ["black", "build", "flake8", "flake8-black", "isort", "jupyter-console", "mkdocs", "mkdocs-include-markdown-plugin", "mkdocstrings[python]", "pytest", "pytest-asyncio", "pytest-trio", "sphinx", "toml", "tox", "trio", "trio", "trio-typing", "twine", "twisted", "validate-pyproject[all]"]
[[package]]
name = "pygments"
version = "2.18.0"
description = "Pygments is a syntax highlighting package written in Python."
optional = false
python-versions = ">=3.8"
files = [
{file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"},
{file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"},
]
[package.extras]
windows-terminal = ["colorama (>=0.4.6)"]
[[package]]
name = "pygtrie"
version = "2.5.0"
@@ -1603,30 +1816,48 @@ files = [
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
]
[[package]]
name = "rich"
version = "13.7.1"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
optional = false
python-versions = ">=3.7.0"
files = [
{file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"},
{file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"},
]
[package.dependencies]
markdown-it-py = ">=2.2.0"
pygments = ">=2.13.0,<3.0.0"
[package.extras]
jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]]
name = "ruff"
version = "0.4.2"
version = "0.4.3"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.4.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:8d14dc8953f8af7e003a485ef560bbefa5f8cc1ad994eebb5b12136049bbccc5"},
{file = "ruff-0.4.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:24016ed18db3dc9786af103ff49c03bdf408ea253f3cb9e3638f39ac9cf2d483"},
{file = "ruff-0.4.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2e06459042ac841ed510196c350ba35a9b24a643e23db60d79b2db92af0c2b"},
{file = "ruff-0.4.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3afabaf7ba8e9c485a14ad8f4122feff6b2b93cc53cd4dad2fd24ae35112d5c5"},
{file = "ruff-0.4.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:799eb468ea6bc54b95527143a4ceaf970d5aa3613050c6cff54c85fda3fde480"},
{file = "ruff-0.4.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ec4ba9436a51527fb6931a8839af4c36a5481f8c19e8f5e42c2f7ad3a49f5069"},
{file = "ruff-0.4.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6a2243f8f434e487c2a010c7252150b1fdf019035130f41b77626f5655c9ca22"},
{file = "ruff-0.4.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8772130a063f3eebdf7095da00c0b9898bd1774c43b336272c3e98667d4fb8fa"},
{file = "ruff-0.4.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ab165ef5d72392b4ebb85a8b0fbd321f69832a632e07a74794c0e598e7a8376"},
{file = "ruff-0.4.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1f32cadf44c2020e75e0c56c3408ed1d32c024766bd41aedef92aa3ca28eef68"},
{file = "ruff-0.4.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:22e306bf15e09af45ca812bc42fa59b628646fa7c26072555f278994890bc7ac"},
{file = "ruff-0.4.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:82986bb77ad83a1719c90b9528a9dd663c9206f7c0ab69282af8223566a0c34e"},
{file = "ruff-0.4.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:652e4ba553e421a6dc2a6d4868bc3b3881311702633eb3672f9f244ded8908cd"},
{file = "ruff-0.4.2-py3-none-win32.whl", hash = "sha256:7891ee376770ac094da3ad40c116258a381b86c7352552788377c6eb16d784fe"},
{file = "ruff-0.4.2-py3-none-win_amd64.whl", hash = "sha256:5ec481661fb2fd88a5d6cf1f83403d388ec90f9daaa36e40e2c003de66751798"},
{file = "ruff-0.4.2-py3-none-win_arm64.whl", hash = "sha256:cbd1e87c71bca14792948c4ccb51ee61c3296e164019d2d484f3eaa2d360dfaf"},
{file = "ruff-0.4.2.tar.gz", hash = "sha256:33bcc160aee2520664bc0859cfeaebc84bb7323becff3f303b8f1f2d81cb4edc"},
{file = "ruff-0.4.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b70800c290f14ae6fcbb41bbe201cf62dfca024d124a1f373e76371a007454ce"},
{file = "ruff-0.4.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:08a0d6a22918ab2552ace96adeaca308833873a4d7d1d587bb1d37bae8728eb3"},
{file = "ruff-0.4.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba1f14df3c758dd7de5b55fbae7e1c8af238597961e5fb628f3de446c3c40c5"},
{file = "ruff-0.4.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:819fb06d535cc76dfddbfe8d3068ff602ddeb40e3eacbc90e0d1272bb8d97113"},
{file = "ruff-0.4.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bfc9e955e6dc6359eb6f82ea150c4f4e82b660e5b58d9a20a0e42ec3bb6342b"},
{file = "ruff-0.4.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:510a67d232d2ebe983fddea324dbf9d69b71c4d2dfeb8a862f4a127536dd4cfb"},
{file = "ruff-0.4.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc9ff11cd9a092ee7680a56d21f302bdda14327772cd870d806610a3503d001f"},
{file = "ruff-0.4.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29efff25bf9ee685c2c8390563a5b5c006a3fee5230d28ea39f4f75f9d0b6f2f"},
{file = "ruff-0.4.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18b00e0bcccf0fc8d7186ed21e311dffd19761cb632241a6e4fe4477cc80ef6e"},
{file = "ruff-0.4.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:262f5635e2c74d80b7507fbc2fac28fe0d4fef26373bbc62039526f7722bca1b"},
{file = "ruff-0.4.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7363691198719c26459e08cc17c6a3dac6f592e9ea3d2fa772f4e561b5fe82a3"},
{file = "ruff-0.4.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:eeb039f8428fcb6725bb63cbae92ad67b0559e68b5d80f840f11914afd8ddf7f"},
{file = "ruff-0.4.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:927b11c1e4d0727ce1a729eace61cee88a334623ec424c0b1c8fe3e5f9d3c865"},
{file = "ruff-0.4.3-py3-none-win32.whl", hash = "sha256:25cacda2155778beb0d064e0ec5a3944dcca9c12715f7c4634fd9d93ac33fd30"},
{file = "ruff-0.4.3-py3-none-win_amd64.whl", hash = "sha256:7a1c3a450bc6539ef00da6c819fb1b76b6b065dec585f91456e7c0d6a0bbc725"},
{file = "ruff-0.4.3-py3-none-win_arm64.whl", hash = "sha256:71ca5f8ccf1121b95a59649482470c5601c60a416bf189d553955b0338e34614"},
{file = "ruff-0.4.3.tar.gz", hash = "sha256:ff0a3ef2e3c4b6d133fbedcf9586abfbe38d076041f2dc18ffb2c7e0485d5a07"},
]
[[package]]
@@ -1756,6 +1987,22 @@ anyio = ">=3.4.0,<5"
[package.extras]
full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"]
[[package]]
name = "strenum"
version = "0.4.15"
description = "An Enum that inherits from str."
optional = false
python-versions = "*"
files = [
{file = "StrEnum-0.4.15-py3-none-any.whl", hash = "sha256:a30cda4af7cc6b5bf52c8055bc4bf4b2b6b14a93b574626da33df53cf7740659"},
{file = "StrEnum-0.4.15.tar.gz", hash = "sha256:878fb5ab705442070e4dd1929bb5e2249511c0bcf2b0eeacf3bcd80875c82eff"},
]
[package.extras]
docs = ["myst-parser[linkify]", "sphinx", "sphinx-rtd-theme"]
release = ["twine"]
test = ["pylint", "pytest", "pytest-black", "pytest-cov", "pytest-pylint"]
[[package]]
name = "tarina"
version = "0.4.4"
@@ -1881,6 +2128,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"
@@ -2420,6 +2678,21 @@ files = [
idna = ">=2.0"
multidict = ">=4.0"
[[package]]
name = "zipp"
version = "3.18.1"
description = "Backport of pathlib-compatible object wrapper for zip files"
optional = false
python-versions = ">=3.8"
files = [
{file = "zipp-3.18.1-py3-none-any.whl", hash = "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b"},
{file = "zipp-3.18.1.tar.gz", hash = "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715"},
]
[package.extras]
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
[[package]]
name = "zstandard"
version = "0.22.0"
@@ -2484,4 +2757,4 @@ cffi = ["cffi (>=1.11)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "2dea237ec3f9010dd57b8227a8911e59f09b2e01da056b3f787aa32bd76cf2d6"
content-hash = "0bdc912fa16ac0774edb3c5343152ecbec81f8a9764bb7e41e32dbf8259127ad"

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = 'nonebot-plugin-tetris-stats'
version = '1.0.0.a16'
version = '1.1.2'
description = '一款基于 NoneBot2 的用于查询 Tetris 相关游戏数据的插件'
authors = ['scdhh <wallfjjd@gmail.com>']
readme = 'README.md'
@@ -10,7 +10,7 @@ license = 'AGPL-3.0'
[tool.poetry.dependencies]
python = '^3.10'
nonebot2 = '^2.2.0'
nonebot2 = { extras = ["fastapi"], version = "^2.3.0" }
lxml = '^5.1.0'
pandas = '>=1.4.3,<3.0.0'
playwright = '^1.41.2'
@@ -23,6 +23,10 @@ nonebot-plugin-alconna = ">=0.40"
nonebot-plugin-apscheduler = "^0.4.0"
aiocache = "^0.12.2"
zstandard = "^0.22.0"
jinja2 = "^3.1.3"
nonebot-plugin-userinfo = "^0.2.4"
pillow = "^10.3.0"
rich = "^13.7.1"
[tool.poetry.group.dev.dependencies]
mypy = '>=1.9'
@@ -30,13 +34,13 @@ types-ujson = '^5.9.0'
pandas-stubs = '>=1.5.2,<3.0.0'
ruff = '>=0.3.0'
types-aiofiles = "^23.2.0.20240106"
nonebot2 = { extras = ["fastapi"], version = "^2.2.0" }
types-lxml = "^2024.2.9"
nonebot-plugin-orm = { extras = ["default"], version = ">=0.3,<0.8" }
nonebot-adapter-onebot = "^2.4.1"
nonebot-adapter-satori = "^0.11.3"
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'
@@ -47,6 +51,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
@@ -87,14 +95,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]