Compare commits

...

131 Commits

Author SHA1 Message Date
08a1a427b4 🔖 1.6.2 2024-10-29 00:44:55 +08:00
呵呵です
d4e91c8521 🐛 修复被 ban 的爆炸 (#510) 2024-10-28 16:43:38 +00:00
dbde1181ce 🔖 1.6.1 2024-10-28 22:06:30 +08:00
呵呵です
86fe4f0766 🐛 修复 handle_history_data 的索引越界问题 (#509) 2024-10-28 14:01:11 +00:00
呵呵です
381f2505d6 🐛 修复 leagueflow 没有有效数据爆炸 (#508) 2024-10-28 13:58:38 +00:00
b3a77f5296 💚 修复 basedpyright
https://github.com/DetachHead/basedpyright/issues/819
2024-10-27 21:28:19 +08:00
274f30f82a 🔖 1.6.0 2024-10-27 21:06:12 +08:00
efb1ddb260 🔧 更新 renovate 配置 2024-10-27 20:58:39 +08:00
7e3f49bc9e 🔧 更新 bumpversion 配置 2024-10-27 20:53:06 +08:00
665772ed66 添加 release 依赖 bump-my-version 2024-10-27 20:49:56 +08:00
renovate[bot]
44fda8a19e ⬆️ Upgrade github/codeql-action action to v3 (#506)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-27 12:07:27 +00:00
renovate[bot]
6921bf4e37 ⬆️ Upgrade actions/checkout action to v4 (#504)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-27 20:05:17 +08:00
renovate[bot]
c3c97c1c8b 🔧 Configure Renovate (#500)
* Add renovate.json

* 🔧 更新配置文件

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: shoucandanghehe <wallfjjd@gmail.com>
2024-10-27 19:58:46 +08:00
呵呵です
1d33872c9b 🧑‍💻 使用 uv 管理项目 (#502)
* 🧑‍💻 使用 uv 管理项目

* 📝 更新 CONTRIBUTING.md

* 🔥 移除 Dependabot

*  修改默认安装的依赖组

* 💚 使用 uv 运行 CI

*  将特殊适配的适配器移动到 dev 依赖组

* 🚨 make mypy happy
2024-10-27 18:46:46 +08:00
呵呵です
b2d5a1e729 🌐 支持i18n (#501)
*  支持 i18n #410

* 🚨 更改noqa方式

* 📝 添加 CONTRIBUTING.md 文件

* 🌐 将 i18n 默认语言设置为 en-US

* 📝 添加英文版 CONTRIBUTING.md
2024-10-26 18:29:51 +08:00
呵呵です
a0fd9eaed3 历史tr图表 (#499)
*  实现获取 leagueflow

*  TETR.IO 适配 v1 模板

*  限制 v2 history 数量
2024-10-26 18:05:58 +08:00
呵呵です
593723aa76 🎨 重命名变量 (#495) 2024-10-25 04:29:54 +00:00
dependabot[bot]
73d97d8458 ⬆️ Bump mypy from 1.12.1 to 1.13.0 (#491)
Bumps [mypy](https://github.com/python/mypy) from 1.12.1 to 1.13.0.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.12.1...v1.13.0)

---
updated-dependencies:
- dependency-name: mypy
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-25 04:25:03 +00:00
dependabot[bot]
d6f11655c1 ⬆️ Bump ruff from 0.7.0 to 0.7.1 (#493)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.7.0 to 0.7.1.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.7.0...0.7.1)

---
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-10-25 04:22:01 +00:00
dependabot[bot]
376e85e36e ⬆️ Bump basedpyright from 1.19.0 to 1.19.1 (#490)
Bumps [basedpyright](https://github.com/detachhead/basedpyright) from 1.19.0 to 1.19.1.
- [Release notes](https://github.com/detachhead/basedpyright/releases)
- [Commits](https://github.com/detachhead/basedpyright/compare/v1.19.0...v1.19.1)

---
updated-dependencies:
- dependency-name: basedpyright
  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-10-25 04:19:02 +00:00
dependabot[bot]
45116a1418 ⬆️ Bump rich from 13.9.2 to 13.9.3 (#492)
Bumps [rich](https://github.com/Textualize/rich) from 13.9.2 to 13.9.3.
- [Release notes](https://github.com/Textualize/rich/releases)
- [Changelog](https://github.com/Textualize/rich/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Textualize/rich/compare/v13.9.2...v13.9.3)

---
updated-dependencies:
- dependency-name: rich
  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-10-25 04:16:12 +00:00
dependabot[bot]
a42d3e3837 ⬆️ Bump nonebot-adapter-onebot from 2.4.5 to 2.4.6 (#494)
Bumps [nonebot-adapter-onebot](https://github.com/nonebot/adapter-onebot) from 2.4.5 to 2.4.6.
- [Release notes](https://github.com/nonebot/adapter-onebot/releases)
- [Commits](https://github.com/nonebot/adapter-onebot/compare/v2.4.5...v2.4.6)

---
updated-dependencies:
- dependency-name: nonebot-adapter-onebot
  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-10-25 12:13:02 +08:00
pre-commit-ci[bot]
b333c54c7d ⬆️ auto update by pre-commit hooks (#487)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.6.9 → v0.7.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.9...v0.7.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-10-23 14:57:41 +00:00
dependabot[bot]
8840402d2f ⬆️ Bump playwright from 1.47.0 to 1.48.0 (#483)
Bumps [playwright](https://github.com/Microsoft/playwright-python) from 1.47.0 to 1.48.0.
- [Release notes](https://github.com/Microsoft/playwright-python/releases)
- [Commits](https://github.com/Microsoft/playwright-python/compare/v1.47.0...v1.48.0)

---
updated-dependencies:
- dependency-name: playwright
  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-10-22 16:49:43 +00:00
dependabot[bot]
e8b64b23f5 ⬆️ Bump mypy from 1.12.0 to 1.12.1 (#486)
Bumps [mypy](https://github.com/python/mypy) from 1.12.0 to 1.12.1.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.12.0...v1.12.1)

---
updated-dependencies:
- dependency-name: mypy
  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-10-22 16:45:55 +00:00
dependabot[bot]
40762a3180 ⬆️ Bump nonebot-plugin-user from 0.4.3 to 0.4.4 (#485)
Bumps [nonebot-plugin-user](https://github.com/he0119/nonebot-plugin-user) from 0.4.3 to 0.4.4.
- [Release notes](https://github.com/he0119/nonebot-plugin-user/releases)
- [Changelog](https://github.com/he0119/nonebot-plugin-user/blob/main/CHANGELOG.md)
- [Commits](https://github.com/he0119/nonebot-plugin-user/compare/v0.4.3...v0.4.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-22 16:42:11 +00:00
dependabot[bot]
a2c6ad8328 ⬆️ Bump yarl from 1.15.4 to 1.16.0 (#488)
Bumps [yarl](https://github.com/aio-libs/yarl) from 1.15.4 to 1.16.0.
- [Release notes](https://github.com/aio-libs/yarl/releases)
- [Changelog](https://github.com/aio-libs/yarl/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/yarl/compare/v1.15.4...v1.16.0)

---
updated-dependencies:
- dependency-name: yarl
  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-10-22 16:38:33 +00:00
呵呵です
c7d93069ef 🐛 兼容pydantic v1 (#489) 2024-10-22 16:00:22 +00:00
4b514df2db 移除依赖 zstandard 2024-10-22 22:39:11 +08:00
47c83be1b5 🔖 1.5.5 2024-10-19 18:55:07 +08:00
6c0e092f51 🔊 优化 limit 日志 2024-10-19 18:52:18 +08:00
04b9cd9eae 🚨 type ignore 2024-10-19 18:52:17 +08:00
呵呵です
61b5fcb137 为 TETR.IO 引入全局速率限制 (#481) 2024-10-19 10:34:56 +00:00
c0540769c8 🔖 1.5.4 2024-10-19 17:40:00 +08:00
dependabot[bot]
0e19943046 ⬆️ Bump ruff from 0.6.9 to 0.7.0 (#479)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.6.9 to 0.7.0.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.6.9...0.7.0)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-19 09:37:36 +00:00
dependabot[bot]
7e1d2e8cb0 ⬆️ Bump nonebot-plugin-alconna from 0.53.0 to 0.53.1 (#478)
Bumps [nonebot-plugin-alconna](https://github.com/nonebot/plugin-alconna) from 0.53.0 to 0.53.1.
- [Release notes](https://github.com/nonebot/plugin-alconna/releases)
- [Commits](https://github.com/nonebot/plugin-alconna/compare/v0.53.0...v0.53.1)

---
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-10-19 09:33:54 +00:00
dependabot[bot]
8931cfb5a7 ⬆️ Bump basedpyright from 1.18.4 to 1.19.0 (#474)
Bumps [basedpyright](https://github.com/detachhead/basedpyright) from 1.18.4 to 1.19.0.
- [Release notes](https://github.com/detachhead/basedpyright/releases)
- [Commits](https://github.com/detachhead/basedpyright/compare/v1.18.4...v1.19.0)

---
updated-dependencies:
- dependency-name: basedpyright
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-19 09:30:23 +00:00
呵呵です
ea8a18c1b1 🐛 修复 茶服 api 查询链接拼接错误 (#480) 2024-10-19 09:27:13 +00:00
ef1acb0f16 🚨 ignore reportGeneralTypeIssues
https://github.com/microsoft/pyright/issues/9220
2024-10-19 17:20:42 +08:00
dependabot[bot]
f7bb667254 ⬆️ Bump yarl from 1.15.3 to 1.15.4 (#476)
Bumps [yarl](https://github.com/aio-libs/yarl) from 1.15.3 to 1.15.4.
- [Release notes](https://github.com/aio-libs/yarl/releases)
- [Changelog](https://github.com/aio-libs/yarl/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/yarl/compare/v1.15.3...v1.15.4)

---
updated-dependencies:
- dependency-name: yarl
  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-10-18 10:34:24 +08:00
dependabot[bot]
fa94c1beeb ⬆️ Bump viztracer from 0.16.3 to 0.17.0 (#471)
Bumps [viztracer](https://github.com/gaogaotiantian/viztracer) from 0.16.3 to 0.17.0.
- [Release notes](https://github.com/gaogaotiantian/viztracer/releases)
- [Commits](https://github.com/gaogaotiantian/viztracer/compare/0.16.3...0.17.0)

---
updated-dependencies:
- dependency-name: viztracer
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-16 03:09:52 +00:00
dependabot[bot]
4e1e91a977 ⬆️ Bump yarl from 1.14.0 to 1.15.3 (#473)
Bumps [yarl](https://github.com/aio-libs/yarl) from 1.14.0 to 1.15.3.
- [Release notes](https://github.com/aio-libs/yarl/releases)
- [Changelog](https://github.com/aio-libs/yarl/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/yarl/compare/v1.14.0...v1.15.3)

---
updated-dependencies:
- dependency-name: yarl
  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-10-16 03:06:00 +00:00
dependabot[bot]
0f6a00819b ⬆️ Bump mypy from 1.11.2 to 1.12.0 (#469)
Bumps [mypy](https://github.com/python/mypy) from 1.11.2 to 1.12.0.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.11.2...v1.12.0)

---
updated-dependencies:
- dependency-name: mypy
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-16 03:02:21 +00:00
dependabot[bot]
b56385b412 ⬆️ Bump pillow from 10.4.0 to 11.0.0 (#470)
Bumps [pillow](https://github.com/python-pillow/Pillow) from 10.4.0 to 11.0.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/10.4.0...11.0.0)

---
updated-dependencies:
- dependency-name: pillow
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-16 02:58:27 +00:00
dependabot[bot]
7eea235f52 ⬆️ Bump starlette from 0.38.2 to 0.40.0 (#472)
Bumps [starlette](https://github.com/encode/starlette) from 0.38.2 to 0.40.0.
- [Release notes](https://github.com/encode/starlette/releases)
- [Changelog](https://github.com/encode/starlette/blob/master/docs/release-notes.md)
- [Commits](https://github.com/encode/starlette/compare/0.38.2...0.40.0)

---
updated-dependencies:
- dependency-name: starlette
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-16 10:54:36 +08:00
dependabot[bot]
8a06b572ed ⬆️ Bump arclet-alconna from 1.8.30 to 1.8.31 (#468)
Bumps [arclet-alconna](https://github.com/ArcletProject/Alconna) from 1.8.30 to 1.8.31.
- [Release notes](https://github.com/ArcletProject/Alconna/releases)
- [Changelog](https://github.com/ArcletProject/Alconna/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/ArcletProject/Alconna/compare/v1.8.30...v1.8.31)

---
updated-dependencies:
- dependency-name: arclet-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-10-15 23:06:13 +08:00
6867245be3 🔖 1.5.3 2024-10-13 01:14:14 +08:00
dependabot[bot]
eebff0a8ad ⬆️ Bump pandas-stubs from 2.2.2.240909 to 2.2.3.241009 (#466)
Bumps [pandas-stubs](https://github.com/pandas-dev/pandas-stubs) from 2.2.2.240909 to 2.2.3.241009.
- [Changelog](https://github.com/pandas-dev/pandas-stubs/blob/main/docs/release_procedure.md)
- [Commits](https://github.com/pandas-dev/pandas-stubs/compare/v2.2.2.240909...v2.2.3.241009)

---
updated-dependencies:
- dependency-name: pandas-stubs
  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-10-12 16:48:51 +00:00
74eef41506 🏷️ 添加一些 TypeAlias 2024-10-13 00:46:11 +08:00
5eb4771259 TETR.IO list 自动将 country 转大写 2024-10-13 00:45:36 +08:00
7a3a4d936d 🐛 修正一些 trigger 2024-10-13 00:38:37 +08:00
pre-commit-ci[bot]
03ca7c4486 ⬆️ auto update by pre-commit hooks (#463)
* ⬆️ auto update by pre-commit hooks

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.6.8 → v0.6.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.8...v0.6.9)

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-10-10 21:43:13 +00:00
dependabot[bot]
b043d1da59 ⬆️ Bump yarl from 1.13.1 to 1.14.0 (#464)
Bumps [yarl](https://github.com/aio-libs/yarl) from 1.13.1 to 1.14.0.
- [Release notes](https://github.com/aio-libs/yarl/releases)
- [Changelog](https://github.com/aio-libs/yarl/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/yarl/compare/v1.13.1...v1.14.0)

---
updated-dependencies:
- dependency-name: yarl
  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-10-10 21:24:46 +00:00
dependabot[bot]
c9659201b1 ⬆️ Bump nonebot-plugin-alconna from 0.52.3 to 0.53.0 (#462)
Bumps [nonebot-plugin-alconna](https://github.com/nonebot/plugin-alconna) from 0.52.3 to 0.53.0.
- [Release notes](https://github.com/nonebot/plugin-alconna/releases)
- [Commits](https://github.com/nonebot/plugin-alconna/compare/v0.52.3...v0.53.0)

---
updated-dependencies:
- dependency-name: nonebot-plugin-alconna
  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-10-10 21:20:07 +00:00
dependabot[bot]
617d3ec658 ⬆️ Bump ruff from 0.6.8 to 0.6.9 (#461)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.6.8 to 0.6.9.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.6.8...0.6.9)

---
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-10-10 21:16:06 +00:00
dependabot[bot]
57a1992675 ⬆️ Bump rich from 13.9.1 to 13.9.2 (#460)
Bumps [rich](https://github.com/Textualize/rich) from 13.9.1 to 13.9.2.
- [Release notes](https://github.com/Textualize/rich/releases)
- [Changelog](https://github.com/Textualize/rich/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Textualize/rich/compare/v13.9.1...v13.9.2)

---
updated-dependencies:
- dependency-name: rich
  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-10-10 21:11:59 +00:00
dependabot[bot]
8d1d2f329e ⬆️ Bump basedpyright from 1.18.3 to 1.18.4 (#465)
Bumps [basedpyright](https://github.com/detachhead/basedpyright) from 1.18.3 to 1.18.4.
- [Release notes](https://github.com/detachhead/basedpyright/releases)
- [Commits](https://github.com/detachhead/basedpyright/compare/v1.18.3...v1.18.4)

---
updated-dependencies:
- dependency-name: basedpyright
  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-10-11 05:07:53 +08:00
dependabot[bot]
fa6cbd5c6d ⬆️ Bump rich from 13.8.1 to 13.9.1 (#459)
Bumps [rich](https://github.com/Textualize/rich) from 13.8.1 to 13.9.1.
- [Release notes](https://github.com/Textualize/rich/releases)
- [Changelog](https://github.com/Textualize/rich/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Textualize/rich/compare/v13.8.1...v13.9.1)

---
updated-dependencies:
- dependency-name: rich
  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-10-03 04:33:33 +08:00
pre-commit-ci[bot]
9f0f0b87f4 ⬆️ auto update by pre-commit hooks (#457)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.6.7 → v0.6.8](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.7...v0.6.8)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-10-02 15:59:02 +00:00
dependabot[bot]
96c298b1b8 ⬆️ Bump ruff from 0.6.7 to 0.6.8 (#451)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.6.7 to 0.6.8.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.6.7...0.6.8)

---
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-10-02 15:35:25 +00:00
dependabot[bot]
df5ced235d ⬆️ Bump basedpyright from 1.17.5 to 1.18.3 (#458)
Bumps [basedpyright](https://github.com/detachhead/basedpyright) from 1.17.5 to 1.18.3.
- [Release notes](https://github.com/detachhead/basedpyright/releases)
- [Commits](https://github.com/detachhead/basedpyright/compare/v1.17.5...v1.18.3)

---
updated-dependencies:
- dependency-name: basedpyright
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-02 15:31:34 +00:00
dependabot[bot]
af83c7a2d9 ⬆️ Bump nonebot-adapter-qq from 1.5.1 to 1.5.2 (#455)
Bumps [nonebot-adapter-qq](https://github.com/nonebot/adapter-qq) from 1.5.1 to 1.5.2.
- [Release notes](https://github.com/nonebot/adapter-qq/releases)
- [Commits](https://github.com/nonebot/adapter-qq/compare/v1.5.1...v1.5.2)

---
updated-dependencies:
- dependency-name: nonebot-adapter-qq
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-02 15:26:22 +00:00
dependabot[bot]
bc41a91034 ⬆️ Bump yarl from 1.11.1 to 1.13.1 (#454)
Bumps [yarl](https://github.com/aio-libs/yarl) from 1.11.1 to 1.13.1.
- [Release notes](https://github.com/aio-libs/yarl/releases)
- [Changelog](https://github.com/aio-libs/yarl/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/yarl/compare/v1.11.1...v1.13.1)

---
updated-dependencies:
- dependency-name: yarl
  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-10-02 15:22:33 +00:00
dependabot[bot]
d97291d1bc ⬆️ Bump aiocache from 0.12.2 to 0.12.3 (#449)
Bumps [aiocache](https://github.com/aio-libs/aiocache) from 0.12.2 to 0.12.3.
- [Release notes](https://github.com/aio-libs/aiocache/releases)
- [Changelog](https://github.com/aio-libs/aiocache/blob/master/.gitchangelog.rc)
- [Commits](https://github.com/aio-libs/aiocache/compare/v0.12.2...v0.12.3)

---
updated-dependencies:
- dependency-name: aiocache
  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-10-02 23:18:54 +08:00
5b56de9de1 🚨 添加一个 noqa( 2024-10-02 03:42:34 +08:00
0898a81331 优化数据库迁移脚本 2024-10-02 03:22:43 +08:00
pre-commit-ci[bot]
d464059c0a ⬆️ auto update by pre-commit hooks (#447)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.6.5 → v0.6.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.5...v0.6.7)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-09-24 06:21:04 +00:00
dependabot[bot]
6ea8b9328c ⬆️ Bump nonebot-plugin-alconna from 0.52.2 to 0.52.3 (#446)
Bumps [nonebot-plugin-alconna](https://github.com/nonebot/plugin-alconna) from 0.52.2 to 0.52.3.
- [Release notes](https://github.com/nonebot/plugin-alconna/releases)
- [Commits](https://github.com/nonebot/plugin-alconna/compare/v0.52.2...v0.52.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-09-24 06:18:24 +00:00
dependabot[bot]
773ff5545c ⬆️ Bump ruff from 0.6.6 to 0.6.7 (#445)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.6.6 to 0.6.7.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.6.6...0.6.7)

---
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-09-24 06:14:09 +00:00
dependabot[bot]
94710b938b ⬆️ Bump nonebot-adapter-satori from 0.12.5 to 0.12.6 (#444)
Bumps [nonebot-adapter-satori](https://github.com/nonebot/adapter-satori) from 0.12.5 to 0.12.6.
- [Release notes](https://github.com/nonebot/adapter-satori/releases)
- [Commits](https://github.com/nonebot/adapter-satori/compare/v0.12.5...v0.12.6)

---
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-09-24 14:10:06 +08:00
ec09bb734d 📌 将 arclet-alconna 固定至 2.0.0 以下 2024-09-24 14:03:38 +08:00
dependabot[bot]
9e9a642847 ⬆️ Bump pandas from 2.2.2 to 2.2.3 (#443)
Bumps [pandas](https://github.com/pandas-dev/pandas) from 2.2.2 to 2.2.3.
- [Release notes](https://github.com/pandas-dev/pandas/releases)
- [Commits](https://github.com/pandas-dev/pandas/compare/v2.2.2...v2.2.3)

---
updated-dependencies:
- dependency-name: pandas
  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-09-21 06:48:48 +00:00
dependabot[bot]
04e0b14e72 ⬆️ Bump ruff from 0.6.5 to 0.6.6 (#442)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.6.5 to 0.6.6.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.6.5...0.6.6)

---
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-09-21 14:44:44 +08:00
dependabot[bot]
20ce9c64be ⬆️ Bump nonebot-adapter-onebot from 2.4.4 to 2.4.5 (#440)
Bumps [nonebot-adapter-onebot](https://github.com/nonebot/adapter-onebot) from 2.4.4 to 2.4.5.
- [Release notes](https://github.com/nonebot/adapter-onebot/releases)
- [Commits](https://github.com/nonebot/adapter-onebot/compare/v2.4.4...v2.4.5)

---
updated-dependencies:
- dependency-name: nonebot-adapter-onebot
  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-09-19 15:16:22 +00:00
dependabot[bot]
8af07bf031 ⬆️ Bump basedpyright from 1.17.4 to 1.17.5 (#441)
Bumps [basedpyright](https://github.com/detachhead/basedpyright) from 1.17.4 to 1.17.5.
- [Release notes](https://github.com/detachhead/basedpyright/releases)
- [Commits](https://github.com/detachhead/basedpyright/compare/v1.17.4...v1.17.5)

---
updated-dependencies:
- dependency-name: basedpyright
  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-09-19 13:20:49 +00:00
pre-commit-ci[bot]
3a904f67ad ⬆️ auto update by pre-commit hooks (#439)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.6.4 → v0.6.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.4...v0.6.5)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-09-19 12:24:51 +00:00
dependabot[bot]
fc9b751ac4 ⬆️ Bump types-lxml from 2024.8.7 to 2024.9.16 (#438)
Bumps [types-lxml](https://github.com/abelcheung/types-lxml) from 2024.8.7 to 2024.9.16.
- [Release notes](https://github.com/abelcheung/types-lxml/releases)
- [Commits](https://github.com/abelcheung/types-lxml/compare/2024.08.07...2024.09.16)

---
updated-dependencies:
- dependency-name: types-lxml
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-19 20:21:19 +08:00
cb4c6b96f0 🔖 1.5.2 2024-09-14 05:10:32 +08:00
dependabot[bot]
25c3777c0f ⬆️ Bump playwright from 1.46.0 to 1.47.0 (#437)
Bumps [playwright](https://github.com/Microsoft/playwright-python) from 1.46.0 to 1.47.0.
- [Release notes](https://github.com/Microsoft/playwright-python/releases)
- [Commits](https://github.com/Microsoft/playwright-python/compare/v1.46.0...v1.47.0)

---
updated-dependencies:
- dependency-name: playwright
  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-09-13 21:05:13 +00:00
dependabot[bot]
193fd1da2a ⬆️ Bump ruff from 0.6.4 to 0.6.5 (#436)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.6.4 to 0.6.5.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.6.4...0.6.5)

---
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-09-13 21:02:31 +00:00
dependabot[bot]
2cd609dd40 ⬆️ Bump basedpyright from 1.17.3 to 1.17.4 (#435)
Bumps [basedpyright](https://github.com/detachhead/basedpyright) from 1.17.3 to 1.17.4.
- [Release notes](https://github.com/detachhead/basedpyright/releases)
- [Commits](https://github.com/detachhead/basedpyright/compare/v1.17.3...v1.17.4)

---
updated-dependencies:
- dependency-name: basedpyright
  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-09-13 20:58:38 +00:00
a206098805 ️ 试图提高截图速度 2024-09-14 04:50:37 +08:00
dependabot[bot]
d493ba5f0d ⬆️ Bump yarl from 1.11.0 to 1.11.1 (#433)
Bumps [yarl](https://github.com/aio-libs/yarl) from 1.11.0 to 1.11.1.
- [Release notes](https://github.com/aio-libs/yarl/releases)
- [Changelog](https://github.com/aio-libs/yarl/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/yarl/compare/v1.11.0...v1.11.1)

---
updated-dependencies:
- dependency-name: yarl
  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-09-11 11:58:41 +00:00
dependabot[bot]
581d1f9674 ⬆️ Bump pandas-stubs from 2.2.2.240807 to 2.2.2.240909 (#434)
Bumps [pandas-stubs](https://github.com/pandas-dev/pandas-stubs) from 2.2.2.240807 to 2.2.2.240909.
- [Changelog](https://github.com/pandas-dev/pandas-stubs/blob/main/docs/release_procedure.md)
- [Commits](https://github.com/pandas-dev/pandas-stubs/compare/v2.2.2.240807...v2.2.2.240909)

---
updated-dependencies:
- dependency-name: pandas-stubs
  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-09-11 11:54:55 +00:00
dependabot[bot]
01c99e8a8c ⬆️ Bump rich from 13.8.0 to 13.8.1 (#432)
Bumps [rich](https://github.com/Textualize/rich) from 13.8.0 to 13.8.1.
- [Release notes](https://github.com/Textualize/rich/releases)
- [Changelog](https://github.com/Textualize/rich/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Textualize/rich/compare/v13.8.0...v13.8.1)

---
updated-dependencies:
- dependency-name: rich
  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-09-11 19:51:24 +08:00
dependabot[bot]
eb3f4bea04 ⬆️ Bump yarl from 1.10.0 to 1.11.0 (#431)
Bumps [yarl](https://github.com/aio-libs/yarl) from 1.10.0 to 1.11.0.
- [Release notes](https://github.com/aio-libs/yarl/releases)
- [Changelog](https://github.com/aio-libs/yarl/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/yarl/compare/v1.10.0...v1.11.0)

---
updated-dependencies:
- dependency-name: yarl
  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-09-09 19:38:35 +00:00
dependabot[bot]
ebbbd68b05 ⬆️ Bump basedpyright from 1.17.2 to 1.17.3 (#430)
Bumps [basedpyright](https://github.com/detachhead/basedpyright) from 1.17.2 to 1.17.3.
- [Release notes](https://github.com/detachhead/basedpyright/releases)
- [Commits](https://github.com/detachhead/basedpyright/compare/v1.17.2...v1.17.3)

---
updated-dependencies:
- dependency-name: basedpyright
  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-09-10 03:35:03 +08:00
pre-commit-ci[bot]
10e0eb815e ⬆️ auto update by pre-commit hooks (#422)
* ⬆️ auto update by pre-commit hooks

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.6.2 → v0.6.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.2...v0.6.4)

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-09-08 16:49:56 +08:00
dependabot[bot]
a57b04e181 ⬆️ Bump yarl from 1.9.4 to 1.10.0 (#429)
Bumps [yarl](https://github.com/aio-libs/yarl) from 1.9.4 to 1.10.0.
- [Release notes](https://github.com/aio-libs/yarl/releases)
- [Changelog](https://github.com/aio-libs/yarl/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/yarl/compare/v1.9.4...v1.10.0)

---
updated-dependencies:
- dependency-name: yarl
  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-09-08 08:45:09 +00:00
dependabot[bot]
cc2e71f1a5 ⬆️ Bump nonebot-adapter-satori from 0.12.3 to 0.12.5 (#426)
Bumps [nonebot-adapter-satori](https://github.com/nonebot/adapter-satori) from 0.12.3 to 0.12.5.
- [Release notes](https://github.com/nonebot/adapter-satori/releases)
- [Commits](https://github.com/nonebot/adapter-satori/compare/v0.12.3...v0.12.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-09-08 08:41:25 +00:00
dependabot[bot]
3384263bb2 ⬆️ Bump basedpyright from 1.17.1 to 1.17.2 (#425)
Bumps [basedpyright](https://github.com/detachhead/basedpyright) from 1.17.1 to 1.17.2.
- [Release notes](https://github.com/detachhead/basedpyright/releases)
- [Commits](https://github.com/detachhead/basedpyright/compare/v1.17.1...v1.17.2)

---
updated-dependencies:
- dependency-name: basedpyright
  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-09-08 08:37:45 +00:00
dependabot[bot]
68f210dc4f ⬆️ Bump ruff from 0.6.2 to 0.6.4 (#428)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.6.2 to 0.6.4.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.6.2...0.6.4)

---
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-09-08 16:34:07 +08:00
dependabot[bot]
00a85fe3e9 ⬆️ Bump nonebot-plugin-alconna from 0.52.1 to 0.52.2 (#419)
Bumps [nonebot-plugin-alconna](https://github.com/nonebot/plugin-alconna) from 0.52.1 to 0.52.2.
- [Release notes](https://github.com/nonebot/plugin-alconna/releases)
- [Commits](https://github.com/nonebot/plugin-alconna/compare/v0.52.1...v0.52.2)

---
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-08-29 17:02:45 +00:00
dependabot[bot]
a10a7584ae ⬆️ Bump nonebot-plugin-user from 0.4.2 to 0.4.3 (#418)
Bumps [nonebot-plugin-user](https://github.com/he0119/nonebot-plugin-user) from 0.4.2 to 0.4.3.
- [Release notes](https://github.com/he0119/nonebot-plugin-user/releases)
- [Changelog](https://github.com/he0119/nonebot-plugin-user/blob/main/CHANGELOG.md)
- [Commits](https://github.com/he0119/nonebot-plugin-user/compare/v0.4.2...v0.4.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-29 16:58:37 +00:00
dependabot[bot]
95aac5e321 ⬆️ Bump basedpyright from 1.17.0 to 1.17.1 (#417)
Bumps [basedpyright](https://github.com/detachhead/basedpyright) from 1.17.0 to 1.17.1.
- [Release notes](https://github.com/detachhead/basedpyright/releases)
- [Commits](https://github.com/detachhead/basedpyright/compare/v1.17.0...v1.17.1)

---
updated-dependencies:
- dependency-name: basedpyright
  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-08-30 00:56:00 +08:00
89d8c938e2 👷 改成在 CI 中运行 2024-08-28 00:50:19 +08:00
84db42f1ce 👷 改用 basedpyright 2024-08-28 00:40:24 +08:00
dependabot[bot]
0a660922bb ⬆️ Bump nonebot-plugin-alconna from 0.51.4 to 0.52.1 (#411)
Bumps [nonebot-plugin-alconna](https://github.com/nonebot/plugin-alconna) from 0.51.4 to 0.52.1.
- [Release notes](https://github.com/nonebot/plugin-alconna/releases)
- [Commits](https://github.com/nonebot/plugin-alconna/compare/v0.51.4...v0.52.1)

---
updated-dependencies:
- dependency-name: nonebot-plugin-alconna
  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-08-26 22:35:47 +00:00
dependabot[bot]
56bc98cc79 ⬆️ Bump mypy from 1.11.1 to 1.11.2 (#412)
Bumps [mypy](https://github.com/python/mypy) from 1.11.1 to 1.11.2.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.11.1...v1.11.2)

---
updated-dependencies:
- dependency-name: mypy
  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-08-26 22:33:40 +00:00
pre-commit-ci[bot]
be61683b51 ⬆️ auto update by pre-commit hooks (#413)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.6.1 → v0.6.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.1...v0.6.2)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-08-27 06:31:38 +08:00
ccd5706a95 🔖 1.5.1 2024-08-26 11:43:51 +08:00
b69240caa5 👷 添加 Pyright 类型检查 2024-08-26 11:41:03 +08:00
49d00f4d0e 添加开发依赖 pyright 2024-08-26 11:38:44 +08:00
389a850025 🐛 修复打过但是没数据的爆炸
为啥会没数据??
2024-08-26 11:38:14 +08:00
20dcc2bc3d 🔖 1.5.0 2024-08-25 23:17:32 +08:00
606dddbca2 适配新赛季 list 2024-08-25 23:16:33 +08:00
f509b03cd0 🔖 1.4.18 2024-08-24 21:09:28 +08:00
6293d088db 适配新赛季 rank 2024-08-24 21:06:45 +08:00
97e2abed78 添加 debug 依赖 matplotlib pyqt6 2024-08-24 21:01:30 +08:00
dependabot[bot]
5ea3fcb234 ⬆️ Bump types-pillow from 10.2.0.20240520 to 10.2.0.20240822 (#409)
Bumps [types-pillow](https://github.com/python/typeshed) from 10.2.0.20240520 to 10.2.0.20240822.
- [Commits](https://github.com/python/typeshed/commits)

---
updated-dependencies:
- dependency-name: types-pillow
  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-08-22 19:37:03 +00:00
dependabot[bot]
ca33ba1310 ⬆️ Bump ruff from 0.6.1 to 0.6.2 (#408)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.6.1 to 0.6.2.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.6.1...0.6.2)

---
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-08-23 03:34:57 +08:00
3629a2ff4a 🚨 make type checker happy 2024-08-22 01:14:59 +08:00
a2108c9776 localstore 使用 get_plugin_xxx_dir 2024-08-21 05:11:36 +08:00
7133cd9384 🔖 1.4.17 2024-08-20 08:58:31 +08:00
pre-commit-ci[bot]
406bc7674e ⬆️ auto update by pre-commit hooks (#406)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.5.7 → v0.6.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.7...v0.6.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-08-19 23:40:59 +00:00
呵呵です
259b38fda5 支持设置代理 (#407)
*  添加依赖 yarl

*  添加依赖 msgspec

*  移除依赖 ujson

* ♻️ 重构 request 使其支持分别设置代理

* ♻️ 重构 resource 接口

* ️ 不再重复获取 Config

* ♻️ 使用 yarl 替换 urllib.parse

* ️ 给 get_self_netloc 加个 cache

*  request 使用 proxy

*  更新模板使用 proxy

* 🐛 修复删除 ujson 依赖后 迁移脚本报错的bug
2024-08-19 23:37:51 +00:00
dependabot[bot]
414345ae5c ⬆️ Bump ruff from 0.6.0 to 0.6.1 (#405)
* ⬆️ Bump ruff from 0.6.0 to 0.6.1

Bumps [ruff](https://github.com/astral-sh/ruff) from 0.6.0 to 0.6.1.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.6.0...0.6.1)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* 🚨 auto fix by pre-commit hooks

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-08-19 17:51:20 +00:00
dependabot[bot]
341cbd86cd ⬆️ Bump nonebot-plugin-user from 0.4.1 to 0.4.2 (#404)
* ⬆️ Bump nonebot-plugin-user from 0.4.1 to 0.4.2

Bumps [nonebot-plugin-user](https://github.com/he0119/nonebot-plugin-user) from 0.4.1 to 0.4.2.
- [Release notes](https://github.com/he0119/nonebot-plugin-user/releases)
- [Changelog](https://github.com/he0119/nonebot-plugin-user/blob/main/CHANGELOG.md)
- [Commits](https://github.com/he0119/nonebot-plugin-user/compare/v0.4.1...v0.4.2)

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

Signed-off-by: dependabot[bot] <support@github.com>

* 🚨 auto fix by pre-commit hooks

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-08-19 16:06:57 +00:00
dependabot[bot]
bf7804738e ⬆️ Bump nonebot-adapter-qq from 1.5.0 to 1.5.1 (#403)
* ⬆️ Bump nonebot-adapter-qq from 1.5.0 to 1.5.1

Bumps [nonebot-adapter-qq](https://github.com/nonebot/adapter-qq) from 1.5.0 to 1.5.1.
- [Release notes](https://github.com/nonebot/adapter-qq/releases)
- [Commits](https://github.com/nonebot/adapter-qq/compare/v1.5.0...v1.5.1)

---
updated-dependencies:
- dependency-name: nonebot-adapter-qq
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* 🚨 auto fix by pre-commit hooks

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-08-19 15:52:37 +00:00
dependabot[bot]
553f373671 ⬆️ Bump nonebot2 from 2.3.2 to 2.3.3 (#402)
* ⬆️ Bump nonebot2 from 2.3.2 to 2.3.3

Bumps [nonebot2](https://github.com/nonebot/nonebot2) from 2.3.2 to 2.3.3.
- [Release notes](https://github.com/nonebot/nonebot2/releases)
- [Changelog](https://github.com/nonebot/nonebot2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nonebot/nonebot2/compare/v2.3.2...v2.3.3)

---
updated-dependencies:
- dependency-name: nonebot2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* 🚨 auto fix by pre-commit hooks

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-08-19 15:49:27 +00:00
e53e164a52 🔖 1.4.16 2024-08-18 01:25:43 +08:00
2cd7d89c3e 截图前等待 networkidle
还是得等)
2024-08-18 01:19:03 +08:00
b8b6d5f6c8 🔖 1.4.15 2024-08-17 22:41:32 +08:00
7a44c0dca5 🐛 修 s1 没打的爆炸 2024-08-17 22:40:47 +08:00
4155d8eb42 🔖 1.4.14 2024-08-17 19:50:52 +08:00
4cc942d226 🐛 修 40l 无 hold 爆炸 2024-08-17 19:50:25 +08:00
996dd565d8 🔖 1.4.13 2024-08-17 18:43:11 +08:00
5b0660e45b 🐛 修第一赛季最后没有段位爆炸 2024-08-17 18:41:31 +08:00
8d1ebc06d1 🔖 1.4.12 2024-08-17 05:07:27 +08:00
c57aa48048 🐛 修没打过的爆炸 2024-08-17 05:06:59 +08:00
ad90562fdf 🐛 修国家为空爆炸 2024-08-17 04:45:06 +08:00
79 changed files with 5587 additions and 4646 deletions

View File

@@ -1,12 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "pip" # See documentation for possible values
directory: "/" # Location of package manifests
target-branch: "main"
schedule:
interval: "daily"

View File

@@ -12,24 +12,25 @@ jobs:
id-token: write
contents: write
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install poetry
run: pipx install poetry
shell: bash
- uses: actions/setup-python@v4
- uses: astral-sh/setup-uv@v3
name: Setup UV
with:
python-version: '3.11'
cache: "poetry"
enable-cache: true
- run: poetry install
- name: "Set up Python"
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"
- run: uv sync
shell: bash
- name: Get Version
id: version
run: |
echo "VERSION=$(poetry version -s)" >> $GITHUB_OUTPUT
echo "VERSION=$(uvx pdm show --version)" >> $GITHUB_OUTPUT
echo "TAG_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
@@ -38,10 +39,10 @@ jobs:
run: exit 1
- name: Build Package
run: poetry build
run: uv build
- name: Publish Package to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
run: uv publish
- name: Publish Package to GitHub Release
run: gh release create ${{ steps.version.outputs.TAG_NAME }} dist/*.tar.gz dist/*.whl -t "🔖 ${{ steps.version.outputs.TAG_NAME }}" --generate-notes

View File

@@ -1,27 +1,33 @@
name: TypeCheck
on:
push:
push:
jobs:
Mypy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
TypeCheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install poetry
run: pipx install poetry
shell: bash
- uses: astral-sh/setup-uv@v3
name: Setup UV
with:
enable-cache: true
- uses: actions/setup-python@v5
with:
python-version: '3.10'
cache: 'poetry'
- name: "Set up Python"
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"
- run: poetry install
shell: bash
- run: uv sync
shell: bash
- name: Run Mypy
shell: bash
run: |
poetry run mypy ./nonebot_plugin_tetris_stats
- name: Run Mypy
shell: bash
run: |
uv run mypy ./nonebot_plugin_tetris_stats
- name: Run BasedPyright
shell: bash
run: |
uv run basedpyright ./nonebot_plugin_tetris_stats/

View File

@@ -38,11 +38,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -56,7 +56,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
uses: github/codeql-action/autobuild@v3
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -69,4 +69,4 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v3

View File

@@ -7,13 +7,13 @@ ci:
autoupdate_commit_msg: ':arrow_up: auto update by pre-commit hooks'
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.5.7
rev: v0.7.0
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
stages: [commit]
stages: [pre-commit]
- id: ruff-format
stages: [commit]
stages: [pre-commit]
- repo: https://github.com/nonebot/nonemoji
rev: v0.1.4

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.10

58
CONTRIBUTING.en-US.md Normal file
View File

@@ -0,0 +1,58 @@
# How to Contribute?
## Setting Up the Environment
### For Developers with Basic Python Knowledge
First, you need install [uv](https://docs.astral.sh/uv/).
Then:
```bash
# Set up the basic Python environment
uv python install 3.10
# Clone the repository
git clone https://github.com/A-Minos/nonebot-plugin-tetris-stats.git
cd nonebot-plugin-tetris-stats
# Install dependencies
uv sync
```
## Development
### Code Development
1. For static code analysis, use [ruff](https://docs.astral.sh/ruff/). You can install the corresponding plugin for your IDE or use the command line with `ruff check ./nonebot_plugin_tetris_stats/` to check the code.
2. For code formatting, use [ruff](https://docs.astral.sh/ruff/). You can install the corresponding plugin for your IDE or use the command line with `ruff format ./nonebot_plugin_tetris_stats/` to format the code.
3. For type checking, use both [basedpyright](https://docs.basedpyright.com/latest/) and [mypy](https://www.mypy-lang.org/). You can install the corresponding plugins for your IDE or use the following commands in the terminal to check the code:
```bash
# basedpyright
basedpyright ./nonebot_plugin_tetris_stats/
# mypy
mypy ./nonebot_plugin_tetris_stats/
```
### Internationalization
This project uses [Tarina](https://github.com/ArcletProject/Tarina) for internationalization support.
#### Adding a New Language
1. Navigate to the `./nonebot_plugin_tetris_stats/i18n/` directory.
2. Run `tarina-lang create {language_code}` * Please note that the language code should preferably follow the [IETF language tag](https://en.wikipedia.org/wiki/IETF_language_tag) standard.
3. Edit the generated `./nonebot_plugin_tetris_stats/i18n/{language_code}.json` file.
#### Updating an Existing Language
1. Navigate to the `./nonebot_plugin_tetris_stats/i18n/` directory.
2. Edit the corresponding `./nonebot_plugin_tetris_stats/i18n/{language_code}.json` file.
#### Adding New Entries
1. Navigate to the `./nonebot_plugin_tetris_stats/i18n/` directory.
2. Edit the `.template.json` file.
3. Run `tarina-lang schema && tarina-lang model`.
4. Modify the language files, adding new entries at least to `en-US.json`.

57
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,57 @@
# 我该如何参与开发?
## 配置环境
首先你需要安装 [uv](https://docs.astral.sh/uv/)。
然后:
```bash
# 配置基础 Python 环境
uv python install 3.10
# 克隆仓库
git clone https://github.com/A-Minos/nonebot-plugin-tetris-stats.git
cd nonebot-plugin-tetris-stats
# 安装依赖
uv sync
```
## 开发
### 代码开发
1. 代码静态检查使用 [ruff](https://docs.astral.sh/ruff/)你可以为你的ide安装对应插件来使用也可以在命令行使用`ruff check ./nonebot_plugin_tetris_stats/`来检查代码。
2. 代码格式化使用 [ruff](https://docs.astral.sh/ruff/)你可以为你的ide安装对应插件来使用也可以在命令行使用`ruff format ./nonebot_plugin_tetris_stats/`来格式化代码。
3. 类型检查同时使用 [basedpyright](https://docs.basedpyright.com/latest/) 和 [mypy](https://www.mypy-lang.org/)你可以为你的ide安装对应插件来使用。
也可以在命令行使用下面的命令来检查代码:
```bash
# basedpyright
basedpyright ./nonebot_plugin_tetris_stats/
# mypy
mypy ./nonebot_plugin_tetris_stats/
```
### 国际化
本项目使用 [Tarina](https://github.com/ArcletProject/Tarina) 提供国际化支持。
#### 添加新的语言
1. 进入 `./nonebot_plugin_tetris_stats/i18n/` 目录。
2. 运行 `tarina-lang create {语言代码}` * 请注意,语言代码最好符合 [IETF语言标签](https://zh.wikipedia.org/wiki/IETF%E8%AF%AD%E8%A8%80%E6%A0%87%E7%AD%BE) 的规范。
3. 编辑生成的 `./nonebot_plugin_tetris_stats/i18n/{语言代码}.json` 文件。
#### 更新已有语言
1. 进入 `./nonebot_plugin_tetris_stats/i18n/` 目录。
2. 编辑对应的 `./nonebot_plugin_tetris_stats/i18n/{语言代码}.json` 文件。
#### 添加新的条目
1. 进入 `./nonebot_plugin_tetris_stats/i18n/` 目录。
2. 编辑 `.template.json` 文件。
3. 运行 `tarina-lang schema && tarina-lang model`
4. 修改语言文件,至少为`en-US.json`添加新的条目。

View File

@@ -1,14 +1,27 @@
from nonebot_plugin_localstore import get_cache_dir, get_data_dir
from nonebot import get_plugin_config
from nonebot_plugin_localstore import get_plugin_cache_dir, get_plugin_data_dir
from pydantic import BaseModel, Field
CACHE_PATH = get_cache_dir('nonebot_plugin_tetris_stats')
DATA_PATH = get_data_dir('nonebot_plugin_tetris_stats')
CACHE_PATH = get_plugin_cache_dir()
DATA_PATH = get_plugin_data_dir()
class Proxy(BaseModel):
main: str | None = None
github: str | None = None
tetrio: str | None = None
tos: str | None = None
top: str | None = None
class ScopedConfig(BaseModel):
request_timeout: float = 30.0
screenshot_quality: float = 2
proxy: Proxy = Field(default_factory=Proxy)
class Config(BaseModel):
tetris: ScopedConfig = Field(default_factory=ScopedConfig)
config = get_plugin_config(Config)

View File

@@ -19,7 +19,6 @@ from sqlalchemy import desc, select
from sqlalchemy.dialects import sqlite
from sqlalchemy.ext.automap import automap_base
from sqlalchemy.orm import Session
from ujson import dumps, loads
if TYPE_CHECKING:
from collections.abc import Sequence
@@ -30,7 +29,9 @@ branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def migrate_old_data() -> None:
def migrate_old_data() -> None: # noqa: C901
from json import dumps, loads
Base = automap_base() # noqa: N806
Base.prepare(autoload_with=op.get_bind())
OldHistoricalData = Base.classes.nonebot_plugin_tetris_stats_historicaldata # noqa: N806
@@ -46,6 +47,9 @@ def migrate_old_data() -> None:
TimeRemainingColumn(),
) as progress,
):
if session.query(OldHistoricalData).count() == 0:
logger.info('空表, 跳过')
return
task_id = progress.add_task('[cyan]Migrating:', total=session.query(OldHistoricalData).count())
pointer = 0
while pointer < session.query(OldHistoricalData).order_by(desc(OldHistoricalData.id)).limit(1).one().id:

View File

@@ -0,0 +1,119 @@
"""add TETRIOLeagueStats
迁移 ID: 5a1b93948494
父迁移: cfeab6961dce
创建时间: 2024-08-24 00:22:41.359500
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import sqlalchemy as sa
from alembic import op
if TYPE_CHECKING:
from collections.abc import Sequence
revision: str = '5a1b93948494'
down_revision: str | Sequence[str] | None = 'cfeab6961dce'
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade(name: str = '') -> None:
if name:
return
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
'nonebot_plugin_tetris_stats_tetrioleaguestats',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('update_time', sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('pk_nonebot_plugin_tetris_stats_tetrioleaguestats')),
info={'bind_key': 'nonebot_plugin_tetris_stats'},
)
with op.batch_alter_table('nonebot_plugin_tetris_stats_tetrioleaguestats', schema=None) as batch_op:
batch_op.create_index(
batch_op.f('ix_nonebot_plugin_tetris_stats_tetrioleaguestats_update_time'), ['update_time'], unique=False
)
op.create_table(
'nonebot_plugin_tetris_stats_tetrioleaguehistorical',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('request_id', sa.Uuid(), nullable=False),
sa.Column('data', sa.JSON(), nullable=False),
sa.Column('update_time', sa.DateTime(), nullable=False),
sa.Column('stats_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(
['stats_id'],
['nonebot_plugin_tetris_stats_tetrioleaguestats.id'],
name=op.f(
'fk_nonebot_plugin_tetris_stats_tetrioleaguehistorical_stats_id_nonebot_plugin_tetris_stats_tetrioleaguestats'
),
),
sa.PrimaryKeyConstraint('id', name=op.f('pk_nonebot_plugin_tetris_stats_tetrioleaguehistorical')),
info={'bind_key': 'nonebot_plugin_tetris_stats'},
)
with op.batch_alter_table('nonebot_plugin_tetris_stats_tetrioleaguehistorical', schema=None) as batch_op:
batch_op.create_index(
batch_op.f('ix_nonebot_plugin_tetris_stats_tetrioleaguehistorical_request_id'), ['request_id'], unique=False
)
batch_op.create_index(
batch_op.f('ix_nonebot_plugin_tetris_stats_tetrioleaguehistorical_update_time'),
['update_time'],
unique=False,
)
op.create_table(
'nonebot_plugin_tetris_stats_tetrioleaguestatsfield',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('rank', sa.String(length=2), nullable=False),
sa.Column('tr_line', sa.Float(), nullable=False),
sa.Column('player_count', sa.Integer(), nullable=False),
sa.Column('low_pps', sa.JSON(), nullable=False),
sa.Column('low_apm', sa.JSON(), nullable=False),
sa.Column('low_vs', sa.JSON(), nullable=False),
sa.Column('avg_pps', sa.Float(), nullable=False),
sa.Column('avg_apm', sa.Float(), nullable=False),
sa.Column('avg_vs', sa.Float(), nullable=False),
sa.Column('high_pps', sa.JSON(), nullable=False),
sa.Column('high_apm', sa.JSON(), nullable=False),
sa.Column('high_vs', sa.JSON(), nullable=False),
sa.Column('stats_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(
['stats_id'],
['nonebot_plugin_tetris_stats_tetrioleaguestats.id'],
name=op.f(
'fk_nonebot_plugin_tetris_stats_tetrioleaguestatsfield_stats_id_nonebot_plugin_tetris_stats_tetrioleaguestats'
),
),
sa.PrimaryKeyConstraint('id', name=op.f('pk_nonebot_plugin_tetris_stats_tetrioleaguestatsfield')),
info={'bind_key': 'nonebot_plugin_tetris_stats'},
)
with op.batch_alter_table('nonebot_plugin_tetris_stats_tetrioleaguestatsfield', schema=None) as batch_op:
batch_op.create_index(
batch_op.f('ix_nonebot_plugin_tetris_stats_tetrioleaguestatsfield_rank'), ['rank'], unique=False
)
# ### end Alembic commands ###
def downgrade(name: str = '') -> None:
if name:
return
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('nonebot_plugin_tetris_stats_tetrioleaguestatsfield', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_nonebot_plugin_tetris_stats_tetrioleaguestatsfield_rank'))
op.drop_table('nonebot_plugin_tetris_stats_tetrioleaguestatsfield')
with op.batch_alter_table('nonebot_plugin_tetris_stats_tetrioleaguehistorical', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_nonebot_plugin_tetris_stats_tetrioleaguehistorical_update_time'))
batch_op.drop_index(batch_op.f('ix_nonebot_plugin_tetris_stats_tetrioleaguehistorical_request_id'))
op.drop_table('nonebot_plugin_tetris_stats_tetrioleaguehistorical')
with op.batch_alter_table('nonebot_plugin_tetris_stats_tetrioleaguestats', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_nonebot_plugin_tetris_stats_tetrioleaguestats_update_time'))
op.drop_table('nonebot_plugin_tetris_stats_tetrioleaguestats')
# ### end Alembic commands ###

View File

@@ -13,7 +13,6 @@ from typing import TYPE_CHECKING
from alembic import op
from sqlalchemy.ext.automap import automap_base
from sqlalchemy.orm import Session
from ujson import dumps, loads
if TYPE_CHECKING:
from collections.abc import Sequence
@@ -27,6 +26,7 @@ depends_on: str | Sequence[str] | None = None
def upgrade(name: str = '') -> None:
if name:
return
from json import dumps, loads
Base = automap_base() # noqa: N806
connection = op.get_bind()
@@ -50,6 +50,7 @@ def upgrade(name: str = '') -> None:
def downgrade(name: str = '') -> None:
if name:
return
from json import dumps, loads
Base = automap_base() # noqa: N806
connection = op.get_bind()

View File

@@ -28,12 +28,6 @@ depends_on: str | Sequence[str] | None = None
def upgrade(name: str = '') -> None: # noqa: C901
if name:
return
from nonebot_plugin_tetris_stats.version import __version__
if __version__ != '1.0.3':
msg = '本迁移需要1.0.3版本, 请先锁定版本至1.0.3版本再执行本迁移'
logger.critical(msg)
raise RuntimeError(msg)
from nonebot.compat import PYDANTIC_V2, type_validate_json
from pydantic import BaseModel, ValidationError
@@ -46,10 +40,6 @@ def upgrade(name: str = '') -> None: # noqa: C901
TimeRemainingColumn,
)
from nonebot_plugin_tetris_stats.game_data_processor.schemas import ( # type: ignore[import-untyped]
BaseProcessedData,
)
Base = automap_base() # noqa: N806
Base.prepare(autoload_with=op.get_bind())
HistoricalData = Base.classes.nonebot_plugin_tetris_stats_historicaldata # noqa: N806
@@ -62,18 +52,33 @@ def upgrade(name: str = '') -> None: # noqa: C901
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()
if count == 0:
logger.info('空表, 跳过')
return
from nonebot_plugin_tetris_stats.version import __version__
if __version__ != '1.0.3':
msg = '本迁移需要1.0.3版本, 请先锁定版本至1.0.3版本再执行本迁移'
logger.critical(msg)
raise RuntimeError(msg)
from nonebot_plugin_tetris_stats.game_data_processor.schemas import ( # type: ignore[import-untyped]
BaseProcessedData,
)
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 Progress(
TextColumn('[progress.description]{task.description}'),
BarColumn(),

View File

@@ -26,12 +26,7 @@ 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':
msg = '本迁移需要1.0.4版本, 请先锁定版本至1.0.4版本再执行本迁移'
logger.critical(msg)
raise RuntimeError(msg)
from nonebot.compat import type_validate_json
from pydantic import ValidationError
from rich.progress import (
@@ -46,8 +41,6 @@ def upgrade(name: str = '') -> None:
from sqlalchemy.ext.automap import automap_base
from sqlalchemy.orm import Session
from nonebot_plugin_tetris_stats.game_data_processor.schemas import BaseUser # type: ignore[import-untyped]
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(
@@ -60,37 +53,48 @@ def upgrade(name: str = '') -> None:
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)
if count == 0:
logger.info('空表, 跳过')
else:
from nonebot_plugin_tetris_stats.version import __version__
if __version__ != '1.0.4':
msg = '本迁移需要1.0.4版本, 请先锁定版本至1.0.4版本再执行本迁移'
logger.critical(msg)
raise RuntimeError(msg)
from nonebot_plugin_tetris_stats.game_data_processor.schemas import BaseUser # type: ignore[import-untyped]
models: list[type[BaseUser]] = BaseUser.__subclasses__()
def json_to_model(value: str) -> BaseUser:
for i in models:
try:
j.user_unique_identifier = model.unique_identifier
except ValueError:
session.delete(j)
progress.update(task_id, advance=1)
session.commit()
return type_validate_json(i, value)
except ValidationError: # noqa: PERF203
...
raise ValueError
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')

View File

@@ -55,12 +55,12 @@ async def create_or_update_bind(
game_account=game_account,
)
session.add(bind)
message = BindStatus.SUCCESS
status = BindStatus.SUCCESS
else:
bind.game_account = game_account
message = BindStatus.UPDATE
status = BindStatus.UPDATE
await session.commit()
return message
return status
T = TypeVar('T', 'TETRIOHistoricalData', 'TOPHistoricalData', 'TOSHistoricalData')

View File

@@ -7,6 +7,7 @@ from nonebot.typing import T_Handler
from nonebot_plugin_alconna import AlcMatches, Alconna, At, CommandMeta, on_alconna
from .. import ns
from ..i18n.model import Lang
from ..utils.exception import MessageFormatError, NeedCatchError
command: Alconna = Alconna(
@@ -30,7 +31,7 @@ def add_block_handlers(handler: Callable[[T_Handler], T_Handler]) -> None:
@handler
async def _(bot: Bot, matcher: Matcher, target: At):
if isinstance(target, At) and target.target == bot.self_id:
await matcher.finish('不能查询bot的信息')
await matcher.finish(Lang.interaction.wrong.query_bot())
from . import tetrio, top, tos # noqa: F401, E402

View File

@@ -1 +0,0 @@
CANT_VERIFY_MESSAGE = '* 由于无法验证绑定信息, 不能保证查询到的用户为本人\n'

View File

@@ -23,7 +23,7 @@ command = Subcommand(
)
from . import bind, config, query, record # noqa: E402
from . import bind, config, list, query, rank, record # noqa: E402
main_command.add(command)
@@ -31,6 +31,8 @@ __all__ = [
'alc',
'bind',
'config',
'list',
'query',
'rank',
'record',
]

View File

@@ -1,6 +1,5 @@
from .player import Player
from .schemas.user import User
from .schemas.user_info import UserInfoSuccess
from .tetra_league import full_export as tetra_league_full_export
__all__ = ['Player', 'User', 'UserInfoSuccess', 'tetra_league_full_export']
__all__ = ['Player', 'User', 'UserInfoSuccess']

View File

@@ -1,30 +1,37 @@
from asyncio import Lock
from datetime import datetime, timezone
from datetime import datetime, timedelta, timezone
from typing import ClassVar
from weakref import WeakValueDictionary
from aiocache import Cache as ACache # type: ignore[import-untyped]
from nonebot.compat import type_validate_json
from nonebot.log import logger
from yarl import URL
from ....config.config import config
from ....utils.limit import limit
from ....utils.request import Request
from .schemas.base import FailedModel, SuccessModel
UTC = timezone.utc
request = Request(config.tetris.proxy.tetrio or config.tetris.proxy.main)
request.request = limit(timedelta(seconds=1))(request.request) # type: ignore[method-assign]
class Cache:
cache = ACache(ACache.MEMORY)
task: ClassVar[WeakValueDictionary[str, Lock]] = WeakValueDictionary()
task: ClassVar[WeakValueDictionary[URL, Lock]] = WeakValueDictionary()
@classmethod
async def get(cls, url: str) -> bytes:
async def get(cls, url: URL, extra_headers: dict | None = None) -> bytes:
lock = cls.task.setdefault(url, Lock())
async with lock:
if (cached_data := await cls.cache.get(url)) is not None:
logger.debug(f'{url}: Cache hit!')
return cached_data
response_data = await Request.request(url)
response_data = await request.request(url, extra_headers, enable_anti_cloudflare=True)
parsed_data: SuccessModel | FailedModel = type_validate_json(SuccessModel | FailedModel, response_data) # type: ignore[arg-type]
if isinstance(parsed_data, SuccessModel):
await cls.cache.add(

View File

@@ -0,0 +1,89 @@
from typing import Literal, overload
from uuid import UUID
from nonebot.compat import type_validate_json
from yarl import URL
from ....utils.exception import RequestError
from ..constant import BASE_URL
from .cache import Cache
from .schemas.base import FailedModel
from .schemas.leaderboards import Parameter
from .schemas.leaderboards.by import By, BySuccessModel
from .schemas.leaderboards.solo import Solo, SoloSuccessModel
from .schemas.leaderboards.zenith import Zenith, ZenithSuccessModel
async def by(
by_type: Literal['league', 'xp', 'ar'], parameter: Parameter, x_session_id: UUID | None = None
) -> BySuccessModel:
model: By = type_validate_json(
By, # type: ignore[arg-type]
await get(
BASE_URL / f'users/by/{by_type}',
parameter,
{'X-Session-ID': str(x_session_id)} if x_session_id is not None else None,
),
)
if isinstance(model, FailedModel):
msg = f'排行榜信息请求错误:\n{model.error}'
raise RequestError(msg)
return model
@overload
async def records(
records_type: Literal['40l', 'blitz'],
scope: str = '_global',
revolution_id: str | None = None,
*,
parameter: Parameter,
) -> SoloSuccessModel: ...
@overload
async def records(
records_type: Literal['zenith', 'zenithex'],
scope: str = '_global',
revolution_id: str | None = None,
*,
parameter: Parameter,
) -> ZenithSuccessModel: ...
async def records(
records_type: Literal['40l', 'blitz', 'zenith', 'zenithex'],
scope: str = '_global',
revolution_id: str | None = None,
*,
parameter: Parameter,
) -> SoloSuccessModel | ZenithSuccessModel:
model: Solo | Zenith
match records_type:
case '40l' | 'blitz':
model = type_validate_json(
Solo, # type: ignore[arg-type]
await get(
BASE_URL / 'records' / f'{records_type}{scope}{revolution_id if revolution_id is not None else ""}',
parameter,
),
)
case 'zenith' | 'zenithex':
model = type_validate_json(
Zenith, # type: ignore[arg-type]
await get(
BASE_URL / 'records' / f'{records_type}{scope}{revolution_id if revolution_id is not None else ""}',
parameter,
),
)
case _:
msg = f'records_type: {records_type} is not supported'
raise ValueError(msg)
if isinstance(model, FailedModel):
msg = f'排行榜信息请求错误:\n{model.error}' # type: ignore[attr-defined]
raise RequestError(msg)
return model
async def get(url: URL, parameter: Parameter, extra_headers: dict | None = None) -> bytes:
return await Cache.get(url % parameter.to_params(), extra_headers)

View File

@@ -7,11 +7,11 @@ from nonebot.compat import type_validate_json
from ....db import anti_duplicate_add
from ....utils.exception import RequestError
from ....utils.request import splice_url
from ..constant import BASE_URL, USER_ID, USER_NAME
from .cache import Cache
from .models import TETRIOHistoricalData
from .schemas.base import FailedModel
from .schemas.labs.leagueflow import LeagueFlow, LeagueFlowSuccess
from .schemas.records.solo import Solo as SoloRecord
from .schemas.records.solo import SoloSuccessModel as RecordsSoloSuccessModel
from .schemas.summaries import (
@@ -85,15 +85,11 @@ class Player:
self._user_info: UserInfoSuccess | None = None
self._summaries: dict[Summaries, SummariesModel] = {}
self._records: dict[RecordKey, RecordsSoloSuccessModel] = {}
self._leagueflow: LeagueFlowSuccess | None = None
@property
def _request_user_parameter(self) -> str:
if self.user_id is not None:
return self.user_id
if self.user_name is not None:
return self.user_name.lower()
msg = 'Invalid user'
raise ValueError(msg)
return self.user_id or cast(str, self.user_name).lower()
@property
async def user(self) -> User:
@@ -117,7 +113,7 @@ class Player:
async def get_info(self) -> UserInfoSuccess:
"""Get User Info"""
if self._user_info is None:
raw_user_info = await Cache.get(splice_url([BASE_URL, 'users/', f'{self._request_user_parameter}']))
raw_user_info = await Cache.get(BASE_URL / 'users' / self._request_user_parameter)
user_info: UserInfo = type_validate_json(UserInfo, raw_user_info) # type: ignore[arg-type]
if isinstance(user_info, FailedModel):
msg = f'用户信息请求错误:\n{user_info.error}'
@@ -147,7 +143,7 @@ class Player:
async def get_summaries(self, summaries_type: Summaries) -> SummariesModel:
if summaries_type not in self._summaries:
raw_summaries = await Cache.get(
splice_url([BASE_URL, 'users/', f'{self._request_user_parameter}/', 'summaries/', summaries_type])
BASE_URL / 'users' / self._request_user_parameter / 'summaries' / summaries_type
)
summaries: SummariesModel | FailedModel = type_validate_json(
self.__SUMMARIES_MAPPING[summaries_type] | FailedModel, # type: ignore[arg-type]
@@ -167,6 +163,18 @@ class Player:
)
return self._summaries[summaries_type]
async def get_leagueflow(self) -> LeagueFlowSuccess:
if self._leagueflow is None:
leagueflow: LeagueFlow = type_validate_json(
LeagueFlow, # type: ignore[arg-type]
await Cache.get(BASE_URL / 'labs/leagueflow' / self._request_user_parameter),
)
if isinstance(leagueflow, FailedModel):
msg = f'League 历史记录请求错误:\n{leagueflow.error}'
raise RequestError(msg)
self._leagueflow = leagueflow
return self._leagueflow
@property
async def sprint(self) -> SummariesSoloSuccessModel:
return await self.get_summaries('40l')
@@ -217,16 +225,7 @@ class Player:
async def get_records(self, mode_type: RecordModeType, records_type: RecordType) -> RecordsSoloSuccessModel:
if (record_key := RecordKey(mode_type, records_type)) not in self._records:
raw_records = await Cache.get(
splice_url(
[
BASE_URL,
'users/',
f'{self._request_user_parameter}/',
'records/',
f'{mode_type}/',
records_type,
]
)
BASE_URL / 'users' / self._request_user_parameter / 'records' / mode_type / records_type,
)
records: RecordsSoloSuccessModel | FailedModel = type_validate_json(SoloRecord, raw_records) # type: ignore[arg-type]
if isinstance(records, FailedModel):

View File

@@ -1,7 +1,9 @@
from datetime import datetime
from typing import Literal
from pydantic import BaseModel
from pydantic import BaseModel, Field
from ...typing import Prisecter
class AggregateStats(BaseModel):
@@ -39,11 +41,31 @@ class Garbage(BaseModel):
cleared: int
class P(BaseModel): # what is P
class P(BaseModel):
pri: float
sec: float
ter: float
def to_prisecter(self) -> Prisecter:
return Prisecter(f'{self.pri}:{self.sec}:{self.ter}')
# fmt: off
class ArCounts(BaseModel):
bronze: int | None = Field(default=None, alias='1') # pyright: ignore [reportGeneralTypeIssues]
silver: int | None = Field(default=None, alias='2') # pyright: ignore [reportGeneralTypeIssues]
gold: int | None = Field(default=None, alias='3') # pyright: ignore [reportGeneralTypeIssues]
platinum: int | None = Field(default=None, alias='4') # pyright: ignore [reportGeneralTypeIssues]
diamond: int | None = Field(default=None, alias='5') # pyright: ignore [reportGeneralTypeIssues]
issued: int | None = Field(default=None, alias='100') # pyright: ignore [reportGeneralTypeIssues]
top3: int | None = Field(default=None, alias='t3')
top5: int | None = Field(default=None, alias='t5')
top10: int | None = Field(default=None, alias='t10')
top25: int | None = Field(default=None, alias='t25')
top50: int | None = Field(default=None, alias='t50')
top100: int | None = Field(default=None, alias='t100')
# fmt: on
class Cache(BaseModel):
status: str

View File

@@ -21,7 +21,7 @@ class Stats(BaseModel):
level_lines: int
level_lines_needed: int
inputs: int
holds: int
holds: int = 0
time: Time | None = None # ?: 不知道是之后都没有了还是还会有
score: int
zenlevel: int

View File

@@ -0,0 +1,43 @@
from datetime import datetime
from enum import IntEnum
from typing import Literal, NamedTuple
from pydantic import BaseModel, Field
from ..base import FailedModel
from ..base import SuccessModel as BaseSuccessModel
class Result(IntEnum):
VICTORY = 1
DEFEAT = 2
VICTORY_BY_DISQUALIFICATION = 3
DEFEAT_BY_DISQUALIFICATION = 4
TIE = 5
NO_CONTEST = 6
MATCH_NULLIFIED = 7
class Point(NamedTuple):
timestamp_offset: int
result: Result
post_match_tr: int
opponent_pre_match_tr: int
"""If the opponent was unranked, same as post_match_tr."""
class Data(BaseModel):
start_time: datetime = Field(..., alias='startTime')
points: list[Point] = Field(..., min_length=1)
class Empty(BaseModel):
start_time: Literal[9007199254740991] = Field(..., alias='startTime')
points: list = Field(..., max_length=0)
class LeagueFlowSuccess(BaseSuccessModel):
data: Data | Empty
LeagueFlow = LeagueFlowSuccess | FailedModel

View File

@@ -0,0 +1,18 @@
from typing import Any
from nonebot.compat import PYDANTIC_V2
from pydantic import BaseModel, Field
from ...typing import Prisecter
class Parameter(BaseModel):
after: Prisecter | None = None
before: Prisecter | None = None
limit: int = Field(default=25, ge=1, le=100)
country: str | None = None
def to_params(self) -> dict[str, Any]:
if PYDANTIC_V2:
return self.model_dump(exclude_defaults=True)
return self.dict(exclude_defaults=True)

View File

@@ -1,27 +0,0 @@
from pydantic import BaseModel, Field
from ..base import SuccessModel
from .base import Entry as BaseEntry
class ArCounts(BaseModel):
bronze: int | None = Field(None, alias='1')
silver: int | None = Field(None, alias='2')
gold: int | None = Field(None, alias='3')
platinum: int | None = Field(None, alias='4')
diamond: int | None = Field(None, alias='5')
issued: int | None = Field(None, alias='100')
top10: int | None = Field(None, alias='t10')
class Entry(BaseEntry):
ar: int
ar_counts: ArCounts
class Data(BaseModel):
entries: list[Entry]
class ArSuccessModel(SuccessModel):
data: Data

View File

@@ -1,30 +0,0 @@
from datetime import datetime
from pydantic import BaseModel, Field
from ...typing import Rank
from ..base import P
class League(BaseModel):
gamesplayed: int
gameswon: int
rating: int
rank: Rank
decaying: bool
class Entry(BaseModel):
id: str = Field(..., alias='_id')
username: str
role: str
xp: float
league: League
supporter: bool | None = None
verified: bool
country: str | None = None
ts: datetime
gamesplayed: int
gameswon: int
gametime: float
p: P

View File

@@ -0,0 +1,50 @@
from datetime import datetime
from typing import Literal
from pydantic import BaseModel, Field
from ...typing import Rank, ValidRank
from ..base import ArCounts, FailedModel, P, SuccessModel
class League(BaseModel):
gamesplayed: int
gameswon: int
tr: float
gxe: float
rank: Rank
bestrank: ValidRank
glicko: float
rd: float
apm: float
pps: float
vs: float
decaying: bool
class Entry(BaseModel):
id: str = Field(..., alias='_id')
username: str
role: Literal['anon', 'user', 'bot', 'halfmod', 'mod', 'admin', 'sysop']
ts: datetime | None = None
xp: float
country: str | None = None
supporter: bool | None = None
league: League
gamesplayed: int
gameswon: int
gametime: float
ar: int
ar_counts: ArCounts
p: P
class Data(BaseModel):
entries: list[Entry]
class BySuccessModel(SuccessModel):
data: Data
By = BySuccessModel | FailedModel

View File

@@ -1,6 +1,6 @@
from pydantic import BaseModel
from ..base import SuccessModel
from ..base import FailedModel, SuccessModel
from ..summaries.solo import Record
@@ -10,3 +10,6 @@ class Data(BaseModel):
class SoloSuccessModel(SuccessModel):
data: Data
Solo = SoloSuccessModel | FailedModel

View File

@@ -1,12 +0,0 @@
from pydantic import BaseModel
from ..base import SuccessModel
from .base import Entry
class Data(BaseModel):
entries: list[Entry]
class XpSuccessModel(SuccessModel):
data: Data

View File

@@ -1,6 +1,6 @@
from pydantic import BaseModel
from ..base import SuccessModel
from ..base import FailedModel, SuccessModel
from ..summaries.zenith import Record
@@ -10,3 +10,6 @@ class Data(BaseModel):
class ZenithSuccessModel(SuccessModel):
data: Data
Zenith = ZenithSuccessModel | FailedModel

View File

@@ -7,5 +7,4 @@ class User(BaseModel):
avatar_revision: int | None
banner_revision: int | None
country: str | None
verified: int | None = None
supporter: int

View File

@@ -1,14 +1,22 @@
from typing import Literal
from nonebot.compat import PYDANTIC_V2
from pydantic import BaseModel, Field
from ...typing import Rank, S1Rank, S1ValidRank, ValidRank
from ...typing import Rank, S1Rank, S1ValidRank
from ..base import SuccessModel
if PYDANTIC_V2:
from pydantic import field_validator
else:
from pydantic import validator
class PastInner(BaseModel):
season: str
username: str
country: str
placement: int
country: str | None = None
placement: int | None = None
gamesplayed: int
gameswon: int
glicko: float
@@ -24,10 +32,75 @@ class PastInner(BaseModel):
class Past(BaseModel):
first: PastInner = Field(..., alias='1')
first: PastInner | None = Field(default=None, alias='1') # pyright: ignore [reportGeneralTypeIssues]
class Data(BaseModel):
class BaseData(BaseModel):
decaying: bool
past: Past
class NeverPlayedData(BaseData):
gamesplayed: Literal[0]
gameswon: Literal[0]
glicko: Literal[-1]
rd: Literal[-1]
gxe: Literal[-1]
tr: Literal[-1]
rank: Literal['z']
apm: None = None
pps: None = None
vs: None = None
standing: Literal[-1]
standing_local: Literal[-1]
prev_rank: None
prev_at: Literal[-1]
next_rank: None
next_at: Literal[-1]
percentile: Literal[-1]
percentile_rank: Literal['z']
class NeverRatedData(BaseData):
gamesplayed: Literal[1, 2, 3, 4, 5, 6, 7, 8, 9]
gameswon: int
glicko: Literal[-1]
rd: Literal[-1]
gxe: Literal[-1]
tr: Literal[-1]
apm: float
pps: float
vs: float
rank: Literal['z']
standing: Literal[-1]
standing_local: Literal[-1]
prev_rank: None
prev_at: Literal[-1]
next_rank: None
next_at: Literal[-1]
percentile: Literal[-1]
percentile_rank: Literal['z']
if PYDANTIC_V2:
@field_validator('apm', 'pps', 'vs', mode='before')
@classmethod
def _(cls, value: float | None) -> float:
if value is None:
return 0
return value
else:
@validator('apm', 'pps', 'vs', pre=True, always=True)
@classmethod
def _(cls, value: float | None) -> float:
if value is None:
return 0
return value
class RatedData(BaseData):
gamesplayed: int
gameswon: int
glicko: float
@@ -35,21 +108,23 @@ class Data(BaseModel):
gxe: float
tr: float
rank: Rank
bestrank: Rank = Field('z')
bestrank: Rank
standing: int
apm: float
pps: float
vs: float
decaying: bool
standing: int
standing_local: int
prev_rank: ValidRank | None = None
prev_rank: Rank | None = None
prev_at: int
next_rank: ValidRank | None = None
next_rank: Rank | None = None
next_at: int
percentile: float
percentile_rank: Rank
past: Past
percentile_rank: str
class InvalidData(BaseModel):
"""I don't know what osk is doing, but the return value is an empty dictionary"""
class LeagueSuccessModel(SuccessModel):
data: Data
data: NeverPlayedData | NeverRatedData | RatedData | InvalidData

View File

@@ -1,59 +0,0 @@
from pydantic import BaseModel, Field
from ..typing import Rank
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 | None = None
verified: bool
country: str | None = None
class _League(BaseModel):
gamesplayed: int
gameswon: int
rating: float
rank: Rank
bestrank: Rank
decaying: bool
class ValidLeague(_League):
glicko: float
rd: float
apm: float
pps: float
vs: float
class ValidUser(_User):
league: ValidLeague
class InvalidLeague(_League):
glicko: float | None = None
rd: float | None = None
apm: float | None = None
pps: float | None = None
vs: float | None = None
class InvalidUser(_User):
league: InvalidLeague
class Data(BaseModel):
users: list[ValidUser | InvalidUser]
class TetraLeagueSuccess(BaseSuccessModel):
data: Data
TetraLeague = TetraLeagueSuccess | FailedModel

View File

@@ -3,7 +3,7 @@ from typing import Literal
from pydantic import BaseModel, Field
from .base import FailedModel
from .base import ArCounts, FailedModel
from .base import SuccessModel as BaseSuccessModel
@@ -14,13 +14,19 @@ class Badge(BaseModel):
ts: datetime | Literal[False] | None = None
class Discord(BaseModel):
class Connection(BaseModel):
id: str
username: str
display_username: str
class Connections(BaseModel):
discord: Discord | None = None
discord: Connection | None = None
twitch: Connection | None = None
twitter: Connection | None = None
reddit: Connection | None = None
youtube: Connection | None = None
steam: Connection | None = None
class Distinguishment(BaseModel):
@@ -28,9 +34,9 @@ class Distinguishment(BaseModel):
class Data(BaseModel):
id: str = Field(..., alias='_id')
id: str = Field(default=..., alias='_id')
username: str
role: Literal['anon', 'user', 'bot', 'halfmod', 'mod', 'admin', 'sysop', 'banned']
role: Literal['anon', 'user', 'bot', 'halfmod', 'mod', 'admin', 'sysop', 'hidden', 'banned']
ts: datetime | None = None
botmaster: str | None = None
badges: list[Badge]
@@ -42,7 +48,6 @@ class Data(BaseModel):
badstanding: bool | None = None
supporter: bool | None = None # osk说是必有, 但实际上不是 fkosk
supporter_tier: int
verified: bool | None = None
avatar_revision: int | None = None
"""This user's avatar ID. Get their avatar at
@@ -57,6 +62,9 @@ class Data(BaseModel):
connections: Connections
friend_count: int | None = None
distinguishment: Distinguishment | None = None
achievements: list[int]
ar: int
ar_counts: ArCounts
class UserInfoSuccess(BaseSuccessModel):

View File

@@ -1,55 +0,0 @@
from typing import Literal, NamedTuple, TypedDict, overload
from urllib.parse import urlencode
from nonebot.compat import type_validate_json
from ....utils.exception import RequestError
from ....utils.request import splice_url
from ..constant import BASE_URL
from .cache import Cache
from .schemas.base import FailedModel
from .schemas.tetra_league import TetraLeague, TetraLeagueSuccess
class Parameter(TypedDict, total=False):
after: float
before: float
limit: int
country: str
async def leaderboard(parameter: Parameter | None = None) -> TetraLeagueSuccess:
league: TetraLeague = type_validate_json(
TetraLeague, # type: ignore[arg-type]
(await Cache.get(splice_url([BASE_URL, 'users/lists/league', f'?{urlencode(parameter or {})}']))),
)
if isinstance(league, FailedModel):
msg = f'排行榜数据请求错误:\n{league.error}'
raise RequestError(msg)
return league
class FullExport(NamedTuple):
model: TetraLeagueSuccess
original: bytes
@overload
async def full_export(*, with_original: Literal[False]) -> TetraLeagueSuccess: ...
@overload
async def full_export(*, with_original: Literal[True]) -> FullExport: ...
async def full_export(*, with_original: bool) -> TetraLeagueSuccess | FullExport:
full: TetraLeague = type_validate_json(
TetraLeague, # type: ignore[arg-type]
(data := await Cache.get(splice_url([BASE_URL, 'users/lists/league/all']))),
)
if isinstance(full, FailedModel):
msg = f'排行榜数据请求错误:\n{full.error}'
raise RequestError(msg)
if with_original:
return FullExport(full, data)
return full

View File

@@ -1,4 +1,4 @@
from typing import Literal
from typing import Literal, NewType
S1ValidRank = Literal[
'x+',
@@ -43,3 +43,5 @@ Records = Literal[
'blitz_recent',
'blitz_progression',
]
Prisecter = NewType('Prisecter', str)

View File

@@ -1,5 +1,4 @@
from hashlib import md5
from urllib.parse import urlencode
from arclet.alconna import Arg, ArgFlag
from nonebot_plugin_alconna import Args, Subcommand
@@ -9,6 +8,7 @@ from nonebot_plugin_session import EventSession
from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[import-untyped]
from nonebot_plugin_user import User
from nonebot_plugin_userinfo import BotUserInfo, UserInfo
from yarl import URL
from ...db import BindStatus, create_or_update_bind, trigger
from ...utils.host import HostPage, get_self_netloc
@@ -67,7 +67,10 @@ async def _(nb_user: User, account: Player, event_session: EventSession, bot_inf
platform='TETR.IO',
status='unknown',
user=People(
avatar=f'http://{netloc}/host/resource/tetrio/avatars/{user.ID}?{urlencode({"revision": avatar_revision})}'
avatar=str(
URL(f'http://{netloc}/host/resource/tetrio/avatars/{user.ID}')
% {'revision': avatar_revision}
)
if (avatar_revision := (await account.avatar_revision)) is not None and avatar_revision != 0
else Avatar(type='identicon', hash=md5(user.ID.encode()).hexdigest()), # noqa: S324
name=user.name.upper(),

View File

@@ -1,13 +1,16 @@
from re import compile
from typing import Literal
from yarl import URL
from .api.typing import ValidRank
GAME_TYPE: Literal['IO'] = 'IO'
BASE_URL = 'https://ch.tetr.io/api/'
BASE_URL = URL('https://ch.tetr.io/api/')
RANK_PERCENTILE: dict[ValidRank, float] = {
'x+': 0.2,
'x': 1,
'u': 5,
'ss': 11,

View File

@@ -0,0 +1,91 @@
from nonebot_plugin_alconna import Args, Option, Subcommand
from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_session import EventSession
from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[import-untyped]
from ...db import trigger
from ...utils.host import HostPage, get_self_netloc
from ...utils.metrics import get_metrics
from ...utils.render import render
from ...utils.render.schemas.tetrio.user.list_v2 import List, TetraLeague, User
from ...utils.screenshot import screenshot
from .. import alc
from . import command
from .api.leaderboards import by
from .api.schemas.base import P
from .api.schemas.leaderboards import Parameter
from .constant import GAME_TYPE
command.add(
Subcommand(
'list',
Option('--max-tr', Args['max_tr', float], help_text='TR的上限'),
Option('--min-tr', Args['min_tr', float], help_text='TR的下限'),
Option('--limit', Args['limit', int], help_text='查询数量'),
Option('--country', Args['country', str], help_text='国家代码'),
help_text='查询 TETR.IO 段位排行榜',
)
)
@alc.assign('TETRIO.list')
async def _(
event_session: EventSession,
max_tr: float | None = None,
min_tr: float | None = None,
limit: int | None = None,
country: str | None = None,
):
country = country.upper() if country is not None else None
async with trigger(
session_persist_id=await get_session_persist_id(event_session),
game_platform=GAME_TYPE,
command_type='list',
command_args=[
f'{key} {value}'
for key, value in zip(
('--max-tr', '--min-tr', '--limit', '--country'), (max_tr, min_tr, limit, country), strict=True
)
if value is not None
],
):
parameter = Parameter(
# ?: 似乎是只需要 pri 至少 league 榜的返回值只有 pri
after=P(pri=max_tr, sec=0, ter=0).to_prisecter() if max_tr is not None else None,
before=P(pri=min_tr, sec=0, ter=0).to_prisecter() if min_tr is not None else None,
limit=limit or 25,
country=country,
)
league = await by('league', parameter)
async with HostPage(
await render(
'v2/tetrio/user/list',
List(
show_index=True,
users=[
User(
id=i.id,
name=i.username.upper(),
avatar=f'https://tetr.io/user-content/avatars/{i.id}.jpg',
country=i.country,
tetra_league=TetraLeague(
rank=i.league.rank,
tr=round(i.league.tr, 2),
glicko=round(i.league.glicko, 2),
rd=round(i.league.rd, 2),
decaying=i.league.decaying,
pps=(metrics := get_metrics(pps=i.league.pps, apm=i.league.apm, vs=i.league.vs)).pps,
apm=metrics.apm,
apl=metrics.apl,
vs=metrics.vs,
adpl=metrics.adpl,
),
xp=i.xp,
join_at=None,
)
for i in league.data.entries
],
),
)
) as page_hash:
await UniMessage.image(raw=await screenshot(f'http://{get_self_netloc()}/host/{page_hash}.html')).finish()

View File

@@ -1,10 +1,53 @@
from nonebot_plugin_orm import Model
from sqlalchemy import String
from sqlalchemy.orm import Mapped, MappedAsDataclass, mapped_column
from datetime import datetime
from uuid import UUID
from nonebot_plugin_orm import Model
from sqlalchemy import DateTime, ForeignKey, String
from sqlalchemy.orm import Mapped, MappedAsDataclass, mapped_column, relationship
from ...db.models import PydanticType
from .api.schemas.leaderboards.by import BySuccessModel, Entry
from .api.typing import ValidRank
from .typing import Template
class TETRIOUserConfig(MappedAsDataclass, Model):
id: Mapped[int] = mapped_column(primary_key=True)
query_template: Mapped[Template] = mapped_column(String(2))
class TETRIOLeagueStats(MappedAsDataclass, Model):
id: Mapped[int] = mapped_column(init=False, primary_key=True)
raw: Mapped[list['TETRIOLeagueHistorical']] = relationship(back_populates='stats', lazy='noload')
fields: Mapped[list['TETRIOLeagueStatsField']] = relationship(back_populates='stats')
update_time: Mapped[datetime] = mapped_column(DateTime, index=True)
class TETRIOLeagueHistorical(MappedAsDataclass, Model):
id: Mapped[int] = mapped_column(init=False, primary_key=True)
request_id: Mapped[UUID] = mapped_column(index=True)
data: Mapped[BySuccessModel] = mapped_column(PydanticType([], {BySuccessModel}))
update_time: Mapped[datetime] = mapped_column(DateTime, index=True)
stats_id: Mapped[int] = mapped_column(ForeignKey('nonebot_plugin_tetris_stats_tetrioleaguestats.id'), init=False)
stats: Mapped['TETRIOLeagueStats'] = relationship(back_populates='raw')
entry_type = PydanticType([], {Entry})
class TETRIOLeagueStatsField(MappedAsDataclass, Model):
id: Mapped[int] = mapped_column(init=False, primary_key=True)
rank: Mapped[ValidRank] = mapped_column(String(2), index=True)
tr_line: Mapped[float]
player_count: Mapped[int]
low_pps: Mapped[Entry] = mapped_column(entry_type)
low_apm: Mapped[Entry] = mapped_column(entry_type)
low_vs: Mapped[Entry] = mapped_column(entry_type)
avg_pps: Mapped[float]
avg_apm: Mapped[float]
avg_vs: Mapped[float]
high_pps: Mapped[Entry] = mapped_column(entry_type)
high_apm: Mapped[Entry] = mapped_column(entry_type)
high_vs: Mapped[Entry] = mapped_column(entry_type)
stats_id: Mapped[int] = mapped_column(ForeignKey('nonebot_plugin_tetris_stats_tetrioleaguestats.id'), init=False)
stats: Mapped['TETRIOLeagueStats'] = relationship(back_populates='fields')

View File

@@ -1,269 +0,0 @@
from asyncio import gather
from datetime import datetime, timedelta, timezone
from hashlib import md5
from typing import TYPE_CHECKING, TypeVar
from urllib.parse import urlencode
from arclet.alconna import Arg, ArgFlag
from nonebot import get_driver
from nonebot.adapters import Event
from nonebot.matcher import Matcher
from nonebot_plugin_alconna import Args, At, Option, Subcommand
from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_orm import get_session
from nonebot_plugin_session import EventSession
from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[import-untyped]
from nonebot_plugin_user import User as NBUser
from nonebot_plugin_user import get_user
from sqlalchemy import select
from ...db import query_bind_info, trigger
from ...utils.host import HostPage, get_self_netloc
from ...utils.metrics import get_metrics
from ...utils.render import render
from ...utils.render.schemas.base import Avatar
from ...utils.render.schemas.tetrio.user.info_v2 import (
Badge,
Blitz,
Sprint,
Statistic,
TetraLeague,
TetraLeagueStatistic,
Zen,
)
from ...utils.render.schemas.tetrio.user.info_v2 import Info as V2TemplateInfo
from ...utils.render.schemas.tetrio.user.info_v2 import User as V2TemplateUser
from ...utils.screenshot import screenshot
from ...utils.typing import Me
from .. import add_block_handlers, alc
from ..constant import CANT_VERIFY_MESSAGE
from . import command, get_player
from .api import Player
from .constant import GAME_TYPE
from .models import TETRIOUserConfig
from .typing import Template
if TYPE_CHECKING:
from .api.schemas.summaries import SoloSuccessModel, ZenSuccessModel
from .api.schemas.summaries.league import LeagueSuccessModel
from .api.schemas.user import User
from .api.schemas.user_info import UserInfoSuccess
UTC = timezone.utc
driver = get_driver()
command.add(
Subcommand(
'query',
Args(
Arg(
'target',
At | Me,
notice='@想要查询的人 / 自己',
flags=[ArgFlag.HIDDEN, ArgFlag.OPTIONAL],
),
Arg(
'account',
get_player,
notice='TETR.IO 用户名 / ID',
flags=[ArgFlag.HIDDEN, ArgFlag.OPTIONAL],
),
),
Option(
'--template',
Arg('template', Template),
alias=['-T'],
help_text='要使用的查询模板',
),
help_text='查询 TETR.IO 游戏信息',
),
)
alc.shortcut(
'(?i:io)(?i:查询|查|query|stats)',
command='tstats TETR.IO query',
humanized='io查',
)
alc.shortcut(
'fkosk',
command='tstats TETR.IO query',
arguments=[''],
fuzzy=False,
humanized='An Easter egg!',
)
add_block_handlers(alc.assign('TETRIO.query'))
@alc.assign('TETRIO.query')
async def _( # noqa: PLR0913
user: NBUser,
event: Event,
matcher: Matcher,
target: At | Me,
event_session: EventSession,
template: Template | None = None,
):
async with trigger(
session_persist_id=await get_session_persist_id(event_session),
game_platform=GAME_TYPE,
command_type='query',
command_args=[f'--default-template {template}'] if template is not None else [],
):
async with get_session() as session:
bind = await query_bind_info(
session=session,
user=await get_user(
event_session.platform, target.target if isinstance(target, At) else event.get_user_id()
),
game_platform=GAME_TYPE,
)
if template is None:
template = await session.scalar(
select(TETRIOUserConfig.query_template).where(TETRIOUserConfig.id == user.id)
)
if bind is None:
await matcher.finish('未查询到绑定信息')
message = UniMessage(CANT_VERIFY_MESSAGE)
player = Player(user_id=bind.game_account, trust=True)
await (message + UniMessage.image(raw=await make_query_image_v2(player))).finish()
@alc.assign('TETRIO.query')
async def _(user: NBUser, account: Player, event_session: EventSession, template: Template | None = None):
async with trigger(
session_persist_id=await get_session_persist_id(event_session),
game_platform=GAME_TYPE,
command_type='query',
command_args=[f'--default-template {template}'] if template is not None else [],
):
async with get_session() as session:
if template is None:
template = await session.scalar(
select(TETRIOUserConfig.query_template).where(TETRIOUserConfig.id == user.id)
)
await (UniMessage.image(raw=await make_query_image_v2(account))).finish()
N = TypeVar('N', int, float)
def handling_special_value(value: N) -> N | None:
return value if value != -1 else None
async def make_query_image_v2(player: Player) -> bytes:
user: User
user_info: UserInfoSuccess
league: LeagueSuccessModel
sprint: SoloSuccessModel
blitz: SoloSuccessModel
zen: ZenSuccessModel
avatar_revision: int | None
banner_revision: int | None
# TODO)) 有没有什么办法能让这类型推导成功)
user, user_info, league, sprint, blitz, zen, avatar_revision, banner_revision = await gather( # type: ignore[assignment]
player.user,
player.get_info(),
player.league,
player.sprint,
player.blitz,
player.zen,
player.avatar_revision,
player.banner_revision,
)
if sprint.data.record is not None:
duration = timedelta(milliseconds=sprint.data.record.results.stats.finaltime).total_seconds()
sprint_value = f'{duration:.3f}s' if duration < 60 else f'{duration // 60:.0f}m {duration % 60:.3f}s' # noqa: PLR2004
else:
sprint_value = 'N/A'
play_time: str | None
if (game_time := handling_special_value(user_info.data.gametime)) is not None:
if game_time // 3600 > 0:
play_time = f'{game_time//3600:.0f}h {game_time % 3600 // 60:.0f}m {game_time % 60:.0f}s'
elif game_time // 60 > 0:
play_time = f'{game_time//60:.0f}m {game_time % 60:.0f}s'
else:
play_time = f'{game_time:.0f}s'
else:
play_time = game_time
netloc = get_self_netloc()
async with HostPage(
await render(
'v2/tetrio/user/info',
V2TemplateInfo(
user=V2TemplateUser(
id=user.ID,
name=user.name.upper(),
bio=user_info.data.bio,
banner=f'http://{netloc}/host/resource/tetrio/banners/{user.ID}?{urlencode({"revision": banner_revision})}'
if banner_revision is not None and banner_revision != 0
else None,
avatar=f'http://{netloc}/host/resource/tetrio/avatars/{user.ID}?{urlencode({"revision": avatar_revision})}'
if avatar_revision is not None and avatar_revision != 0
else Avatar(
type='identicon',
hash=md5(user.ID.encode()).hexdigest(), # noqa: S324
),
badges=[
Badge(
id=i.id,
description=i.label,
group=i.group,
receive_at=i.ts if isinstance(i.ts, datetime) else None,
)
for i in user_info.data.badges
],
country=user_info.data.country,
role=user_info.data.role,
xp=user_info.data.xp,
friend_count=user_info.data.friend_count,
supporter_tier=user_info.data.supporter_tier,
bad_standing=user_info.data.badstanding or False,
verified=user_info.data.verified or False,
playtime=play_time,
join_at=user_info.data.ts,
),
tetra_league=TetraLeague(
rank=league.data.rank,
highest_rank=league.data.bestrank,
tr=round(league.data.tr, 2),
glicko=round(league.data.glicko, 2),
rd=round(league.data.rd, 2),
global_rank=league.data.standing,
country_rank=league.data.standing_local,
pps=(metrics := get_metrics(pps=league.data.pps, apm=league.data.apm, vs=league.data.vs)).pps,
apm=metrics.apm,
apl=metrics.apl,
vs=metrics.vs,
adpl=metrics.adpl,
statistic=TetraLeagueStatistic(total=league.data.gamesplayed, wins=league.data.gameswon),
decaying=league.data.decaying,
history=None,
),
statistic=Statistic(
total=handling_special_value(user_info.data.gamesplayed),
wins=handling_special_value(user_info.data.gameswon),
),
sprint=Sprint(
time=sprint_value,
global_rank=sprint.data.rank,
play_at=sprint.data.record.ts,
)
if sprint.data.record is not None
else None,
blitz=Blitz(
score=blitz.data.record.results.stats.score,
global_rank=blitz.data.rank,
play_at=blitz.data.record.ts,
)
if blitz.data.record is not None
else None,
zen=Zen(level=zen.data.level, score=zen.data.score),
),
),
) as page_hash:
return await screenshot(f'http://{netloc}/host/{page_hash}.html')

View File

@@ -0,0 +1,135 @@
from datetime import timezone
from arclet.alconna import Arg, ArgFlag
from nonebot import get_driver
from nonebot.adapters import Event
from nonebot.matcher import Matcher
from nonebot_plugin_alconna import Args, At, Option, Subcommand
from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_orm import get_session
from nonebot_plugin_session import EventSession
from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[import-untyped]
from nonebot_plugin_user import User as NBUser
from nonebot_plugin_user import get_user
from sqlalchemy import select
from ....db import query_bind_info, trigger
from ....i18n import Lang
from ....utils.exception import FallbackError
from ....utils.typing import Me
from ... import add_block_handlers, alc
from .. import command, get_player
from ..api import Player
from ..constant import GAME_TYPE
from ..models import TETRIOUserConfig
from ..typing import Template
from .v1 import make_query_image_v1
from .v2 import make_query_image_v2
UTC = timezone.utc
driver = get_driver()
command.add(
Subcommand(
'query',
Args(
Arg(
'target',
At | Me,
notice='@想要查询的人 / 自己',
flags=[ArgFlag.HIDDEN, ArgFlag.OPTIONAL],
),
Arg(
'account',
get_player,
notice='TETR.IO 用户名 / ID',
flags=[ArgFlag.HIDDEN, ArgFlag.OPTIONAL],
),
),
Option(
'--template',
Arg('template', Template),
alias=['-T'],
help_text='要使用的查询模板',
),
help_text='查询 TETR.IO 游戏信息',
),
)
alc.shortcut(
'(?i:io)(?i:查询|查|query|stats)',
command='tstats TETR.IO query',
humanized='io查',
)
alc.shortcut(
'fkosk',
command='tstats TETR.IO query',
arguments=[''],
fuzzy=False,
humanized='An Easter egg!',
)
add_block_handlers(alc.assign('TETRIO.query'))
async def make_query_result(player: Player, template: Template) -> UniMessage:
if template == 'v1':
try:
return UniMessage.image(raw=await make_query_image_v1(player))
except FallbackError:
template = 'v2'
if template == 'v2':
return UniMessage.image(raw=await make_query_image_v2(player))
return None
@alc.assign('TETRIO.query')
async def _( # noqa: PLR0913
user: NBUser,
event: Event,
matcher: Matcher,
target: At | Me,
event_session: EventSession,
template: Template | None = None,
):
async with trigger(
session_persist_id=await get_session_persist_id(event_session),
game_platform=GAME_TYPE,
command_type='query',
command_args=[f'--template {template}'] if template is not None else [],
):
async with get_session() as session:
bind = await query_bind_info(
session=session,
user=await get_user(
event_session.platform, target.target if isinstance(target, At) else event.get_user_id()
),
game_platform=GAME_TYPE,
)
if template is None:
template = await session.scalar(
select(TETRIOUserConfig.query_template).where(TETRIOUserConfig.id == user.id)
)
if bind is None:
await matcher.finish('未查询到绑定信息')
player = Player(user_id=bind.game_account, trust=True)
await (
UniMessage.i18n(Lang.interaction.warning.unverified) + await make_query_result(player, template or 'v1')
).finish()
@alc.assign('TETRIO.query')
async def _(user: NBUser, account: Player, event_session: EventSession, template: Template | None = None):
async with trigger(
session_persist_id=await get_session_persist_id(event_session),
game_platform=GAME_TYPE,
command_type='query',
command_args=[f'--template {template}'] if template is not None else [],
):
async with get_session() as session:
if template is None:
template = await session.scalar(
select(TETRIOUserConfig.query_template).where(TETRIOUserConfig.id == user.id)
)
await (await make_query_result(account, template or 'v1')).finish()

View File

@@ -0,0 +1,56 @@
from collections.abc import Callable
from datetime import timedelta
from typing import TypeVar, overload
from zoneinfo import ZoneInfo
from ....utils.exception import FallbackError
from ....utils.render.schemas.tetrio.user.base import TetraLeagueHistoryData
from ..api.schemas.labs.leagueflow import Empty, LeagueFlowSuccess
from ..api.schemas.summaries.league import InvalidData, LeagueSuccessModel, NeverPlayedData, NeverRatedData, RatedData
def flow_to_history(
leagueflow: LeagueFlowSuccess,
handle: Callable[[list[TetraLeagueHistoryData]], list[TetraLeagueHistoryData]] | None = None,
) -> list[TetraLeagueHistoryData]:
if isinstance(leagueflow.data, Empty):
raise FallbackError
start_time = leagueflow.data.start_time.astimezone(ZoneInfo('Asia/Shanghai'))
ret = [
TetraLeagueHistoryData(
record_at=start_time + timedelta(milliseconds=i.timestamp_offset),
tr=i.post_match_tr,
)
for i in leagueflow.data.points
if start_time + timedelta(milliseconds=i.timestamp_offset)
]
return ret if handle is None else handle(ret)
N = TypeVar('N', int, float)
def handling_special_value(value: N) -> N | None:
return value if value != -1 else None
L = TypeVar('L', NeverPlayedData, NeverRatedData, RatedData)
@overload
def get_league_data(user_info: LeagueSuccessModel, league_type: type[L]) -> L: ...
@overload
def get_league_data(
user_info: LeagueSuccessModel, league_type: None = None
) -> NeverPlayedData | NeverRatedData | RatedData: ...
def get_league_data(
user_info: LeagueSuccessModel, league_type: type[L] | None = None
) -> L | NeverPlayedData | NeverRatedData | RatedData:
league = user_info.data
if isinstance(league, InvalidData):
raise FallbackError
if league_type is None:
return league
if isinstance(league, league_type):
return league
raise FallbackError

View File

@@ -0,0 +1,197 @@
from asyncio import gather
from datetime import datetime, timedelta
from hashlib import md5
from math import ceil, floor
from zoneinfo import ZoneInfo
from yarl import URL
from ....utils.exception import FallbackError, WhatTheFuckError
from ....utils.host import HostPage, get_self_netloc
from ....utils.render import render
from ....utils.render.schemas.base import Avatar, Ranking
from ....utils.render.schemas.tetrio.user.base import TetraLeagueHistoryData
from ....utils.render.schemas.tetrio.user.info_v1 import Info, Radar, TetraLeague, TetraLeagueHistory, User
from ....utils.screenshot import screenshot
from ..api import Player
from ..api.schemas.summaries.league import RatedData
from ..constant import TR_MAX, TR_MIN
from .tools import flow_to_history, get_league_data
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: TetraLeagueHistoryData,
behind_point: TetraLeagueHistoryData,
point_time: datetime,
) -> TetraLeagueHistoryData:
"""根据给出的 previous_point 和 behind_point, 推算 point_time 点处的数据
Args:
previous_point (Data): 前面的数据点
behind_point (Data): 后面的数据点
point_time (datetime): 要推算的点的位置
Returns:
Data: 要推算的点的数据
"""
# 求两个点的斜率
slope = (behind_point.tr - previous_point.tr) / (
datetime.timestamp(behind_point.record_at) - datetime.timestamp(previous_point.record_at)
)
return TetraLeagueHistoryData(
record_at=point_time,
tr=previous_point.tr + slope * (datetime.timestamp(point_time) - datetime.timestamp(previous_point.record_at)),
)
def handle_history_data(data: list[TetraLeagueHistoryData]) -> list[TetraLeagueHistoryData]: # noqa: C901, PLR0912
# 按照 记录时间 对数据进行排序
data.sort(key=lambda x: x.record_at)
# 定义时间边界, 右边界为当前时间的当天零点, 左边界为右边界前推9天
# 返回值的[0]和[-1]分别应满足left_border和right_border
zero = datetime.now(ZoneInfo('Asia/Shanghai')).replace(hour=0, minute=0, second=0, microsecond=0)
left_border = zero - timedelta(days=9)
right_border = zero.replace(microsecond=1000)
lefts: list[TetraLeagueHistoryData] = []
in_border: list[TetraLeagueHistoryData] = []
rights: list[TetraLeagueHistoryData] = []
# 根据 记录时间 将数据分类到对应的列表中
for i in data:
if i.record_at < left_border:
lefts.append(i)
elif i.record_at < right_border:
in_border.append(i)
else:
rights.append(i)
ret: list[TetraLeagueHistoryData] = []
# 处理左边界的点
if lefts and in_border: # 如果边界左侧和边界内都有值则推算
ret.append(get_specified_point(lefts[-1], in_border[0], left_border))
elif lefts and not in_border: # 如果边界左侧有值但是边界内没有值则直接取左侧的最后一个值
ret.append(TetraLeagueHistoryData(tr=lefts[-1].tr, record_at=left_border))
elif not lefts and in_border: # 如果边界左侧没有值但是边界内有值则直接取边界内的第一个值
ret.append(TetraLeagueHistoryData(tr=in_border[0].tr, record_at=left_border))
elif not lefts and not in_border and rights: # 如果边界左侧和边界内都没有值但是边界右侧有值则直接取边界右侧的第一个值 # fmt: skip
ret.append(TetraLeagueHistoryData(tr=rights[0].tr, record_at=left_border))
else: # 暂时没想到其他情况
raise WhatTheFuckError
# 添加边界内数据
ret.extend(in_border)
# 处理右边界的点
if in_border and rights: # 如果边界内和边界右侧都有值则推算
ret.append(get_specified_point(in_border[-1], rights[0], right_border))
elif not in_border and rights: # 如果边界内没有值但是边界右侧有值则直接取右侧的第一个值
ret.append(TetraLeagueHistoryData(tr=rights[0].tr, record_at=right_border))
elif in_border and not rights: # 如果边界内有值但是边界右侧没有值则直接取边界内的最后一个值
ret.append(TetraLeagueHistoryData(tr=in_border[-1].tr, record_at=right_border))
elif not in_border and not rights and lefts: # 如果边界内和边界右侧都没有值但是边界左侧有值则直接取边界左侧的最后一个值 # fmt: skip
ret.append(TetraLeagueHistoryData(tr=lefts[-1].tr, record_at=right_border))
else: # 暂时没想到其他情况
raise WhatTheFuckError
return ret
async def make_query_image_v1(player: Player) -> bytes:
(
(user, user_info, league, sprint, blitz, leagueflow),
(avatar_revision,),
) = await gather(
gather(player.user, player.get_info(), player.league, player.sprint, player.blitz, player.get_leagueflow()),
gather(player.avatar_revision),
)
league_data = get_league_data(league, RatedData)
if league_data.vs is None:
raise FallbackError
histories = flow_to_history(leagueflow, handle_history_data)
value_max, value_min = get_value_bounds([i.tr for i in histories])
split_value, offset = get_split(value_max, value_min)
if sprint.data.record is not None:
duration = timedelta(milliseconds=sprint.data.record.results.stats.finaltime).total_seconds()
sprint_value = f'{duration:.3f}s' if duration < 60 else f'{duration // 60:.0f}m {duration % 60:.3f}s' # noqa: PLR2004
else:
sprint_value = 'N/A'
blitz_value = f'{blitz.data.record.results.stats.score:,}' if blitz.data.record is not None else 'N/A'
netloc = get_self_netloc()
async with HostPage(
page=await render(
'v1/tetrio/info',
Info(
user=User(
avatar=str(
URL(f'http://{netloc}/host/resource/tetrio/avatars/{user.ID}') % {'revision': avatar_revision}
)
if avatar_revision is not None and avatar_revision != 0
else Avatar(
type='identicon',
hash=md5(user.ID.encode()).hexdigest(), # noqa: S324
),
name=user.name.upper(),
bio=user_info.data.bio,
),
ranking=Ranking(
rating=round(league_data.glicko, 2),
rd=round(league_data.rd, 2),
),
tetra_league=TetraLeague(
rank=league_data.rank,
tr=round(league_data.tr, 2),
global_rank=league_data.standing,
pps=league_data.pps,
lpm=round(lpm := (league_data.pps * 24), 2),
apm=league_data.apm,
apl=round(league_data.apm / lpm, 2),
vs=league_data.vs,
adpm=round(adpm := (league_data.vs * 0.6), 2),
adpl=round(adpm / lpm, 2),
),
tetra_league_history=TetraLeagueHistory(
data=histories,
split_interval=split_value,
min_tr=value_min,
max_tr=value_max,
offset=offset,
),
radar=Radar(
app=(app := (league_data.apm / (60 * league_data.pps))),
dsps=(dsps := ((league_data.vs / 100) - (league_data.apm / 60))),
dspp=(dspp := (dsps / league_data.pps)),
ci=150 * dspp - 125 * app + 50 * (league_data.vs / league_data.apm) - 25,
ge=2 * ((app * dsps) / league_data.pps),
),
sprint=sprint_value,
blitz=blitz_value,
),
)
) as page_hash:
return await screenshot(f'http://{netloc}/host/{page_hash}.html')

View File

@@ -0,0 +1,138 @@
from asyncio import gather
from datetime import datetime, timedelta
from hashlib import md5
from yarl import URL
from ....utils.exception import FallbackError
from ....utils.host import HostPage, get_self_netloc
from ....utils.metrics import get_metrics
from ....utils.render import render
from ....utils.render.schemas.base import Avatar
from ....utils.render.schemas.tetrio.user.info_v2 import (
Badge,
Blitz,
Info,
Sprint,
Statistic,
TetraLeague,
TetraLeagueStatistic,
User,
Zen,
)
from ....utils.screenshot import screenshot
from ..api import Player
from ..api.schemas.summaries.league import InvalidData, NeverPlayedData, NeverRatedData
from .tools import flow_to_history, handling_special_value
async def make_query_image_v2(player: Player) -> bytes:
(
(user, user_info, league, sprint, blitz, zen),
(avatar_revision, banner_revision, leagueflow),
) = await gather(
gather(player.user, player.get_info(), player.league, player.sprint, player.blitz, player.zen),
gather(player.avatar_revision, player.banner_revision, player.get_leagueflow()),
)
if sprint.data.record is not None:
duration = timedelta(milliseconds=sprint.data.record.results.stats.finaltime).total_seconds()
sprint_value = f'{duration:.3f}s' if duration < 60 else f'{duration // 60:.0f}m {duration % 60:.3f}s' # noqa: PLR2004
else:
sprint_value = 'N/A'
play_time: str | None
if (game_time := handling_special_value(user_info.data.gametime)) is not None:
if game_time // 3600 > 0:
play_time = f'{game_time//3600:.0f}h {game_time % 3600 // 60:.0f}m {game_time % 60:.0f}s'
elif game_time // 60 > 0:
play_time = f'{game_time//60:.0f}m {game_time % 60:.0f}s'
else:
play_time = f'{game_time:.0f}s'
else:
play_time = game_time
try:
history = flow_to_history(leagueflow, lambda x: x[-100:])
except FallbackError:
history = None
netloc = get_self_netloc()
async with HostPage(
await render(
'v2/tetrio/user/info',
Info(
user=User(
id=user.ID,
name=user.name.upper(),
bio=user_info.data.bio,
banner=str(
URL(f'http://{netloc}/host/resource/tetrio/banners/{user.ID}') % {'revision': banner_revision}
)
if banner_revision is not None and banner_revision != 0
else None,
avatar=str(
URL(f'http://{netloc}/host/resource/tetrio/avatars/{user.ID}') % {'revision': avatar_revision}
)
if avatar_revision is not None and avatar_revision != 0
else Avatar(
type='identicon',
hash=md5(user.ID.encode()).hexdigest(), # noqa: S324
),
badges=[
Badge(
id=i.id,
description=i.label,
group=i.group,
receive_at=i.ts if isinstance(i.ts, datetime) else None,
)
for i in user_info.data.badges
],
country=user_info.data.country,
role=user_info.data.role,
xp=user_info.data.xp,
friend_count=user_info.data.friend_count,
supporter_tier=user_info.data.supporter_tier,
bad_standing=user_info.data.badstanding or False,
playtime=play_time,
join_at=user_info.data.ts,
),
tetra_league=TetraLeague(
rank=league.data.rank,
highest_rank='z' if isinstance(league.data, NeverRatedData) else league.data.bestrank,
tr=round(league.data.tr, 2),
glicko=round(league.data.glicko, 2),
rd=round(league.data.rd, 2),
global_rank=league.data.standing,
country_rank=league.data.standing_local,
pps=(metrics := get_metrics(pps=league.data.pps, apm=league.data.apm, vs=league.data.vs)).pps,
apm=metrics.apm,
apl=metrics.apl,
vs=metrics.vs,
adpl=metrics.adpl,
statistic=TetraLeagueStatistic(total=league.data.gamesplayed, wins=league.data.gameswon),
decaying=league.data.decaying,
history=history,
)
if not isinstance(league.data, NeverPlayedData | InvalidData)
else None,
statistic=Statistic(
total=handling_special_value(user_info.data.gamesplayed),
wins=handling_special_value(user_info.data.gameswon),
),
sprint=Sprint(
time=sprint_value,
global_rank=sprint.data.rank,
play_at=sprint.data.record.ts,
)
if sprint.data.record is not None
else None,
blitz=Blitz(
score=blitz.data.record.results.stats.score,
global_rank=blitz.data.rank,
play_at=blitz.data.record.ts,
)
if blitz.data.record is not None
else None,
zen=Zen(level=zen.data.level, score=zen.data.score),
),
),
) as page_hash:
return await screenshot(f'http://{netloc}/host/{page_hash}.html')

View File

@@ -0,0 +1,153 @@
from collections import defaultdict
from collections.abc import Callable, Sequence
from datetime import datetime, timedelta, timezone
from math import floor
from statistics import mean
from typing import TYPE_CHECKING
from uuid import uuid4
from nonebot import get_driver
from nonebot_plugin_alconna import Subcommand
from nonebot_plugin_apscheduler import scheduler
from nonebot_plugin_orm import get_session
from sqlalchemy import select
from ....utils.exception import RequestError
from ....utils.retry import retry
from .. import alc
from .. import command as base_command
from ..api.leaderboards import by
from ..api.schemas.base import P
from ..api.schemas.leaderboards import Parameter
from ..api.schemas.leaderboards.by import Entry
from ..constant import RANK_PERCENTILE
from ..models import TETRIOLeagueHistorical, TETRIOLeagueStats, TETRIOLeagueStatsField
if TYPE_CHECKING:
from ..api.schemas.leaderboards.by import BySuccessModel
from ..api.typing import Rank
UTC = timezone.utc
driver = get_driver()
command = Subcommand('rank', help_text='查询 TETR.IO 段位信息')
def wrapper(slot: int | str, content: str | None) -> str | None:
if slot == 'rank' and not content:
return '--all'
if content is not None:
return f'--detail {content.lower()}'
return content
alc.shortcut(
r'(?i:io)(?i:段位|段|rank)\s*(?P<rank>[a-zA-Z+-]{0,2})',
command='tstats TETR.IO rank {rank}',
humanized='iorank',
fuzzy=False,
wrapper=wrapper,
)
def _pps(user: Entry) -> float:
return user.league.pps
def _apm(user: Entry) -> float:
return user.league.apm
def _vs(user: Entry) -> float:
return user.league.vs
def _min(users: Sequence[Entry], field: Callable[[Entry], float]) -> Entry:
return min(users, key=field)
def _max(users: Sequence[Entry], field: Callable[[Entry], float]) -> Entry:
return max(users, key=field)
def find_special_player(
users: Sequence[Entry],
field: Callable[[Entry], float],
sort: Callable[[Sequence[Entry], Callable[[Entry], float]], Entry],
) -> Entry:
return sort(users, field)
@scheduler.scheduled_job('cron', hour='0,6,12,18', minute=0)
async def get_tetra_league_data() -> None:
x_session_id = uuid4()
retry_by = retry(max_attempts=10, exception_type=RequestError)(by)
prisecter = P(pri=9007199254740991, sec=9007199254740991, ter=9007199254740991) # * from ch.tetr.io
results: list[BySuccessModel] = []
while True:
model = await retry_by('league', Parameter(after=prisecter.to_prisecter(), limit=100), x_session_id)
prisecter = model.data.entries[-1].p
results.append(model)
if len(model.data.entries) < 100: # 分页值 # noqa: PLR2004
break
players: list[Entry] = []
for result in results:
players.extend(result.data.entries)
players.sort(key=lambda x: x.league.tr, reverse=True)
rank_player_mapping: defaultdict[Rank, list[Entry]] = defaultdict(list)
for player in players:
rank_player_mapping[player.league.rank].append(player)
stats = TETRIOLeagueStats(raw=[], fields=[], update_time=datetime.now(UTC))
fields: list[TETRIOLeagueStatsField] = []
for rank, percentile in RANK_PERCENTILE.items():
offset = floor((percentile / 100) * len(players)) - 1
tr_line = players[offset].league.tr
rank_players = rank_player_mapping[rank]
fields.append(
TETRIOLeagueStatsField(
rank=rank,
tr_line=tr_line,
player_count=len(rank_players),
low_pps=find_special_player(rank_players, _pps, _min),
low_apm=find_special_player(rank_players, _apm, _min),
low_vs=find_special_player(rank_players, _vs, _min),
avg_pps=mean(_pps(i) for i in rank_players),
avg_apm=mean(_apm(i) for i in rank_players),
avg_vs=mean(_vs(i) for i in rank_players),
high_pps=find_special_player(rank_players, _pps, _max),
high_apm=find_special_player(rank_players, _apm, _max),
high_vs=find_special_player(rank_players, _vs, _max),
stats=stats,
)
)
historicals = [
TETRIOLeagueHistorical(request_id=x_session_id, data=model, update_time=model.cache.cached_at, stats=stats)
for model in results
]
stats.raw = historicals
stats.fields = fields
async with get_session() as session:
session.add(stats)
await session.commit()
@driver.on_startup
async def _() -> None:
async with get_session() as session:
latest_time = await session.scalar(
select(TETRIOLeagueStats.update_time).order_by(TETRIOLeagueStats.id.desc()).limit(1)
)
if latest_time is None or datetime.now(tz=UTC) - latest_time.replace(tzinfo=UTC) > timedelta(hours=6):
await get_tetra_league_data()
from . import all, detail # noqa: E402
base_command.add(command)
__all__ = ['all', 'detail']

View File

@@ -0,0 +1,115 @@
from datetime import timedelta
from arclet.alconna import Arg
from nonebot_plugin_alconna import Option, Subcommand, UniMessage
from nonebot_plugin_orm import get_session
from nonebot_plugin_session import EventSession
from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[import-untyped]
from sqlalchemy import func, select
from sqlalchemy.orm import selectinload
from ....db import trigger
from ....utils.host import HostPage, get_self_netloc
from ....utils.metrics import get_metrics
from ....utils.render import render
from ....utils.render.schemas.tetrio.rank.v1 import Data as DataV1
from ....utils.render.schemas.tetrio.rank.v1 import ItemData as ItemDataV1
from ....utils.render.schemas.tetrio.rank.v2 import AverageData as AverageDataV2
from ....utils.render.schemas.tetrio.rank.v2 import Data as DataV2
from ....utils.render.schemas.tetrio.rank.v2 import ItemData as ItemDataV2
from ....utils.screenshot import screenshot
from .. import alc
from ..constant import GAME_TYPE
from ..models import TETRIOLeagueStats
from ..typing import Template
from . import command
command.add(
Subcommand(
'--all', Option('--template', Arg('template', Template), alias=['-T'], help_text='要使用的查询模板'), dest='all'
)
)
@alc.assign('TETRIO.rank.all')
async def _(event_session: EventSession, template: Template | None = None):
async with trigger(
session_persist_id=await get_session_persist_id(event_session),
game_platform=GAME_TYPE,
command_type='rank',
command_args=['--all'] + ([f'--template {template}'] if template is not None else []),
):
async with get_session() as session:
latest_data = (
await session.scalars(
select(TETRIOLeagueStats)
.order_by(TETRIOLeagueStats.id.desc())
.limit(1)
.options(selectinload(TETRIOLeagueStats.fields))
)
).one()
compare_data = (
await session.scalars(
select(TETRIOLeagueStats)
.order_by(
func.abs(
func.julianday(TETRIOLeagueStats.update_time)
- func.julianday(latest_data.update_time - timedelta(hours=24))
)
)
.limit(1)
.options(selectinload(TETRIOLeagueStats.fields))
)
).one()
match template:
case 'v1' | None:
await UniMessage.image(raw=await make_image_v1(latest_data, compare_data)).finish()
case 'v2':
await UniMessage.image(raw=await make_image_v2(latest_data, compare_data)).finish()
async def make_image_v1(latest_data: TETRIOLeagueStats, compare_data: TETRIOLeagueStats) -> bytes:
async with HostPage(
await render(
'v1/tetrio/rank',
DataV1(
items={
i[0].rank: ItemDataV1(
trending=round(i[0].tr_line - i[1].tr_line, 2),
require_tr=round(i[0].tr_line, 2),
players=i[0].player_count,
)
for i in zip(latest_data.fields, compare_data.fields, strict=True)
},
updated_at=latest_data.update_time,
),
)
) as page_hash:
return await screenshot(f'http://{get_self_netloc()}/host/{page_hash}.html')
async def make_image_v2(latest_data: TETRIOLeagueStats, compare_data: TETRIOLeagueStats) -> bytes:
async with HostPage(
await render(
'v2/tetrio/rank',
DataV2(
items={
i[0].rank: ItemDataV2(
require_tr=round(i[0].tr_line, 2),
trending=round(i[0].tr_line - i[1].tr_line, 2),
average_data=AverageDataV2(
pps=(metrics := get_metrics(pps=i[0].avg_pps, apm=i[0].avg_apm, vs=i[0].avg_vs)).pps,
apm=metrics.apm,
apl=metrics.apl,
vs=metrics.vs,
adpl=metrics.adpl,
),
players=i[0].player_count,
)
for i in zip(latest_data.fields, compare_data.fields, strict=True)
},
updated_at=latest_data.update_time,
),
)
) as page_hash:
return await screenshot(f'http://{get_self_netloc()}/host/{page_hash}.html')

View File

@@ -0,0 +1,128 @@
from datetime import timedelta, timezone
from zoneinfo import ZoneInfo
from arclet.alconna import Arg
from nonebot import get_driver
from nonebot_plugin_alconna import Option, UniMessage
from nonebot_plugin_orm import get_session
from nonebot_plugin_session import EventSession
from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[import-untyped]
from sqlalchemy import func, select
from sqlalchemy.orm import selectinload
from ....db import trigger
from ....utils.host import HostPage, get_self_netloc
from ....utils.metrics import get_metrics
from ....utils.render import render
from ....utils.render.schemas.tetrio.rank.detail import Data, SpecialData
from ....utils.screenshot import screenshot
from .. import alc
from ..api.typing import ValidRank
from ..constant import GAME_TYPE
from ..models import TETRIOLeagueStats
from . import command
UTC = timezone.utc
driver = get_driver()
command.add(Option('--detail', Arg('rank', ValidRank), alias=['-D']))
@alc.assign('TETRIO.rank')
async def _(rank: ValidRank, event_session: EventSession):
async with trigger(
session_persist_id=await get_session_persist_id(event_session),
game_platform=GAME_TYPE,
command_type='rank',
command_args=[f'--detail {rank}'],
):
async with get_session() as session:
latest_data = (
await session.scalars(
select(TETRIOLeagueStats)
.order_by(TETRIOLeagueStats.id.desc())
.limit(1)
.options(selectinload(TETRIOLeagueStats.fields))
)
).one()
compare_data = (
await session.scalars(
select(TETRIOLeagueStats)
.order_by(
func.abs(
func.julianday(TETRIOLeagueStats.update_time)
- func.julianday(latest_data.update_time - timedelta(hours=24))
)
)
.limit(1)
.options(selectinload(TETRIOLeagueStats.fields))
)
).one()
await UniMessage.image(
raw=await make_image(
rank,
latest_data,
compare_data,
)
).finish()
async def make_image(rank: ValidRank, latest: TETRIOLeagueStats, compare: TETRIOLeagueStats) -> bytes:
latest_data = next(filter(lambda x: x.rank == rank, latest.fields))
compare_data = next(filter(lambda x: x.rank == rank, compare.fields))
avg = get_metrics(pps=latest_data.avg_pps, apm=latest_data.avg_apm, vs=latest_data.avg_vs)
low_pps = get_metrics(
pps=latest_data.low_pps.league.pps, apm=latest_data.low_pps.league.apm, vs=latest_data.low_pps.league.vs
)
low_apm = get_metrics(
pps=latest_data.low_apm.league.pps, apm=latest_data.low_apm.league.apm, vs=latest_data.low_apm.league.vs
)
low_vs = get_metrics(
pps=latest_data.low_vs.league.pps, apm=latest_data.low_vs.league.apm, vs=latest_data.low_vs.league.vs
)
max_pps = get_metrics(
pps=latest_data.high_pps.league.pps, apm=latest_data.high_pps.league.apm, vs=latest_data.high_pps.league.vs
)
max_apm = get_metrics(
pps=latest_data.high_apm.league.pps, apm=latest_data.high_apm.league.apm, vs=latest_data.high_apm.league.vs
)
max_vs = get_metrics(
pps=latest_data.high_vs.league.pps, apm=latest_data.high_vs.league.apm, vs=latest_data.high_vs.league.vs
)
async with HostPage(
await render(
'v2/tetrio/rank/detail',
Data(
name=latest_data.rank,
trending=round(latest_data.tr_line - compare_data.tr_line, 2),
require_tr=round(latest_data.tr_line, 2),
players=latest_data.player_count,
minimum_data=SpecialData(
apm=low_apm.apm,
pps=low_pps.pps,
lpm=low_pps.lpm,
vs=low_vs.vs,
adpm=low_vs.adpm,
apm_holder=latest_data.low_apm.username.upper(),
pps_holder=latest_data.low_pps.username.upper(),
vs_holder=latest_data.low_vs.username.upper(),
),
average_data=SpecialData(
apm=avg.apm, pps=avg.pps, lpm=avg.lpm, vs=avg.vs, adpm=avg.adpm, apl=avg.apl, adpl=avg.adpl
),
maximum_data=SpecialData(
apm=max_apm.apm,
pps=max_pps.pps,
lpm=max_pps.lpm,
vs=max_vs.vs,
adpm=max_vs.adpm,
apm_holder=latest_data.high_apm.username.upper(),
pps_holder=latest_data.high_pps.username.upper(),
vs_holder=latest_data.high_vs.username.upper(),
),
updated_at=latest.update_time.replace(tzinfo=UTC).astimezone(ZoneInfo('Asia/Shanghai')),
),
)
) as page_hash:
return await screenshot(f'http://{get_self_netloc()}/host/{page_hash}.html')

View File

@@ -1,7 +1,6 @@
from asyncio import gather
from datetime import timedelta
from hashlib import md5
from urllib.parse import urlencode
from nonebot.adapters import Event
from nonebot.matcher import Matcher
@@ -11,8 +10,10 @@ from nonebot_plugin_orm import get_session
from nonebot_plugin_session import EventSession
from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[import-untyped]
from nonebot_plugin_user import get_user
from yarl import URL
from ....db import query_bind_info, trigger
from ....i18n import Lang
from ....utils.exception import RecordNotFoundError
from ....utils.host import HostPage, get_self_netloc
from ....utils.metrics import get_metrics
@@ -22,7 +23,6 @@ from ....utils.render.schemas.tetrio.record.base import Finesse, Max, Mini, Tspi
from ....utils.render.schemas.tetrio.record.blitz import Record, Statistic
from ....utils.screenshot import screenshot
from ....utils.typing import Me
from ...constant import CANT_VERIFY_MESSAGE
from .. import alc
from ..api.player import Player
from ..constant import GAME_TYPE
@@ -60,9 +60,10 @@ async def _(
)
if bind is None:
await matcher.finish('未查询到绑定信息')
message = UniMessage(CANT_VERIFY_MESSAGE)
player = Player(user_id=bind.game_account, trust=True)
await (message + UniMessage.image(raw=await make_blitz_image(player))).finish()
await (
UniMessage.i18n(Lang.interaction.warning.unverified) + UniMessage.image(raw=await make_blitz_image(player))
).finish()
@alc.assign('TETRIO.record.blitz')
@@ -94,7 +95,9 @@ async def make_blitz_image(player: Player) -> bytes:
user=User(
id=user.ID,
name=user.name.upper(),
avatar=f'http://{netloc}/host/resource/tetrio/avatars/{user.ID}?{urlencode({"revision": avatar_revision})}'
avatar=str(
URL(f'http://{netloc}/host/resource/tetrio/avatars/{user.ID}') % {'revision': avatar_revision}
)
if (avatar_revision := (await player.avatar_revision)) is not None and avatar_revision != 0
else Avatar(
type='identicon',

View File

@@ -1,7 +1,6 @@
from asyncio import gather
from datetime import timedelta
from hashlib import md5
from urllib.parse import urlencode
from nonebot.adapters import Event
from nonebot.matcher import Matcher
@@ -11,8 +10,10 @@ from nonebot_plugin_orm import get_session
from nonebot_plugin_session import EventSession
from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[import-untyped]
from nonebot_plugin_user import get_user
from yarl import URL
from ....db import query_bind_info, trigger
from ....i18n import Lang
from ....utils.exception import RecordNotFoundError
from ....utils.host import HostPage, get_self_netloc
from ....utils.metrics import get_metrics
@@ -22,7 +23,6 @@ from ....utils.render.schemas.tetrio.record.base import Finesse, Max, Mini, Stat
from ....utils.render.schemas.tetrio.record.sprint import Record
from ....utils.screenshot import screenshot
from ....utils.typing import Me
from ...constant import CANT_VERIFY_MESSAGE
from .. import alc
from ..api.player import Player
from ..constant import GAME_TYPE
@@ -60,9 +60,10 @@ async def _(
)
if bind is None:
await matcher.finish('未查询到绑定信息')
message = UniMessage(CANT_VERIFY_MESSAGE)
player = Player(user_id=bind.game_account, trust=True)
await (message + UniMessage.image(raw=await make_sprint_image(player))).finish()
await (
UniMessage.i18n(Lang.interaction.warning.unverified) + UniMessage.image(raw=await make_sprint_image(player))
).finish()
@alc.assign('TETRIO.record.sprint')
@@ -95,7 +96,9 @@ async def make_sprint_image(player: Player) -> bytes:
user=User(
id=user.ID,
name=user.name.upper(),
avatar=f'http://{netloc}/host/resource/tetrio/avatars/{user.ID}?{urlencode({"revision": avatar_revision})}'
avatar=str(
URL(f'http://{netloc}/host/resource/tetrio/avatars/{user.ID}') % {'revision': avatar_revision}
)
if (avatar_revision := (await player.avatar_revision)) is not None and avatar_revision != 0
else Avatar(
type='identicon',

View File

@@ -1,13 +1,13 @@
from contextlib import suppress
from datetime import datetime, timezone
from io import StringIO
from urllib.parse import urlencode
from lxml import etree
from pandas import read_html
from ....config.config import config
from ....db import anti_duplicate_add
from ....utils.request import Request, splice_url
from ....utils.request import Request
from ..constant import BASE_URL, USER_NAME
from .models import TOPHistoricalData
from .schemas.user import User
@@ -15,6 +15,8 @@ from .schemas.user_profile import Data, UserProfile
UTC = timezone.utc
request = Request(config.tetris.proxy.top or config.tetris.proxy.main)
class Player:
def __init__(self, *, user_name: str, trust: bool = False) -> None:
@@ -35,8 +37,7 @@ class Player:
async def get_profile(self) -> UserProfile:
"""获取用户信息"""
if self._user_profile is None:
url = splice_url([BASE_URL, 'profile.php', f'?{urlencode({"user":self.user_name})}'])
raw_user_profile = await Request.request(url, is_json=False)
raw_user_profile = await request.request(BASE_URL / 'profile.php' % {'user': self.user_name}, is_json=False)
self._user_profile = self._parse_profile(raw_user_profile)
await anti_duplicate_add(
TOPHistoricalData(

View File

@@ -1,8 +1,10 @@
from re import compile
from typing import Literal
from yarl import URL
GAME_TYPE: Literal['TOP'] = 'TOP'
BASE_URL = 'http://tetrisonline.pl/top/'
BASE_URL = URL('http://tetrisonline.pl/top/')
USER_NAME = compile(r'^[a-zA-Z0-9_]{1,16}$')

View File

@@ -8,6 +8,7 @@ from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[im
from nonebot_plugin_user import get_user
from ...db import query_bind_info, trigger
from ...i18n import Lang
from ...utils.exception import FallbackError
from ...utils.host import HostPage, get_self_netloc
from ...utils.metrics import TetrisMetricsBasicWithLPM, get_metrics
@@ -18,7 +19,6 @@ from ...utils.render.schemas.top_info import Data as InfoData
from ...utils.render.schemas.top_info import Info
from ...utils.screenshot import screenshot
from ...utils.typing import Me
from ..constant import CANT_VERIFY_MESSAGE
from . import alc
from .api import Player
from .api.schemas.user_profile import Data, UserProfile
@@ -44,7 +44,7 @@ async def _(event: Event, matcher: Matcher, target: At | Me, event_session: Even
if bind is None:
await matcher.finish('未查询到绑定信息')
await (
UniMessage(CANT_VERIFY_MESSAGE)
UniMessage.i18n(Lang.interaction.warning.unverified)
+ await make_query_result(await Player(user_name=bind.game_account, trust=True).get_profile())
).finish()

View File

@@ -1,13 +1,14 @@
from datetime import datetime, timezone
from typing import overload
from urllib.parse import urlencode
from typing import cast, overload
from httpx import TimeoutException
from nonebot.compat import type_validate_json
from yarl import URL
from ....config.config import config
from ....db import anti_duplicate_add
from ....utils.exception import RequestError
from ....utils.request import Request, splice_url
from ....utils.request import Request
from ..constant import BASE_URL, USER_NAME
from .models import TOSHistoricalData
from .schemas.user import User
@@ -16,6 +17,8 @@ from .schemas.user_profile import UserProfile
UTC = timezone.utc
request = Request(config.tetris.proxy.tos or config.tetris.proxy.main)
class Player:
@overload
@@ -57,28 +60,14 @@ class Player:
"""获取用户信息"""
if self._user_info is None:
if self.teaid is not None:
url = [
splice_url(
[
i,
'getTeaIdInfo',
f'?{urlencode({"teaId":self.teaid})}',
]
)
for i in BASE_URL
]
path = 'getTeaIdInfo'
query = {'teaId': self.teaid}
else:
url = [
splice_url(
[
i,
'getUsernameInfo',
f'?{urlencode({"username":self.user_name})}',
]
)
for i in BASE_URL
]
raw_user_info = await Request.failover_request(url, failover_code=[502], failover_exc=(TimeoutException,))
path = 'getUsernameInfo'
query = {'username': cast(str, self.user_name)}
raw_user_info = await request.failover_request(
[i / path % query for i in BASE_URL], failover_code=[502], failover_exc=(TimeoutException,)
)
user_info: UserInfo = type_validate_json(UserInfo, raw_user_info) # type: ignore[arg-type]
if not isinstance(user_info, UserInfoSuccess):
msg = f'用户信息请求错误:\n{user_info.error}'
@@ -98,17 +87,11 @@ class Player:
"""获取用户数据"""
if other_parameter is None:
other_parameter = {}
params = urlencode(dict(sorted(other_parameter.items())))
params = (URL('') % dict(sorted(other_parameter.items()))).human_repr()
if self._user_profile.get(params) is None:
raw_user_profile = await Request.failover_request(
raw_user_profile = await request.failover_request(
[
splice_url(
[
i,
'getProfile',
f'?{urlencode({"id":self.teaid or self.user_name,**other_parameter})}',
]
)
i / 'getProfile' % {'id': self.teaid or cast(str, self.user_name), **other_parameter}
for i in BASE_URL
],
failover_code=[502],

View File

@@ -1,11 +1,13 @@
from re import compile
from typing import Literal
from yarl import URL
GAME_TYPE: Literal['TOS'] = 'TOS'
BASE_URL = {
'https://teatube.cn:8888/',
'http://cafuuchino1.studio26f.org:19970',
URL('https://teatube.cn:8888/'),
URL('http://cafuuchino1.studio26f.org:19970'),
}
USER_NAME = compile(

View File

@@ -14,6 +14,7 @@ from nonebot_plugin_user import get_user
from nonebot_plugin_userinfo import EventUserInfo, UserInfo
from ...db import query_bind_info, trigger
from ...i18n import Lang
from ...utils.exception import RequestError
from ...utils.host import HostPage, get_self_netloc
from ...utils.image import get_avatar
@@ -24,7 +25,6 @@ from ...utils.render.schemas.base import People, Ranking
from ...utils.render.schemas.tos_info import Info, Multiplayer, Radar
from ...utils.screenshot import screenshot
from ...utils.typing import Me, Number
from ..constant import CANT_VERIFY_MESSAGE
from . import alc
from .api import Player
from .api.schemas.user_info import UserInfoSuccess
@@ -124,7 +124,7 @@ async def _(
)
if bind is None:
await matcher.finish('未查询到绑定信息')
message = CANT_VERIFY_MESSAGE
message = UniMessage.i18n(Lang.interaction.warning.unverified)
player = Player(teaid=bind.game_account, trust=True)
user_info, game_data = await gather(player.get_info(), get_game_data(player))
if game_data is not None:

View File

@@ -0,0 +1,5 @@
{
"default": "en-US",
"frozen": [],
"require": []
}

View File

@@ -0,0 +1,72 @@
{
"title": "Lang Schema",
"description": "Schema for lang file",
"type": "object",
"properties": {
"interaction": {
"title": "Interaction",
"description": "Scope 'interaction' of lang item",
"type": "object",
"additionalProperties": false,
"properties": {
"wrong": {
"title": "Wrong",
"description": "Scope 'wrong' of lang item",
"type": "object",
"additionalProperties": false,
"properties": {
"query_bot": {
"title": "query_bot",
"description": "value of lang item type 'query_bot'",
"type": "string"
}
}
},
"warning": {
"title": "Warning",
"description": "Scope 'warning' of lang item",
"type": "object",
"additionalProperties": false,
"properties": {
"unverified": {
"title": "unverified",
"description": "value of lang item type 'unverified'",
"type": "string"
}
}
}
}
},
"error": {
"title": "Error",
"description": "Scope 'error' of lang item",
"type": "object",
"additionalProperties": false,
"properties": {
"MessageFormatError": {
"title": "Messageformaterror",
"description": "Scope 'MessageFormatError' of lang item",
"type": "object",
"additionalProperties": false,
"properties": {
"TETR.IO": {
"title": "TETR.IO",
"description": "value of lang item type 'TETR.IO'",
"type": "string"
},
"TOS": {
"title": "TOS",
"description": "value of lang item type 'TOS'",
"type": "string"
},
"TOP": {
"title": "TOP",
"description": "value of lang item type 'TOP'",
"type": "string"
}
}
}
}
}
}
}

View File

@@ -0,0 +1,16 @@
{
"$schema": ".template.schema.json",
"scopes": [
{
"scope": "interaction",
"types": [
{ "subtype": "wrong", "types": ["query_bot"] },
{ "subtype": "warning", "types": ["unverified"] }
]
},
{
"scope": "error",
"types": [{ "subtype": "MessageFormatError", "types": ["TETR.IO", "TOS", "TOP"] }]
}
]
}

View File

@@ -0,0 +1,54 @@
{
"title": "Template",
"description": "Template for lang items to generate schema for lang files",
"type": "object",
"properties": {
"scopes": {
"title": "Scopes",
"description": "All scopes of lang items",
"type": "array",
"uniqueItems": true,
"items": {
"title": "Scope",
"description": "First level of all lang items",
"type": "object",
"properties": {
"scope": {
"type": "string",
"description": "Scope name"
},
"types": {
"type": "array",
"description": "All types of lang items",
"uniqueItems": true,
"items": {
"oneOf": [
{
"type": "string",
"description": "Value of lang item"
},
{
"type": "object",
"properties": {
"subtype": {
"type": "string",
"description": "Subtype name of lang item"
},
"types": {
"type": "array",
"description": "All subtypes of lang items",
"uniqueItems": true,
"items": {
"$ref": "#/properties/scopes/items/properties/types/items"
}
}
}
}
]
}
}
}
}
}
}
}

View File

@@ -0,0 +1,12 @@
# This file is @generated by tarina.lang CLI tool
# It is not intended for manual editing.
# ruff: noqa: E402, F401, PLC0414
from pathlib import Path
from tarina.lang import lang # type: ignore[import-untyped]
lang.load(Path(__file__).parent)
from .model import Lang as Lang

View File

@@ -0,0 +1,16 @@
{
"$schema": ".lang.schema.json",
"interaction": {
"wrong": { "query_bot": "Can't query bot's information" },
"warning": {
"unverified": "* Because I can't verify account linking information, I can't guarantee the info I found is yourself/themself."
}
},
"error": {
"MessageFormatError": {
"TETR.IO": "Username/ID is invalid",
"TOS": "Username/ID is invalid",
"TOP": "Username is invalid"
}
}
}

View File

@@ -0,0 +1,32 @@
# This file is @generated by tarina.lang CLI tool
# It is not intended for manual editing.
from tarina.lang.model import LangItem, LangModel # type: ignore[import-untyped]
class InteractionWrong:
query_bot: LangItem = LangItem('interaction', 'wrong.query_bot')
class InteractionWarning:
unverified: LangItem = LangItem('interaction', 'warning.unverified')
class Interaction:
wrong = InteractionWrong
warning = InteractionWarning
class ErrorMessageformaterror:
TETR_IO: LangItem = LangItem('error', 'MessageFormatError.TETR.IO')
TOS: LangItem = LangItem('error', 'MessageFormatError.TOS')
TOP: LangItem = LangItem('error', 'MessageFormatError.TOP')
class Error:
MessageFormatError = ErrorMessageformaterror
class Lang(LangModel):
interaction = Interaction
error = Error

View File

@@ -0,0 +1,14 @@
{
"$schema": ".lang.schema.json",
"interaction": {
"wrong": { "query_bot": "不能查询bot的信息" },
"warning": { "unverified": "* 由于无法验证绑定信息, 不能保证查询到的用户为本人" }
},
"error": {
"MessageFormatError": {
"TETR.IO": "用户名/ID不合法",
"TOS": "用户名/ID不合法",
"TOP": "用户名不合法"
}
}
}

View File

@@ -1,12 +1,14 @@
import sys
from collections.abc import Callable, Coroutine
from os import environ
from platform import system
from re import sub
from typing import Any, ClassVar
from nonebot import get_driver
from nonebot.log import logger
from playwright.__main__ import main
from playwright.async_api import Browser, async_playwright
from playwright.async_api import Browser, BrowserContext, async_playwright
driver = get_driver()
@@ -27,6 +29,7 @@ class BrowserManager:
"""浏览器管理类"""
_browser: Browser | None = None
_contexts: ClassVar[dict[str, BrowserContext]] = {}
@classmethod
async def init_playwright(cls) -> None:
@@ -72,7 +75,11 @@ class BrowserManager:
async def _start_browser(cls) -> Browser:
"""启动浏览器实例"""
playwright = await async_playwright().start()
cls._browser = await playwright.firefox.launch()
cls._browser = await playwright.firefox.launch(
firefox_user_prefs={
'network.http.max-persistent-connections-per-server': 64,
},
)
return cls._browser
@classmethod
@@ -80,8 +87,26 @@ class BrowserManager:
"""获取浏览器实例"""
return cls._browser or await cls._start_browser()
@classmethod
async def get_context(
cls, context_id: str = 'default', factory: Callable[[], Coroutine[Any, Any, BrowserContext]] | None = None
) -> BrowserContext:
"""获取浏览器上下文"""
return cls._contexts.setdefault(
context_id, await factory() if factory is not None else await (await cls.get_browser()).new_context()
)
@classmethod
async def del_context(cls, context_id: str) -> None:
"""删除浏览器上下文"""
if context_id in cls._contexts:
await cls._contexts[context_id].close()
del cls._contexts[context_id]
@classmethod
async def close_browser(cls) -> None:
"""关闭浏览器实例"""
for i in cls._contexts.values():
await i.close()
if isinstance(cls._browser, Browser):
await cls._browser.close()

View File

@@ -1,16 +1,20 @@
from functools import cache
from hashlib import sha256
from ipaddress import IPv4Address, IPv6Address
from pathlib import Path as FilePath
from typing import TYPE_CHECKING, ClassVar, Literal
from fastapi import FastAPI, Path, status
from aiofiles import open
from fastapi import BackgroundTasks, FastAPI, Path, status
from fastapi.responses import FileResponse, HTMLResponse, Response
from fastapi.staticfiles import StaticFiles
from nonebot import get_app, get_driver
from nonebot.log import logger
from yarl import URL
from ..config.config import CACHE_PATH
from ..games.tetrio.api.cache import request
from .image import img_to_png
from .request import Request
from .templates import TEMPLATES_DIR
if TYPE_CHECKING:
@@ -22,6 +26,7 @@ driver = get_driver()
global_config = driver.config
BASE_URL = URL('https://tetr.io/user-content/')
if not isinstance(app, FastAPI):
msg = '本插件需要 FastAPI 驱动器才能运行'
@@ -63,20 +68,30 @@ def _(page_hash: str) -> HTMLResponse:
@app.get('/host/resource/tetrio/{resource_type}/{user_id}', status_code=status.HTTP_200_OK)
async def _(
resource_type: Literal['avatars', 'banners'], revision: int, user_id: str = Path(regex=r'^[a-f0-9]{24}$')
resource_type: Literal['avatars', 'banners'],
revision: int,
background_tasks: BackgroundTasks,
user_id: str = Path(regex=r'^[a-f0-9]{24}$'),
) -> Response:
if not (path := CACHE_PATH / 'tetrio' / resource_type / f'{user_id}_{revision}.png').exists():
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(
img_to_png(
await Request.request(
f'https://tetr.io/user-content/{resource_type}/{user_id}.jpg?rv={revision}', is_json=False
)
image = img_to_png(
await request.request(
BASE_URL / resource_type / f'{user_id}.jpg' % {'rv': revision},
is_json=False,
)
)
background_tasks.add_task(write_cache, path=path, data=image)
return Response(content=image, media_type='image/png')
return FileResponse(path)
async def write_cache(path: FilePath, data: bytes) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
async with open(path, 'wb') as file:
await file.write(data)
@cache
def get_self_netloc() -> str:
host: IPv4Address | IPv6Address | IPvAnyAddress = global_config.host
if isinstance(host, IPv4Address):

View File

@@ -0,0 +1,33 @@
from asyncio import Lock, sleep
from collections.abc import Callable, Coroutine
from datetime import timedelta
from functools import wraps
from time import time
from typing import Any, ParamSpec, TypeVar
from nonebot.log import logger
P = ParamSpec('P')
T = TypeVar('T')
def limit(limit: timedelta) -> Callable[[Callable[P, Coroutine[Any, Any, T]]], Callable[P, Coroutine[Any, Any, T]]]:
limit_seconds = limit.total_seconds()
def decorator(func: Callable[P, Coroutine[Any, Any, T]]) -> Callable[P, Coroutine[Any, Any, T]]:
last_call = 0.0
lock = Lock()
@wraps(func)
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
nonlocal last_call
async with lock:
if (diff := (time() - last_call)) < limit_seconds:
logger.debug(f'func: {func.__name__} trigger limit, wait {(limit_time:=limit_seconds-diff):.3f}s')
await sleep(limit_time)
last_call = time()
return await func(*args, **kwargs)
return wrapper
return decorator

View File

@@ -3,7 +3,7 @@ from typing import Literal
from pydantic import BaseModel
from ......games.tetrio.api.typing import Rank, ValidRank
from ......games.tetrio.api.typing import Rank
from .....typing import Number
from ...base import Avatar
from .base import TetraLeagueHistoryData
@@ -21,7 +21,7 @@ class User(BaseModel):
name: str
country: str | None
role: Literal['anon', 'user', 'bot', 'halfmod', 'mod', 'admin', 'sysop', 'banned']
role: Literal['anon', 'user', 'bot', 'halfmod', 'mod', 'admin', 'sysop', 'hidden', 'banned']
avatar: str | Avatar
banner: str | None
@@ -31,7 +31,6 @@ class User(BaseModel):
friend_count: int | None
supporter_tier: int
verified: bool
bad_standing: bool
badges: list[Badge]

View File

@@ -26,7 +26,6 @@ class User(BaseModel):
name: str
avatar: str | Avatar
country: str | None
verified: bool
tetra_league: TetraLeague
xp: Number
join_at: datetime | None

View File

@@ -1,54 +1,79 @@
from collections.abc import Sequence
from http import HTTPStatus
from urllib.parse import urljoin, urlparse
from typing import Any
from aiofiles import open
from httpx import AsyncClient, HTTPError
from nonebot import get_driver, get_plugin_config
from msgspec import DecodeError, Struct, json
from nonebot import get_driver
from nonebot.log import logger
from playwright.async_api import Response
from ujson import JSONDecodeError, dumps, loads
from yarl import URL
from ..config.config import CACHE_PATH, Config
from ..config.config import CACHE_PATH, config
from .browser import BrowserManager
from .exception import RequestError
driver = get_driver()
config = get_plugin_config(Config)
@driver.on_startup
async def _():
await Request.init_cache()
await Request.read_cache()
class CloudflareCache(Struct):
headers: dict[str, Any] | None = None
cookies: dict[str, Any] | None = None
@driver.on_shutdown
async def _():
await Request.write_cache()
encoder = json.Encoder()
decoder = json.Decoder()
def splice_url(url_list: list[str]) -> str:
url = ''
if len(url_list):
url = url_list.pop(0)
for i in url_list:
url = urljoin(url, i)
return url
class AntiCloudflare:
cache_decoder = json.Decoder(type=CloudflareCache)
def __init__(self, domain_suffix: str) -> None:
self.domain_suffix = domain_suffix
self.cache_path = CACHE_PATH / f'{self.domain_suffix}_cloudflare_cache.json'
self._headers: dict | None = None
self._cookies: dict | None = None
self.read_cache()
class Request:
"""网络请求相关类"""
def read_cache(self) -> None:
"""读取缓存文件"""
try:
cache: CloudflareCache = self.cache_decoder.decode(self.cache_path.read_text(encoding='UTF-8'))
self._headers = cache.headers
self._cookies = cache.cookies
except (OSError, DecodeError):
self.cache_path.unlink()
self.write_cache()
_CACHE_FILE = CACHE_PATH / 'cloudflare_cache.json'
_headers: dict | None = None
_cookies: dict | None = None
def write_cache(self) -> None:
"""写入缓存文件"""
self.cache_path.write_bytes(json.encode(CloudflareCache(headers=self.headers, cookies=self.cookies)))
@classmethod
async def _anti_cloudflare(cls, url: str) -> bytes:
@property
def headers(self) -> dict | None:
return self._headers
@headers.setter
def headers(self, value: dict | None) -> None:
self._headers = value
self.write_cache()
@property
def cookies(self) -> dict | None:
return self._cookies
@cookies.setter
def cookies(self, value: dict | None) -> None:
self._cookies = value
self.write_cache()
async def __call__(self, url: str, proxy: str | None = None) -> bytes:
"""用firefox硬穿五秒盾"""
browser = await BrowserManager.get_browser()
async with await browser.new_context() as context, await context.new_page() as page:
async with (
await browser.new_context(proxy={'server': proxy} if proxy is not None else None) as context,
await context.new_page() as page,
):
response = await page.goto(url)
attempts = 0
while attempts < 60: # noqa: PLR2004
@@ -61,84 +86,70 @@ class Request:
logger.warning('疑似触发了 Cloudflare 的验证码')
break
try:
loads(text)
except JSONDecodeError:
decoder.decode(text)
except DecodeError:
await page.wait_for_timeout(1000)
else:
if not isinstance(response, Response):
msg = 'api请求失败'
raise RequestError(msg)
cls._headers = await response.request.all_headers()
self.headers = await response.request.all_headers()
try:
cls._cookies = {
self.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
self.cookies = None
return await response.body()
msg = '绕过五秒盾失败'
raise RequestError(msg)
@classmethod
async def init_cache(cls) -> None:
"""初始化缓存文件"""
if not cls._CACHE_FILE.exists():
async with open(file=cls._CACHE_FILE, mode='w', encoding='UTF-8') as file:
await file.write(dumps({'headers': cls._headers, 'cookies': cls._cookies}))
@classmethod
async def read_cache(cls) -> None:
"""读取缓存文件"""
try:
async with open(file=cls._CACHE_FILE, mode='r', encoding='UTF-8') as file:
json = loads(await file.read())
except FileNotFoundError:
await cls.init_cache()
except (PermissionError, JSONDecodeError):
cls._CACHE_FILE.unlink()
await cls.init_cache()
else:
cls._headers = json['headers']
cls._cookies = json['cookies']
class Request:
"""网络请求相关类"""
@classmethod
async def write_cache(cls) -> None:
"""写入缓存文件"""
try:
async with open(file=cls._CACHE_FILE, mode='r+', encoding='UTF-8') as file:
await file.write(dumps({'headers': cls._headers, 'cookies': cls._cookies}))
except FileNotFoundError:
await cls.init_cache()
except (PermissionError, JSONDecodeError):
cls._CACHE_FILE.unlink()
await cls.init_cache()
def __init__(self, proxy: str | None) -> None:
self.proxy = proxy
self.anti_cloudflares: dict[str, AntiCloudflare] = {}
@classmethod
async def request(cls, url: str, *, is_json: bool = True) -> bytes:
async def request(
self,
url: URL,
extra_headers: dict | None = None,
*,
is_json: bool = True,
enable_anti_cloudflare: bool = False,
) -> bytes:
"""请求api"""
if (anti_cloudflare := self.anti_cloudflares.get(url.host or '')) is not None:
cookies = anti_cloudflare.cookies
headers = anti_cloudflare.headers
else:
cookies = None
headers = None
headers = headers if extra_headers is None else extra_headers if headers is None else headers | extra_headers
try:
async with AsyncClient(cookies=cls._cookies, timeout=config.tetris.request_timeout) as session:
response = await session.get(url, headers=cls._headers)
async with AsyncClient(cookies=cookies, timeout=config.tetris.request_timeout, proxy=self.proxy) as session:
response = await session.get(str(url), headers=headers)
if response.status_code != HTTPStatus.OK:
msg = f'请求错误 code: {response.status_code} {HTTPStatus(response.status_code).phrase}\n{response.text}'
raise RequestError(msg, status_code=response.status_code)
if is_json:
loads(response.content)
decoder.decode(response.content)
return response.content
except HTTPError as e:
msg = f'请求错误 \n{e!r}'
raise RequestError(msg) from e
except JSONDecodeError:
if urlparse(url).netloc.lower().endswith('tetr.io'):
return await cls._anti_cloudflare(url)
except DecodeError: # 由于捕获的是 DecodeError 所以一定是 is_json = True
if enable_anti_cloudflare and url.host is not None:
return await self.anti_cloudflares.setdefault(url.host, AntiCloudflare(url.host))(str(url), self.proxy)
raise
@classmethod
async def failover_request(
cls,
urls: Sequence[str],
self,
urls: Sequence[URL],
*,
failover_code: Sequence[int],
failover_exc: tuple[type[BaseException], ...],
@@ -148,7 +159,7 @@ class Request:
for i in urls:
logger.debug(f'尝试请求 {i}')
try:
return await cls.request(i, is_json=is_json)
return await self.request(i, is_json=is_json)
except RequestError as e:
if e.status_code in failover_code: # 如果状态码在 failover_code 中, 则继续尝试下一个URL
error_list.append(e)

View File

@@ -1,21 +1,20 @@
from nonebot import get_plugin_config
from playwright.async_api import TimeoutError, ViewportSize
from playwright.async_api import BrowserContext, TimeoutError, ViewportSize
from ..config.config import Config
from ..config.config import config
from .browser import BrowserManager
from .retry import retry
from .time_it import time_it
config = get_plugin_config(Config)
async def context_factory() -> BrowserContext:
return await (await BrowserManager.get_browser()).new_context(device_scale_factor=config.tetris.screenshot_quality)
@retry(exception_type=TimeoutError, reply='截图失败, 重试中')
@time_it
async def screenshot(url: str) -> bytes:
browser = await BrowserManager.get_browser()
async with (
await browser.new_page(device_scale_factor=config.tetris.screenshot_quality) as page,
):
context = await BrowserManager.get_context('screenshot', factory=context_factory)
async with await context.new_page() as page:
await page.goto(url)
size: ViewportSize = await page.evaluate("""
() => {
@@ -27,4 +26,5 @@ async def screenshot(url: str) -> bytes:
};
""")
await page.set_viewport_size(size)
return await page.locator('id=content').screenshot(timeout=5000, type='png')
await page.wait_for_load_state('networkidle')
return await page.locator('id=content').screenshot(animations='disabled', timeout=5000, type='png')

View File

@@ -13,7 +13,7 @@ from nonebot.permission import SUPERUSER
from nonebot_plugin_alconna import Alconna, Args, Option, on_alconna
from rich.progress import Progress
from ..config.config import CACHE_PATH, DATA_PATH
from ..config.config import CACHE_PATH, DATA_PATH, config
driver = get_driver()
@@ -24,7 +24,7 @@ alc = on_alconna(Alconna('更新模板', Option('--revision', Args['revision', s
async def download_templates(tag: str) -> Path:
logger.info(f'开始下载模板 {tag}')
async with AsyncClient() as client:
async with AsyncClient(proxy=config.tetris.proxy.github or config.tetris.proxy.main) as client:
if tag == 'latest':
logger.info('目标为 latest, 正在获取最新版本号')
tag = (
@@ -105,7 +105,7 @@ async def init_templates(tag: str) -> bool:
async def check_tag(tag: str) -> bool:
async with AsyncClient() as client:
async with AsyncClient(proxy=config.tetris.proxy.github or config.tetris.proxy.main) as client:
return (
await client.get(f'https://github.com/A-Minos/tetris-stats-templates/releases/tag/{tag}')
).status_code != HTTPStatus.NOT_FOUND

View File

@@ -1,11 +1,11 @@
from typing import Literal
from typing import Literal, TypeAlias
Number = float | int
GameType = Literal['IO', 'TOP', 'TOS']
BaseCommandType = Literal['bind', 'query']
TETRIOCommandType = BaseCommandType | Literal['rank', 'config', 'list', 'record']
AllCommandType = BaseCommandType | TETRIOCommandType
Me = Literal[
Number: TypeAlias = float | int
GameType: TypeAlias = Literal['IO', 'TOP', 'TOS']
BaseCommandType: TypeAlias = Literal['bind', 'query']
TETRIOCommandType: TypeAlias = BaseCommandType | Literal['rank', 'config', 'list', 'record']
AllCommandType: TypeAlias = BaseCommandType | TETRIOCommandType
Me: TypeAlias = Literal[
'',
'自己',
'我等',

3743
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,135 +1,167 @@
[tool.poetry]
name = 'nonebot-plugin-tetris-stats'
version = '1.4.11'
description = '一款基于 NoneBot2 的用于查询 Tetris 相关游戏数据的插件'
authors = ['scdhh <wallfjjd@gmail.com>']
readme = 'README.md'
homepage = 'https://github.com/shoucandanghehe/nonebot-plugin-tetris-stats'
repository = 'https://github.com/shoucandanghehe/nonebot-plugin-tetris-stats'
license = 'AGPL-3.0'
[project]
name = "nonebot-plugin-tetris-stats"
version = "1.6.2"
description = "一款基于 NoneBot2 的用于查询 Tetris 相关游戏数据的插件"
readme = "README.md"
authors = [{ name = "shoucandanghehe", email = "wallfjjd@gmail.com" }]
requires-python = ">=3.10"
dependencies = [
"aiocache>=0.12.3",
"aiofiles>=24.1.0",
"arclet-alconna<2",
"async-lru>=2.0.4",
"httpx>=0.27.2",
"jinja2>=3.1.4",
"lxml>=5.3.0",
"msgspec>=0.18.6",
"nonebot-plugin-alconna>=0.53.1",
"nonebot-plugin-apscheduler>=0.5.0",
"nonebot-plugin-localstore>=0.7.1",
"nonebot-plugin-orm>=0.7.6",
"nonebot-plugin-session>=0.3.2",
"nonebot-plugin-session-orm>=0.2.0",
"nonebot-plugin-user>=0.4.4",
"nonebot-plugin-userinfo>=0.2.6",
"nonebot2[fastapi]>=2.3.3",
"pandas>=2.2.3",
"pillow>=11.0.0",
"playwright>=1.48.0",
"rich>=13.9.3",
"yarl>=1.16.0",
]
classifiers = [
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
]
keywords = ["nonebot2"]
[tool.poetry.dependencies]
python = '^3.10'
nonebot2 = { extras = ['fastapi'], version = '^2.3.0' }
nonebot-plugin-alconna = '>=0.48.0'
nonebot-plugin-apscheduler = '>=0.4,<0.6'
nonebot-plugin-localstore = '>=0.6,<0.8'
nonebot-plugin-orm = '>=0.1.1,<0.8.0'
nonebot-plugin-session = '^0.3.1'
nonebot-plugin-session-orm = '^0.2.0'
nonebot-plugin-user = '>=0.2,<0.5'
nonebot-plugin-userinfo = '^0.2.4'
aiocache = '^0.12.2'
aiofiles = '>=23.2.1,<25.0.0'
async-lru = '^2.0.4'
httpx = '^0.27.0'
jinja2 = '^3.1.3'
lxml = '^5.1.0'
pandas = '>=1.4.3,<3.0.0'
pillow = '^10.3.0'
playwright = '^1.41.2'
rich = '^13.7.1'
ujson = '^5.9.0'
zstandard = '>=0.22,<0.24'
[tool.poetry.group.dev.dependencies]
mypy = '>=1.9'
ruff = '>=0.3.0'
types-aiofiles = '>=23.2.0.20240106,<25.0.0.0'
types-lxml = '^2024.2.9'
types-pillow = '^10.2.0.20240423'
types-ujson = '^5.9.0'
pandas-stubs = '>=1.5.2,<3.0.0'
nonebot2 = { extras = ['all'], version = '^2.3.0' }
nonebot-adapter-discord = '^0.1.3'
nonebot-adapter-kaiheila = '^0.3.4'
nonebot-adapter-onebot = '^2.4.1'
nonebot-adapter-qq = '^1.4.4'
nonebot-adapter-satori = '>=0.11.4,<0.13.0'
nonebot-plugin-orm = { extras = ['default'], version = '>=0.3,<0.8' }
[tool.poetry.group.debug.dependencies]
memory-profiler = '^0.61.0'
objprint = '^0.2.2'
viztracer = '^0.16.2'
[project.urls]
Homepage = "https://github.com/A-Minos/nonebot-plugin-tetris-stats"
Repository = "https://github.com/A-Minos/nonebot-plugin-tetris-stats"
Issues = "https://github.com/A-Minos/nonebot-plugin-tetris-stats/issues"
[build-system]
requires = ['poetry-core>=1.0.0']
build-backend = 'poetry.core.masonry.api'
requires = ["hatchling"]
build-backend = "hatchling.build"
[dependency-groups]
dev = [
"basedpyright>=1.19.1",
"mypy>=1.13.0",
"nonebot-adapter-discord>=0.1.8",
"nonebot-adapter-kaiheila>=0.3.4",
"nonebot-adapter-onebot>=2.4.6",
"nonebot-adapter-qq>=1.5.3",
"ruff>=0.7.1",
]
typecheck = [
"pandas-stubs>=2.2.3.241009",
"types-aiofiles>=24.1.0.20240626",
"types-lxml>=2024.9.16",
"types-pillow>=10.2.0.20240822",
]
test = ["nonebot-adapter-satori>=0.12.6", "nonebot-plugin-orm[default]>=0.7.6", "nonebot2[aiohttp,fastapi]>=2.3.3"]
debug = ["matplotlib>=3.9.2", "memory-profiler>=0.61.0", "objprint>=0.2.3", "pyqt6>=6.7.1", "viztracer>=0.17.0"]
release = ["bump-my-version>=0.28.0"]
[tool.uv]
default-groups = ["dev", "typecheck"]
[tool.ruff]
line-length = 120
target-version = 'py310'
target-version = "py310"
[tool.ruff.lint]
select = [
'F', # pyflakes
'E', # pycodestyle errors
'W', # pycodestyle warnings
'C90', # mccabe
'I', # isort
'N', # PEP8-naming
'UP', # pyupgrade
'YTT', # flake8-2020
'ANN', # flake8-annotations
'ASYNC', # flake8-async
'S', # flake8-bandit
'BLE', # flake8-blind-except
'FBT', # flake8-boolean-trap
'B', # flake8-bugbear
'A', # flake8-builtins
'COM', # flake8-commas
'C4', # flake8-comprehensions
'DTZ', # flake8-datetimez
'T10', # flake8-debugger
'EM', # flake8-errmsg
'FA', # flake8-future-annotations
'ISC', # flake8-implicit-str-concat
'ICN', # flake8-import-conventions
'PIE', # flake8-pie
'T20', # flake8-print
'PYI', # flake8-pyi
'Q', # flake8-quotes
'RSE', # flake8-raise
'RET', # flake8-return
'SLF', # flake8-self
'SLOT', # flake8-slots
'SIM', # flake8-simplify
'TID', # flake8-tidy-imports
'TCH', # flake8-type-checking
'ARG', # flake8-unused-arguments
'PTH', # flake8-use-pathlib
'ERA', # eradicate
'PD', # pandas-vet
'PGH', # pygrep-hooks
'PL', # pylint
'TRY', # tryceratops
'FLY', # flynt
'FAST', # FastAPI
'PERF', # Perflint
'FURB', # refurb
'RUF', # Ruff-specific rules
"F", # pyflakes
"E", # pycodestyle errors
"W", # pycodestyle warnings
"C90", # mccabe
"I", # isort
"N", # PEP8-naming
"UP", # pyupgrade
"YTT", # flake8-2020
"ANN", # flake8-annotations
"ASYNC", # flake8-async
"S", # flake8-bandit
"BLE", # flake8-blind-except
"FBT", # flake8-boolean-trap
"B", # flake8-bugbear
"A", # flake8-builtins
"COM", # flake8-commas
"C4", # flake8-comprehensions
"DTZ", # flake8-datetimez
"T10", # flake8-debugger
"EM", # flake8-errmsg
"FA", # flake8-future-annotations
"ISC", # flake8-implicit-str-concat
"ICN", # flake8-import-conventions
"PIE", # flake8-pie
"T20", # flake8-print
"PYI", # flake8-pyi
"Q", # flake8-quotes
"RSE", # flake8-raise
"RET", # flake8-return
"SLF", # flake8-self
"SLOT", # flake8-slots
"SIM", # flake8-simplify
"TID", # flake8-tidy-imports
"TCH", # flake8-type-checking
"ARG", # flake8-unused-arguments
"PTH", # flake8-use-pathlib
"ERA", # eradicate
"PD", # pandas-vet
"PGH", # pygrep-hooks
"PL", # pylint
"TRY", # tryceratops
"FLY", # flynt
"FAST", # FastAPI
"PERF", # Perflint
"FURB", # refurb
"RUF", # Ruff-specific rules
]
ignore = [
'E501', # 过长的行由 ruff format 处理, 剩余的都是字符串
'ANN101', # 由 type checker 自动推断
'ANN102', # 由 type checker 自动推断
'ANN202', # 向 NoneBot 注册的函数
'TRY003',
'COM812', # 强制尾随逗号
'TID252', # 相对导入
'ISC001', # format warning
"E501", # 过长的行由 ruff format 处理, 剩余的都是字符串
"ANN101", # 由 type checker 自动推断
"ANN102", # 由 type checker 自动推断
"ANN202", # 向 NoneBot 注册的函数
"TRY003",
"COM812", # 强制尾随逗号
"TID252", # 相对导入
"ISC001", # format warning
]
flake8-quotes = { inline-quotes = 'single', multiline-quotes = 'double' }
flake8-quotes = { inline-quotes = "single", multiline-quotes = "double" }
[tool.ruff.lint.flake8-annotations]
mypy-init-return = true
[tool.ruff.lint.flake8-builtins]
builtins-ignorelist = ['id']
builtins-ignorelist = ["id"]
[tool.ruff.format]
quote-style = 'single'
quote-style = "single"
[tool.basedpyright]
pythonVersion = "3.10"
pythonPlatform = "All"
defineConstant = { PYDANTIC_V2 = true }
typeCheckingMode = "standard"
[tool.bumpversion]
current_version = "1.6.2"
tag = true
sign_tags = true
tag_name = "{new_version}"
commit = true
message = ":bookmark: {new_version}"
[[tool.bumpversion.files]]
filename = "pyproject.toml"
search = "version = \"{current_version}\""
replace = "version = \"{new_version}\""
[tool.nonebot]
plugins = ['nonebot_plugin_tetris_stats']
plugins = ["nonebot_plugin_tetris_stats"]

21
renovate.json Normal file
View File

@@ -0,0 +1,21 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"labels": ["dependencies"],
"dependencyDashboardTitle": "📌 Dependency Dashboard",
"dependencyDashboardLabels": ["dependencies"],
"commitMessagePrefix": "⬆️",
"commitMessageAction": "Upgrade",
"packageRules": [
{
"matchUpdateTypes": "pin",
"commitMessagePrefix": "📌",
"commitMessageAction": "Pin"
},
{
"matchUpdateTypes": "rollback",
"commitMessagePrefix": "⬇️",
"commitMessageAction": "Downgrade"
}
],
"extends": ["config:recommended"]
}

3148
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff