Compare commits

...

48 Commits
1.6.0 ... 1.8.3

Author SHA1 Message Date
0bc3b86820 🔖 1.8.3
Some checks failed
Code Coverage / Test (macos-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (macos-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (macos-latest, 3.12) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.12) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.12) (push) Has been cancelled
TypeCheck / TypeCheck (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Code Coverage / check (push) Has been cancelled
2025-05-08 00:00:12 +08:00
pre-commit-ci[bot]
57d0b15242 ⬆️ auto update by pre-commit hooks (#538)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.6 → v0.11.8](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.6...v0.11.8)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-05-07 15:52:35 +00:00
呵呵です
56fe45efcf 适配新成就模板 (#540)
*  完善Achievement模型

*  添加一些alias

*  更新模板 schemas

* 🐛 修复类型错误
2025-05-07 23:51:13 +08:00
13f005179f 添加 schema 注释到 pyproject.toml
Some checks failed
Code Coverage / Test (macos-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (macos-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (macos-latest, 3.12) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.12) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.12) (push) Has been cancelled
TypeCheck / TypeCheck (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Code Coverage / check (push) Has been cancelled
2025-04-28 04:13:05 +08:00
243a8a286d 🔖 1.8.2 2025-04-28 04:09:04 +08:00
8b7de6745b 添加 development 模式 2025-04-28 04:08:55 +08:00
d0fc82d88d 添加开发依赖 nonebot-plugin-tarina-lang-turbo 2025-04-28 04:06:48 +08:00
bb4da8accc 🔖 1.8.1
Some checks are pending
Code Coverage / Test (macos-latest, 3.10) (push) Waiting to run
Code Coverage / Test (macos-latest, 3.11) (push) Waiting to run
Code Coverage / Test (macos-latest, 3.12) (push) Waiting to run
Code Coverage / Test (ubuntu-latest, 3.10) (push) Waiting to run
Code Coverage / Test (ubuntu-latest, 3.11) (push) Waiting to run
Code Coverage / Test (ubuntu-latest, 3.12) (push) Waiting to run
Code Coverage / Test (windows-latest, 3.10) (push) Waiting to run
Code Coverage / Test (windows-latest, 3.11) (push) Waiting to run
Code Coverage / Test (windows-latest, 3.12) (push) Waiting to run
Code Coverage / check (push) Blocked by required conditions
TypeCheck / TypeCheck (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
2025-04-27 15:18:25 +08:00
56e06a7001 🐛 修复 _lang 为私有变量不会默认序列化的bug 2025-04-27 15:17:12 +08:00
renovate[bot]
7c0b3cd240 ⬆️ Upgrade astral-sh/setup-uv action to v6 (#537)
Some checks failed
Code Coverage / Test (macos-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (macos-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (macos-latest, 3.12) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.12) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.12) (push) Has been cancelled
TypeCheck / TypeCheck (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Code Coverage / check (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-24 22:50:26 +08:00
dca0619021 🔖 1.8.0
Some checks are pending
Code Coverage / Test (macos-latest, 3.10) (push) Waiting to run
Code Coverage / Test (macos-latest, 3.11) (push) Waiting to run
Code Coverage / Test (macos-latest, 3.12) (push) Waiting to run
Code Coverage / Test (ubuntu-latest, 3.10) (push) Waiting to run
Code Coverage / Test (ubuntu-latest, 3.11) (push) Waiting to run
Code Coverage / Test (ubuntu-latest, 3.12) (push) Waiting to run
Code Coverage / Test (windows-latest, 3.10) (push) Waiting to run
Code Coverage / Test (windows-latest, 3.11) (push) Waiting to run
Code Coverage / Test (windows-latest, 3.12) (push) Waiting to run
Code Coverage / check (push) Blocked by required conditions
TypeCheck / TypeCheck (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
2025-04-24 03:07:04 +08:00
pre-commit-ci[bot]
f56dce6918 ⬆️ auto update by pre-commit hooks (#535)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.2 → v0.11.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.2...v0.11.6)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-04-24 03:01:28 +08:00
呵呵です
ff3eb79967 迁移到新模板 (#536)
Some checks are pending
Code Coverage / Test (macos-latest, 3.10) (push) Waiting to run
Code Coverage / Test (macos-latest, 3.11) (push) Waiting to run
Code Coverage / Test (macos-latest, 3.12) (push) Waiting to run
Code Coverage / Test (ubuntu-latest, 3.10) (push) Waiting to run
Code Coverage / Test (ubuntu-latest, 3.11) (push) Waiting to run
Code Coverage / Test (ubuntu-latest, 3.12) (push) Waiting to run
Code Coverage / Test (windows-latest, 3.10) (push) Waiting to run
Code Coverage / Test (windows-latest, 3.11) (push) Waiting to run
Code Coverage / Test (windows-latest, 3.12) (push) Waiting to run
Code Coverage / check (push) Blocked by required conditions
TypeCheck / TypeCheck (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
*  添加依赖 strenum

* 🐛 优化等待逻辑,修复截图爆炸

*  使用新模板

* ️ 关闭自动转译

*  同步新模板 schemas

* 🌐 添加模板语言的映射

*  适配 bind

*  更新模板

*  全部适配

* 🚨 make mypy happy

* Update nonebot_plugin_tetris_stats/games/tos/query.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

*  使用用户设置语言

* 🚨 auto fix by pre-commit hooks

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-04-23 19:25:50 +08:00
pre-commit-ci[bot]
0ac917f95e ⬆️ auto update by pre-commit hooks (#534)
Some checks failed
Code Coverage / Test (macos-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (macos-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (macos-latest, 3.12) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.12) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.12) (push) Has been cancelled
TypeCheck / TypeCheck (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Code Coverage / check (push) Has been cancelled
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.0 → v0.11.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.0...v0.11.2)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-03-28 16:35:48 +08:00
9806050e33 🔖 1.7.2
Some checks failed
Code Coverage / Test (macos-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (macos-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (macos-latest, 3.12) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.12) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.12) (push) Has been cancelled
TypeCheck / TypeCheck (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Code Coverage / check (push) Has been cancelled
2025-03-20 03:35:19 +08:00
461327749f 🐛 修爆炸 2025-03-20 03:34:34 +08:00
renovate[bot]
208582d313 ⬆️ Upgrade dependency prettier to v3.5.3 (#533)
Some checks are pending
Code Coverage / Test (macos-latest, 3.10) (push) Waiting to run
Code Coverage / Test (macos-latest, 3.11) (push) Waiting to run
Code Coverage / Test (macos-latest, 3.12) (push) Waiting to run
Code Coverage / Test (ubuntu-latest, 3.10) (push) Waiting to run
Code Coverage / Test (ubuntu-latest, 3.11) (push) Waiting to run
Code Coverage / Test (ubuntu-latest, 3.12) (push) Waiting to run
Code Coverage / Test (windows-latest, 3.10) (push) Waiting to run
Code Coverage / Test (windows-latest, 3.11) (push) Waiting to run
Code Coverage / Test (windows-latest, 3.12) (push) Waiting to run
Code Coverage / check (push) Blocked by required conditions
TypeCheck / TypeCheck (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-19 06:13:30 +08:00
pre-commit-ci[bot]
90f655259d ⬆️ auto update by pre-commit hooks (#532)
* ⬆️ auto update by pre-commit hooks

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

* 🚨 auto fix by pre-commit hooks

* 🚨 添加一个 noqa(

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: shoucandanghehe <wallfjjd@gmail.com>
2025-03-19 06:11:18 +08:00
renovate[bot]
2ef400ca28 ⬆️ Upgrade dependency prettier to v3.5.2 (#531)
Some checks failed
Code Coverage / Test (macos-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (macos-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (macos-latest, 3.12) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.12) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.12) (push) Has been cancelled
TypeCheck / TypeCheck (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Code Coverage / check (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-25 02:02:57 +08:00
renovate[bot]
616a64bd6a ⬆️ Upgrade dependency prettier to v3.5.1 (#530)
Some checks failed
Code Coverage / Test (macos-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (macos-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (macos-latest, 3.12) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.12) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.12) (push) Has been cancelled
TypeCheck / TypeCheck (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Code Coverage / check (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-14 02:40:56 +08:00
pre-commit-ci[bot]
bb8943d4c3 ⬆️ auto update by pre-commit hooks (#529)
Some checks failed
Code Coverage / Test (macos-latest, 3.10) (push) Waiting to run
Code Coverage / Test (macos-latest, 3.11) (push) Waiting to run
Code Coverage / Test (macos-latest, 3.12) (push) Waiting to run
Code Coverage / Test (ubuntu-latest, 3.10) (push) Waiting to run
Code Coverage / Test (ubuntu-latest, 3.11) (push) Waiting to run
Code Coverage / Test (ubuntu-latest, 3.12) (push) Waiting to run
Code Coverage / Test (windows-latest, 3.10) (push) Waiting to run
Code Coverage / Test (windows-latest, 3.11) (push) Waiting to run
Code Coverage / Test (windows-latest, 3.12) (push) Waiting to run
Code Coverage / check (push) Blocked by required conditions
TypeCheck / TypeCheck (push) Waiting to run
CodeQL / Analyze (python) (push) Has been cancelled
* ⬆️ auto update by pre-commit hooks

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

* 🚨 auto fix by pre-commit hooks

* ♻️ 重命名 typing 为 typedefs

* ♻️ 使用 Annotated 代替默认值

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: shoucandanghehe <wallfjjd@gmail.com>
2025-02-13 04:18:36 +08:00
5e45db8cf5 🔖 1.7.1
Some checks failed
Code Coverage / Test (macos-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (macos-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (macos-latest, 3.12) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.12) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.12) (push) Has been cancelled
TypeCheck / TypeCheck (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Code Coverage / check (push) Has been cancelled
2024-12-21 01:46:08 +08:00
renovate[bot]
2020deadac ⬆️ Upgrade astral-sh/setup-uv action to v5 (#527)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-20 17:43:24 +00:00
呵呵です
ce7bce6e20 🐛 修爆炸 (#528)
* 🐛 修爆炸

* 🐛 修复类型错误
2024-12-21 01:41:58 +08:00
pre-commit-ci[bot]
d4b690f682 ⬆️ auto update by pre-commit hooks (#524)
Some checks failed
Code Coverage / Test (macos-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (macos-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (macos-latest, 3.12) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.12) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.12) (push) Has been cancelled
TypeCheck / TypeCheck (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Code Coverage / check (push) Has been cancelled
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.8.1 → v0.8.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.1...v0.8.3)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-12-18 21:28:36 +00:00
c4bde71546 🔖 1.7.0 2024-12-19 05:26:15 +08:00
呵呵です
f56f993c69 使用随机/特殊 UA (#526)
*  添加依赖 fake-useragent

*  使用随机/特殊 UA
2024-12-19 05:24:48 +08:00
呵呵です
cfcda6f597 添加 unbind 指令 (#525)
*  添加依赖 nonebot-plugin-waiter

*  添加 unbind 指令
2024-12-19 03:59:22 +08:00
renovate[bot]
96f5d4559d ⬆️ Upgrade dependency prettier to v3.4.2 (#523)
Some checks failed
Code Coverage / Test (macos-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (macos-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (macos-latest, 3.12) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.12) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.12) (push) Has been cancelled
TypeCheck / TypeCheck (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Code Coverage / check (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-08 02:59:36 +08:00
renovate[bot]
23f412b4f4 ⬆️ Upgrade dependency prettier to v3.4.1 (#522)
Some checks failed
Code Coverage / Test (macos-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (macos-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (macos-latest, 3.12) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.12) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.12) (push) Has been cancelled
TypeCheck / TypeCheck (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Code Coverage / check (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-03 12:27:03 +00:00
renovate[bot]
25b0d2bcdc ⬆️ Upgrade astral-sh/setup-uv action to v4 (#521)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-03 12:25:13 +00:00
pre-commit-ci[bot]
a116f9901c ⬆️ auto update by pre-commit hooks (#520)
* ⬆️ auto update by pre-commit hooks

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

* 🚨 auto fix by pre-commit hooks

* 🚨 fix ruff

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: shoucandanghehe <wallfjjd@gmail.com>
2024-12-03 20:23:19 +08:00
renovate[bot]
82befd631e ⬆️ Upgrade codecov/codecov-action action to v5 (#519)
Some checks failed
Code Coverage / Test (macos-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (macos-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (macos-latest, 3.12) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.12) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.12) (push) Has been cancelled
TypeCheck / TypeCheck (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Code Coverage / check (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-15 12:50:50 +08:00
232389dd07 🔖 1.6.3
Some checks failed
Code Coverage / Test (macos-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (macos-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (macos-latest, 3.12) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (ubuntu-latest, 3.12) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.10) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.11) (push) Has been cancelled
Code Coverage / Test (windows-latest, 3.12) (push) Has been cancelled
TypeCheck / TypeCheck (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Code Coverage / check (push) Has been cancelled
2024-11-12 19:08:32 +08:00
呵呵です
ce81015406 🐛 修正 TETR.IO 的字段类型 (#518) 2024-11-12 19:07:34 +08:00
pre-commit-ci[bot]
3d7b903f59 ⬆️ auto update by pre-commit hooks (#517)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.7.2 → v0.7.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.7.2...v0.7.3)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-11-12 18:54:12 +08:00
pre-commit-ci[bot]
c5d499434e ⬆️ auto update by pre-commit hooks (#516)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.7.1 → v0.7.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.7.1...v0.7.2)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-11-05 17:53:14 +08:00
呵呵です
194fed24c9 🔧 配置 prettier (#515)
*  添加开发依赖 prettier

* 🎨 格式化
2024-11-01 01:45:56 +08:00
renovate[bot]
1173c39e7a ⬆️ Upgrade re-actors/alls-green digest to 223e4bb (#514)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-01 01:38:39 +08:00
呵呵です
dfb19f150a 添加单元测试 (#513)
*  添加测试依赖 nonebug

*  添加测试依赖 pytest-asyncio

* 🔧 配置 pytest

* 🙈 更新 .gitignore

*  添加 test 依赖 pytest-cov

* 🔧 配置 pytest

*  添加测试

* 👷 添加 Test CI

* 💚 暂时移除 3.13 的测试
2024-11-01 01:33:18 +08:00
呵呵です
7555297e1e 🐛 修正 S1ValidRank (#512) 2024-10-29 04:19:51 +00:00
pre-commit-ci[bot]
587aa4a0de ⬆️ auto update by pre-commit hooks (#511)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.7.0 → v0.7.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.7.0...v0.7.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-10-29 12:03:27 +08:00
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
102 changed files with 3926 additions and 1987 deletions

View File

@@ -3,7 +3,7 @@ name: Release CI
on:
push:
tags:
- "*"
- '*'
jobs:
release:
@@ -14,15 +14,15 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v3
- uses: astral-sh/setup-uv@v6
name: Setup UV
with:
enable-cache: true
- name: "Set up Python"
- name: 'Set up Python'
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"
python-version-file: '.python-version'
- run: uv sync
shell: bash

58
.github/workflows/Test.yml vendored Normal file
View File

@@ -0,0 +1,58 @@
name: Code Coverage
on:
push:
branches:
- 'main'
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
name: Test
runs-on: ${{ matrix.os }}
strategy:
matrix:
# python-version: ['3.10', '3.11', '3.12', '3.13']
python-version: ['3.10', '3.11', '3.12']
os: [ubuntu-latest, windows-latest, macos-latest]
fail-fast: false
env:
OS: ${{ matrix.os }}
PYTHON_VERSION: ${{ matrix.python-version }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup uv
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-suffix: ${{ env.PYTHON_VERSION }}_${{ env.OS }}
- name: Install Dependencies
run: |
uv python pin ${{ env.PYTHON_VERSION }}
uv sync --group test
- name: Run tests
run: uv run pytest --cov=nonebot_plugin_tetris_stats --cov-report xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
env_vars: OS,PYTHON_VERSION
check:
if: always()
needs: test
runs-on: ubuntu-latest
steps:
- name: Decide whether the needed jobs succeeded or failed
uses: re-actors/alls-green@223e4bb7a751b91f43eda76992bcfbf23b8b0302
with:
jobs: ${{ toJSON(needs) }}

View File

@@ -9,15 +9,15 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v3
- uses: astral-sh/setup-uv@v6
name: Setup UV
with:
enable-cache: true
- name: "Set up Python"
- name: 'Set up Python'
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"
python-version-file: '.python-version'
- run: uv sync
shell: bash

View File

@@ -9,14 +9,14 @@
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
name: 'CodeQL'
on:
push:
branches: [ main ]
branches: [main]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
branches: [main]
schedule:
- cron: '17 6 * * 5'
@@ -32,41 +32,40 @@ jobs:
strategy:
fail-fast: false
matrix:
language: [ 'python' ]
language: ['python']
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Checkout repository
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
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.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
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.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# 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@v3
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# 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
# 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@v3
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# 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
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3

546
.gitignore vendored
View File

@@ -1,24 +1,528 @@
.idea
# Created by https://www.toptal.com/developers/gitignore/api/linux,macos,python,pycharm,windows,visualstudiocode,node
# Edit at https://www.toptal.com/developers/gitignore?templates=linux,macos,python,pycharm,windows,visualstudiocode,node
### Linux ###
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### macOS Patch ###
# iCloud generated files
*.icloud
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
test_*
Untitled*
*copy*
.vscode
*dev*
*_cache*
*backup*
*.pyc
node_modules
.prettier*
package.json
pnpm-lock.yaml
*.drawio.svg
package-lock.json
*Zone.Identifier
.env*
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
### Node Patch ###
# Serverless Webpack directories
.webpack/
# Optional stylelint cache
# SvelteKit build / generate output
.svelte-kit
### PyCharm ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### PyCharm Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
# *.iml
# modules.xml
# .idea/misc.xml
# *.ipr
# Sonarlint plugin
# https://plugins.jetbrains.com/plugin/7973-sonarlint
.idea/**/sonarlint/
# SonarQube Plugin
# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
.idea/**/sonarIssues.xml
# Markdown Navigator plugin
# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
.idea/**/markdown-navigator.xml
.idea/**/markdown-navigator-enh.xml
.idea/**/markdown-navigator/
# Cache file creation bug
# See https://youtrack.jetbrains.com/issue/JBR-2257
.idea/$CACHE_FILE$
# CodeStream plugin
# https://plugins.jetbrains.com/plugin/12206-codestream
.idea/codestream.xml
# Azure Toolkit for IntelliJ plugin
# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
.idea/**/azureSettings.xml
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
### Python Patch ###
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
poetry.toml
# ruff
.ruff_cache/
# LSP config files
pyrightconfig.json
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
### Windows ###
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# End of https://www.toptal.com/developers/gitignore/api/linux,macos,python,pycharm,windows,visualstudiocode,node
# NoneBot2
bot.py
TODO
*.fish
extracted_skin_mino_*
sample_*
.env*
# Misc
ignore_*
*.backup
TODO*

View File

@@ -1,22 +1,22 @@
default_install_hook_types: [pre-commit, prepare-commit-msg]
ci:
autofix_commit_msg: ':rotating_light: auto fix by pre-commit hooks'
autofix_prs: true
autoupdate_branch: main
autoupdate_schedule: weekly
autoupdate_commit_msg: ':arrow_up: auto update by pre-commit hooks'
autofix_commit_msg: ':rotating_light: auto fix by pre-commit hooks'
autofix_prs: true
autoupdate_branch: main
autoupdate_schedule: weekly
autoupdate_commit_msg: ':arrow_up: auto update by pre-commit hooks'
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.7.0
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
stages: [pre-commit]
- id: ruff-format
stages: [pre-commit]
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.8
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
stages: [pre-commit]
- id: ruff-format
stages: [pre-commit]
- repo: https://github.com/nonebot/nonemoji
rev: v0.1.4
hooks:
- id: nonemoji
stages: [prepare-commit-msg]
- repo: https://github.com/nonebot/nonemoji
rev: v0.1.4
hooks:
- id: nonemoji
stages: [prepare-commit-msg]

View File

@@ -42,7 +42,7 @@ This project uses [Tarina](https://github.com/ArcletProject/Tarina) for internat
#### 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.
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

View File

@@ -24,7 +24,7 @@ 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
@@ -41,7 +41,7 @@ mypy ./nonebot_plugin_tetris_stats/
#### 添加新的语言
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) 的规范。
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` 文件。
#### 更新已有语言

View File

@@ -10,6 +10,7 @@ require_plugins = {
'nonebot_plugin_session',
'nonebot_plugin_user',
'nonebot_plugin_userinfo',
'nonebot_plugin_waiter',
}
for i in require_plugins:

View File

@@ -18,6 +18,7 @@ class ScopedConfig(BaseModel):
request_timeout: float = 30.0
screenshot_quality: float = 2
proxy: Proxy = Field(default_factory=Proxy)
development: bool = False
class Config(BaseModel):

View File

@@ -11,7 +11,7 @@ from nonebot_plugin_orm import AsyncSession, get_session
from nonebot_plugin_user import User
from sqlalchemy import select
from ..utils.typing import AllCommandType, BaseCommandType, GameType, TETRIOCommandType
from ..utils.typedefs import AllCommandType, BaseCommandType, GameType, TETRIOCommandType
from .models import Bind, TriggerHistoricalData
UTC = timezone.utc
@@ -63,6 +63,23 @@ async def create_or_update_bind(
return status
async def remove_bind(
session: AsyncSession,
user: User,
game_platform: GameType,
) -> bool:
bind = await query_bind_info(
session=session,
user=user,
game_platform=game_platform,
)
if bind is not None:
await session.delete(bind)
await session.commit()
return True
return False
T = TypeVar('T', 'TETRIOHistoricalData', 'TOPHistoricalData', 'TOSHistoricalData')
lock = Lock()

View File

@@ -9,7 +9,7 @@ from sqlalchemy import JSON, DateTime, Dialect, String, TypeDecorator
from sqlalchemy.orm import Mapped, MappedAsDataclass, mapped_column
from typing_extensions import override
from ..utils.typing import AllCommandType, GameType
from ..utils.typedefs import AllCommandType, GameType
class PydanticType(TypeDecorator):

View File

@@ -44,7 +44,7 @@ async def _(matcher: Matcher, account: MessageFormatError):
@alc.handle()
async def _(matcher: Matcher, matches: AlcMatches):
if matches.head_matched and matches.options != {} or matches.main_args == {}:
if (matches.head_matched and matches.options != {}) or matches.main_args == {}:
await matcher.finish(
(f'{matches.error_info!r}\n' if matches.error_info is not None else '')
+ f'输入"{matches.header_result} --help"查看帮助'

View File

@@ -3,7 +3,7 @@ from typing import Generic, TypeVar
from pydantic import BaseModel
from ..utils.typing import GameType
from ..utils.typedefs import GameType
T = TypeVar('T', bound=GameType)

View File

@@ -23,7 +23,7 @@ command = Subcommand(
)
from . import bind, config, list, query, rank, record # noqa: E402
from . import bind, config, list, query, rank, record, unbind # noqa: A004, E402
main_command.add(command)
@@ -35,4 +35,5 @@ __all__ = [
'query',
'rank',
'record',
'unbind',
]

View File

@@ -1,10 +1,12 @@
from typing import Literal, overload
from uuid import UUID
from nonebot import __version__ as __nonebot_version__
from nonebot.compat import type_validate_json
from yarl import URL
from ....utils.exception import RequestError
from ....version import __version__
from ..constant import BASE_URL
from .cache import Cache
from .schemas.base import FailedModel
@@ -22,7 +24,12 @@ async def by(
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,
{
'X-Session-ID': str(x_session_id),
'User-Agent': f'nonebot-plugin-tetris-stats/{__version__} (Windows NT 10.0; Win64; x64) NoneBot2/{__nonebot_version__}',
}
if x_session_id is not None
else None,
),
)
if isinstance(model, FailedModel):

View File

@@ -7,7 +7,7 @@ from sqlalchemy.orm import Mapped, MappedAsDataclass, mapped_column
from ....db.models import PydanticType
from .schemas.base import SuccessModel
from .typing import Records, Summaries
from .typedefs import Records, Summaries
class TETRIOHistoricalData(MappedAsDataclass, Model):

View File

@@ -27,7 +27,7 @@ from .schemas.summaries.base import User as SummariesUser
from .schemas.summaries.league import LeagueSuccessModel
from .schemas.user import User
from .schemas.user_info import UserInfo, UserInfoSuccess
from .typing import Records, Summaries
from .typedefs import Records, Summaries
class RecordModeType(str, Enum):
@@ -46,7 +46,7 @@ class RecordKey(NamedTuple):
record_type: RecordType
def to_records(self) -> Records:
return cast(Records, f'{self.mode_type.value}_{self.record_type.value}')
return cast('Records', f'{self.mode_type.value}_{self.record_type.value}')
class Player:
@@ -89,7 +89,7 @@ class Player:
@property
def _request_user_parameter(self) -> str:
return self.user_id or cast(str, self.user_name).lower()
return self.user_id or cast('str', self.user_name).lower()
@property
async def user(self) -> User:

View File

@@ -3,7 +3,7 @@ from typing import Literal
from pydantic import BaseModel, Field
from ...typing import Prisecter
from ...typedefs import Prisecter
class AggregateStats(BaseModel):

View File

@@ -12,11 +12,11 @@ class Time(BaseModel):
zero: bool
locked: bool
prev: int
frameoffset: int
frameoffset: int | None = None
class Stats(BaseModel):
seed: int | None = None # ?: 不知道是之后都没有了还是还会有
seed: float | None = None # ?: 不知道是之后都没有了还是还会有
lines: int
level_lines: int
level_lines_needed: int
@@ -24,8 +24,8 @@ class Stats(BaseModel):
holds: int = 0
time: Time | None = None # ?: 不知道是之后都没有了还是还会有
score: int
zenlevel: int
zenprogress: int
zenlevel: int | None = None
zenprogress: int | None = None
level: int
combo: int
currentcombopower: int | None = None

View File

@@ -1,6 +1,6 @@
from datetime import datetime
from enum import IntEnum
from typing import NamedTuple
from typing import Literal, NamedTuple
from pydantic import BaseModel, Field
@@ -28,11 +28,16 @@ class Point(NamedTuple):
class Data(BaseModel):
start_time: datetime = Field(..., alias='startTime')
points: list[Point]
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
data: Data | Empty
LeagueFlow = LeagueFlowSuccess | FailedModel

View File

@@ -3,7 +3,7 @@ from typing import Any
from nonebot.compat import PYDANTIC_V2
from pydantic import BaseModel, Field
from ...typing import Prisecter
from ...typedefs import Prisecter
class Parameter(BaseModel):

View File

@@ -3,11 +3,11 @@ from typing import Literal
from pydantic import BaseModel, Field
from ...typing import Rank, ValidRank
from ...typedefs import Rank, ValidRank
from ..base import ArCounts, FailedModel, P, SuccessModel
class League(BaseModel):
class BaseLeague(BaseModel):
gamesplayed: int
gameswon: int
tr: float
@@ -16,13 +16,22 @@ class League(BaseModel):
bestrank: ValidRank
glicko: float
rd: float
apm: float
pps: float
vs: float
decaying: bool
class Entry(BaseModel):
class InvalidLeague(BaseLeague):
pps: float | None
apm: None
vs: None
class League(BaseLeague):
pps: float
apm: float
vs: float
class BaseEntry(BaseModel):
id: str = Field(..., alias='_id')
username: str
role: Literal['anon', 'user', 'bot', 'halfmod', 'mod', 'admin', 'sysop']
@@ -30,7 +39,6 @@ class Entry(BaseModel):
xp: float
country: str | None = None
supporter: bool | None = None
league: League
gamesplayed: int
gameswon: int
gametime: float
@@ -39,8 +47,16 @@ class Entry(BaseModel):
p: P
class InvalidEntry(BaseEntry):
league: InvalidLeague
class Entry(BaseEntry):
league: League
class Data(BaseModel):
entries: list[Entry]
entries: list[Entry | InvalidEntry]
class BySuccessModel(SuccessModel):

View File

@@ -14,8 +14,8 @@ __all__ = [
'SoloSuccessModel',
'SummariesModel',
'Zen',
'ZenSuccessModel',
'Zenith',
'ZenithEx',
'ZenithSuccessModel',
'ZenSuccessModel',
]

View File

@@ -1,25 +1,93 @@
from typing import TypeAlias
from datetime import datetime
from enum import IntEnum
from typing import Literal, TypeAlias
from pydantic import BaseModel
from pydantic import BaseModel, Field
from ..base import FailedModel, SuccessModel
class RankType(IntEnum):
PERCENTILE = 1
ISSUE = 2
ZENITH = 3
PERCENTILELAX = 4
PERCENTILEVLAX = 5
PERCENTILEMLAX = 6
class ValueType(IntEnum):
NONE = 0
NUMBER = 1
TIME = 2
TIME_INV = 3
FLOOR = 4
ISSUE = 5
NUMBER_INV = 6
class ArType(IntEnum):
UNRANKED = 0
RANKED = 1
COMPETITIVE = 2
class Rank(IntEnum):
NONE = 0
BRONZE = 1
SILVER = 2
GOLD = 3
PLATINUM = 4
DIAMOND = 5
ISSUED = 100
class Ally(BaseModel):
id: str = Field(alias='_id')
username: str
role: Literal['anon', 'user', 'bot', 'halfmod', 'mod', 'admin', 'sysop', 'hidden', 'banned']
country: str | None = None
supporter: bool
avatar_revision: int | None = None
class X(BaseModel):
ally: Ally | None = None
class Achievement(BaseModel):
# 这**都是些啥
k: int
o: int
rt: int
vt: int
achievement_id: int = Field(alias='k')
category: str
primary_name: str = Field(alias='name')
objective: str = Field(alias='object')
flavor_text: str = Field(alias='desc')
order: int = Field(alias='o')
rank_type: RankType = Field(alias='rt')
value_type: ValueType = Field(alias='vt')
ar_type: ArType = Field(alias='art')
min: int
deci: int
name: str
object: str
category: str
hidden: bool
desc: str
nolb: bool
event: str | None = None
event_past: bool | None = None
disabled: bool | None = None
pair: bool | None = None
achieved_score: float | None = Field(None, alias='v')
a: float | None = None
t: datetime | None = None
pos: int | None = None
total: int | None = None
rank: Rank | None = None
x: X | None = None
n: str
stub: bool
tiebreak: int
notifypb: bool
id: str | None = Field(None, alias='_id')
progress: float | None = None
stub: bool | None = None
class AchievementsSuccessModel(SuccessModel):

View File

@@ -3,7 +3,7 @@ from typing import Literal
from nonebot.compat import PYDANTIC_V2
from pydantic import BaseModel, Field
from ...typing import Rank, S1Rank, S1ValidRank
from ...typedefs import Rank, S1Rank, S1ValidRank
from ..base import SuccessModel
if PYDANTIC_V2:
@@ -122,5 +122,9 @@ class RatedData(BaseData):
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: NeverPlayedData | NeverRatedData | RatedData
data: NeverPlayedData | NeverRatedData | RatedData | InvalidData

View File

@@ -86,7 +86,7 @@ class Record(BaseModel):
pb: bool
oncepb: bool
ts: datetime
revolution: None
revolution: str | None
user: User
otherusers: list
leaderboards: list[str]
@@ -97,7 +97,7 @@ class Record(BaseModel):
class Best(BaseModel):
record: None # WTF
record: Record | None
rank: int

View File

@@ -1,7 +1,6 @@
from typing import Literal, NewType
S1ValidRank = Literal[
'x+',
'x',
'u',
'ss',

View File

@@ -13,6 +13,7 @@ from yarl import URL
from ...db import BindStatus, create_or_update_bind, trigger
from ...utils.host import HostPage, get_self_netloc
from ...utils.image import get_avatar
from ...utils.lang import get_lang
from ...utils.render import Bind, render
from ...utils.render.schemas.base import Avatar, People
from ...utils.screenshot import screenshot
@@ -65,7 +66,7 @@ async def _(nb_user: User, account: Player, event_session: EventSession, bot_inf
'v1/binding',
Bind(
platform='TETR.IO',
status='unknown',
type='unknown',
user=People(
avatar=str(
URL(f'http://{netloc}/host/resource/tetrio/avatars/{user.ID}')
@@ -79,7 +80,8 @@ async def _(nb_user: User, account: Player, event_session: EventSession, bot_inf
avatar=await get_avatar(bot_info, 'Data URI', '../../static/logo/logo.svg'),
name=bot_info.user_name,
),
command='io查我',
prompt='io查我',
lang=get_lang(),
),
)
) as page_hash:

View File

@@ -11,7 +11,7 @@ from ...db import trigger
from . import alc, command
from .constant import GAME_TYPE
from .models import TETRIOUserConfig
from .typing import Template
from .typedefs import Template
command.add(
Subcommand(

View File

@@ -1,9 +1,9 @@
from re import compile
from re import compile # noqa: A004
from typing import Literal
from yarl import URL
from .api.typing import ValidRank
from .api.typedefs import ValidRank
GAME_TYPE: Literal['IO'] = 'IO'

View File

@@ -5,15 +5,17 @@ from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[im
from ...db import trigger
from ...utils.host import HostPage, get_self_netloc
from ...utils.lang import get_lang
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.render.schemas.v2.tetrio.user.list import Data, 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 .api.schemas.leaderboards.by import Entry
from .constant import GAME_TYPE
command.add(
@@ -62,12 +64,15 @@ async def _(
'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,
data=[
Data(
user=User(
id=i.id,
name=i.username.upper(),
avatar=f'https://tetr.io/user-content/avatars/{i.id}.jpg',
country=i.country,
xp=i.xp,
),
tetra_league=TetraLeague(
rank=i.league.rank,
tr=round(i.league.tr, 2),
@@ -80,11 +85,11 @@ async def _(
vs=metrics.vs,
adpl=metrics.adpl,
),
xp=i.xp,
join_at=None,
)
for i in league.data.entries
if isinstance(i, Entry)
],
lang=get_lang(),
),
)
) as page_hash:

View File

@@ -7,8 +7,8 @@ from sqlalchemy.orm import Mapped, MappedAsDataclass, mapped_column, relationshi
from ...db.models import PydanticType
from .api.schemas.leaderboards.by import BySuccessModel, Entry
from .api.typing import ValidRank
from .typing import Template
from .api.typedefs import ValidRank
from .typedefs import Template
class TETRIOUserConfig(MappedAsDataclass, Model):

View File

@@ -16,13 +16,13 @@ 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 ....utils.typedefs 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 ..typedefs import Template
from .v1 import make_query_image_v1
from .v2 import make_query_image_v2

View File

@@ -4,20 +4,22 @@ 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 LeagueFlowSuccess
from ..api.schemas.summaries.league import LeagueSuccessModel, NeverPlayedData, NeverRatedData, RatedData
from ....utils.render.schemas.base import HistoryData
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]:
handle: Callable[[list[HistoryData]], list[HistoryData]] | None = None,
) -> list[HistoryData]:
if isinstance(leagueflow.data, Empty):
raise FallbackError
start_time = leagueflow.data.start_time.astimezone(ZoneInfo('Asia/Shanghai'))
ret = [
TetraLeagueHistoryData(
HistoryData(
record_at=start_time + timedelta(milliseconds=i.timestamp_offset),
tr=i.post_match_tr,
score=i.post_match_tr,
)
for i in leagueflow.data.points
if start_time + timedelta(milliseconds=i.timestamp_offset)
@@ -45,6 +47,8 @@ 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):

View File

@@ -1,17 +1,18 @@
from asyncio import gather
from datetime import datetime, timedelta
from datetime import timedelta
from hashlib import md5
from math import ceil, floor
from zoneinfo import ZoneInfo
from yarl import URL
from ....utils.chart import get_split, get_value_bounds, handle_history_data
from ....utils.exception import FallbackError
from ....utils.host import HostPage, get_self_netloc
from ....utils.lang import get_lang
from ....utils.metrics import get_metrics
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.render.schemas.base import Avatar, Trending
from ....utils.render.schemas.v1.base import History
from ....utils.render.schemas.v1.tetrio.user.info import Info, Multiplayer, Singleplayer, User
from ....utils.screenshot import screenshot
from ..api import Player
from ..api.schemas.summaries.league import RatedData
@@ -19,84 +20,6 @@ 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]:
data.sort(key=lambda x: x.record_at)
right_border = datetime.now(ZoneInfo('Asia/Shanghai')).replace(hour=0, minute=0, second=0, microsecond=0)
left_border = right_border - timedelta(days=9)
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:
ret.append(get_specified_point(lefts[-1], in_border[0], left_border))
else:
ret.append(TetraLeagueHistoryData(tr=in_border[0].tr, record_at=left_border))
ret.extend(in_border)
if rights:
ret.append(get_specified_point(in_border[-1], rights[0], right_border.replace(microsecond=1000)))
else:
ret.append(TetraLeagueHistoryData(tr=in_border[-1].tr, record_at=right_border.replace(microsecond=1000)))
return ret
async def make_query_image_v1(player: Player) -> bytes:
(
(user, user_info, league, sprint, blitz, leagueflow),
@@ -109,8 +32,8 @@ async def make_query_image_v1(player: Player) -> bytes:
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)
values = get_value_bounds([i.score for i in histories])
split_value, offset = get_split(values, TR_MAX, TR_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
@@ -118,6 +41,9 @@ async def make_query_image_v1(player: Player) -> bytes:
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()
dsps: float
dspp: float
# make mypy happy
async with HostPage(
page=await render(
'v1/tetrio/info',
@@ -134,38 +60,40 @@ async def make_query_image_v1(player: Player) -> bytes:
name=user.name.upper(),
bio=user_info.data.bio,
),
ranking=Ranking(
rating=round(league_data.glicko, 2),
multiplayer=Multiplayer(
glicko=f'{round(league_data.glicko, 2):,}',
rd=round(league_data.rd, 2),
),
tetra_league=TetraLeague(
rank=league_data.rank,
tr=round(league_data.tr, 2),
tr=f'{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(
history=History(
data=histories,
split_interval=split_value,
min_value=values.value_min,
max_value=values.value_max,
offset=offset,
),
lpm=(metrics := get_metrics(pps=league_data.pps, apm=league_data.apm, vs=league_data.vs)).lpm,
pps=metrics.pps,
lpm_trending=Trending.KEEP,
apm=metrics.apm,
apl=metrics.apl,
apm_trending=Trending.KEEP,
adpm=metrics.adpm,
vs=metrics.vs,
adpl=metrics.adpl,
adpm_trending=Trending.KEEP,
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,
singleplayer=Singleplayer(
sprint=sprint_value,
blitz=blitz_value,
),
lang=get_lang(),
),
)
) as page_hash:

View File

@@ -4,12 +4,16 @@ from hashlib import md5
from yarl import URL
from ....utils.exception import FallbackError
from ....utils.host import HostPage, get_self_netloc
from ....utils.lang import get_lang
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 (
from ....utils.render.schemas.v2.tetrio.user.info import (
Achievement,
Badge,
Best,
Blitz,
Info,
Sprint,
@@ -17,21 +21,37 @@ from ....utils.render.schemas.tetrio.user.info_v2 import (
TetraLeague,
TetraLeagueStatistic,
User,
Week,
Zen,
Zenith,
)
from ....utils.screenshot import screenshot
from ..api import Player
from ..api.schemas.summaries.league import NeverPlayedData, NeverRatedData
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),
(avatar_revision, banner_revision, leagueflow, zenith, zenithex, achievements),
) = 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()),
gather(
player.user,
player.get_info(),
player.league,
player.sprint,
player.blitz,
player.zen,
),
gather(
player.avatar_revision,
player.banner_revision,
player.get_leagueflow(),
player.get_summaries('zenith'),
player.get_summaries('zenithex'),
player.get_summaries('achievements'),
),
)
if sprint.data.record is not None:
duration = timedelta(milliseconds=sprint.data.record.results.stats.finaltime).total_seconds()
@@ -42,96 +62,149 @@ async def make_query_image_v2(player: Player) -> bytes:
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'
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'
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,
async with HostPage(
await render(
'v2/tetrio/user/info',
Info(
user=User(
id=user.ID,
name=user.name.upper(),
country=user_info.data.country,
role=user_info.data.role,
botmaster=user_info.data.botmaster,
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
),
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=flow_to_history(leagueflow, lambda x: x[-100:]),
banner=str(
URL(f'http://{netloc}/host/resource/tetrio/banners/{user.ID}') % {'revision': banner_revision}
)
if not isinstance(league.data, NeverPlayedData)
if banner_revision is not None and banner_revision != 0
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),
bio=user_info.data.bio,
friend_count=user_info.data.friend_count,
supporter_tier=user_info.data.supporter_tier,
bad_standing=user_info.data.badstanding or False,
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
],
xp=user_info.data.xp,
ar=user_info.data.ar,
achievements=[
Achievement(
key=i.achievement_id,
rank_type=i.rank_type,
ar_type=i.ar_type,
stub=i.stub,
rank=i.rank,
achieved_score=i.achieved_score,
pos=i.pos,
progress=i.progress,
total=i.total,
)
for i in achievements.data
],
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,
zenith=Zenith(
week=Week(
altitude=zenith.data.record.results.stats.zenith.altitude,
global_rank=zenith.data.rank,
country_rank=zenith.data.rank_local,
play_at=zenith.data.record.ts,
)
if zenith.data.record is not None
else None,
best=Best(
altitude=zenith.data.best.record.results.stats.zenith.altitude,
global_rank=zenith.data.best.rank,
play_at=zenith.data.best.record.ts,
)
if zenith.data.best.record is not None
else None,
),
zenithex=Zenith(
week=Week(
altitude=zenithex.data.record.results.stats.zenith.altitude,
global_rank=zenithex.data.rank,
country_rank=zenithex.data.rank_local,
play_at=zenithex.data.record.ts,
)
if zenithex.data.record is not None
else None,
best=Best(
altitude=zenithex.data.best.record.results.stats.zenith.altitude,
global_rank=zenithex.data.best.rank,
play_at=zenithex.data.best.record.ts,
)
if zenithex.data.best.record is not None
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,
country_rank=sprint.data.rank_local,
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,
country_rank=blitz.data.rank_local,
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),
lang=get_lang(),
),
) as page_hash
):
),
) as page_hash:
return await screenshot(f'http://{netloc}/host/{page_hash}.html')

View File

@@ -12,6 +12,7 @@ from nonebot_plugin_apscheduler import scheduler
from nonebot_plugin_orm import get_session
from sqlalchemy import select
from ....config.config import config
from ....utils.exception import RequestError
from ....utils.retry import retry
from .. import alc
@@ -25,7 +26,7 @@ from ..models import TETRIOLeagueHistorical, TETRIOLeagueStats, TETRIOLeagueStat
if TYPE_CHECKING:
from ..api.schemas.leaderboards.by import BySuccessModel
from ..api.typing import Rank
from ..api.typedefs import Rank
UTC = timezone.utc
@@ -95,7 +96,7 @@ async def get_tetra_league_data() -> None:
players: list[Entry] = []
for result in results:
players.extend(result.data.entries)
players.extend([i for i in result.data.entries if isinstance(i, Entry)])
players.sort(key=lambda x: x.league.tr, reverse=True)
rank_player_mapping: defaultdict[Rank, list[Entry]] = defaultdict(list)
@@ -136,17 +137,19 @@ async def get_tetra_league_data() -> None:
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()
if not config.tetris.development:
@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
from . import all, detail # noqa: A004, E402
base_command.add(command)

View File

@@ -10,18 +10,19 @@ from sqlalchemy.orm import selectinload
from ....db import trigger
from ....utils.host import HostPage, get_self_netloc
from ....utils.lang import get_lang
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.render.schemas.v1.tetrio.rank import Data as DataV1
from ....utils.render.schemas.v1.tetrio.rank import ItemData as ItemDataV1
from ....utils.render.schemas.v2.tetrio.rank import AverageData as AverageDataV2
from ....utils.render.schemas.v2.tetrio.rank import Data as DataV2
from ....utils.render.schemas.v2.tetrio.rank 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 ..typedefs import Template
from . import command
command.add(
@@ -82,6 +83,7 @@ async def make_image_v1(latest_data: TETRIOLeagueStats, compare_data: TETRIOLeag
for i in zip(latest_data.fields, compare_data.fields, strict=True)
},
updated_at=latest_data.update_time,
lang=get_lang(),
),
)
) as page_hash:
@@ -109,6 +111,7 @@ async def make_image_v2(latest_data: TETRIOLeagueStats, compare_data: TETRIOLeag
for i in zip(latest_data.fields, compare_data.fields, strict=True)
},
updated_at=latest_data.update_time,
lang=get_lang(),
),
)
) as page_hash:

View File

@@ -12,12 +12,13 @@ from sqlalchemy.orm import selectinload
from ....db import trigger
from ....utils.host import HostPage, get_self_netloc
from ....utils.lang import get_lang
from ....utils.metrics import get_metrics
from ....utils.render import render
from ....utils.render.schemas.tetrio.rank.detail import Data, SpecialData
from ....utils.render.schemas.v2.tetrio.rank.detail import Data, SpecialData
from ....utils.screenshot import screenshot
from .. import alc
from ..api.typing import ValidRank
from ..api.typedefs import ValidRank
from ..constant import GAME_TYPE
from ..models import TETRIOLeagueStats
from . import command
@@ -122,6 +123,7 @@ async def make_image(rank: ValidRank, latest: TETRIOLeagueStats, compare: TETRIO
vs_holder=latest_data.high_vs.username.upper(),
),
updated_at=latest.update_time.replace(tzinfo=UTC).astimezone(ZoneInfo('Asia/Shanghai')),
lang=get_lang(),
),
)
) as page_hash:

View File

@@ -1,7 +1,7 @@
from arclet.alconna import Arg, ArgFlag
from nonebot_plugin_alconna import Args, At, Subcommand
from ....utils.typing import Me
from ....utils.typedefs import Me
from .. import command as base_command
from .. import get_player

View File

@@ -16,13 +16,14 @@ 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.lang import get_lang
from ....utils.metrics import get_metrics
from ....utils.render import render
from ....utils.render.schemas.base import Avatar
from ....utils.render.schemas.tetrio.record.base import Finesse, Max, Mini, Tspins, User
from ....utils.render.schemas.tetrio.record.blitz import Record, Statistic
from ....utils.render.schemas.v2.tetrio.record.base import Finesse, Max, Mini, Tspins, User
from ....utils.render.schemas.v2.tetrio.record.blitz import Record, Statistic
from ....utils.screenshot import screenshot
from ....utils.typing import Me
from ....utils.typedefs import Me
from .. import alc
from ..api.player import Player
from ..constant import GAME_TYPE
@@ -145,6 +146,7 @@ async def make_blitz_image(player: Player) -> bytes:
level=stats.level,
),
play_at=blitz.data.record.ts,
lang=get_lang(),
),
)
) as page_hash:

View File

@@ -16,13 +16,14 @@ 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.lang import get_lang
from ....utils.metrics import get_metrics
from ....utils.render import render
from ....utils.render.schemas.base import Avatar
from ....utils.render.schemas.tetrio.record.base import Finesse, Max, Mini, Statistic, Tspins, User
from ....utils.render.schemas.tetrio.record.sprint import Record
from ....utils.render.schemas.v2.tetrio.record.base import Finesse, Max, Mini, Statistic, Tspins, User
from ....utils.render.schemas.v2.tetrio.record.sprint import Record
from ....utils.screenshot import screenshot
from ....utils.typing import Me
from ....utils.typedefs import Me
from .. import alc
from ..api.player import Player
from ..constant import GAME_TYPE
@@ -90,7 +91,7 @@ async def make_sprint_image(player: Player) -> bytes:
netloc = get_self_netloc()
async with HostPage(
page=await render(
'v2/tetrio/record/40l',
'v2/tetrio/record/sprint',
Record(
type='best',
user=User(
@@ -145,6 +146,7 @@ async def make_sprint_image(player: Player) -> bytes:
),
),
play_at=sprint.data.record.ts,
lang=get_lang(),
),
)
) as page_hash:

View File

@@ -0,0 +1,77 @@
from hashlib import md5
from nonebot_plugin_alconna import 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
from nonebot_plugin_userinfo import BotUserInfo, UserInfo
from nonebot_plugin_waiter import suggest # type: ignore[import-untyped]
from yarl import URL
from ...db import query_bind_info, remove_bind, trigger
from ...utils.host import HostPage, get_self_netloc
from ...utils.image import get_avatar
from ...utils.lang import get_lang
from ...utils.render import Bind, render
from ...utils.render.schemas.base import Avatar, People
from ...utils.screenshot import screenshot
from . import alc, command
from .api import Player
from .constant import GAME_TYPE
command.add(Subcommand('unbind', help_text='解除绑定 TETR.IO 账号'))
alc.shortcut(
'(?i:io)(?i:解除绑定|解绑|unbind)',
command='tstats TETR.IO unbind',
humanized='io解绑',
)
@alc.assign('TETRIO.unbind')
async def _(nb_user: User, event_session: EventSession, bot_info: UserInfo = BotUserInfo()): # noqa: B008
async with (
trigger(
session_persist_id=await get_session_persist_id(event_session),
game_platform=GAME_TYPE,
command_type='unbind',
command_args=[],
),
get_session() as session,
):
if (bind := await query_bind_info(session=session, user=nb_user, game_platform=GAME_TYPE)) is None:
await UniMessage('您还未绑定 TETR.IO 账号').finish()
resp = await suggest('您确定要解绑吗?', ['', ''])
if resp is None or resp.extract_plain_text() == '':
return
player = Player(user_id=bind.game_account, trust=True)
user = await player.user
netloc = get_self_netloc()
async with HostPage(
await render(
'v1/binding',
Bind(
platform='TETR.IO',
type='unlink',
user=People(
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', hash=md5(user.ID.encode()).hexdigest()), # noqa: S324
name=user.name.upper(),
),
bot=People(
avatar=await get_avatar(bot_info, 'Data URI', '../../static/logo/logo.svg'),
name=bot_info.user_name,
),
prompt='io绑定{游戏ID}',
lang=get_lang(),
),
)
) as page_hash:
await UniMessage.image(raw=await screenshot(f'http://{netloc}/host/{page_hash}.html')).send()
await remove_bind(session=session, user=nb_user, game_platform=GAME_TYPE)

View File

@@ -2,7 +2,7 @@ from arclet.alconna import Arg, ArgFlag
from nonebot_plugin_alconna import Args, At, Subcommand
from ...utils.exception import MessageFormatError
from ...utils.typing import Me
from ...utils.typedefs import Me
from .. import add_block_handlers, alc, command
from .api import Player
from .constant import USER_NAME
@@ -29,6 +29,10 @@ command.add(
),
help_text='绑定 TOP 账号',
),
Subcommand(
'unbind',
help_text='解除绑定 TOP 账号',
),
Subcommand(
'query',
Args(
@@ -51,9 +55,22 @@ command.add(
)
)
alc.shortcut('(?i:top)(?i:绑定|绑|bind)', {'command': 'tstats TOP bind', 'humanized': 'top绑定'})
alc.shortcut('(?i:top)(?i:查询|查|query|stats)', {'command': 'tstats TOP query', 'humanized': 'top查'})
alc.shortcut(
'(?i:top)(?i:绑定|绑|bind)',
command='tstats TOP bind',
humanized='top绑定',
)
alc.shortcut(
'(?i:top)(?i:解除绑定|解绑|unbind)',
command='tstats TOP unbind',
humanized='top解绑',
)
alc.shortcut(
'(?i:top)(?i:查询|查|query|stats)',
command='tstats TOP query',
humanized='top查',
)
add_block_handlers(alc.assign('TOP.query'))
from . import bind, query # noqa: E402, F401
from . import bind, query, unbind # noqa: E402, F401

View File

@@ -8,6 +8,7 @@ from nonebot_plugin_userinfo import BotUserInfo, EventUserInfo, UserInfo
from ...db import BindStatus, create_or_update_bind, trigger
from ...utils.host import HostPage, get_self_netloc
from ...utils.image import get_avatar
from ...utils.lang import get_lang
from ...utils.render import Bind, render
from ...utils.render.schemas.base import People
from ...utils.screenshot import screenshot
@@ -44,7 +45,7 @@ async def _(
'v1/binding',
Bind(
platform=GAME_TYPE,
status='unknown',
type='unknown',
user=People(
avatar=await get_avatar(event_user_info, 'Data URI', None),
name=user.user_name,
@@ -53,7 +54,8 @@ async def _(
avatar=await get_avatar(bot_info, 'Data URI', '../../static/logo/logo.svg'),
name=bot_info.user_name,
),
command='top查我',
prompt='top查我',
lang=get_lang(),
),
)
) as page_hash:

View File

@@ -1,4 +1,4 @@
from re import compile
from re import compile # noqa: A004
from typing import Literal
from yarl import URL

View File

@@ -11,14 +11,15 @@ 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.lang import get_lang
from ...utils.metrics import TetrisMetricsBasicWithLPM, get_metrics
from ...utils.render import render
from ...utils.render.avatar import get_avatar
from ...utils.render.schemas.base import People
from ...utils.render.schemas.top_info import Data as InfoData
from ...utils.render.schemas.top_info import Info
from ...utils.render.schemas.base import People, Trending
from ...utils.render.schemas.v1.top.info import Data as InfoData
from ...utils.render.schemas.v1.top.info import Info
from ...utils.screenshot import screenshot
from ...utils.typing import Me
from ...utils.typedefs import Me
from . import alc
from .api import Player
from .api.schemas.user_profile import Data, UserProfile
@@ -79,8 +80,23 @@ async def make_query_image(profile: UserProfile) -> bytes:
'v1/top/info',
Info(
user=People(avatar=get_avatar(profile.user_name), name=profile.user_name),
today=InfoData(pps=today.pps, lpm=today.lpm, apm=today.apm, apl=today.apl),
history=InfoData(pps=history.pps, lpm=history.lpm, apm=history.apm, apl=history.apl),
today=InfoData(
pps=today.pps,
lpm=today.lpm,
lpm_trending=Trending.KEEP,
apm=today.apm,
apl=today.apl,
apm_trending=Trending.KEEP,
),
historical=InfoData(
pps=history.pps,
lpm=history.lpm,
lpm_trending=Trending.KEEP,
apm=history.apm,
apl=history.apl,
apm_trending=Trending.KEEP,
),
lang=get_lang(),
),
)
) as page_hash:

View File

@@ -0,0 +1,65 @@
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
from nonebot_plugin_userinfo import BotUserInfo, EventUserInfo, UserInfo
from nonebot_plugin_waiter import suggest # type: ignore[import-untyped]
from ...db import query_bind_info, remove_bind, trigger
from ...utils.host import HostPage, get_self_netloc
from ...utils.image import get_avatar
from ...utils.lang import get_lang
from ...utils.render import Bind, render
from ...utils.render.schemas.base import People
from ...utils.screenshot import screenshot
from . import alc
from .api import Player
from .constant import GAME_TYPE
@alc.assign('TOP.unbind')
async def _(
nb_user: User,
event_session: EventSession,
event_user_info: UserInfo = EventUserInfo(), # noqa: B008
bot_info: UserInfo = BotUserInfo(), # noqa: B008
):
async with (
trigger(
session_persist_id=await get_session_persist_id(event_session),
game_platform=GAME_TYPE,
command_type='unbind',
command_args=[],
),
get_session() as session,
):
if (bind := await query_bind_info(session=session, user=nb_user, game_platform=GAME_TYPE)) is None:
await UniMessage('您还未绑定 TOP 账号').finish()
resp = await suggest('您确定要解绑吗?', ['', ''])
if resp is None or resp.extract_plain_text() == '':
return
player = Player(user_name=bind.game_account, trust=True)
user = await player.user
netloc = get_self_netloc()
async with HostPage(
await render(
'v1/binding',
Bind(
platform='TOP',
type='unlink',
user=People(
avatar=await get_avatar(event_user_info, 'Data URI', None),
name=user.user_name,
),
bot=People(
avatar=await get_avatar(bot_info, 'Data URI', '../../static/logo/logo.svg'),
name=bot_info.user_name,
),
prompt='top绑定{游戏ID}',
lang=get_lang(),
),
)
) as page_hash:
await UniMessage.image(raw=await screenshot(f'http://{netloc}/host/{page_hash}.html')).send()
await remove_bind(session=session, user=nb_user, game_platform=GAME_TYPE)

View File

@@ -2,7 +2,7 @@ from arclet.alconna import Arg, ArgFlag
from nonebot_plugin_alconna import Args, At, Subcommand
from ...utils.exception import MessageFormatError
from ...utils.typing import Me
from ...utils.typedefs import Me
from .. import add_block_handlers, alc, command
from .api import Player
from .constant import USER_NAME
@@ -34,6 +34,10 @@ command.add(
),
help_text='绑定 茶服 账号',
),
Subcommand(
'unbind',
help_text='解除绑定 TOS 账号',
),
Subcommand(
'query',
Args(
@@ -56,9 +60,22 @@ command.add(
)
)
alc.shortcut('(?i:tos|茶服)(?i:绑定|绑|bind)', {'command': 'tstats TOS bind', 'humanized': '茶服绑定'})
alc.shortcut('(?i:tos|茶服)(?i:查询|查|query|stats)', {'command': 'tstats TOS query', 'humanized': '茶服查'})
alc.shortcut(
'(?i:tos|茶服)(?i:绑定|绑|bind)',
command='tstats TOS bind',
humanized='茶服绑定',
)
alc.shortcut(
'(?i:tos|茶服)(?i:解除绑定|解绑|unbind)',
command='tstats TOS unbind',
humanized='茶服解绑',
)
alc.shortcut(
'(?i:tos|茶服)(?i:查询|查|query|stats)',
command='tstats TOS query',
humanized='茶服查',
)
add_block_handlers(alc.assign('TOS.query'))
from . import bind, query # noqa: E402, F401
from . import bind, query, unbind # noqa: E402, F401

View File

@@ -64,7 +64,7 @@ class Player:
query = {'teaId': self.teaid}
else:
path = 'getUsernameInfo'
query = {'username': cast(str, self.user_name)}
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,)
)
@@ -91,7 +91,7 @@ class Player:
if self._user_profile.get(params) is None:
raw_user_profile = await request.failover_request(
[
i / 'getProfile' % {'id': self.teaid or cast(str, 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

@@ -8,6 +8,7 @@ from nonebot_plugin_userinfo import BotUserInfo, EventUserInfo, UserInfo
from ...db import BindStatus, create_or_update_bind, trigger
from ...utils.host import HostPage, get_self_netloc
from ...utils.image import get_avatar
from ...utils.lang import get_lang
from ...utils.render import Bind, render
from ...utils.render.schemas.base import People
from ...utils.screenshot import screenshot
@@ -45,7 +46,7 @@ async def _(
'v1/binding',
Bind(
platform=GAME_TYPE,
status='unknown',
type='unknown',
user=People(
avatar=await get_avatar(event_user_info, 'Data URI', None), name=user_info.data.name
),
@@ -53,7 +54,8 @@ async def _(
avatar=await get_avatar(bot_info, 'Data URI', '../../static/logo/logo.svg'),
name=bot_info.user_remark or bot_info.user_displayname or bot_info.user_name,
),
command='茶服查我',
prompt='茶服查我',
lang=get_lang(),
),
)
) as page_hash:

View File

@@ -1,4 +1,4 @@
from re import compile
from re import compile # noqa: A004
from typing import Literal
from yarl import URL

View File

@@ -1,7 +1,8 @@
from asyncio import gather
from datetime import timedelta
from datetime import datetime, timedelta, timezone
from http import HTTPStatus
from typing import Literal, NamedTuple
from zoneinfo import ZoneInfo
from nonebot.adapters import Event
from nonebot.matcher import Matcher
@@ -12,21 +13,27 @@ 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 nonebot_plugin_userinfo import EventUserInfo, UserInfo
from sqlalchemy import select
from ...db import query_bind_info, trigger
from ...i18n import Lang
from ...utils.chart import get_split, get_value_bounds, handle_history_data
from ...utils.exception import RequestError
from ...utils.host import HostPage, get_self_netloc
from ...utils.image import get_avatar
from ...utils.lang import get_lang
from ...utils.metrics import TetrisMetricsProWithLPMADPM, get_metrics
from ...utils.render import render
from ...utils.render.avatar import get_avatar as get_random_avatar
from ...utils.render.schemas.base import People, Ranking
from ...utils.render.schemas.tos_info import Info, Multiplayer, Radar
from ...utils.render.schemas.base import HistoryData, People, Trending
from ...utils.render.schemas.v1.base import History
from ...utils.render.schemas.v1.tos.info import Info, Multiplayer, Singleplayer
from ...utils.screenshot import screenshot
from ...utils.typing import Me, Number
from ...utils.time_it import time_it
from ...utils.typedefs import Me, Number
from . import alc
from .api import Player
from .api.models import TOSHistoricalData
from .api.schemas.user_info import UserInfoSuccess
from .constant import GAME_TYPE
@@ -148,6 +155,7 @@ async def _(account: Player, event_session: EventSession):
command_args=[],
):
user_info, game_data = await gather(account.get_info(), get_game_data(account))
await get_historical_data(user_info.data.teaid)
if game_data is not None:
await UniMessage.image(raw=await make_query_image(user_info, game_data, None)).finish()
await make_query_text(user_info, game_data).finish()
@@ -156,7 +164,7 @@ async def _(account: Player, event_session: EventSession):
class GameData(NamedTuple):
game_num: int
metrics: TetrisMetricsProWithLPMADPM
OR: Number
or_: Number
dspp: Number
ge: Number
@@ -199,12 +207,49 @@ async def get_game_data(player: Player, query_num: int = 50) -> GameData | None:
return GameData(
game_num=num,
metrics=metrics,
OR=total_offset / total_receive * 100,
or_=total_offset / total_receive * 100,
dspp=total_dig / total_pieses,
ge=2 * ((total_attack * total_dig) / total_pieses**2),
)
@time_it
async def get_historical_data(unique_identifier: str) -> list[HistoryData]:
async with get_session() as session:
user_infos = (
await session.scalars(
select(TOSHistoricalData)
.where(TOSHistoricalData.user_unique_identifier == unique_identifier)
.where(TOSHistoricalData.api_type == 'User Info')
.where(
TOSHistoricalData.update_time
> (
datetime.now(ZoneInfo('Asia/Shanghai')).replace(hour=0, minute=0, second=0, microsecond=0)
- timedelta(days=9)
).replace(tzinfo=timezone.utc)
)
.order_by(TOSHistoricalData.id.asc())
)
).all()
if user_infos:
extra_info = (
await session.scalars(
select(TOSHistoricalData)
.where(TOSHistoricalData.id < user_infos[0].id)
.where(TOSHistoricalData.user_unique_identifier == unique_identifier)
.where(TOSHistoricalData.api_type == 'User Info')
.limit(1)
)
).one_or_none()
if extra_info is not None:
user_infos = [extra_info, *user_infos]
return [
HistoryData(score=float(i.data.data.rating_now), record_at=i.update_time.astimezone(ZoneInfo('Asia/Shanghai')))
for i in user_infos
if isinstance(i.data, UserInfoSuccess)
]
async def make_query_image(user_info: UserInfoSuccess, game_data: GameData, event_user_info: UserInfo | None) -> bytes:
metrics = game_data.metrics
sprint_value = (
@@ -216,6 +261,8 @@ async def make_query_image(user_info: UserInfoSuccess, game_data: GameData, even
if user_info.data.pb_sprint != '2147483647'
else 'N/A'
)
data = handle_history_data(await get_historical_data(user_info.data.teaid))
values = get_value_bounds([i.score for i in data])
async with HostPage(
await render(
'v1/tos/info',
@@ -226,26 +273,38 @@ async def make_query_image(user_info: UserInfoSuccess, game_data: GameData, even
else get_random_avatar(user_info.data.teaid),
name=user_info.data.name,
),
ranking=Ranking(rating=float(user_info.data.ranking), rd=round(float(user_info.data.rd_now), 2)),
multiplayer=Multiplayer(
pps=metrics.pps,
history=History(
data=data,
max_value=values.value_max,
min_value=values.value_min,
split_interval=(split := get_split(value_bound=values, min_value=0)).split_value,
offset=split.offset,
),
rating=round(float(user_info.data.rating_now), 2),
rd=round(float(user_info.data.rd_now), 2),
lpm=metrics.lpm,
pps=metrics.pps,
lpm_trending=Trending.KEEP,
apm=metrics.apm,
apl=metrics.apl,
vs=metrics.vs,
apm_trending=Trending.KEEP,
adpm=metrics.adpm,
vs=metrics.vs,
adpl=metrics.adpl,
),
radar=Radar(
adpm_trending=Trending.KEEP,
app=(app := (metrics.apm / (60 * metrics.pps))),
OR=game_data.OR,
or_=game_data.or_,
dspp=game_data.dspp,
ci=150 * game_data.dspp - 125 * app + 50 * (metrics.vs / metrics.apm) - 25,
ge=game_data.ge,
),
sprint=sprint_value,
challenge=f'{int(user_info.data.pb_challenge):,}' if user_info.data.pb_challenge != '0' else 'N/A',
marathon=f'{int(user_info.data.pb_marathon):,}' if user_info.data.pb_marathon != '0' else 'N/A',
singleplayer=Singleplayer(
sprint=sprint_value,
challenge=f'{int(user_info.data.pb_challenge):,}' if user_info.data.pb_challenge != '0' else 'N/A',
marathon=f'{int(user_info.data.pb_marathon):,}' if user_info.data.pb_marathon != '0' else 'N/A',
),
lang=get_lang(),
),
)
) as page_hash:
@@ -258,7 +317,7 @@ def make_query_text(user_info: UserInfoSuccess, game_data: GameData | None) -> U
if user_data.ranked_games == '0':
message += '暂无段位统计数据'
else:
message += f', 段位分 {round(float(user_data.rating_now),2)}±{round(float(user_data.rd_now),2)} ({round(float(user_data.vol_now),2)}) '
message += f', 段位分 {round(float(user_data.rating_now), 2)}±{round(float(user_data.rd_now), 2)} ({round(float(user_data.vol_now), 2)}) '
if game_data is None:
message += ', 暂无游戏数据'
else:
@@ -266,7 +325,7 @@ def make_query_text(user_info: UserInfoSuccess, game_data: GameData | None) -> U
message += f"\nL'PM: {game_data.metrics.lpm} ( {game_data.metrics.pps} pps )"
message += f'\nAPM: {game_data.metrics.apm} ( x{game_data.metrics.apl} )'
message += f'\nADPM: {game_data.metrics.adpm} ( x{game_data.metrics.adpl} ) ( {game_data.metrics.vs}vs )'
message += f'\n40L: {float(user_data.pb_sprint)/1000:.2f}s' if user_data.pb_sprint != '2147483647' else ''
message += f'\n40L: {float(user_data.pb_sprint) / 1000:.2f}s' if user_data.pb_sprint != '2147483647' else ''
message += f'\nMarathon: {user_data.pb_marathon}' if user_data.pb_marathon != '0' else ''
message += f'\nChallenge: {user_data.pb_challenge}' if user_data.pb_challenge != '0' else ''
return UniMessage(message)

View File

@@ -0,0 +1,68 @@
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
from nonebot_plugin_userinfo import BotUserInfo, EventUserInfo, UserInfo
from nonebot_plugin_waiter import suggest # type: ignore[import-untyped]
from ...db import query_bind_info, remove_bind, trigger
from ...utils.host import HostPage, get_self_netloc
from ...utils.image import get_avatar
from ...utils.lang import get_lang
from ...utils.render import Bind, render
from ...utils.render.avatar import get_avatar as get_random_avatar
from ...utils.render.schemas.base import People
from ...utils.screenshot import screenshot
from . import alc
from .api import Player
from .constant import GAME_TYPE
@alc.assign('TOP.unbind')
async def _(
nb_user: User,
event_session: EventSession,
event_user_info: UserInfo = EventUserInfo(), # noqa: B008
bot_info: UserInfo = BotUserInfo(), # noqa: B008
):
async with (
trigger(
session_persist_id=await get_session_persist_id(event_session),
game_platform=GAME_TYPE,
command_type='unbind',
command_args=[],
),
get_session() as session,
):
if (bind := await query_bind_info(session=session, user=nb_user, game_platform=GAME_TYPE)) is None:
await UniMessage('您还未绑定 TOP 账号').finish()
resp = await suggest('您确定要解绑吗?', ['', ''])
if resp is None or resp.extract_plain_text() == '':
return
player = Player(user_name=bind.game_account, trust=True)
user = await player.user
netloc = get_self_netloc()
async with HostPage(
await render(
'v1/binding',
Bind(
platform='TOS',
type='unlink',
user=People(
avatar=await get_avatar(event_user_info, 'Data URI', None)
if event_user_info is not None
else get_random_avatar(user.teaid),
name=user.name,
),
bot=People(
avatar=await get_avatar(bot_info, 'Data URI', '../../static/logo/logo.svg'),
name=bot_info.user_name,
),
prompt='茶服绑定{游戏ID}',
lang=get_lang(),
),
)
) as page_hash:
await UniMessage.image(raw=await screenshot(f'http://{netloc}/host/{page_hash}.html')).send()
await remove_bind(session=session, user=nb_user, game_platform=GAME_TYPE)

View File

@@ -67,6 +67,19 @@
}
}
}
},
"template": {
"title": "Template",
"description": "Scope 'template' of lang item",
"type": "object",
"additionalProperties": false,
"properties": {
"template_language": {
"title": "template_language",
"description": "value of lang item type 'template_language'",
"type": "string"
}
}
}
}
}

View File

@@ -11,6 +11,7 @@
{
"scope": "error",
"types": [{ "subtype": "MessageFormatError", "types": ["TETR.IO", "TOS", "TOP"] }]
}
},
{ "scope": "template", "types": ["template_language"] }
]
}

View File

@@ -12,5 +12,8 @@
"TOS": "Username/ID is invalid",
"TOP": "Username is invalid"
}
},
"template": {
"template_language": "en-US"
}
}

View File

@@ -1,7 +1,7 @@
# 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]
from tarina.lang.model import LangItem, LangModel
class InteractionWrong:
@@ -27,6 +27,11 @@ class Error:
MessageFormatError = ErrorMessageformaterror
class Template:
template_language: LangItem = LangItem('template', 'template_language')
class Lang(LangModel):
interaction = Interaction
error = Error
template = Template

View File

@@ -10,5 +10,8 @@
"TOS": "用户名/ID不合法",
"TOP": "用户名不合法"
}
},
"template": {
"template_language": "zh-CN"
}
}

View File

@@ -10,6 +10,8 @@ from nonebot.log import logger
from playwright.__main__ import main
from playwright.async_api import Browser, BrowserContext, async_playwright
from ..config.config import config
driver = get_driver()
global_config = driver.config
@@ -76,6 +78,7 @@ class BrowserManager:
"""启动浏览器实例"""
playwright = await async_playwright().start()
cls._browser = await playwright.firefox.launch(
headless=not config.tetris.development,
firefox_user_prefs={
'network.http.max-persistent-connections-per-server': 64,
},

View File

@@ -0,0 +1,122 @@
from datetime import datetime, timedelta
from math import ceil, floor, inf
from typing import NamedTuple
from zoneinfo import ZoneInfo
from .exception import WhatTheFuckError
from .render.schemas.base import HistoryData
from .typedefs import Number
class ValueBound(NamedTuple):
value_max: int
value_min: int
class Split(NamedTuple):
split_value: int
offset: int
def get_value_bounds(values: list[int | float]) -> ValueBound:
value_max = 10 * ceil(max(values) / 10)
value_min = 10 * floor(min(values) / 10)
return ValueBound(value_max, value_min)
def get_split(value_bound: ValueBound, max_value: Number = inf, min_value: Number = -inf) -> Split:
offset = 0
overflow = 0
while True:
if (new_max_value := value_bound.value_max + offset + overflow) > max_value:
overflow -= 1
continue
if (new_min_value := value_bound.value_min - offset + overflow) < min_value:
overflow += 1
continue
if ((new_max_value - new_min_value) / 40).is_integer():
split_value = int((value_bound.value_max + offset - (value_bound.value_min - offset)) / 4)
break
offset += 1
return Split(split_value, offset + overflow)
def get_specified_point(
previous_point: HistoryData,
behind_point: HistoryData,
point_time: datetime,
) -> HistoryData:
"""根据给出的 previous_point 和 behind_point, 推算 point_time 点处的数据
Args:
previous_point (Data): 前面的数据点
behind_point (Data): 后面的数据点
point_time (datetime): 要推算的点的位置
Returns:
Data: 要推算的点的数据
"""
# 求两个点的斜率
slope = (behind_point.score - previous_point.score) / (
datetime.timestamp(behind_point.record_at) - datetime.timestamp(previous_point.record_at)
)
return HistoryData(
record_at=point_time,
score=previous_point.score
+ slope * (datetime.timestamp(point_time) - datetime.timestamp(previous_point.record_at)),
)
def handle_history_data(data: list[HistoryData]) -> list[HistoryData]: # 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[HistoryData] = []
in_border: list[HistoryData] = []
rights: list[HistoryData] = []
# 根据 记录时间 将数据分类到对应的列表中
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[HistoryData] = []
# 处理左边界的点
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(HistoryData(score=lefts[-1].score, record_at=left_border))
elif not lefts and in_border: # 如果边界左侧没有值但是边界内有值则直接取边界内的第一个值
ret.append(HistoryData(score=in_border[0].score, record_at=left_border))
elif not lefts and not in_border and rights: # 如果边界左侧和边界内都没有值但是边界右侧有值则直接取边界右侧的第一个值 # fmt: skip
ret.append(HistoryData(score=rights[0].score, 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(HistoryData(score=rights[0].score, record_at=right_border))
elif in_border and not rights: # 如果边界内有值但是边界右侧没有值则直接取边界内的最后一个值
ret.append(HistoryData(score=in_border[-1].score, record_at=right_border))
elif not in_border and not rights and lefts: # 如果边界内和边界右侧都没有值但是边界左侧有值则直接取边界左侧的最后一个值 # fmt: skip
ret.append(HistoryData(score=lefts[-1].score, record_at=right_border))
else: # 暂时没想到其他情况
raise WhatTheFuckError
return ret

View File

@@ -2,9 +2,9 @@ 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 typing import TYPE_CHECKING, Annotated, ClassVar, Literal
from aiofiles import open
from aiofiles import open as aopen
from fastapi import BackgroundTasks, FastAPI, Path, status
from fastapi.responses import FileResponse, HTMLResponse, Response
from fastapi.staticfiles import StaticFiles
@@ -12,7 +12,7 @@ from nonebot import get_app, get_driver
from nonebot.log import logger
from yarl import URL
from ..config.config import CACHE_PATH
from ..config.config import CACHE_PATH, config
from ..games.tetrio.api.cache import request
from .image import img_to_png
from .templates import TEMPLATES_DIR
@@ -45,15 +45,21 @@ class HostPage:
async def __aenter__(self) -> str:
return self.page_hash
async def __aexit__(self, exc_type, exc, tb) -> None: # noqa: ANN001
self.pages.pop(self.page_hash, None)
if not config.tetris.development:
async def __aexit__(self, exc_type, exc, tb) -> None: # noqa: ANN001
self.pages.pop(self.page_hash, None)
else:
async def __aexit__(self, exc_type, exc, tb) -> None: # noqa: ANN001
pass
@driver.on_startup
def _():
app.mount(
'/host/assets',
StaticFiles(directory=TEMPLATES_DIR / 'assets'),
'/host/_nuxt',
StaticFiles(directory=TEMPLATES_DIR / '_nuxt'),
name='assets',
)
logger.success('assets mounted')
@@ -69,9 +75,9 @@ 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'],
user_id: Annotated[str, Path(regex=r'^[a-f0-9]{24}$')],
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():
image = img_to_png(
@@ -87,7 +93,7 @@ async def _(
async def write_cache(path: FilePath, data: bytes) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
async with open(path, 'wb') as file:
async with aopen(path, 'wb') as file:
await file.write(data)

View File

@@ -0,0 +1,8 @@
from typing import cast
from ..i18n.model import Lang
from .typedefs import Lang as LangType
def get_lang() -> LangType:
return cast('LangType', Lang.template.template_language())

View File

@@ -23,7 +23,9 @@ def limit(limit: timedelta) -> Callable[[Callable[P, Coroutine[Any, Any, T]]], C
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')
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)

View File

@@ -1,6 +1,6 @@
from typing import overload
from .typing import Number
from .typedefs import Number
class TetrisMetricsBaseWithPPS:

View File

@@ -4,20 +4,26 @@ from jinja2 import Environment, FileSystemLoader
from nonebot.compat import PYDANTIC_V2
from ..templates import TEMPLATES_DIR
from .schemas.base import Base
from .schemas.bind import Bind
from .schemas.tetrio.rank.detail import Data as TETRIORankDetailData
from .schemas.tetrio.rank.v1 import Data as TETRIORankDataV1
from .schemas.tetrio.rank.v2 import Data as TETRIORankDataV2
from .schemas.tetrio.record.blitz import Record as TETRIORecordBlitz
from .schemas.tetrio.record.sprint import Record as TETRIORecordSprint
from .schemas.tetrio.user.info_v1 import Info as TETRIOUserInfoV1
from .schemas.tetrio.user.info_v2 import Info as TETRIOUserInfoV2
from .schemas.tetrio.user.list_v2 import List as TETRIOUserListV2
from .schemas.top_info import Info as TOPInfo
from .schemas.tos_info import Info as TOSInfo
from .schemas.v1.tetrio.rank import Data as TETRIORankDataV1
from .schemas.v1.tetrio.user.info import Info as TETRIOUserInfoV1
from .schemas.v1.top.info import Info as TOPInfoV1
from .schemas.v1.tos.info import Info as TOSInfoV1
from .schemas.v2.tetrio.rank import Data as TETRIORankDataV2
from .schemas.v2.tetrio.rank.detail import Data as TETRIORankDetailDataV2
from .schemas.v2.tetrio.record.blitz import Record as TETRIORecordBlitzV2
from .schemas.v2.tetrio.record.sprint import Record as TETRIORecordSprintV2
from .schemas.v2.tetrio.tetra_league import Data as TETRIOTetraLeagueDataV2
from .schemas.v2.tetrio.user.info import Info as TETRIOUserInfoV2
from .schemas.v2.tetrio.user.list import List as TETRIOUserListV2
env = Environment(
loader=FileSystemLoader(TEMPLATES_DIR), autoescape=True, trim_blocks=True, lstrip_blocks=True, enable_async=True
loader=FileSystemLoader(TEMPLATES_DIR),
autoescape=False, # noqa: S701
trim_blocks=True,
lstrip_blocks=True,
enable_async=True,
)
@@ -28,48 +34,26 @@ async def render(render_type: Literal['v1/tetrio/info'], data: TETRIOUserInfoV1)
@overload
async def render(render_type: Literal['v1/tetrio/rank'], data: TETRIORankDataV1) -> str: ...
@overload
async def render(render_type: Literal['v1/top/info'], data: TOPInfo) -> str: ...
async def render(render_type: Literal['v1/top/info'], data: TOPInfoV1) -> str: ...
@overload
async def render(render_type: Literal['v1/tos/info'], data: TOSInfo) -> str: ...
async def render(render_type: Literal['v1/tos/info'], data: TOSInfoV1) -> str: ...
@overload
async def render(render_type: Literal['v2/tetrio/rank'], data: TETRIORankDataV2) -> str: ...
@overload
async def render(render_type: Literal['v2/tetrio/rank/detail'], data: TETRIORankDetailDataV2) -> str: ...
@overload
async def render(render_type: Literal['v2/tetrio/record/blitz'], data: TETRIORecordBlitzV2) -> str: ...
@overload
async def render(render_type: Literal['v2/tetrio/record/sprint'], data: TETRIORecordSprintV2) -> str: ...
@overload
async def render(render_type: Literal['v2/tetrio/tetra-league'], data: TETRIOTetraLeagueDataV2) -> str: ...
@overload
async def render(render_type: Literal['v2/tetrio/user/info'], data: TETRIOUserInfoV2) -> str: ...
@overload
async def render(render_type: Literal['v2/tetrio/user/list'], data: TETRIOUserListV2) -> str: ...
@overload
async def render(render_type: Literal['v2/tetrio/record/40l'], data: TETRIORecordSprint) -> str: ...
@overload
async def render(render_type: Literal['v2/tetrio/record/blitz'], data: TETRIORecordBlitz) -> str: ...
@overload
async def render(render_type: Literal['v2/tetrio/rank'], data: TETRIORankDataV2) -> str: ...
@overload
async def render(render_type: Literal['v2/tetrio/rank/detail'], data: TETRIORankDetailData) -> str: ...
async def render(
render_type: Literal[
'v1/binding',
'v1/tetrio/info',
'v1/tetrio/rank',
'v1/top/info',
'v1/tos/info',
'v2/tetrio/user/info',
'v2/tetrio/user/list',
'v2/tetrio/record/40l',
'v2/tetrio/record/blitz',
'v2/tetrio/rank',
'v2/tetrio/rank/detail',
],
data: Bind
| TETRIOUserInfoV1
| TETRIORankDataV1
| TOPInfo
| TOSInfo
| TETRIOUserInfoV2
| TETRIOUserListV2
| TETRIORecordSprint
| TETRIORecordBlitz
| TETRIORankDataV2
| TETRIORankDetailData,
render_type: str,
data: Base,
) -> str:
if PYDANTIC_V2:
return await env.get_template('index.html').render_async(

View File

@@ -156,7 +156,7 @@ class SkinManager:
class Skin(ABC):
def __new__(cls, *args: Any, **kwargs: Any) -> Self: # noqa: ANN401, ARG003
def __new__(cls, *args: Any, **kwargs: Any) -> Self: # noqa: ANN401, ARG004
instance = super().__new__(cls)
SkinManager.register(instance)
return instance

View File

@@ -1,8 +1,14 @@
from datetime import datetime
from typing import Literal
from pydantic import BaseModel
from strenum import StrEnum
from ...typing import Number
from ...typedefs import Lang, Number
class Base(BaseModel):
lang: Lang
class Avatar(BaseModel):
@@ -18,3 +24,14 @@ class People(BaseModel):
class Ranking(BaseModel):
rating: Number
rd: Number
class HistoryData(BaseModel):
score: Number
record_at: datetime
class Trending(StrEnum):
UP = 'up'
KEEP = 'keep'
DOWN = 'down'

View File

@@ -1,13 +1,11 @@
from typing import Literal
from pydantic import BaseModel
from .base import People
from .base import Base, People
class Bind(BaseModel):
class Bind(Base):
platform: Literal['TETR.IO', 'TOP', 'TOS']
status: Literal['error', 'success', 'unknown', 'unlink', 'unverified']
type: Literal['success', 'unknown', 'unlink', 'unverified', 'error']
user: People
bot: People
command: str
prompt: str

View File

@@ -1,31 +0,0 @@
from datetime import datetime
from pydantic import BaseModel
from ......games.tetrio.api.typing import ValidRank
class SpecialData(BaseModel):
apm: float
pps: float
lpm: float
vs: float
adpm: float
apl: float | None = None
adpl: float | None = None
apm_holder: str | None = None
pps_holder: str | None = None
vs_holder: str | None = None
class Data(BaseModel):
name: ValidRank
trending: float
require_tr: float
players: int
minimum_data: SpecialData
average_data: SpecialData
maximum_data: SpecialData
updated_at: datetime

View File

@@ -1,25 +0,0 @@
from datetime import datetime
from pydantic import BaseModel
from ......games.tetrio.api.typing import ValidRank
class AverageData(BaseModel):
pps: float
apm: float
apl: float
vs: float
adpl: float
class ItemData(BaseModel):
require_tr: float
trending: float
average_data: AverageData
players: int
class Data(BaseModel):
items: dict[ValidRank, ItemData]
updated_at: datetime

View File

@@ -1,10 +0,0 @@
from datetime import datetime
from pydantic import BaseModel
from .....typing import Number
class TetraLeagueHistoryData(BaseModel):
record_at: datetime
tr: Number

View File

@@ -1,49 +0,0 @@
from pydantic import BaseModel
from ......games.tetrio.api.typing import Rank
from .....typing import Number
from ...base import People, Ranking
from .base import TetraLeagueHistoryData
class User(People):
bio: str | None
class TetraLeague(BaseModel):
rank: Rank
tr: Number
global_rank: Number
pps: Number
lpm: Number
apm: Number
apl: Number
vs: Number
adpm: Number
adpl: Number
class TetraLeagueHistory(BaseModel):
data: list[TetraLeagueHistoryData]
split_interval: Number
min_tr: Number
max_tr: Number
offset: Number
class Radar(BaseModel):
app: Number
dsps: Number
dspp: Number
ci: Number
ge: Number
class Info(BaseModel):
user: User
ranking: Ranking
tetra_league: TetraLeague
tetra_league_history: TetraLeagueHistory
radar: Radar
sprint: str
blitz: str

View File

@@ -1,17 +0,0 @@
from pydantic import BaseModel
from ...typing import Number
from .base import People
class Data(BaseModel):
pps: Number
lpm: Number
apm: Number
apl: Number
class Info(BaseModel):
user: People
today: Data
history: Data

View File

@@ -1,32 +0,0 @@
from pydantic import BaseModel, Field
from ...typing import Number
from .base import People, Ranking
class Multiplayer(BaseModel):
pps: Number
lpm: Number
apm: Number
apl: Number
vs: Number
adpm: Number
adpl: Number
class Radar(BaseModel):
app: Number
OR: Number = Field(serialization_alias='or')
dspp: Number
ci: Number
ge: Number
class Info(BaseModel):
user: People
ranking: Ranking
multiplayer: Multiplayer
radar: Radar
sprint: str
challenge: str
marathon: str

View File

@@ -0,0 +1,12 @@
from pydantic import BaseModel
from ....typedefs import Number
from ..base import HistoryData
class History(BaseModel):
data: list[HistoryData]
split_interval: Number
min_value: Number
max_value: Number
offset: Number

View File

@@ -2,7 +2,8 @@ from datetime import datetime
from pydantic import BaseModel
from ......games.tetrio.api.typing import ValidRank
from ......games.tetrio.api.typedefs import ValidRank
from ...base import Base
class ItemData(BaseModel):
@@ -11,6 +12,6 @@ class ItemData(BaseModel):
players: int
class Data(BaseModel):
class Data(Base):
items: dict[ValidRank, ItemData]
updated_at: datetime

View File

@@ -0,0 +1,50 @@
from pydantic import BaseModel
from .......games.tetrio.api.typedefs import Rank
from ......typedefs import Number
from ....base import Base, People, Trending
from ...base import History
class User(People):
bio: str | None
class Multiplayer(BaseModel):
glicko: str
rd: Number
rank: Rank
tr: str
global_rank: Number
history: History
lpm: Number
pps: Number
lpm_trending: Trending
apm: Number
apl: Number
apm_trending: Trending
adpm: Number
vs: Number
adpl: Number
adpm_trending: Trending
app: Number
ci: Number
dspp: Number
dsps: Number
ge: Number
class Singleplayer(BaseModel):
sprint: str
blitz: str
class Info(Base):
user: User
multiplayer: Multiplayer
singleplayer: Singleplayer

View File

@@ -0,0 +1,19 @@
from pydantic import BaseModel
from .....typedefs import Number
from ...base import Base, People, Trending
class Data(BaseModel):
pps: Number
lpm: Number
lpm_trending: Trending
apm: Number
apl: Number
apm_trending: Trending
class Info(Base):
user: People
today: Data
historical: Data

View File

@@ -0,0 +1,42 @@
from pydantic import BaseModel, Field
from .....typedefs import Number
from ...base import Base, People, Trending
from ..base import History
class Multiplayer(BaseModel):
history: History
rating: Number
rd: Number
lpm: Number
pps: Number
lpm_trending: Trending
apm: Number
apl: Number
apm_trending: Trending
adpm: Number
vs: Number
adpl: Number
adpm_trending: Trending
app: Number
ci: Number
dspp: Number
or_: Number = Field(serialization_alias='or')
ge: Number
class Singleplayer(BaseModel):
sprint: str
challenge: str
marathon: str
class Info(Base):
user: People
multiplayer: Multiplayer
singleplayer: Singleplayer

View File

@@ -0,0 +1,27 @@
from datetime import datetime
from pydantic import BaseModel
from .......games.tetrio.api.typedefs import ValidRank
from ......typedefs import Number
from ....base import Base
class AverageData(BaseModel):
pps: Number
apm: Number
apl: Number
vs: Number
adpl: Number
class ItemData(BaseModel):
require_tr: Number
trending: Number
average_data: AverageData
players: Number
class Data(Base):
items: dict[ValidRank, ItemData]
updated_at: datetime

View File

@@ -0,0 +1,33 @@
from datetime import datetime
from pydantic import BaseModel
from .......games.tetrio.api.typedefs import ValidRank
from ......typedefs import Number
from ....base import Base
class SpecialData(BaseModel):
apm: Number
pps: Number
lpm: Number
vs: Number
adpm: Number
apl: Number | None = None
adpl: Number | None = None
apm_holder: str | None = None
pps_holder: str | None = None
vs_holder: str | None = None
class Data(Base):
name: ValidRank
trending: Number
require_tr: Number
players: Number
minimum_data: SpecialData
average_data: SpecialData
maximum_data: SpecialData
updated_at: datetime

View File

@@ -3,7 +3,7 @@ from typing import Literal
from pydantic import BaseModel
from ...base import People
from ....base import Base, People
class User(People):
@@ -61,7 +61,7 @@ class Statistic(BaseModel):
finesse: Finesse
class Record(BaseModel):
class Record(Base):
type: Literal['best', 'personal_best', 'recent', 'disputed']
user: User

View File

@@ -0,0 +1,39 @@
from datetime import datetime
from pydantic import BaseModel
from .....typedefs import Number
from ...base import Base
class StatisticalData(BaseModel):
pps: Number
apm: Number
apl: Number
vs: Number
adpl: Number
class User(BaseModel):
id: str
name: str
class Handling(BaseModel):
arr: Number
das: Number
sdf: Number
class Game(BaseModel):
user: User
points: Number
average_data: StatisticalData
data: list[StatisticalData]
handling: Handling
class Data(Base):
replay_id: str
games: list[Game]
play_at: datetime

View File

@@ -3,10 +3,11 @@ from typing import Literal
from pydantic import BaseModel
from ......games.tetrio.api.typing import Rank
from .....typing import Number
from ...base import Avatar
from .base import TetraLeagueHistoryData
from .......games.tetrio.api.schemas.summaries.achievements import ArType, RankType
from .......games.tetrio.api.schemas.summaries.achievements import Rank as AchievementRank
from .......games.tetrio.api.typedefs import Rank
from ......typedefs import Number
from ....base import Avatar, Base, HistoryData
class Badge(BaseModel):
@@ -16,12 +17,25 @@ class Badge(BaseModel):
receive_at: datetime | None
class Achievement(BaseModel):
key: int
rank_type: RankType
ar_type: ArType
stub: bool | None
rank: AchievementRank | None
achieved_score: float | None
pos: int | None
progress: float | None
total: int | None
class User(BaseModel):
id: str
name: str
country: str | None
role: Literal['anon', 'user', 'bot', 'halfmod', 'mod', 'admin', 'sysop', 'hidden', 'banned']
botmaster: str | None
avatar: str | Avatar
banner: str | None
@@ -36,6 +50,9 @@ class User(BaseModel):
badges: list[Badge]
xp: Number
ar: Number
achievements: list[Achievement]
playtime: str | None
join_at: datetime | None
@@ -74,18 +91,20 @@ class TetraLeague(BaseModel):
decaying: bool
history: list[TetraLeagueHistoryData] | None
history: list[HistoryData] | None
class Sprint(BaseModel):
time: str
global_rank: int | None
global_rank: Number | None
country_rank: Number | None
play_at: datetime
class Blitz(BaseModel):
score: int
score: Number
global_rank: int | None
country_rank: int | None
play_at: datetime
@@ -94,9 +113,29 @@ class Zen(BaseModel):
score: int
class Info(BaseModel):
class Week(BaseModel):
altitude: Number
global_rank: int | None
country_rank: int | None
play_at: datetime
class Best(BaseModel):
altitude: Number
global_rank: int | None
play_at: datetime
class Zenith(BaseModel):
week: Week | None
best: Best | None
class Info(Base):
user: User
tetra_league: TetraLeague | None
zenith: Zenith | None
zenithex: Zenith | None
statistic: Statistic | None
sprint: Sprint | None
blitz: Blitz | None

View File

@@ -1,24 +1,23 @@
from datetime import datetime
from pydantic import BaseModel
from ......games.tetrio.api.typing import Rank
from .....typing import Number
from ...base import Avatar
from .......games.tetrio.api.typedefs import Rank
from ......typedefs import Number
from ....base import Avatar, Base
class TetraLeague(BaseModel):
pps: Number
apm: Number
apl: Number
vs: Number | None
adpl: Number | None
rank: Rank
tr: Number
glicko: Number | None
rd: Number | None
decaying: bool
pps: Number
apm: Number
apl: Number
vs: Number | None
adpl: Number | None
class User(BaseModel):
@@ -26,11 +25,14 @@ class User(BaseModel):
name: str
avatar: str | Avatar
country: str | None
tetra_league: TetraLeague
xp: Number
join_at: datetime | None
class List(BaseModel):
class Data(BaseModel):
user: User
tetra_league: TetraLeague
class List(Base):
show_index: bool
users: list[User]
data: list[Data]

View File

@@ -2,6 +2,7 @@ from collections.abc import Sequence
from http import HTTPStatus
from typing import Any
from fake_useragent import UserAgent
from httpx import AsyncClient, HTTPError
from msgspec import DecodeError, Struct, json
from nonebot import get_driver
@@ -113,6 +114,8 @@ class Request:
def __init__(self, proxy: str | None) -> None:
self.proxy = proxy
self.anti_cloudflares: dict[str, AntiCloudflare] = {}
self.client = AsyncClient(timeout=config.tetris.request_timeout, proxy=self.proxy)
self.ua = UserAgent()
async def request(
self,
@@ -129,16 +132,20 @@ class Request:
else:
cookies = None
headers = None
headers = headers if extra_headers is None else extra_headers if headers is None else headers | extra_headers
if headers is None:
headers = {}
if extra_headers:
headers.update(extra_headers)
headers.setdefault('User-Agent', self.ua.random)
try:
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:
decoder.decode(response.content)
return response.content
response = await self.client.get(str(url), cookies=cookies, 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:
decoder.decode(response.content)
except HTTPError as e:
msg = f'请求错误 \n{e!r}'
raise RequestError(msg) from e
@@ -146,6 +153,8 @@ class Request:
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
else:
return response.content
async def failover_request(
self,

View File

@@ -1,4 +1,4 @@
from playwright.async_api import BrowserContext, TimeoutError, ViewportSize
from playwright.async_api import BrowserContext, TimeoutError, ViewportSize # noqa: A004
from ..config.config import config
from .browser import BrowserManager
@@ -16,6 +16,7 @@ async def screenshot(url: str) -> bytes:
context = await BrowserManager.get_context('screenshot', factory=context_factory)
async with await context.new_page() as page:
await page.goto(url)
await page.wait_for_selector('#content')
size: ViewportSize = await page.evaluate("""
() => {
const element = document.querySelector('#content');
@@ -26,5 +27,4 @@ async def screenshot(url: str) -> bytes:
};
""")
await page.set_viewport_size(size)
await page.wait_for_load_state('networkidle')
return await page.locator('id=content').screenshot(animations='disabled', timeout=5000, type='png')

View File

@@ -5,7 +5,7 @@ from shutil import rmtree
from time import time_ns
from zipfile import ZipFile
from aiofiles import open
from aiofiles import open as aopen
from httpx import AsyncClient
from nonebot import get_driver
from nonebot.log import logger
@@ -30,7 +30,7 @@ async def download_templates(tag: str) -> Path:
tag = (
(
await client.get(
'https://github.com/A-Minos/tetris-stats-templates/releases/latest', follow_redirects=True
'https://github.com/A-Minos/tetris-stats-templates-new/releases/latest', follow_redirects=True
)
)
.url.path.strip('/')
@@ -43,10 +43,10 @@ async def download_templates(tag: str) -> Path:
async with (
client.stream(
'GET',
f'https://github.com/A-Minos/tetris-stats-templates/releases/download/{tag}/dist.zip',
f'https://github.com/A-Minos/tetris-stats-templates-new/releases/download/{tag}/dist.zip',
follow_redirects=True,
) as response,
open(path, 'wb') as file,
aopen(path, 'wb') as file,
):
response.raise_for_status()
progress.update(task_id, total=int(response.headers.get('content-length', 0)) or None)
@@ -76,7 +76,7 @@ async def check_hash(hash_file_path: Path) -> bool:
if not file_path.is_file():
logger.error(f'{file_path.name} 不存在或不是文件')
return False
async with open(file_path, 'rb') as file:
async with aopen(file_path, 'rb') as file:
while True:
chunk = await file.read(65535)
if not chunk:
@@ -107,7 +107,7 @@ async def init_templates(tag: str) -> bool:
async def check_tag(tag: str) -> bool:
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}')
await client.get(f'https://github.com/A-Minos/tetris-stats-templates-new/releases/tag/{tag}')
).status_code != HTTPStatus.NOT_FOUND

View File

@@ -2,7 +2,7 @@ from typing import Literal, TypeAlias
Number: TypeAlias = float | int
GameType: TypeAlias = Literal['IO', 'TOP', 'TOS']
BaseCommandType: TypeAlias = Literal['bind', 'query']
BaseCommandType: TypeAlias = Literal['bind', 'unbind', 'query']
TETRIOCommandType: TypeAlias = BaseCommandType | Literal['rank', 'config', 'list', 'record']
AllCommandType: TypeAlias = BaseCommandType | TETRIOCommandType
Me: TypeAlias = Literal[
@@ -59,3 +59,5 @@ Me: TypeAlias = Literal[
'self',
'oneself',
]
Lang: TypeAlias = Literal['zh-CN', 'en-US']

11
package.json Normal file
View File

@@ -0,0 +1,11 @@
{
"prettier": {
"singleQuote": true,
"printWidth": 120,
"tabWidth": 2,
"endOfLine": "lf"
},
"devDependencies": {
"prettier": "^3.3.3"
}
}

24
pnpm-lock.yaml generated Normal file
View File

@@ -0,0 +1,24 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
devDependencies:
prettier:
specifier: ^3.3.3
version: 3.5.3
packages:
prettier@3.5.3:
resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==}
engines: {node: '>=14'}
hasBin: true
snapshots:
prettier@3.5.3: {}

View File

@@ -1,6 +1,8 @@
#:schema https://json.schemastore.org/uv.json
[project]
name = "nonebot-plugin-tetris-stats"
version = "1.6.0"
version = "1.8.3"
description = "一款基于 NoneBot2 的用于查询 Tetris 相关游戏数据的插件"
readme = "README.md"
authors = [{ name = "shoucandanghehe", email = "wallfjjd@gmail.com" }]
@@ -10,6 +12,7 @@ dependencies = [
"aiofiles>=24.1.0",
"arclet-alconna<2",
"async-lru>=2.0.4",
"fake-useragent>=2.0.3",
"httpx>=0.27.2",
"jinja2>=3.1.4",
"lxml>=5.3.0",
@@ -22,11 +25,13 @@ dependencies = [
"nonebot-plugin-session-orm>=0.2.0",
"nonebot-plugin-user>=0.4.4",
"nonebot-plugin-userinfo>=0.2.6",
"nonebot-plugin-waiter>=0.8.0",
"nonebot2[fastapi]>=2.3.3",
"pandas>=2.2.3",
"pillow>=11.0.0",
"playwright>=1.48.0",
"rich>=13.9.3",
"strenum>=0.4.15",
"yarl>=1.16.0",
]
classifiers = [
@@ -55,6 +60,7 @@ dev = [
"nonebot-adapter-kaiheila>=0.3.4",
"nonebot-adapter-onebot>=2.4.6",
"nonebot-adapter-qq>=1.5.3",
"nonebot-plugin-tarina-lang-turbo>=0.1.1",
"ruff>=0.7.1",
]
typecheck = [
@@ -63,7 +69,14 @@ typecheck = [
"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"]
test = [
"nonebot-adapter-satori>=0.12.6",
"nonebot-plugin-orm[default]>=0.7.6",
"nonebot2[aiohttp,fastapi]>=2.3.3",
"nonebug>=0.4.1",
"pytest-asyncio>=0.24.0",
"pytest-cov>=6.0.0",
]
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"]
@@ -125,8 +138,6 @@ select = [
]
ignore = [
"E501", # 过长的行由 ruff format 处理, 剩余的都是字符串
"ANN101", # 由 type checker 自动推断
"ANN102", # 由 type checker 自动推断
"ANN202", # 向 NoneBot 注册的函数
"TRY003",
"COM812", # 强制尾随逗号
@@ -151,17 +162,21 @@ defineConstant = { PYDANTIC_V2 = true }
typeCheckingMode = "standard"
[tool.bumpversion]
current_version = "1.6.0"
current_version = "1.8.3"
tag = true
sign_tags = true
tag_name = "{new_version}"
commit = true
message = "🔖 {new_version}"
message = ":bookmark: {new_version}"
[[tool.bumpversion.files]]
filename = "pyproject.toml"
search = "version = \"{current_version}\""
replace = "version = \"{new_version}\""
[tool.pytest.ini_options]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "session"
[tool.nonebot]
plugins = ["nonebot_plugin_tetris_stats"]
plugins = ["nonebot_plugin_tetris_stats", "nonebot_plugin_tarina_lang_turbo"]

0
tests/__init__.py Normal file
View File

21
tests/conftest.py Normal file
View File

@@ -0,0 +1,21 @@
import os
import nonebot
import pytest
from nonebot.adapters.onebot.v11 import Adapter as OnebotV11Adapter
from nonebug import NONEBOT_INIT_KWARGS, NONEBOT_START_LIFESPAN # type: ignore[import-untyped]
os.environ['ENVIRONMENT'] = 'test'
def pytest_configure(config: pytest.Config) -> None:
config.stash[NONEBOT_INIT_KWARGS] = {'log_level': 'DEBUG'}
config.stash[NONEBOT_START_LIFESPAN] = False
@pytest.fixture(scope='session', autouse=True)
async def after_nonebot_init(after_nonebot_init: None) -> None: # noqa: ARG001
driver = nonebot.get_driver()
driver.register_adapter(OnebotV11Adapter)
nonebot.load_from_toml('pyproject.toml')

28
tests/fake_event.py Normal file
View File

@@ -0,0 +1,28 @@
from datetime import datetime
from typing import Literal
from zoneinfo import ZoneInfo
from nonebot.adapters.onebot.v11 import GroupMessageEvent
from nonebot.adapters.onebot.v11.event import Sender
from pydantic import Field
class FakeGroupMessageEvent(GroupMessageEvent):
time: int = Field(default_factory=lambda: int(datetime.now(tz=ZoneInfo('Asia/Shanghai')).timestamp()))
self_id: int = 1
post_type: Literal['message'] = 'message'
sub_type: str = 'normal'
user_id: int = 10
message_type: Literal['group'] = 'group'
group_id: int = 10000
message_id: int = 1
font: int = 0
sender: Sender = Sender(
card='',
nickname='test',
role='member',
)
to_me: bool = False
class Config:
extra = 'allow'

Some files were not shown because too many files have changed in this diff Show More