Compare commits

...

80 Commits

Author SHA1 Message Date
cbc96fc09e 🔖 1.4.11 2024-08-17 04:37:18 +08:00
8e10cfe0d0 🐛 修最佳段位为 z 爆炸 2024-08-17 04:31:14 +08:00
d192f0506d 🔖 1.4.10 2024-08-17 04:21:57 +08:00
44aed656b8 🐛 忘记 push schema 2024-08-17 04:21:33 +08:00
feb662b980 🔖 1.4.9 2024-08-17 04:17:57 +08:00
ed6eb9a5cf 💩 迅速的适配第二赛季 2024-08-17 04:17:41 +08:00
25e281a4c5 🎨 localstore 一律从 config 导入常量使用 2024-08-16 18:55:13 +08:00
a2d69b9113 ️ 尝试提高截图性能 2024-08-16 18:53:12 +08:00
c8907a47a4 💥 插件配置现在使用 ScopedConfig 2024-08-16 18:52:47 +08:00
9fb176b4bc 确保同一个账号生成的随机头像一致 2024-08-16 03:42:11 +08:00
dependabot[bot]
53740265b6 ⬆️ Bump ruff from 0.5.7 to 0.6.0 (#401)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.5.7 to 0.6.0.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.5.7...0.6.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-15 19:16:44 +00:00
dependabot[bot]
e6119074ce ⬆️ Bump nonebot-plugin-user from 0.4.0 to 0.4.1 (#400)
Bumps [nonebot-plugin-user](https://github.com/he0119/nonebot-plugin-user) from 0.4.0 to 0.4.1.
- [Release notes](https://github.com/he0119/nonebot-plugin-user/releases)
- [Changelog](https://github.com/he0119/nonebot-plugin-user/blob/main/CHANGELOG.md)
- [Commits](https://github.com/he0119/nonebot-plugin-user/compare/v0.4.0...v0.4.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-16 03:12:41 +08:00
f7a2e89274 🔖 1.4.8 2024-08-15 17:39:59 +08:00
3fe5a19c4a 🐛 修复 top 没有 recent games 时 出现 0 长 list 的 bug 2024-08-15 17:39:28 +08:00
d35469cdef 🔖 1.4.7 2024-08-15 17:01:27 +08:00
0cbae117aa ️ 将 _parse_profile 改成静态方法 2024-08-15 16:56:10 +08:00
25dc57d911 top query 使用图片回复 close #61 2024-08-15 16:55:13 +08:00
6042417b65 ️ 删除不需要的 async 2024-08-15 16:50:11 +08:00
63cd94a0d7 🔖 1.4.6 2024-08-14 22:25:41 +08:00
eb810d4bd2 茶服查别人时使用随机头像 2024-08-14 20:42:30 +08:00
52df4cf170 绘制随机头像
皮肤来源:[Techmino](https://github.com/26F-Studio/Techmino)
2024-08-14 20:41:54 +08:00
dependabot[bot]
7a6615f6c9 ⬆️ Bump nonebot-plugin-alconna from 0.51.2 to 0.51.4 (#399)
Bumps [nonebot-plugin-alconna](https://github.com/nonebot/plugin-alconna) from 0.51.2 to 0.51.4.
- [Release notes](https://github.com/nonebot/plugin-alconna/releases)
- [Commits](https://github.com/nonebot/plugin-alconna/compare/v0.51.2...v0.51.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-14 19:44:14 +08:00
dependabot[bot]
c363908434 ⬆️ Bump playwright from 1.45.1 to 1.46.0 (#396)
Bumps [playwright](https://github.com/Microsoft/playwright-python) from 1.45.1 to 1.46.0.
- [Release notes](https://github.com/Microsoft/playwright-python/releases)
- [Commits](https://github.com/Microsoft/playwright-python/compare/v1.45.1...v1.46.0)

---
updated-dependencies:
- dependency-name: playwright
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-13 08:34:28 +00:00
dependabot[bot]
e26fb44106 ⬆️ Bump lxml from 5.2.2 to 5.3.0 (#397)
Bumps [lxml](https://github.com/lxml/lxml) from 5.2.2 to 5.3.0.
- [Release notes](https://github.com/lxml/lxml/releases)
- [Changelog](https://github.com/lxml/lxml/blob/master/CHANGES.txt)
- [Commits](https://github.com/lxml/lxml/compare/lxml-5.2.2...lxml-5.3.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-13 08:30:38 +00:00
dependabot[bot]
7e2c04426a ⬆️ Bump nonebot-plugin-alconna from 0.51.1 to 0.51.2 (#398)
Bumps [nonebot-plugin-alconna](https://github.com/nonebot/plugin-alconna) from 0.51.1 to 0.51.2.
- [Release notes](https://github.com/nonebot/plugin-alconna/releases)
- [Commits](https://github.com/nonebot/plugin-alconna/compare/v0.51.1...v0.51.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-13 16:27:04 +08:00
dependabot[bot]
5910f05dfe ⬆️ Bump aiohttp from 3.10.1 to 3.10.2 (#395)
* ⬆️ Bump aiohttp from 3.10.1 to 3.10.2

Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.10.1 to 3.10.2.
- [Release notes](https://github.com/aio-libs/aiohttp/releases)
- [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/aiohttp/compare/v3.10.1...v3.10.2)

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

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

* 🚨 auto fix by pre-commit hooks

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-08-12 11:20:21 +00:00
eebbd08551 🔧 更新 pre-commit 配置 2024-08-12 19:14:09 +08:00
f035b844ab 🔧 toml 使用单引号 2024-08-12 16:20:04 +08:00
197b81f9cf 🔧 启用一些 ruff 规则 2024-08-12 16:10:14 +08:00
5c2ffe13b0 💡 将 TODO 改成 Maple Font 的连字样式) 2024-08-12 14:40:21 +08:00
b0cff16dc6 🚨 修复 pyright 警告
改用 standard 标准(
2024-08-10 14:56:31 +08:00
dependabot[bot]
3c952530d1 ⬆️ Bump ruff from 0.5.6 to 0.5.7 (#394)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.5.6 to 0.5.7.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.5.6...0.5.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-09 15:53:36 +00:00
dependabot[bot]
57dfc8b94a ⬆️ Bump nonebot-plugin-orm from 0.7.5 to 0.7.6 (#393)
Bumps [nonebot-plugin-orm](https://github.com/nonebot/plugin-orm) from 0.7.5 to 0.7.6.
- [Release notes](https://github.com/nonebot/plugin-orm/releases)
- [Commits](https://github.com/nonebot/plugin-orm/compare/v0.7.5...v0.7.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-09 23:50:07 +08:00
1065e62d11 优化 anti_duplicate_add 2024-08-09 16:25:37 +08:00
02e703ea91 适配新 records API 2024-08-09 16:23:12 +08:00
429f99f77e 🗃️ 增加 TETRIOHistoricalData.api_type 字段长度 2024-08-09 16:20:53 +08:00
9a16e2fa21 新赛季 records 模型 2024-08-08 21:46:24 +08:00
0719d549b5 🔥 删除旧 API 的 user_records 模型 2024-08-08 21:46:23 +08:00
9cb2a90197 🐛 修复 茶服 没有 40l 记录时数值显示错误的bug 2024-08-08 20:48:45 +08:00
dependabot[bot]
bb0606a144 ⬆️ Bump types-lxml from 2024.4.14 to 2024.8.7 (#391)
Bumps [types-lxml](https://github.com/abelcheung/types-lxml) from 2024.4.14 to 2024.8.7.
- [Release notes](https://github.com/abelcheung/types-lxml/releases)
- [Commits](https://github.com/abelcheung/types-lxml/compare/2024.04.14...2024.08.07)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-08 07:34:23 +00:00
dependabot[bot]
41068f7152 ⬆️ Bump nonebot-plugin-user from 0.3.0 to 0.4.0 (#392)
Bumps [nonebot-plugin-user](https://github.com/he0119/nonebot-plugin-user) from 0.3.0 to 0.4.0.
- [Release notes](https://github.com/he0119/nonebot-plugin-user/releases)
- [Changelog](https://github.com/he0119/nonebot-plugin-user/blob/main/CHANGELOG.md)
- [Commits](https://github.com/he0119/nonebot-plugin-user/compare/v0.3.0...v0.4.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-08 07:30:50 +00:00
dependabot[bot]
6f98136c0f ⬆️ Bump nonebot-plugin-alconna from 0.51.0 to 0.51.1 (#390)
Bumps [nonebot-plugin-alconna](https://github.com/nonebot/plugin-alconna) from 0.51.0 to 0.51.1.
- [Release notes](https://github.com/nonebot/plugin-alconna/releases)
- [Commits](https://github.com/nonebot/plugin-alconna/compare/v0.51.0...v0.51.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-08 07:27:38 +00:00
dependabot[bot]
62335abaa6 ⬆️ Bump pandas-stubs from 2.2.2.240805 to 2.2.2.240807 (#389)
Bumps [pandas-stubs](https://github.com/pandas-dev/pandas-stubs) from 2.2.2.240805 to 2.2.2.240807.
- [Changelog](https://github.com/pandas-dev/pandas-stubs/blob/main/docs/release_procedure.md)
- [Commits](https://github.com/pandas-dev/pandas-stubs/compare/v2.2.2.240805...v2.2.2.240807)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-08 15:24:06 +08:00
12a934566d 🔖 1.4.5 2024-08-07 16:44:13 +08:00
ff71dba516 给截图加个耗时统计 2024-08-07 16:41:52 +08:00
e029d51494 🔥 忘记删测试用配置了 2024-08-07 14:38:09 +08:00
dependabot[bot]
b1f48da6fe ⬆️ Bump nonebot-plugin-alconna from 0.50.3 to 0.51.0 (#388)
Bumps [nonebot-plugin-alconna](https://github.com/nonebot/plugin-alconna) from 0.50.3 to 0.51.0.
- [Release notes](https://github.com/nonebot/plugin-alconna/releases)
- [Commits](https://github.com/nonebot/plugin-alconna/compare/v0.50.3...v0.51.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-06 23:43:04 +08:00
9a2927542a 🔥 移除不需要的常量 2024-08-06 20:43:00 +08:00
5117e7dbd9 🔖 1.4.4 2024-08-06 20:12:20 +08:00
4bb00cdeb7 👽️ 移除茶服不可用地址 2024-08-06 20:11:50 +08:00
b7cbe2b2a0 🔥 删除不必要的类型转换
上游修了hhh
2024-08-06 16:35:35 +08:00
8bb460fce0 ⬆️ 更新依赖 2024-08-06 16:34:14 +08:00
41bbcdb66c 🔖 1.4.3 2024-08-06 15:46:11 +08:00
160d81476a 🔥 删除不需要的 type: ignore 2024-08-06 15:35:53 +08:00
1e5b00a280 初步重新适配 TETR.IO query 2024-08-06 15:29:32 +08:00
ee53b92559 🔥 删除不需要的函数调用 2024-08-06 15:28:26 +08:00
cd9d29b748 🚨 修复 pyright 类型报错 2024-08-06 15:27:42 +08:00
214ebc5073 移除对 arclet-alconna 的显式依赖声明 2024-08-06 13:41:13 +08:00
485706267e 🐛 更新 TETR.IO summaries solo 模型 2024-08-06 01:34:07 +08:00
12cb5193b3 🎨 优化模板模型路径
~~真的是优化吗~~
2024-08-06 00:03:02 +08:00
dependabot[bot]
461d3450d6 ⬆️ Bump ruff from 0.5.5 to 0.5.6 (#386)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.5.5 to 0.5.6.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.5.5...0.5.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-05 15:55:56 +00:00
dependabot[bot]
64d77dbff2 ⬆️ Bump pandas-stubs from 2.2.2.240603 to 2.2.2.240805 (#385)
Bumps [pandas-stubs](https://github.com/pandas-dev/pandas-stubs) from 2.2.2.240603 to 2.2.2.240805.
- [Changelog](https://github.com/pandas-dev/pandas-stubs/blob/main/docs/release_procedure.md)
- [Commits](https://github.com/pandas-dev/pandas-stubs/compare/v2.2.2.240603...v2.2.2.240805)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-05 15:52:59 +00:00
dependabot[bot]
e5b4d3bc08 ⬆️ Bump arclet-alconna from 1.8.19 to 1.8.21 (#387)
Bumps [arclet-alconna](https://github.com/ArcletProject/Alconna) from 1.8.19 to 1.8.21.
- [Release notes](https://github.com/ArcletProject/Alconna/releases)
- [Changelog](https://github.com/ArcletProject/Alconna/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/ArcletProject/Alconna/compare/v1.8.19...v1.8.21)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-05 15:49:32 +00:00
dependabot[bot]
4208018caf ⬆️ Bump nonebot-plugin-localstore from 0.7.0 to 0.7.1 (#384)
Bumps [nonebot-plugin-localstore](https://github.com/nonebot/plugin-localstore) from 0.7.0 to 0.7.1.
- [Release notes](https://github.com/nonebot/plugin-localstore/releases)
- [Commits](https://github.com/nonebot/plugin-localstore/compare/v0.7.0...v0.7.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-05 15:45:53 +00:00
dependabot[bot]
5032a3eb9a ⬆️ Bump nonebot-plugin-session from 0.3.1 to 0.3.2 (#383)
Bumps [nonebot-plugin-session](https://github.com/noneplugin/nonebot-plugin-session) from 0.3.1 to 0.3.2.
- [Release notes](https://github.com/noneplugin/nonebot-plugin-session/releases)
- [Commits](https://github.com/noneplugin/nonebot-plugin-session/compare/v0.3.1...v0.3.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-05 23:42:23 +08:00
dependabot[bot]
bf9a9953dd ⬆️ Bump nonebot-adapter-qq from 1.4.4 to 1.5.0 (#381)
Bumps [nonebot-adapter-qq](https://github.com/nonebot/adapter-qq) from 1.4.4 to 1.5.0.
- [Release notes](https://github.com/nonebot/adapter-qq/releases)
- [Commits](https://github.com/nonebot/adapter-qq/compare/v1.4.4...v1.5.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-05 04:10:49 +00:00
dependabot[bot]
85feb9cb41 ⬆️ Bump mypy from 1.11.0 to 1.11.1 (#382)
Bumps [mypy](https://github.com/python/mypy) from 1.11.0 to 1.11.1.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.11...v1.11.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-05 04:07:14 +00:00
5a7c54528c 🐛 修正 record 使用的 type 2024-08-05 12:02:50 +08:00
afce74afe8 修改命令注册逻辑 2024-08-05 11:56:48 +08:00
435850819c 🔖 1.4.2 2024-08-04 19:57:47 +08:00
6f439ad357 适配新模板 2024-08-04 19:22:36 +08:00
b74cc1f4a0 🐛 修复 TETR.IO 获取 user 时出现 UnboundLocalError 2024-08-04 19:21:52 +08:00
1a1c2675d1 再次更新模板仓库处理逻辑 2024-08-03 23:52:45 +08:00
1f02c107f5 AR排行榜 API 模型 2024-08-03 16:47:57 +08:00
89c319a500 完善 PluginMetadata 2024-08-02 22:46:00 +08:00
56f9a69c4d 🙈 更新 .gitignore 2024-08-02 22:19:59 +08:00
50431fe7cb 新赛季排行榜 API 模型 2024-08-02 22:15:46 +08:00
71ad53a1f9 适配 TETR.IO rank v1 模板 2024-08-02 22:15:46 +08:00
820393f216 🎨 减少两个 overload 2024-08-02 22:15:45 +08:00
27994cea6b 🗃️ 清空 TETR.IO 旧赛季数据 2024-08-02 22:15:45 +08:00
105 changed files with 2389 additions and 1643 deletions

2
.gitignore vendored
View File

@@ -20,3 +20,5 @@ bot.py
TODO TODO
*.fish *.fish
extracted_skin_mino_* extracted_skin_mino_*
sample_*
TODO*

View File

@@ -2,12 +2,12 @@ default_install_hook_types: [pre-commit, prepare-commit-msg]
ci: ci:
autofix_commit_msg: ':rotating_light: auto fix by pre-commit hooks' autofix_commit_msg: ':rotating_light: auto fix by pre-commit hooks'
autofix_prs: true autofix_prs: true
autoupdate_branch: master autoupdate_branch: main
autoupdate_schedule: monthly autoupdate_schedule: weekly
autoupdate_commit_msg: ':arrow_up: auto update by pre-commit hooks' autoupdate_commit_msg: ':arrow_up: auto update by pre-commit hooks'
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.10 rev: v0.5.7
hooks: hooks:
- id: ruff - id: ruff
args: [--fix, --exit-non-zero-on-fix] args: [--fix, --exit-non-zero-on-fix]

View File

@@ -1,14 +1,19 @@
from nonebot import require from nonebot import require
from nonebot.plugin import PluginMetadata from nonebot.plugin import PluginMetadata, inherit_supported_adapters
require('nonebot_plugin_alconna') require_plugins = {
require('nonebot_plugin_apscheduler') 'nonebot_plugin_alconna',
require('nonebot_plugin_localstore') 'nonebot_plugin_apscheduler',
require('nonebot_plugin_orm') 'nonebot_plugin_localstore',
require('nonebot_plugin_session_orm') 'nonebot_plugin_orm',
require('nonebot_plugin_session') 'nonebot_plugin_session_orm',
require('nonebot_plugin_user') 'nonebot_plugin_session',
require('nonebot_plugin_userinfo') 'nonebot_plugin_user',
'nonebot_plugin_userinfo',
}
for i in require_plugins:
require(i)
from nonebot_plugin_alconna import namespace # noqa: E402 from nonebot_plugin_alconna import namespace # noqa: E402
@@ -16,6 +21,7 @@ with namespace('tetris_stats') as ns:
ns.enable_message_cache = False ns.enable_message_cache = False
from .config import migrations # noqa: E402 from .config import migrations # noqa: E402
from .config.config import Config # noqa: E402
__plugin_meta__ = PluginMetadata( __plugin_meta__ = PluginMetadata(
name='Tetris Stats', name='Tetris Stats',
@@ -23,6 +29,8 @@ __plugin_meta__ = PluginMetadata(
usage='发送 tstats --help 查询使用方法', usage='发送 tstats --help 查询使用方法',
type='application', type='application',
homepage='https://github.com/A-minos/nonebot-plugin-tetris-stats', homepage='https://github.com/A-minos/nonebot-plugin-tetris-stats',
config=Config,
supported_adapters=inherit_supported_adapters(*require_plugins),
extra={ extra={
'orm_version_location': migrations, 'orm_version_location': migrations,
}, },

View File

@@ -1,13 +1,14 @@
from pathlib import Path from nonebot_plugin_localstore import get_cache_dir, get_data_dir
from pydantic import BaseModel, Field
from nonebot_plugin_localstore import get_cache_dir # type: ignore[import-untyped] CACHE_PATH = get_cache_dir('nonebot_plugin_tetris_stats')
from pydantic import BaseModel DATA_PATH = get_data_dir('nonebot_plugin_tetris_stats')
CACHE_PATH: Path = get_cache_dir('nonebot_plugin_tetris_stats')
class ScopedConfig(BaseModel):
request_timeout: float = 30.0
screenshot_quality: float = 2
class Config(BaseModel): class Config(BaseModel):
"""配置类""" tetris: ScopedConfig = Field(default_factory=ScopedConfig)
tetris_req_timeout: float = 30.0
tetris_screenshot_quality: float = 2

View File

@@ -0,0 +1,50 @@
"""Extend api_type field length
迁移 ID: cfeab6961dce
父迁移: f5b4a6d1325b
创建时间: 2024-08-09 14:20:59.789030
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import sqlalchemy as sa
from alembic import op
from nonebot.log import logger
if TYPE_CHECKING:
from collections.abc import Sequence
revision: str = 'cfeab6961dce'
down_revision: str | Sequence[str] | None = 'f5b4a6d1325b'
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade(name: str = '') -> None:
if name:
return
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('nonebot_plugin_tetris_stats_tetriohistoricaldata', schema=None) as batch_op:
batch_op.alter_column(
'api_type', existing_type=sa.VARCHAR(length=16), type_=sa.String(length=32), existing_nullable=False
)
# ### end Alembic commands ###
def downgrade(name: str = '') -> None:
if name:
return
# ### commands auto generated by Alembic - please adjust! ###
logger.warning('新数据可能不支持降级!')
logger.warning('请确认数据库内数据可以迁移到旧版本!')
input('如果确认可以迁移, 请按回车键继续!')
with op.batch_alter_table('nonebot_plugin_tetris_stats_tetriohistoricaldata', schema=None) as batch_op:
batch_op.alter_column(
'api_type', existing_type=sa.String(length=32), type_=sa.VARCHAR(length=16), existing_nullable=False
)
# ### end Alembic commands ###

View File

@@ -0,0 +1,91 @@
"""TETR.IO new season
迁移 ID: f5b4a6d1325b
父迁移: a1195e989cc6
创建时间: 2024-08-01 20:44:48.644912
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import sqlalchemy as sa
from alembic import op
if TYPE_CHECKING:
from collections.abc import Sequence
revision: str = 'f5b4a6d1325b'
down_revision: str | Sequence[str] | None = 'a1195e989cc6'
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade(name: str = '') -> None:
if name:
return
with op.batch_alter_table('nonebot_plugin_tetris_stats_iorank', schema=None) as batch_op:
batch_op.drop_index('ix_nonebot_plugin_tetris_stats_iorank_file_hash')
batch_op.drop_index('ix_nonebot_plugin_tetris_stats_iorank_rank')
batch_op.drop_index('ix_nonebot_plugin_tetris_stats_iorank_update_time')
op.drop_table('nonebot_plugin_tetris_stats_iorank')
with op.batch_alter_table('nonebot_plugin_tetris_stats_tetriohistoricaldata', schema=None) as batch_op:
batch_op.drop_index('ix_nonebot_plugin_tetris_stats_tetriohistoricaldata_api_type')
batch_op.drop_index('ix_nonebot_plugin_tetris_stats_tetriohistoricaldata_update_time')
batch_op.drop_index('ix_nonebot_plugin_tetris_stats_tetriohistoricaldata_user_unique_identifier')
op.drop_table('nonebot_plugin_tetris_stats_tetriohistoricaldata')
op.create_table(
'nonebot_plugin_tetris_stats_tetriohistoricaldata',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_unique_identifier', sa.String(length=24), nullable=False),
sa.Column('api_type', sa.String(length=16), nullable=False),
sa.Column('data', sa.JSON(), nullable=False),
sa.Column('update_time', sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('pk_nonebot_plugin_tetris_stats_tetriohistoricaldata')),
info={'bind_key': 'nonebot_plugin_tetris_stats'},
)
with op.batch_alter_table('nonebot_plugin_tetris_stats_tetriohistoricaldata', schema=None) as batch_op:
batch_op.create_index(
batch_op.f('ix_nonebot_plugin_tetris_stats_tetriohistoricaldata_api_type'), ['api_type'], unique=False
)
batch_op.create_index(
batch_op.f('ix_nonebot_plugin_tetris_stats_tetriohistoricaldata_update_time'), ['update_time'], unique=False
)
batch_op.create_index(
batch_op.f('ix_nonebot_plugin_tetris_stats_tetriohistoricaldata_user_unique_identifier'),
['user_unique_identifier'],
unique=False,
)
def downgrade(name: str = '') -> None:
if name:
return
op.create_table(
'nonebot_plugin_tetris_stats_iorank',
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('rank', sa.VARCHAR(length=2), nullable=False),
sa.Column('tr_line', sa.FLOAT(), nullable=False),
sa.Column('player_count', sa.INTEGER(), nullable=False),
sa.Column('low_pps', sa.JSON(), nullable=False),
sa.Column('low_apm', sa.JSON(), nullable=False),
sa.Column('low_vs', sa.JSON(), nullable=False),
sa.Column('avg_pps', sa.FLOAT(), nullable=False),
sa.Column('avg_apm', sa.FLOAT(), nullable=False),
sa.Column('avg_vs', sa.FLOAT(), nullable=False),
sa.Column('high_pps', sa.JSON(), nullable=False),
sa.Column('high_apm', sa.JSON(), nullable=False),
sa.Column('high_vs', sa.JSON(), nullable=False),
sa.Column('update_time', sa.DATETIME(), nullable=False),
sa.Column('file_hash', sa.VARCHAR(length=128), nullable=True),
sa.PrimaryKeyConstraint('id', name='pk_nonebot_plugin_tetris_stats_iorank'),
)
with op.batch_alter_table('nonebot_plugin_tetris_stats_iorank', schema=None) as batch_op:
batch_op.create_index('ix_nonebot_plugin_tetris_stats_iorank_update_time', ['update_time'], unique=False)
batch_op.create_index('ix_nonebot_plugin_tetris_stats_iorank_rank', ['rank'], unique=False)
batch_op.create_index('ix_nonebot_plugin_tetris_stats_iorank_file_hash', ['file_hash'], unique=False)

View File

@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Literal, TypeVar, overload
from nonebot.exception import FinishedException from nonebot.exception import FinishedException
from nonebot.log import logger from nonebot.log import logger
from nonebot_plugin_orm import AsyncSession, get_session from nonebot_plugin_orm import AsyncSession, get_session
from nonebot_plugin_user import User # type: ignore[import-untyped] from nonebot_plugin_user import User
from sqlalchemy import select from sqlalchemy import select
from ..utils.typing import AllCommandType, BaseCommandType, GameType, TETRIOCommandType from ..utils.typing import AllCommandType, BaseCommandType, GameType, TETRIOCommandType
@@ -68,11 +68,11 @@ T = TypeVar('T', 'TETRIOHistoricalData', 'TOPHistoricalData', 'TOSHistoricalData
lock = Lock() lock = Lock()
async def anti_duplicate_add(cls: type[T], model: T) -> None: async def anti_duplicate_add(model: T) -> None:
async with lock, get_session() as session: async with lock, get_session() as session:
result = ( result = (
await session.scalars( await session.scalars(
select(cls) select(cls := model.__class__)
.where(cls.update_time == model.update_time) .where(cls.update_time == model.update_time)
.where(cls.user_unique_identifier == model.user_unique_identifier) .where(cls.user_unique_identifier == model.user_unique_identifier)
.where(cls.api_type == model.api_type) .where(cls.api_type == model.api_type)

View File

@@ -1,3 +1 @@
BIND_COMMAND: list[str] = ['绑定', 'bind']
QUERY_COMMAND: list[str] = ['', '查询', 'query', 'stats']
CANT_VERIFY_MESSAGE = '* 由于无法验证绑定信息, 不能保证查询到的用户为本人\n' CANT_VERIFY_MESSAGE = '* 由于无法验证绑定信息, 不能保证查询到的用户为本人\n'

View File

@@ -1,20 +1,21 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Generic, TypeVar
from pydantic import BaseModel from pydantic import BaseModel
from ..utils.typing import GameType from ..utils.typing import GameType
T = TypeVar('T', bound=GameType)
class Base(BaseModel):
platform: GameType
class BaseUser(ABC, Base): class BaseUser(BaseModel, ABC, Generic[T]):
"""游戏用户""" """游戏用户"""
def __eq__(self, __value: object) -> bool: platform: T
if isinstance(__value, BaseUser):
return self.unique_identifier == __value.unique_identifier def __eq__(self, other: object) -> bool:
if isinstance(other, BaseUser):
return self.unique_identifier == other.unique_identifier
return False return False
@property @property

View File

@@ -1,16 +1,10 @@
from arclet.alconna import Arg, ArgFlag, Args, Option, Subcommand from nonebot_plugin_alconna import Subcommand
from nonebot_plugin_alconna import At
from ...utils.exception import MessageFormatError from ...utils.exception import MessageFormatError
from ...utils.typing import Me from .. import alc
from .. import command as main_command
# from .. import add_block_handlers, alc, command
from .. import alc, command
from .api import Player from .api import Player
# from .api.typing import ValidRank
from .constant import USER_ID, USER_NAME from .constant import USER_ID, USER_NAME
from .typing import Template
def get_player(user_id_or_name: str) -> Player | MessageFormatError: def get_player(user_id_or_name: str) -> Player | MessageFormatError:
@@ -21,171 +15,22 @@ def get_player(user_id_or_name: str) -> Player | MessageFormatError:
return MessageFormatError('用户名/ID不合法') return MessageFormatError('用户名/ID不合法')
command.add( command = Subcommand(
Subcommand( 'TETR.IO',
'TETR.IO', alias=['TETRIO', 'tetr.io', 'tetrio', 'io'],
Subcommand( dest='TETRIO',
'bind', help_text='TETR.IO 游戏相关指令',
Args(
Arg(
'account',
get_player,
notice='TETR.IO 用户名 / ID',
flags=[ArgFlag.HIDDEN],
)
),
help_text='绑定 TETR.IO 账号',
),
# Subcommand(
# 'query',
# Args(
# Arg(
# 'target',
# At | Me,
# notice='@想要查询的人 / 自己',
# flags=[ArgFlag.HIDDEN, ArgFlag.OPTIONAL],
# ),
# Arg(
# 'account',
# get_player,
# notice='TETR.IO 用户名 / ID',
# flags=[ArgFlag.HIDDEN, ArgFlag.OPTIONAL],
# ),
# ),
# Option(
# '--template',
# Arg('template', Template),
# alias=['-T'],
# help_text='要使用的查询模板',
# ),
# help_text='查询 TETR.IO 游戏信息',
# ),
Subcommand(
'record',
Option(
'--40l',
dest='sprint',
),
Option(
'--blitz',
dest='blitz',
),
Args(
Arg(
'target',
At | Me,
notice='@想要查询的人 / 自己',
flags=[ArgFlag.HIDDEN, ArgFlag.OPTIONAL],
),
Arg(
'account',
get_player,
notice='TETR.IO 用户名 / ID',
flags=[ArgFlag.HIDDEN, ArgFlag.OPTIONAL],
),
),
),
# Subcommand(
# 'list',
# Option('--max-tr', Arg('max_tr', float), help_text='TR的上限'),
# Option('--min-tr', Arg('min_tr', float), help_text='TR的下限'),
# Option('--limit', Arg('limit', int), help_text='查询数量'),
# Option('--country', Arg('country', str), help_text='国家代码'),
# help_text='查询 TETR.IO 段位排行榜',
# ),
# Subcommand(
# 'rank',
# Subcommand(
# '--all',
# Option(
# '--template',
# Arg('template', Template),
# alias=['-T'],
# help_text='要使用的查询模板',
# ),
# dest='all',
# ),
# Option(
# '--detail',
# Arg('rank', ValidRank),
# alias=['-D'],
# ),
# help_text='查询 TETR.IO 段位信息',
# ),
Subcommand(
'config',
Option(
'--default-template',
Arg('template', Template),
alias=['-DT', 'DefaultTemplate'],
),
),
alias=['TETRIO', 'tetr.io', 'tetrio', 'io'],
dest='TETRIO',
help_text='TETR.IO 游戏相关指令',
)
) )
# def rank_wrapper(slot: int | str, content: str | None): from . import bind, config, query, record # noqa: E402
# if slot == 'rank' and not content:
# return '--all'
# if content is not None:
# return f'--detail {content.lower()}'
# return content
main_command.add(command)
alc.shortcut(
'(?i:io)(?i:绑定|绑|bind)',
command='tstats TETR.IO bind',
humanized='io绑定',
)
# alc.shortcut(
# '(?i:io)(?i:查询|查|query|stats)',
# command='tstats TETR.IO query',
# humanized='io查',
# )
alc.shortcut(
'(?i:io)(?i:记录|record)(?i:40l)',
command='tstats TETR.IO record --40l',
humanized='io记录40l',
)
alc.shortcut(
'(?i:io)(?i:记录|record)(?i:blitz)',
command='tstats TETR.IO record --blitz',
humanized='io记录blitz',
)
# alc.shortcut(
# r'(?i:io)(?i:段位|段|rank)\s*(?P<rank>[a-zA-Z+-]{0,2})',
# command='tstats TETR.IO rank {rank}',
# humanized='iorank',
# fuzzy=False,
# wrapper=rank_wrapper,
# )
alc.shortcut(
'(?i:io)(?i:配置|配|config)',
command='tstats TETR.IO config',
humanized='io配置',
)
# alc.shortcut(
# 'fkosk',
# command='tstats TETR.IO query',
# arguments=['我'],
# fuzzy=False,
# humanized='An Easter egg!',
# )
# add_block_handlers(alc.assign('TETRIO.query'))
# from . import bind, config, list, query, rank, record
from . import bind, config, record # noqa: E402
__all__ = [ __all__ = [
'alc',
'bind', 'bind',
'config', 'config',
# 'list', 'query',
# 'query',
# 'rank',
'record', 'record',
] ]

View File

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

View File

@@ -7,12 +7,12 @@ from sqlalchemy.orm import Mapped, MappedAsDataclass, mapped_column
from ....db.models import PydanticType from ....db.models import PydanticType
from .schemas.base import SuccessModel from .schemas.base import SuccessModel
from .typing import Summaries from .typing import Records, Summaries
class TETRIOHistoricalData(MappedAsDataclass, Model): class TETRIOHistoricalData(MappedAsDataclass, Model):
id: Mapped[int] = mapped_column(init=False, primary_key=True) id: Mapped[int] = mapped_column(init=False, primary_key=True)
user_unique_identifier: Mapped[str] = mapped_column(String(24), index=True) user_unique_identifier: Mapped[str] = mapped_column(String(24), index=True)
api_type: Mapped[Literal['User Info', Summaries]] = mapped_column(String(16), index=True) api_type: Mapped[Literal['User Info', Records, Summaries]] = mapped_column(String(32), index=True)
data: Mapped[SuccessModel] = mapped_column(PydanticType(get_model=[SuccessModel.__subclasses__], models=set())) data: Mapped[SuccessModel] = mapped_column(PydanticType(get_model=[SuccessModel.__subclasses__], models=set()))
update_time: Mapped[datetime] = mapped_column(DateTime, index=True) update_time: Mapped[datetime] = mapped_column(DateTime, index=True)

View File

@@ -1,5 +1,6 @@
from enum import Enum
from types import MappingProxyType from types import MappingProxyType
from typing import Literal, overload from typing import Literal, NamedTuple, cast, overload
from async_lru import alru_cache from async_lru import alru_cache
from nonebot.compat import type_validate_json from nonebot.compat import type_validate_json
@@ -11,26 +12,51 @@ from ..constant import BASE_URL, USER_ID, USER_NAME
from .cache import Cache from .cache import Cache
from .models import TETRIOHistoricalData from .models import TETRIOHistoricalData
from .schemas.base import FailedModel from .schemas.base import FailedModel
from .schemas.records.solo import Solo as SoloRecord
from .schemas.records.solo import SoloSuccessModel as RecordsSoloSuccessModel
from .schemas.summaries import ( from .schemas.summaries import (
AchievementsSuccessModel, AchievementsSuccessModel,
SoloSuccessModel,
SummariesModel, SummariesModel,
ZenithSuccessModel, ZenithSuccessModel,
ZenSuccessModel, ZenSuccessModel,
) )
from .schemas.summaries import (
SoloSuccessModel as SummariesSoloSuccessModel,
)
from .schemas.summaries.base import User as SummariesUser from .schemas.summaries.base import User as SummariesUser
from .schemas.summaries.league import LeagueSuccessModel
from .schemas.user import User from .schemas.user import User
from .schemas.user_info import UserInfo, UserInfoSuccess from .schemas.user_info import UserInfo, UserInfoSuccess
from .typing import Summaries from .typing import Records, Summaries
class RecordModeType(str, Enum):
Sprint = '40l'
Blitz = 'blitz'
class RecordType(str, Enum):
Top = 'top'
Recent = 'recent'
Progression = 'progression'
class RecordKey(NamedTuple):
mode_type: RecordModeType
record_type: RecordType
def to_records(self) -> Records:
return cast(Records, f'{self.mode_type.value}_{self.record_type.value}')
class Player: class Player:
__SUMMARIES_MAPPING: MappingProxyType[Summaries, type[SummariesModel]] = MappingProxyType( __SUMMARIES_MAPPING: MappingProxyType[Summaries, type[SummariesModel]] = MappingProxyType(
{ {
'40l': SoloSuccessModel, '40l': SummariesSoloSuccessModel,
'blitz': SoloSuccessModel, 'blitz': SummariesSoloSuccessModel,
'zenith': ZenithSuccessModel, 'zenith': ZenithSuccessModel,
'zenithex': ZenithSuccessModel, 'zenithex': ZenithSuccessModel,
'league': LeagueSuccessModel,
'zen': ZenSuccessModel, 'zen': ZenSuccessModel,
'achievements': AchievementsSuccessModel, 'achievements': AchievementsSuccessModel,
} }
@@ -58,6 +84,7 @@ class Player:
self.__user: User | None = None self.__user: User | None = None
self._user_info: UserInfoSuccess | None = None self._user_info: UserInfoSuccess | None = None
self._summaries: dict[Summaries, SummariesModel] = {} self._summaries: dict[Summaries, SummariesModel] = {}
self._records: dict[RecordKey, RecordsSoloSuccessModel] = {}
@property @property
def _request_user_parameter(self) -> str: def _request_user_parameter(self) -> str:
@@ -83,8 +110,8 @@ class Player:
ID=user_info.data.id, ID=user_info.data.id,
name=user_info.data.username, name=user_info.data.username,
) )
self.user_id = user_info.data.id self.user_id = self.__user.ID
self.user_name = user_info.data.username self.user_name = self.__user.name
return self.__user return self.__user
async def get_info(self) -> UserInfoSuccess: async def get_info(self) -> UserInfoSuccess:
@@ -97,7 +124,6 @@ class Player:
raise RequestError(msg) raise RequestError(msg)
self._user_info = user_info self._user_info = user_info
await anti_duplicate_add( await anti_duplicate_add(
TETRIOHistoricalData,
TETRIOHistoricalData( TETRIOHistoricalData(
user_unique_identifier=(await self.user).unique_identifier, user_unique_identifier=(await self.user).unique_identifier,
api_type='User Info', api_type='User Info',
@@ -108,16 +134,14 @@ class Player:
return self._user_info return self._user_info
@overload @overload
async def get_summaries(self, summaries_type: Literal['40l']) -> SoloSuccessModel: ... async def get_summaries(self, summaries_type: Literal['40l', 'blitz']) -> SummariesSoloSuccessModel: ...
@overload @overload
async def get_summaries(self, summaries_type: Literal['blitz']) -> SoloSuccessModel: ... async def get_summaries(self, summaries_type: Literal['zenith', 'zenithex']) -> ZenithSuccessModel: ...
@overload
async def get_summaries(self, summaries_type: Literal['zenith']) -> ZenithSuccessModel: ...
@overload
async def get_summaries(self, summaries_type: Literal['zenithex']) -> ZenithSuccessModel: ...
@overload @overload
async def get_summaries(self, summaries_type: Literal['zen']) -> ZenSuccessModel: ... async def get_summaries(self, summaries_type: Literal['zen']) -> ZenSuccessModel: ...
@overload @overload
async def get_summaries(self, summaries_type: Literal['league']) -> LeagueSuccessModel: ...
@overload
async def get_summaries(self, summaries_type: Literal['achievements']) -> AchievementsSuccessModel: ... async def get_summaries(self, summaries_type: Literal['achievements']) -> AchievementsSuccessModel: ...
async def get_summaries(self, summaries_type: Summaries) -> SummariesModel: async def get_summaries(self, summaries_type: Summaries) -> SummariesModel:
@@ -134,7 +158,6 @@ class Player:
raise RequestError(msg) raise RequestError(msg)
self._summaries[summaries_type] = summaries self._summaries[summaries_type] = summaries
await anti_duplicate_add( await anti_duplicate_add(
TETRIOHistoricalData,
TETRIOHistoricalData( TETRIOHistoricalData(
user_unique_identifier=(await self.user).unique_identifier, user_unique_identifier=(await self.user).unique_identifier,
api_type=summaries_type, api_type=summaries_type,
@@ -145,20 +168,21 @@ class Player:
return self._summaries[summaries_type] return self._summaries[summaries_type]
@property @property
@alru_cache async def sprint(self) -> SummariesSoloSuccessModel:
async def sprint(self) -> SoloSuccessModel:
return await self.get_summaries('40l') return await self.get_summaries('40l')
@property @property
@alru_cache async def blitz(self) -> SummariesSoloSuccessModel:
async def blitz(self) -> SoloSuccessModel:
return await self.get_summaries('blitz') return await self.get_summaries('blitz')
@property @property
@alru_cache
async def zen(self) -> ZenSuccessModel: async def zen(self) -> ZenSuccessModel:
return await self.get_summaries('zen') return await self.get_summaries('zen')
@property
async def league(self) -> LeagueSuccessModel:
return await self.get_summaries('league')
async def _get_local_summaries_user(self) -> SummariesUser | None: async def _get_local_summaries_user(self) -> SummariesUser | None:
allow_summaries: set[Literal['40l', 'blitz', 'zenith', 'zenithex']] = { allow_summaries: set[Literal['40l', 'blitz', 'zenith', 'zenithex']] = {
'40l', '40l',
@@ -189,3 +213,32 @@ class Player:
if (user := (await self._get_local_summaries_user())) is not None: if (user := (await self._get_local_summaries_user())) is not None:
return user.banner_revision return user.banner_revision
return (await self.get_info()).data.banner_revision return (await self.get_info()).data.banner_revision
async def get_records(self, mode_type: RecordModeType, records_type: RecordType) -> RecordsSoloSuccessModel:
if (record_key := RecordKey(mode_type, records_type)) not in self._records:
raw_records = await Cache.get(
splice_url(
[
BASE_URL,
'users/',
f'{self._request_user_parameter}/',
'records/',
f'{mode_type}/',
records_type,
]
)
)
records: RecordsSoloSuccessModel | FailedModel = type_validate_json(SoloRecord, raw_records) # type: ignore[arg-type]
if isinstance(records, FailedModel):
msg = f'用户Summaries数据请求错误:\n{records.error}'
raise RequestError(msg)
self._records[record_key] = records
await anti_duplicate_add(
TETRIOHistoricalData(
user_unique_identifier=(await self.user).unique_identifier,
api_type=record_key.to_records(),
data=records,
update_time=records.cache.cached_at,
),
)
return self._records[record_key]

View File

@@ -1,20 +0,0 @@
from datetime import datetime
from typing import Literal
from pydantic import BaseModel
class Cache(BaseModel):
status: str
cached_at: datetime
cached_until: datetime
class SuccessModel(BaseModel):
success: Literal[True]
cache: Cache
class FailedModel(BaseModel):
success: Literal[False]
error: str

View File

@@ -0,0 +1,61 @@
from datetime import datetime
from typing import Literal
from pydantic import BaseModel
class AggregateStats(BaseModel):
apm: float
pps: float
vsscore: float
class Finesse(BaseModel):
combo: int
faults: int
perfectpieces: int
class Clears(BaseModel):
singles: int
doubles: int
triples: int
quads: int
realtspins: int
minitspins: int
minitspinsingles: int
tspinsingles: int
minitspindoubles: int
tspindoubles: int
tspintriples: int
tspinquads: int
allclear: int
class Garbage(BaseModel):
sent: int
received: int
attack: int | None
cleared: int
class P(BaseModel): # what is P
pri: float
sec: float
ter: float
class Cache(BaseModel):
status: str
cached_at: datetime
cached_until: datetime
class SuccessModel(BaseModel):
success: Literal[True]
cache: Cache
class FailedModel(BaseModel):
success: Literal[False]
error: str

View File

@@ -0,0 +1,65 @@
from datetime import datetime
from typing import Literal
from pydantic import BaseModel, Field
from ..base import P
from . import AggregateStats, Clears, Finesse, Garbage
class Time(BaseModel):
start: int
zero: bool
locked: bool
prev: int
frameoffset: int
class Stats(BaseModel):
seed: int | None = None # ?: 不知道是之后都没有了还是还会有
lines: int
level_lines: int
level_lines_needed: int
inputs: int
holds: int
time: Time | None = None # ?: 不知道是之后都没有了还是还会有
score: int
zenlevel: int
zenprogress: int
level: int
combo: int
currentcombopower: int | None = None
topcombo: int
btb: int
topbtb: int
currentbtbchainpower: int | None = None
tspins: int
piecesplaced: int
clears: Clears
garbage: Garbage
kills: int
finesse: Finesse
finaltime: float
class Results(BaseModel):
aggregatestats: AggregateStats
stats: Stats
gameoverreason: str
class Record(BaseModel):
id: str = Field(..., alias='_id')
replayid: str
stub: bool
gamemode: Literal['40l', 'blitz']
pb: bool
oncepb: bool
ts: datetime
revolution: None
otherusers: list
leaderboards: list[str]
results: Results
extras: dict
disputed: bool
p: P

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,17 @@
from typing import TypeAlias
from pydantic import BaseModel
from ..base import FailedModel, SuccessModel
from ..base.solo import Record
class Data(BaseModel):
entries: list[Record]
class SoloSuccessModel(SuccessModel):
data: Data
Solo: TypeAlias = SoloSuccessModel | FailedModel

View File

@@ -1,20 +1,21 @@
from .achievements import Achievements, AchievementsSuccessModel from .achievements import Achievements, AchievementsSuccessModel
from .solo import Blitz, SoloSuccessModel, Sprint from .league import LeagueSuccessModel
from .solo import Solo, SoloSuccessModel
from .zen import Zen, ZenSuccessModel from .zen import Zen, ZenSuccessModel
from .zenith import Zenith, ZenithEx, ZenithSuccessModel from .zenith import Zenith, ZenithEx, ZenithSuccessModel
SummariesModel = AchievementsSuccessModel | SoloSuccessModel | ZenSuccessModel | ZenithSuccessModel SummariesModel = AchievementsSuccessModel | SoloSuccessModel | ZenSuccessModel | LeagueSuccessModel | ZenithSuccessModel
__all__ = [ __all__ = [
'Achievements', 'Achievements',
'AchievementsSuccessModel', 'AchievementsSuccessModel',
'Blitz', 'LeagueSuccessModel',
'Sprint', 'Solo',
'SoloSuccessModel', 'SoloSuccessModel',
'SummariesModel',
'Zen', 'Zen',
'ZenSuccessModel',
'Zenith', 'Zenith',
'ZenithEx', 'ZenithEx',
'ZenithSuccessModel', 'ZenithSuccessModel',
'SummariesModel', 'ZenSuccessModel',
] ]

View File

@@ -4,26 +4,8 @@ from pydantic import BaseModel
class User(BaseModel): class User(BaseModel):
id: str id: str
username: str username: str
avatar_revision: int avatar_revision: int | None
banner_revision: int banner_revision: int | None
country: str country: str | None
verified: int verified: int | None = None
supporter: int supporter: int
class AggregateStats(BaseModel):
apm: float
pps: float
vsscore: float
class Finesse(BaseModel):
combo: int
faults: int
perfectpieces: int
class P(BaseModel): # what is P
pri: float
sec: float
ter: float

View File

@@ -0,0 +1,55 @@
from pydantic import BaseModel, Field
from ...typing import Rank, S1Rank, S1ValidRank, ValidRank
from ..base import SuccessModel
class PastInner(BaseModel):
season: str
username: str
country: str
placement: int
gamesplayed: int
gameswon: int
glicko: float
gxe: float
tr: float
rd: float
rank: S1Rank
bestrank: S1ValidRank
ranked: bool
apm: float
pps: float
vs: float
class Past(BaseModel):
first: PastInner = Field(..., alias='1')
class Data(BaseModel):
gamesplayed: int
gameswon: int
glicko: float
rd: float
gxe: float
tr: float
rank: Rank
bestrank: Rank = Field('z')
apm: float
pps: float
vs: float
decaying: bool
standing: int
standing_local: int
prev_rank: ValidRank | None = None
prev_at: int
next_rank: ValidRank | None = None
next_at: int
percentile: float
percentile_rank: Rank
past: Past
class LeagueSuccessModel(SuccessModel):
data: Data

View File

@@ -1,92 +1,14 @@
from datetime import datetime from typing import TypeAlias
from typing import Literal, TypeAlias
from pydantic import BaseModel, Field from pydantic import BaseModel
from ..base import FailedModel, SuccessModel from ..base import FailedModel, SuccessModel
from .base import AggregateStats, Finesse, P, User from ..base.solo import Record as BaseRecord
from .base import User
class Time(BaseModel): class Record(BaseRecord):
start: int
zero: bool
locked: bool
prev: int
frameoffset: int
class Clears(BaseModel):
singles: int
doubles: int
triples: int
quads: int
realtspins: int
minitspins: int
minitspinsingles: int
tspinsingles: int
minitspindoubles: int
tspindoubles: int
tspintriples: int
tspinquads: int
allclear: int
class Garbage(BaseModel):
sent: int
received: int
attack: int
cleared: int
class Stats(BaseModel):
seed: int
lines: int
level_lines: int
level_lines_needed: int
inputs: int
holds: int
time: Time
score: int
zenlevel: int
zenprogress: int
level: int
combo: int
currentcombopower: int | None = None
topcombo: int
btb: int
topbtb: int
currentbtbchainpower: int | None = None
tspins: int
piecesplaced: int
clears: Clears
garbage: Garbage
kills: int
finesse: Finesse
finaltime: float
class Results(BaseModel):
aggregatestats: AggregateStats
stats: Stats
gameoverreason: str
class Record(BaseModel):
id: str = Field(..., alias='_id')
replayid: str
stub: bool
gamemode: Literal['40l', 'blitz']
pb: bool
oncepb: bool
ts: datetime
revolution: None
user: User user: User
otherusers: list
leaderboards: list[str]
results: Results
extras: dict
disputed: bool
p: P
class Data(BaseModel): class Data(BaseModel):
@@ -99,5 +21,4 @@ class SoloSuccessModel(SuccessModel):
data: Data data: Data
Sprint: TypeAlias = SoloSuccessModel | FailedModel Solo: TypeAlias = SoloSuccessModel | FailedModel
Blitz: TypeAlias = SoloSuccessModel | FailedModel

View File

@@ -3,38 +3,23 @@ from typing import Literal, TypeAlias
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from ..base import FailedModel, SuccessModel from ..base import AggregateStats, FailedModel, Finesse, P, SuccessModel
from .base import AggregateStats, Finesse, P, User from ..base import Clears as BaseClears
from ..base import Garbage as BaseGarbage
from .base import User
class Clears(BaseModel): class Clears(BaseClears):
singles: int
doubles: int
triples: int
quads: int
pentas: int pentas: int
realtspins: int
minitspins: int
minitspinsingles: int
tspinsingles: int
minitspindoubles: int
tspindoubles: int
minitspintriples: int minitspintriples: int
tspintriples: int
minitspinquads: int minitspinquads: int
tspinquads: int
tspinpentas: int tspinpentas: int
allclear: int
class Garbage(BaseModel): class Garbage(BaseGarbage):
sent: int
sent_nomult: int sent_nomult: int
maxspike: int maxspike: int
maxspike_nomult: int maxspike_nomult: int
received: int
attack: int
cleared: int
class _Zenith(BaseModel): class _Zenith(BaseModel):
@@ -76,7 +61,7 @@ class Stats(BaseModel):
kills: int kills: int
finesse: Finesse finesse: Finesse
zenith: _Zenith zenith: _Zenith
finaltime: int finaltime: float
class Results(BaseModel): class Results(BaseModel):

View File

@@ -6,7 +6,7 @@ from ....schemas import BaseUser
from ...constant import GAME_TYPE from ...constant import GAME_TYPE
class User(BaseUser): class User(BaseUser[Literal['IO']]):
platform: Literal['IO'] = GAME_TYPE platform: Literal['IO'] = GAME_TYPE
ID: str ID: str

View File

@@ -42,7 +42,7 @@ class Data(BaseModel):
badstanding: bool | None = None badstanding: bool | None = None
supporter: bool | None = None # osk说是必有, 但实际上不是 fkosk supporter: bool | None = None # osk说是必有, 但实际上不是 fkosk
supporter_tier: int supporter_tier: int
verified: bool verified: bool | None = None
avatar_revision: int | None = None avatar_revision: int | None = None
"""This user's avatar ID. Get their avatar at """This user's avatar ID. Get their avatar at

View File

@@ -1,122 +0,0 @@
from datetime import datetime
from pydantic import BaseModel, Field
from .....utils.typing import Number
from .base import FailedModel
from .base import SuccessModel as BaseSuccessModel
class Time(BaseModel):
start: int
zero: bool
locked: bool
prev: int
frameoffset: int | None = None
class Clears(BaseModel):
singles: int
doubles: int
triples: int
quads: int
pentas: int | None = None
realtspins: int
minitspins: int
minitspinsingles: int
tspinsingles: int
minitspindoubles: int
tspindoubles: int
tspintriples: int
tspinquads: int
allclear: int
class Garbage(BaseModel):
sent: int
received: int
attack: int | None = None
cleared: int | None = None
class Finesse(BaseModel):
combo: int
faults: int
perfectpieces: int
class EndContext(BaseModel):
seed: Number
lines: int
level_lines: int
level_lines_needed: int
inputs: int
holds: int | None = None
time: Time
score: int
zenlevel: int | None = None
zenprogress: int | None = None
level: int
combo: int
currentcombopower: int | None = None # WTF
topcombo: int
btb: int
topbtb: int
currentbtbchainpower: int | None = None # WTF * 2
tspins: int
piecesplaced: int
clears: Clears
garbage: Garbage
kills: int
finesse: Finesse
final_time: float = Field(..., alias='finalTime')
gametype: str
class _User(BaseModel):
id: str = Field(..., alias='_id')
username: str
class _Record(BaseModel):
id: str = Field(..., alias='_id')
stream: str
replayid: str
user: _User
ts: datetime
ismulti: bool | None = None
class SoloRecord(_Record):
endcontext: EndContext
class MultiRecord(_Record):
endcontext: list[EndContext]
class SoloModeRecord(BaseModel):
record: SoloRecord | None = None
rank: int | None = None
class Records(BaseModel):
sprint: SoloModeRecord = Field(..., alias='40l')
blitz: SoloModeRecord
class Zen(BaseModel):
level: int
score: int
class Data(BaseModel):
records: Records
zen: Zen
class UserRecordsSuccess(BaseSuccessModel):
data: Data
UserRecords = UserRecordsSuccess | FailedModel

View File

@@ -1,6 +1,7 @@
from typing import Literal from typing import Literal
ValidRank = Literal[ S1ValidRank = Literal[
'x+',
'x', 'x',
'u', 'u',
'ss', 'ss',
@@ -19,7 +20,9 @@ ValidRank = Literal[
'd+', 'd+',
'd', 'd',
] ]
S1Rank = S1ValidRank | Literal['z']
ValidRank = Literal['x+'] | S1ValidRank
Rank = ValidRank | Literal['z'] # 未定级 Rank = ValidRank | Literal['z'] # 未定级
Summaries = Literal[ Summaries = Literal[
@@ -27,7 +30,16 @@ Summaries = Literal[
'blitz', 'blitz',
'zenith', 'zenith',
'zenithex', 'zenithex',
# 'league', # 等待正式赛季开始 'league',
'zen', 'zen',
'achievements', 'achievements',
] ]
Records = Literal[
'40l_top',
'40l_recent',
'40l_progression',
'blitz_top',
'blitz_recent',
'blitz_progression',
]

View File

@@ -1,13 +1,14 @@
from asyncio import gather
from hashlib import md5 from hashlib import md5
from urllib.parse import urlencode from urllib.parse import urlencode
from arclet.alconna import Arg, ArgFlag
from nonebot_plugin_alconna import Args, Subcommand
from nonebot_plugin_alconna.uniseg import UniMessage from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_orm import get_session from nonebot_plugin_orm import get_session
from nonebot_plugin_session import EventSession # type: ignore[import-untyped] from nonebot_plugin_session import EventSession
from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[import-untyped] from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[import-untyped]
from nonebot_plugin_user import User # type: ignore[import-untyped] from nonebot_plugin_user import User
from nonebot_plugin_userinfo import BotUserInfo, UserInfo # type: ignore[import-untyped] from nonebot_plugin_userinfo import BotUserInfo, UserInfo
from ...db import BindStatus, create_or_update_bind, trigger from ...db import BindStatus, create_or_update_bind, trigger
from ...utils.host import HostPage, get_self_netloc from ...utils.host import HostPage, get_self_netloc
@@ -15,10 +16,31 @@ from ...utils.image import get_avatar
from ...utils.render import Bind, render from ...utils.render import Bind, render
from ...utils.render.schemas.base import Avatar, People from ...utils.render.schemas.base import Avatar, People
from ...utils.screenshot import screenshot from ...utils.screenshot import screenshot
from . import alc from . import alc, command, get_player
from .api import Player from .api import Player
from .constant import GAME_TYPE from .constant import GAME_TYPE
command.add(
Subcommand(
'bind',
Args(
Arg(
'account',
get_player,
notice='TETR.IO 用户名 / ID',
flags=[ArgFlag.HIDDEN],
)
),
help_text='绑定 TETR.IO 账号',
)
)
alc.shortcut(
'(?i:io)(?i:绑定|绑|bind)',
command='tstats TETR.IO bind',
humanized='io绑定',
)
@alc.assign('TETRIO.bind') @alc.assign('TETRIO.bind')
async def _(nb_user: User, account: Player, event_session: EventSession, bot_info: UserInfo = BotUserInfo()): # noqa: B008 async def _(nb_user: User, account: Player, event_session: EventSession, bot_info: UserInfo = BotUserInfo()): # noqa: B008
@@ -28,7 +50,7 @@ async def _(nb_user: User, account: Player, event_session: EventSession, bot_inf
command_type='bind', command_type='bind',
command_args=[], command_args=[],
): ):
user, user_info = await gather(account.user, account.get_info()) user = await account.user
async with get_session() as session: async with get_session() as session:
bind_status = await create_or_update_bind( bind_status = await create_or_update_bind(
session=session, session=session,

View File

@@ -1,16 +1,37 @@
from arclet.alconna import Arg
from nonebot_plugin_alconna import Option, Subcommand
from nonebot_plugin_alconna.uniseg import UniMessage from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_orm import async_scoped_session from nonebot_plugin_orm import async_scoped_session
from nonebot_plugin_session import EventSession # type: ignore[import-untyped] from nonebot_plugin_session import EventSession
from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[import-untyped] from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[import-untyped]
from nonebot_plugin_user import User # type: ignore[import-untyped] from nonebot_plugin_user import User
from sqlalchemy import select from sqlalchemy import select
from ...db import trigger from ...db import trigger
from . import alc from . import alc, command
from .constant import GAME_TYPE from .constant import GAME_TYPE
from .models import TETRIOUserConfig from .models import TETRIOUserConfig
from .typing import Template from .typing import Template
command.add(
Subcommand(
'config',
Option(
'--default-template',
Arg('template', Template, notice='模板版本'),
alias=['-DT', 'DefaultTemplate'],
help_text='设置默认查询模板',
),
help_text='TETR.IO 查询个性化配置',
),
)
alc.shortcut(
'(?i:io)(?i:配置|配|config)',
command='tstats TETR.IO config',
humanized='io配置',
)
@alc.assign('TETRIO.config') @alc.assign('TETRIO.config')
async def _(user: User, session: async_scoped_session, event_session: EventSession, template: Template): async def _(user: User, session: async_scoped_session, event_session: EventSession, template: Template):

View File

@@ -1,34 +1,10 @@
from datetime import datetime
from nonebot_plugin_orm import Model from nonebot_plugin_orm import Model
from sqlalchemy import JSON, DateTime, String from sqlalchemy import String
from sqlalchemy.orm import Mapped, MappedAsDataclass, mapped_column from sqlalchemy.orm import Mapped, MappedAsDataclass, mapped_column
from .api.typing import ValidRank
from .typing import Template from .typing import Template
class IORank(MappedAsDataclass, Model):
id: Mapped[int] = mapped_column(init=False, primary_key=True)
rank: Mapped[ValidRank] = mapped_column(String(2), index=True)
tr_line: Mapped[float]
player_count: Mapped[int]
low_pps: Mapped[tuple[dict[str, str], float]] = mapped_column(JSON)
low_apm: Mapped[tuple[dict[str, str], float]] = mapped_column(JSON)
low_vs: Mapped[tuple[dict[str, str], float]] = mapped_column(JSON)
avg_pps: Mapped[float]
avg_apm: Mapped[float]
avg_vs: Mapped[float]
high_pps: Mapped[tuple[dict[str, str], float]] = mapped_column(JSON)
high_apm: Mapped[tuple[dict[str, str], float]] = mapped_column(JSON)
high_vs: Mapped[tuple[dict[str, str], float]] = mapped_column(JSON)
update_time: Mapped[datetime] = mapped_column(
DateTime,
index=True,
)
file_hash: Mapped[str | None] = mapped_column(String(128), index=True)
class TETRIOUserConfig(MappedAsDataclass, Model): class TETRIOUserConfig(MappedAsDataclass, Model):
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
query_template: Mapped[Template] = mapped_column(String(2)) query_template: Mapped[Template] = mapped_column(String(2))

View File

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

View File

@@ -1,4 +1,31 @@
from . import blitz, sprint from arclet.alconna import Arg, ArgFlag
from nonebot_plugin_alconna import Args, At, Subcommand
from ....utils.typing import Me
from .. import command as base_command
from .. import get_player
command = Subcommand(
'record',
Args(
Arg(
'target',
At | Me,
notice='@想要查询的人 / 自己',
flags=[ArgFlag.HIDDEN, ArgFlag.OPTIONAL],
),
Arg(
'account',
get_player,
notice='TETR.IO 用户名 / ID',
flags=[ArgFlag.HIDDEN, ArgFlag.OPTIONAL],
),
),
)
from . import blitz, sprint # noqa: E402
base_command.add(command)
__all__ = [ __all__ = [
'blitz', 'blitz',

View File

@@ -5,12 +5,12 @@ from urllib.parse import urlencode
from nonebot.adapters import Event from nonebot.adapters import Event
from nonebot.matcher import Matcher from nonebot.matcher import Matcher
from nonebot_plugin_alconna import At from nonebot_plugin_alconna import At, Option
from nonebot_plugin_alconna.uniseg import UniMessage from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_orm import get_session from nonebot_plugin_orm import get_session
from nonebot_plugin_session import EventSession # type: ignore[import-untyped] from nonebot_plugin_session import EventSession
from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[import-untyped] from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[import-untyped]
from nonebot_plugin_user import get_user # type: ignore[import-untyped] from nonebot_plugin_user import get_user
from ....db import query_bind_info, trigger from ....db import query_bind_info, trigger
from ....utils.exception import RecordNotFoundError from ....utils.exception import RecordNotFoundError
@@ -18,14 +18,23 @@ from ....utils.host import HostPage, get_self_netloc
from ....utils.metrics import get_metrics from ....utils.metrics import get_metrics
from ....utils.render import render from ....utils.render import render
from ....utils.render.schemas.base import Avatar from ....utils.render.schemas.base import Avatar
from ....utils.render.schemas.tetrio.tetrio_record_base import Finesse, Max, Mini, Tspins, User from ....utils.render.schemas.tetrio.record.base import Finesse, Max, Mini, Tspins, User
from ....utils.render.schemas.tetrio.tetrio_record_blitz import Record, Statistic from ....utils.render.schemas.tetrio.record.blitz import Record, Statistic
from ....utils.screenshot import screenshot from ....utils.screenshot import screenshot
from ....utils.typing import Me from ....utils.typing import Me
from ...constant import CANT_VERIFY_MESSAGE from ...constant import CANT_VERIFY_MESSAGE
from .. import alc from .. import alc
from ..api.player import Player from ..api.player import Player
from ..constant import GAME_TYPE from ..constant import GAME_TYPE
from . import command
command.add(Option('--blitz', dest='blitz'))
alc.shortcut(
'(?i:io)(?i:记录|record)(?i:blitz)',
command='tstats TETR.IO record --blitz',
humanized='io记录blitz',
)
@alc.assign('TETRIO.record.blitz') @alc.assign('TETRIO.record.blitz')
@@ -81,6 +90,7 @@ async def make_blitz_image(player: Player) -> bytes:
page=await render( page=await render(
'v2/tetrio/record/blitz', 'v2/tetrio/record/blitz',
Record( Record(
type='best',
user=User( user=User(
id=user.ID, id=user.ID,
name=user.name.upper(), name=user.name.upper(),
@@ -93,6 +103,7 @@ async def make_blitz_image(player: Player) -> bytes:
), ),
replay_id=blitz.data.record.replayid, replay_id=blitz.data.record.replayid,
rank=blitz.data.rank, rank=blitz.data.rank,
personal_rank=1,
statistic=Statistic( statistic=Statistic(
keys=stats.inputs, keys=stats.inputs,
kpp=round(stats.inputs / stats.piecesplaced, 2), kpp=round(stats.inputs / stats.piecesplaced, 2),

View File

@@ -5,12 +5,12 @@ from urllib.parse import urlencode
from nonebot.adapters import Event from nonebot.adapters import Event
from nonebot.matcher import Matcher from nonebot.matcher import Matcher
from nonebot_plugin_alconna import At from nonebot_plugin_alconna import At, Option
from nonebot_plugin_alconna.uniseg import UniMessage from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_orm import get_session from nonebot_plugin_orm import get_session
from nonebot_plugin_session import EventSession # type: ignore[import-untyped] from nonebot_plugin_session import EventSession
from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[import-untyped] from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[import-untyped]
from nonebot_plugin_user import get_user # type: ignore[import-untyped] from nonebot_plugin_user import get_user
from ....db import query_bind_info, trigger from ....db import query_bind_info, trigger
from ....utils.exception import RecordNotFoundError from ....utils.exception import RecordNotFoundError
@@ -18,14 +18,23 @@ from ....utils.host import HostPage, get_self_netloc
from ....utils.metrics import get_metrics from ....utils.metrics import get_metrics
from ....utils.render import render from ....utils.render import render
from ....utils.render.schemas.base import Avatar from ....utils.render.schemas.base import Avatar
from ....utils.render.schemas.tetrio.tetrio_record_base import Finesse, Max, Mini, Tspins, User from ....utils.render.schemas.tetrio.record.base import Finesse, Max, Mini, Statistic, Tspins, User
from ....utils.render.schemas.tetrio.tetrio_record_sprint import Record, Statistic from ....utils.render.schemas.tetrio.record.sprint import Record
from ....utils.screenshot import screenshot from ....utils.screenshot import screenshot
from ....utils.typing import Me from ....utils.typing import Me
from ...constant import CANT_VERIFY_MESSAGE from ...constant import CANT_VERIFY_MESSAGE
from .. import alc from .. import alc
from ..api.player import Player from ..api.player import Player
from ..constant import GAME_TYPE from ..constant import GAME_TYPE
from . import command
command.add(Option('--40l', dest='sprint'))
alc.shortcut(
'(?i:io)(?i:记录|record)(?i:40l)',
command='tstats TETR.IO record --40l',
humanized='io记录40l',
)
@alc.assign('TETRIO.record.sprint') @alc.assign('TETRIO.record.sprint')
@@ -82,6 +91,7 @@ async def make_sprint_image(player: Player) -> bytes:
page=await render( page=await render(
'v2/tetrio/record/40l', 'v2/tetrio/record/40l',
Record( Record(
type='best',
user=User( user=User(
id=user.ID, id=user.ID,
name=user.name.upper(), name=user.name.upper(),
@@ -95,6 +105,7 @@ async def make_sprint_image(player: Player) -> bytes:
time=sprint_value, time=sprint_value,
replay_id=sprint.data.record.replayid, replay_id=sprint.data.record.replayid,
rank=sprint.data.rank, rank=sprint.data.rank,
personal_rank=1,
statistic=Statistic( statistic=Statistic(
keys=stats.inputs, keys=stats.inputs,
kpp=round(stats.inputs / stats.piecesplaced, 2), kpp=round(stats.inputs / stats.piecesplaced, 2),

View File

@@ -1,5 +1,5 @@
from arclet.alconna import Arg, ArgFlag, Args, Subcommand from arclet.alconna import Arg, ArgFlag
from nonebot_plugin_alconna import At from nonebot_plugin_alconna import Args, At, Subcommand
from ...utils.exception import MessageFormatError from ...utils.exception import MessageFormatError
from ...utils.typing import Me from ...utils.typing import Me

View File

@@ -39,7 +39,6 @@ class Player:
raw_user_profile = await Request.request(url, is_json=False) raw_user_profile = await Request.request(url, is_json=False)
self._user_profile = self._parse_profile(raw_user_profile) self._user_profile = self._parse_profile(raw_user_profile)
await anti_duplicate_add( await anti_duplicate_add(
TOPHistoricalData,
TOPHistoricalData( TOPHistoricalData(
user_unique_identifier=(await self.user).unique_identifier, user_unique_identifier=(await self.user).unique_identifier,
api_type='User Profile', api_type='User Profile',
@@ -49,7 +48,8 @@ class Player:
) )
return self._user_profile return self._user_profile
def _parse_profile(self, original_user_profile: bytes) -> UserProfile: @staticmethod
def _parse_profile(original_user_profile: bytes) -> UserProfile:
html = etree.HTML(original_user_profile) html = etree.HTML(original_user_profile)
user_name = html.xpath('//div[@class="mycontent"]/h1/text()')[0].replace("'s profile", '') user_name = html.xpath('//div[@class="mycontent"]/h1/text()')[0].replace("'s profile", '')
today = None today = None
@@ -68,4 +68,4 @@ class Player:
total: list[Data] = [] total: list[Data] = []
for _, value in dataframe.iterrows(): for _, value in dataframe.iterrows():
total.append(Data(lpm=value['lpm'], apm=value['apm'])) total.append(Data(lpm=value['lpm'], apm=value['apm']))
return UserProfile(user_name=user_name, today=today, total=total) return UserProfile(user_name=user_name, today=today, total=total or None)

View File

@@ -6,7 +6,7 @@ from ....schemas import BaseUser
from ...constant import GAME_TYPE from ...constant import GAME_TYPE
class User(BaseUser): class User(BaseUser[Literal['TOP']]):
platform: Literal['TOP'] = GAME_TYPE platform: Literal['TOP'] = GAME_TYPE
user_name: str user_name: str

View File

@@ -1,9 +1,9 @@
from nonebot_plugin_alconna.uniseg import UniMessage from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_orm import get_session from nonebot_plugin_orm import get_session
from nonebot_plugin_session import EventSession # type: ignore[import-untyped] from nonebot_plugin_session import EventSession
from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[import-untyped] from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[import-untyped]
from nonebot_plugin_user import User # type: ignore[import-untyped] from nonebot_plugin_user import User
from nonebot_plugin_userinfo import BotUserInfo, EventUserInfo, UserInfo # type: ignore[import-untyped] from nonebot_plugin_userinfo import BotUserInfo, EventUserInfo, UserInfo
from ...db import BindStatus, create_or_update_bind, trigger from ...db import BindStatus, create_or_update_bind, trigger
from ...utils.host import HostPage, get_self_netloc from ...utils.host import HostPage, get_self_netloc

View File

@@ -3,17 +3,25 @@ from nonebot.matcher import Matcher
from nonebot_plugin_alconna import At from nonebot_plugin_alconna import At
from nonebot_plugin_alconna.uniseg import UniMessage from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_orm import get_session from nonebot_plugin_orm import get_session
from nonebot_plugin_session import EventSession # type: ignore[import-untyped] from nonebot_plugin_session import EventSession
from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[import-untyped] from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[import-untyped]
from nonebot_plugin_user import get_user # type: ignore[import-untyped] from nonebot_plugin_user import get_user
from ...db import query_bind_info, trigger from ...db import query_bind_info, trigger
from ...utils.metrics import get_metrics from ...utils.exception import FallbackError
from ...utils.host import HostPage, get_self_netloc
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.screenshot import screenshot
from ...utils.typing import Me from ...utils.typing import Me
from ..constant import CANT_VERIFY_MESSAGE from ..constant import CANT_VERIFY_MESSAGE
from . import alc from . import alc
from .api import Player from .api import Player
from .api.schemas.user_profile import UserProfile from .api.schemas.user_profile import Data, UserProfile
from .constant import GAME_TYPE from .constant import GAME_TYPE
@@ -35,8 +43,10 @@ async def _(event: Event, matcher: Matcher, target: At | Me, event_session: Even
) )
if bind is None: if bind is None:
await matcher.finish('未查询到绑定信息') await matcher.finish('未查询到绑定信息')
message = CANT_VERIFY_MESSAGE await (
await (message + make_query_text(await Player(user_name=bind.game_account, trust=True).get_profile())).finish() UniMessage(CANT_VERIFY_MESSAGE)
+ await make_query_result(await Player(user_name=bind.game_account, trust=True).get_profile())
).finish()
@alc.assign('TOP.query') @alc.assign('TOP.query')
@@ -47,7 +57,34 @@ async def _(account: Player, event_session: EventSession):
command_type='query', command_type='query',
command_args=[], command_args=[],
): ):
await (make_query_text(await account.get_profile())).finish() await (await make_query_result(await account.get_profile())).finish()
def get_avg_metrics(data: list[Data]) -> TetrisMetricsBasicWithLPM:
total_lpm = total_apm = 0.0
for value in data:
total_lpm += value.lpm
total_apm += value.apm
num = len(data)
return get_metrics(lpm=total_lpm / num, apm=total_apm / num)
async def make_query_image(profile: UserProfile) -> bytes:
if profile.today is None or profile.total is None:
raise FallbackError
today = get_metrics(lpm=profile.today.lpm, apm=profile.today.apm)
history = get_avg_metrics(profile.total)
async with HostPage(
await render(
'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),
),
)
) as page_hash:
return await screenshot(f'http://{get_self_netloc()}/host/{page_hash}.html')
def make_query_text(profile: UserProfile) -> UniMessage: def make_query_text(profile: UserProfile) -> UniMessage:
@@ -60,15 +97,18 @@ def make_query_text(profile: UserProfile) -> UniMessage:
else: else:
message += f'用户 {profile.user_name} 暂无24小时内统计数据' message += f'用户 {profile.user_name} 暂无24小时内统计数据'
if profile.total is not None: if profile.total is not None:
total_lpm = total_apm = 0.0 total = get_avg_metrics(profile.total)
for value in profile.total:
total_lpm += value.lpm
total_apm += value.apm
num = len(profile.total)
total = get_metrics(lpm=total_lpm / num, apm=total_apm / num)
message += '\n历史统计数据为: ' message += '\n历史统计数据为: '
message += f"\nL'PM: {total.lpm} ( {total.pps} pps )" message += f"\nL'PM: {total.lpm} ( {total.pps} pps )"
message += f'\nAPM: {total.apm} ( x{total.apl} )' message += f'\nAPM: {total.apm} ( x{total.apl} )'
else: else:
message += '\n暂无历史统计数据' message += '\n暂无历史统计数据'
return UniMessage(message) return UniMessage(message)
async def make_query_result(profile: UserProfile) -> UniMessage:
try:
return UniMessage.image(raw=await make_query_image(profile))
except FallbackError:
...
return make_query_text(profile)

View File

@@ -1,5 +1,5 @@
from arclet.alconna import Arg, ArgFlag, Args, Subcommand from arclet.alconna import Arg, ArgFlag
from nonebot_plugin_alconna import At from nonebot_plugin_alconna import Args, At, Subcommand
from ...utils.exception import MessageFormatError from ...utils.exception import MessageFormatError
from ...utils.typing import Me from ...utils.typing import Me

View File

@@ -85,7 +85,6 @@ class Player:
raise RequestError(msg) raise RequestError(msg)
self._user_info = user_info self._user_info = user_info
await anti_duplicate_add( await anti_duplicate_add(
TOSHistoricalData,
TOSHistoricalData( TOSHistoricalData(
user_unique_identifier=(await self.user).unique_identifier, user_unique_identifier=(await self.user).unique_identifier,
api_type='User Info', api_type='User Info',
@@ -117,7 +116,6 @@ class Player:
) )
self._user_profile[params] = type_validate_json(UserProfile, raw_user_profile) self._user_profile[params] = type_validate_json(UserProfile, raw_user_profile)
await anti_duplicate_add( await anti_duplicate_add(
TOSHistoricalData,
TOSHistoricalData( TOSHistoricalData(
user_unique_identifier=(await self.user).unique_identifier, user_unique_identifier=(await self.user).unique_identifier,
api_type='User Profile', api_type='User Profile',

View File

@@ -6,7 +6,7 @@ from ....schemas import BaseUser
from ...constant import GAME_TYPE from ...constant import GAME_TYPE
class User(BaseUser): class User(BaseUser[Literal['TOS']]):
platform: Literal['TOS'] = GAME_TYPE platform: Literal['TOS'] = GAME_TYPE
teaid: str teaid: str

View File

@@ -1,9 +1,9 @@
from nonebot_plugin_alconna.uniseg import UniMessage from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_orm import get_session from nonebot_plugin_orm import get_session
from nonebot_plugin_session import EventSession # type: ignore[import-untyped] from nonebot_plugin_session import EventSession
from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[import-untyped] from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[import-untyped]
from nonebot_plugin_user import User # type: ignore[import-untyped] from nonebot_plugin_user import User
from nonebot_plugin_userinfo import BotUserInfo, EventUserInfo, UserInfo # type: ignore[import-untyped] from nonebot_plugin_userinfo import BotUserInfo, EventUserInfo, UserInfo
from ...db import BindStatus, create_or_update_bind, trigger from ...db import BindStatus, create_or_update_bind, trigger
from ...utils.host import HostPage, get_self_netloc from ...utils.host import HostPage, get_self_netloc

View File

@@ -6,9 +6,6 @@ GAME_TYPE: Literal['TOS'] = 'TOS'
BASE_URL = { BASE_URL = {
'https://teatube.cn:8888/', 'https://teatube.cn:8888/',
'http://cafuuchino1.studio26f.org:19970', 'http://cafuuchino1.studio26f.org:19970',
'http://cafuuchino2.studio26f.org:19970',
'http://cafuuchino3.studio26f.org:19970',
'http://cafuuchino4.studio26f.org:19970',
} }
USER_NAME = compile( USER_NAME = compile(

View File

@@ -8,10 +8,10 @@ from nonebot.matcher import Matcher
from nonebot_plugin_alconna import At from nonebot_plugin_alconna import At
from nonebot_plugin_alconna.uniseg import UniMessage from nonebot_plugin_alconna.uniseg import UniMessage
from nonebot_plugin_orm import get_session from nonebot_plugin_orm import get_session
from nonebot_plugin_session import EventSession # type: ignore[import-untyped] from nonebot_plugin_session import EventSession
from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[import-untyped] from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[import-untyped]
from nonebot_plugin_user import get_user # type: ignore[import-untyped] from nonebot_plugin_user import get_user
from nonebot_plugin_userinfo import EventUserInfo, UserInfo # type: ignore[import-untyped] from nonebot_plugin_userinfo import EventUserInfo, UserInfo
from ...db import query_bind_info, trigger from ...db import query_bind_info, trigger
from ...utils.exception import RequestError from ...utils.exception import RequestError
@@ -19,6 +19,7 @@ from ...utils.host import HostPage, get_self_netloc
from ...utils.image import get_avatar from ...utils.image import get_avatar
from ...utils.metrics import TetrisMetricsProWithLPMADPM, get_metrics from ...utils.metrics import TetrisMetricsProWithLPMADPM, get_metrics
from ...utils.render import render 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.base import People, Ranking
from ...utils.render.schemas.tos_info import Info, Multiplayer, Radar from ...utils.render.schemas.tos_info import Info, Multiplayer, Radar
from ...utils.screenshot import screenshot from ...utils.screenshot import screenshot
@@ -57,7 +58,9 @@ def add_special_handlers(
user_info, game_data = await gather(player.get_info(), get_game_data(player)) user_info, game_data = await gather(player.get_info(), get_game_data(player))
if game_data is not None: if game_data is not None:
await UniMessage.image( await UniMessage.image(
raw=await make_query_image(user_info, game_data, event_user_info) raw=await make_query_image(
user_info, game_data, None if isinstance(target, At) else event_user_info
)
).finish() ).finish()
await make_query_text(user_info, game_data).finish() await make_query_text(user_info, game_data).finish()
except RequestError as e: except RequestError as e:
@@ -126,13 +129,18 @@ async def _(
user_info, game_data = await gather(player.get_info(), get_game_data(player)) user_info, game_data = await gather(player.get_info(), get_game_data(player))
if game_data is not None: if game_data is not None:
await ( await (
message + UniMessage.image(raw=await make_query_image(user_info, game_data, event_user_info)) message
+ UniMessage.image(
raw=await make_query_image(
user_info, game_data, None if isinstance(target, At) else event_user_info
)
)
).finish() ).finish()
await (message + make_query_text(user_info, game_data)).finish() await (message + make_query_text(user_info, game_data)).finish()
@alc.assign('TOS.query') @alc.assign('TOS.query')
async def _(account: Player, event_session: EventSession, event_user_info: UserInfo = EventUserInfo()): # noqa: B008 async def _(account: Player, event_session: EventSession):
async with trigger( async with trigger(
session_persist_id=await get_session_persist_id(event_session), session_persist_id=await get_session_persist_id(event_session),
game_platform=GAME_TYPE, game_platform=GAME_TYPE,
@@ -141,7 +149,7 @@ async def _(account: Player, event_session: EventSession, event_user_info: UserI
): ):
user_info, game_data = await gather(account.get_info(), get_game_data(account)) user_info, game_data = await gather(account.get_info(), get_game_data(account))
if game_data is not None: if game_data is not None:
await UniMessage.image(raw=await make_query_image(user_info, game_data, event_user_info)).finish() await UniMessage.image(raw=await make_query_image(user_info, game_data, None)).finish()
await make_query_text(user_info, game_data).finish() await make_query_text(user_info, game_data).finish()
@@ -184,7 +192,7 @@ async def get_game_data(player: Player, query_num: int = 50) -> GameData | None:
break break
if num == 0: if num == 0:
return None return None
# TODO: 如果有效局数小于 {查询数} , 并且没有无dig信息的局, 且 user_profile.data 内有{请求数}个局, 则继续往前获取信息 # TODO)) 如果有效局数小于 {查询数} , 并且没有无dig信息的局, 且 user_profile.data 内有{请求数}个局, 则继续往前获取信息
metrics = get_metrics( metrics = get_metrics(
lpm=weighted_total_lpm / total_time, apm=weighted_total_apm / total_time, adpm=weighted_total_adpm / total_time lpm=weighted_total_lpm / total_time, apm=weighted_total_apm / total_time, adpm=weighted_total_adpm / total_time
) )
@@ -197,15 +205,27 @@ async def get_game_data(player: Player, query_num: int = 50) -> GameData | None:
) )
async def make_query_image(user_info: UserInfoSuccess, game_data: GameData, event_user_info: UserInfo) -> bytes: async def make_query_image(user_info: UserInfoSuccess, game_data: GameData, event_user_info: UserInfo | None) -> bytes:
metrics = game_data.metrics metrics = game_data.metrics
duration = timedelta(milliseconds=float(user_info.data.pb_sprint)).total_seconds() sprint_value = (
sprint_value = f'{duration:.3f}s' if duration < 60 else f'{duration // 60:.0f}m {duration % 60:.3f}s' # noqa: PLR2004 (
f'{duration:.3f}s'
if (duration := timedelta(milliseconds=float(user_info.data.pb_sprint)).total_seconds()) < 60 # noqa: PLR2004
else f'{duration // 60:.0f}m {duration % 60:.3f}s'
)
if user_info.data.pb_sprint != '2147483647'
else 'N/A'
)
async with HostPage( async with HostPage(
await render( await render(
'v1/tos/info', 'v1/tos/info',
Info( Info(
user=People(avatar=await get_avatar(event_user_info, 'Data URI', None), name=user_info.data.name), user=People(
avatar=await get_avatar(event_user_info, 'Data URI', None)
if event_user_info is not None
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)), ranking=Ranking(rating=float(user_info.data.ranking), rd=round(float(user_info.data.rd_now), 2)),
multiplayer=Multiplayer( multiplayer=Multiplayer(
pps=metrics.pps, pps=metrics.pps,

View File

@@ -11,7 +11,7 @@ from nonebot.log import logger
from ..config.config import CACHE_PATH from ..config.config import CACHE_PATH
from .image import img_to_png from .image import img_to_png
from .request import Request from .request import Request
from .templates import templates_dir from .templates import TEMPLATES_DIR
if TYPE_CHECKING: if TYPE_CHECKING:
from pydantic import IPvAnyAddress from pydantic import IPvAnyAddress
@@ -48,14 +48,14 @@ class HostPage:
def _(): def _():
app.mount( app.mount(
'/host/assets', '/host/assets',
StaticFiles(directory=templates_dir / 'assets'), StaticFiles(directory=TEMPLATES_DIR / 'assets'),
name='assets', name='assets',
) )
logger.success('assets mounted') logger.success('assets mounted')
@app.get('/host/{page_hash}.html', status_code=status.HTTP_200_OK) @app.get('/host/{page_hash}.html', status_code=status.HTTP_200_OK)
async def _(page_hash: str) -> HTMLResponse: def _(page_hash: str) -> HTMLResponse:
if page_hash in HostPage.pages: if page_hash in HostPage.pages:
return HTMLResponse(HostPage.pages[page_hash]) return HTMLResponse(HostPage.pages[page_hash])
return NOT_FOUND return NOT_FOUND

View File

@@ -2,7 +2,7 @@ from base64 import b64encode
from io import BytesIO from io import BytesIO
from typing import Literal, overload from typing import Literal, overload
from nonebot_plugin_userinfo import UserInfo # type: ignore[import-untyped] from nonebot_plugin_userinfo import UserInfo
from PIL import Image from PIL import Image

View File

@@ -3,27 +3,30 @@ from typing import Literal, overload
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from nonebot.compat import PYDANTIC_V2 from nonebot.compat import PYDANTIC_V2
from ..templates import templates_dir from ..templates import TEMPLATES_DIR
from .schemas.bind import Bind from .schemas.bind import Bind
from .schemas.tetrio.tetrio_info import Info as TETRIOInfo from .schemas.tetrio.rank.detail import Data as TETRIORankDetailData
from .schemas.tetrio.tetrio_rank import Data as TETRIORankData from .schemas.tetrio.rank.v1 import Data as TETRIORankDataV1
from .schemas.tetrio.tetrio_rank_detail import Data as TETRIORankDetailData from .schemas.tetrio.rank.v2 import Data as TETRIORankDataV2
from .schemas.tetrio.tetrio_record_blitz import Record as TETRIORecordBlitz from .schemas.tetrio.record.blitz import Record as TETRIORecordBlitz
from .schemas.tetrio.tetrio_record_sprint import Record as TETRIORecordSprint from .schemas.tetrio.record.sprint import Record as TETRIORecordSprint
from .schemas.tetrio.tetrio_user_info_v2 import Info as TETRIOUserInfoV2 from .schemas.tetrio.user.info_v1 import Info as TETRIOUserInfoV1
from .schemas.tetrio.tetrio_user_list_v2 import List as TETRIOUserListV2 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.top_info import Info as TOPInfo
from .schemas.tos_info import Info as TOSInfo from .schemas.tos_info import Info as TOSInfo
env = Environment( env = Environment(
loader=FileSystemLoader(templates_dir), autoescape=True, trim_blocks=True, lstrip_blocks=True, enable_async=True loader=FileSystemLoader(TEMPLATES_DIR), autoescape=True, trim_blocks=True, lstrip_blocks=True, enable_async=True
) )
@overload @overload
async def render(render_type: Literal['v1/binding'], data: Bind) -> str: ... async def render(render_type: Literal['v1/binding'], data: Bind) -> str: ...
@overload @overload
async def render(render_type: Literal['v1/tetrio/info'], data: TETRIOInfo) -> str: ... async def render(render_type: Literal['v1/tetrio/info'], data: TETRIOUserInfoV1) -> str: ...
@overload
async def render(render_type: Literal['v1/tetrio/rank'], data: TETRIORankDataV1) -> str: ...
@overload @overload
async def render(render_type: Literal['v1/top/info'], data: TOPInfo) -> str: ... async def render(render_type: Literal['v1/top/info'], data: TOPInfo) -> str: ...
@overload @overload
@@ -37,7 +40,7 @@ async def render(render_type: Literal['v2/tetrio/record/40l'], data: TETRIORecor
@overload @overload
async def render(render_type: Literal['v2/tetrio/record/blitz'], data: TETRIORecordBlitz) -> str: ... async def render(render_type: Literal['v2/tetrio/record/blitz'], data: TETRIORecordBlitz) -> str: ...
@overload @overload
async def render(render_type: Literal['v2/tetrio/rank'], data: TETRIORankData) -> str: ... async def render(render_type: Literal['v2/tetrio/rank'], data: TETRIORankDataV2) -> str: ...
@overload @overload
async def render(render_type: Literal['v2/tetrio/rank/detail'], data: TETRIORankDetailData) -> str: ... async def render(render_type: Literal['v2/tetrio/rank/detail'], data: TETRIORankDetailData) -> str: ...
@@ -46,6 +49,7 @@ async def render(
render_type: Literal[ render_type: Literal[
'v1/binding', 'v1/binding',
'v1/tetrio/info', 'v1/tetrio/info',
'v1/tetrio/rank',
'v1/top/info', 'v1/top/info',
'v1/tos/info', 'v1/tos/info',
'v2/tetrio/user/info', 'v2/tetrio/user/info',
@@ -56,14 +60,15 @@ async def render(
'v2/tetrio/rank/detail', 'v2/tetrio/rank/detail',
], ],
data: Bind data: Bind
| TETRIOInfo | TETRIOUserInfoV1
| TETRIORankDataV1
| TOPInfo | TOPInfo
| TOSInfo | TOSInfo
| TETRIOUserInfoV2 | TETRIOUserInfoV2
| TETRIOUserListV2 | TETRIOUserListV2
| TETRIORecordSprint | TETRIORecordSprint
| TETRIORecordBlitz | TETRIORecordBlitz
| TETRIORankData | TETRIORankDataV2
| TETRIORankDetailData, | TETRIORankDetailData,
) -> str: ) -> str:
if PYDANTIC_V2: if PYDANTIC_V2:

View File

@@ -0,0 +1,34 @@
from base64 import b64encode
from io import BytesIO
from random import Random
from PIL import Image
from PIL.Image import Resampling
from .draw import PIECE_MEMBERS, SkinManager
def get_avatar(send: float | str | bytes | bytearray | None = None) -> str:
random = Random(send) # noqa: S311
skin = (
SkinManager.get_skin(send)
.get_piece(random.choice(PIECE_MEMBERS))
.rotate(
random.randint(-360, 360),
expand=True,
resample=Resampling.BICUBIC,
)
)
skin = skin.crop(skin.getbbox())
background = Image.new('RGBA', (2048, 2048), '#e5e5e5')
skin_ratio = min(1536 / skin.width, 1536 / skin.height)
new_size = (int(skin.width * skin_ratio), int(skin.height * skin_ratio))
skin = skin.resize(new_size, Resampling.BICUBIC)
background.paste(skin, ((background.width - skin.width) // 2, (background.height - skin.height) // 2), mask=skin)
background = background.resize((512, 512), Resampling.LANCZOS)
with BytesIO() as output:
background.save(output, format='PNG')
return f'data:image/png;base64,{b64encode(output.getvalue()).decode("utf-8")}'

View File

@@ -0,0 +1,169 @@
from abc import ABC, abstractmethod
from enum import Enum
from random import Random
from typing import Any, ClassVar
from PIL.Image import Image
from typing_extensions import Self
class Piece(Enum):
Z = (
(True, True, False),
(False, True, True),
)
S = (
(False, True, True),
(True, True, False),
)
J = (
(True, False, False),
(True, True, True),
)
L = (
(False, False, True),
(True, True, True),
)
T = (
(False, True, False),
(True, True, True),
)
I = ( # noqa: E741
(True, True, True, True),
)
O = ( # noqa: E741
(True, True),
(True, True),
)
I5 = (
(True, True, True, True, True), # fmt: skip
)
V = (
(True, False, False),
(True, False, False),
(True, True, True),
)
T5 = (
(True, True, True),
(False, True, False),
(False, True, False),
)
U = (
(True, False, True),
(True, True, True),
)
W = (
(True, False, False),
(True, True, False),
(False, True, True),
)
X = (
(False, True, False),
(True, True, True),
(False, True, False),
)
J5 = (
(True, False, False, False),
(True, True, True, True),
)
L5 = (
(False, False, False, True),
(True, True, True, True),
)
H = (
(False, False, True, True),
(True, True, True, False),
)
N = (
(True, True, False, False),
(False, True, True, True),
)
Y = (
(False, True, False, False),
(True, True, True, True),
)
R = (
(False, False, True, False),
(True, True, True, True),
)
P = (
(True, True, False),
(True, True, True),
)
Q = (
(False, True, True),
(True, True, True),
)
F = (
(True, False, False),
(True, True, True),
(False, True, False),
)
E = (
(False, False, True),
(True, True, True),
(False, True, False),
)
S5 = (
(False, True, True),
(False, True, False),
(True, True, False),
)
Z5 = (
(True, True, False),
(False, True, False),
(False, True, True),
)
PIECE_MEMBERS = tuple(Piece)
class SkinManager:
skin: ClassVar[list['Skin']] = []
@classmethod
def register(cls, skin: 'Skin') -> None:
cls.skin.append(skin)
@classmethod
def get_skin(cls, send: float | str | bytes | bytearray | None = None) -> 'Skin':
return Random(send).choice(cls.skin) # noqa: S311
class Skin(ABC):
def __new__(cls, *args: Any, **kwargs: Any) -> Self: # noqa: ANN401, ARG003
instance = super().__new__(cls)
SkinManager.register(instance)
return instance
@abstractmethod
def get_piece(self, piece: Piece) -> Image:
raise NotImplementedError
from . import tech # noqa: E402, F401

View File

@@ -0,0 +1,94 @@
from enum import Enum
from pathlib import Path
from nonebot import get_driver
from PIL import Image
from PIL.Image import Resampling
from typing_extensions import override
from .. import Piece, Skin
SINGLE = 30
driver = get_driver()
class Block(Enum):
Z = (0, 0, 30, 30)
Y = (30, 0, 60, 30)
L = (60, 0, 90, 30)
O = (90, 0, 120, 30) # noqa: E741
U = (120, 0, 150, 30)
Q = (150, 0, 180, 30)
S = (180, 0, 210, 30)
H = (210, 0, 240, 30)
I = (0, 30, 30, 60) # noqa: E741
F = (30, 30, 60, 60)
J = (60, 30, 90, 60)
R = (90, 30, 120, 60)
C = (120, 30, 150, 60)
T = (150, 30, 180, 60)
W = (180, 30, 210, 60)
N = (210, 30, 240, 60)
piece_block_mapping = {
Piece.Z: Block.Z,
Piece.S: Block.S,
Piece.J: Block.J,
Piece.L: Block.L,
Piece.T: Block.T,
Piece.I: Block.I,
Piece.O: Block.O,
Piece.I5: Block.O,
Piece.V: Block.I,
Piece.T5: Block.C,
Piece.U: Block.U,
Piece.W: Block.W,
Piece.X: Block.O,
Piece.J5: Block.J,
Piece.L5: Block.L,
Piece.H: Block.H,
Piece.N: Block.N,
Piece.R: Block.R,
Piece.Y: Block.Y,
Piece.P: Block.Y,
Piece.Q: Block.Q,
Piece.F: Block.F,
Piece.E: Block.Y,
Piece.S5: Block.S,
Piece.Z5: Block.Z,
}
class TechSkin(Skin):
def __init__(self, path: Path, name: str | None = None) -> None:
self.path = path
self.name = name or path.name
self.image = Image.open(path)
self._block_cache: dict[Block, Image.Image] = {}
def get_block(self, block: Block) -> Image.Image:
return self._block_cache.setdefault(block, self.image.crop(block.value))
def draw_piece(self, block: Block, piece: Piece, scale: int = 10) -> Image.Image:
canvas = Image.new(
'RGBA', (len(piece.value[0]) * SINGLE * scale, len(piece.value) * SINGLE * scale), (0, 0, 0, 0)
)
block_img = self.get_block(block).resize((SINGLE * scale, SINGLE * scale), resample=Resampling.BICUBIC)
for i, row in enumerate(piece.value):
for j, mino in enumerate(row):
if mino:
canvas.paste(block_img, (j * SINGLE * scale, i * SINGLE * scale))
return canvas
@override
def get_piece(self, piece: Piece) -> Image.Image:
return self.draw_piece(piece_block_mapping[piece], piece)
@driver.on_startup
def _():
path = Path(__file__).parent / 'skins'
for i in sorted(path.iterdir()):
TechSkin(i)

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 837 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -2,7 +2,7 @@ from datetime import datetime
from pydantic import BaseModel from pydantic import BaseModel
from .....games.tetrio.api.typing import ValidRank from ......games.tetrio.api.typing import ValidRank
class SpecialData(BaseModel): class SpecialData(BaseModel):

View File

@@ -0,0 +1,16 @@
from datetime import datetime
from pydantic import BaseModel
from ......games.tetrio.api.typing import ValidRank
class ItemData(BaseModel):
trending: float
require_tr: float
players: int
class Data(BaseModel):
items: dict[ValidRank, ItemData]
updated_at: datetime

View File

@@ -2,7 +2,7 @@ from datetime import datetime
from pydantic import BaseModel from pydantic import BaseModel
from .....games.tetrio.api.typing import ValidRank from ......games.tetrio.api.typing import ValidRank
class AverageData(BaseModel): class AverageData(BaseModel):

View File

@@ -1,6 +1,9 @@
from datetime import datetime
from typing import Literal
from pydantic import BaseModel from pydantic import BaseModel
from ..base import People from ...base import People
class User(People): class User(People):
@@ -32,7 +35,7 @@ class Finesse(BaseModel):
accuracy: float accuracy: float
class RecordStatistic(BaseModel): class Statistic(BaseModel):
keys: int keys: int
kpp: float kpp: float
kps: float kps: float
@@ -56,3 +59,15 @@ class RecordStatistic(BaseModel):
all_clear: int all_clear: int
finesse: Finesse finesse: Finesse
class Record(BaseModel):
type: Literal['best', 'personal_best', 'recent', 'disputed']
user: User
replay_id: str
rank: int | None
personal_rank: int | None
play_at: datetime

View File

@@ -0,0 +1,12 @@
from .base import Record as BaseRecord
from .base import Statistic as BaseStatistic
class Statistic(BaseStatistic):
spp: float
level: int
class Record(BaseRecord):
statistic: Statistic

View File

@@ -0,0 +1,7 @@
from .base import Record as BaseRecord
from .base import Statistic
class Record(BaseRecord):
statistic: Statistic
time: str

View File

@@ -1,22 +0,0 @@
from datetime import datetime
from pydantic import BaseModel
from .tetrio_record_base import RecordStatistic, User
class Statistic(RecordStatistic):
spp: float
level: int
class Record(BaseModel):
user: User
replay_id: str
rank: int | None
statistic: Statistic
play_at: datetime

View File

@@ -1,18 +0,0 @@
from datetime import datetime
from pydantic import BaseModel
from .tetrio_record_base import RecordStatistic as Statistic
from .tetrio_record_base import User
class Record(BaseModel):
user: User
time: str
replay_id: str
rank: int | None
statistic: Statistic
play_at: datetime

View File

@@ -2,7 +2,7 @@ from datetime import datetime
from pydantic import BaseModel from pydantic import BaseModel
from ....typing import Number from .....typing import Number
class TetraLeagueHistoryData(BaseModel): class TetraLeagueHistoryData(BaseModel):

View File

@@ -1,8 +1,8 @@
from pydantic import BaseModel from pydantic import BaseModel
from .....games.tetrio.api.typing import Rank from ......games.tetrio.api.typing import Rank
from ....typing import Number from .....typing import Number
from ..base import People, Ranking from ...base import People, Ranking
from .base import TetraLeagueHistoryData from .base import TetraLeagueHistoryData

View File

@@ -3,10 +3,9 @@ from typing import Literal
from pydantic import BaseModel from pydantic import BaseModel
from .....games.tetrio.api.schemas.user_records import Zen from ......games.tetrio.api.typing import Rank, ValidRank
from .....games.tetrio.api.typing import Rank from .....typing import Number
from ....typing import Number from ...base import Avatar
from ..base import Avatar
from .base import TetraLeagueHistoryData from .base import TetraLeagueHistoryData
@@ -58,16 +57,16 @@ class TetraLeague(BaseModel):
tr: Number tr: Number
glicko: Number glicko: Number | None
rd: Number rd: Number | None
global_rank: int | None global_rank: int | None
country_rank: int | None country_rank: int | None
pps: Number pps: Number | None
apm: Number apm: Number | None
apl: Number apl: Number | None
vs: Number | None vs: Number | None
adpl: Number | None adpl: Number | None
@@ -76,7 +75,7 @@ class TetraLeague(BaseModel):
decaying: bool decaying: bool
history: list[TetraLeagueHistoryData] history: list[TetraLeagueHistoryData] | None
class Sprint(BaseModel): class Sprint(BaseModel):
@@ -91,10 +90,15 @@ class Blitz(BaseModel):
play_at: datetime play_at: datetime
class Zen(BaseModel):
level: int
score: int
class Info(BaseModel): class Info(BaseModel):
user: User user: User
tetra_league: TetraLeague | None tetra_league: TetraLeague | None
statistic: Statistic | None statistic: Statistic | None
sprint: Sprint | None sprint: Sprint | None
blitz: Blitz | None blitz: Blitz | None
zen: Zen zen: Zen | None

View File

@@ -2,9 +2,9 @@ from datetime import datetime
from pydantic import BaseModel from pydantic import BaseModel
from .....games.tetrio.api.typing import Rank from ......games.tetrio.api.typing import Rank
from ....typing import Number from .....typing import Number
from ..base import Avatar from ...base import Avatar
class TetraLeague(BaseModel): class TetraLeague(BaseModel):

View File

@@ -119,7 +119,7 @@ class Request:
async def request(cls, url: str, *, is_json: bool = True) -> bytes: async def request(cls, url: str, *, is_json: bool = True) -> bytes:
"""请求api""" """请求api"""
try: try:
async with AsyncClient(cookies=cls._cookies, timeout=config.tetris_req_timeout) as session: async with AsyncClient(cookies=cls._cookies, timeout=config.tetris.request_timeout) as session:
response = await session.get(url, headers=cls._headers) response = await session.get(url, headers=cls._headers)
if response.status_code != HTTPStatus.OK: if response.status_code != HTTPStatus.OK:
msg = f'请求错误 code: {response.status_code} {HTTPStatus(response.status_code).phrase}\n{response.text}' msg = f'请求错误 code: {response.status_code} {HTTPStatus(response.status_code).phrase}\n{response.text}'

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