Compare commits

...

131 Commits

Author SHA1 Message Date
5cb428ed71 🔖 1.0.0.a16 2024-04-30 02:09:34 +08:00
呵呵です
ec392ee384 支持茶服多个api地址的故障转移 (#301)
*  RequestError 新增 status_code 参数

*  新增支持故障转移的请求方法

*  支持茶服多个api地址的故障转移
2024-04-30 01:52:41 +08:00
dependabot[bot]
d037cf6d44 ⬆️ Bump nonebot-adapter-satori from 0.11.2 to 0.11.3 (#300)
Bumps [nonebot-adapter-satori](https://github.com/nonebot/adapter-satori) from 0.11.2 to 0.11.3.
- [Release notes](https://github.com/nonebot/adapter-satori/releases)
- [Commits](https://github.com/nonebot/adapter-satori/compare/v0.11.2...v0.11.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-30 00:02:36 +08:00
dependabot[bot]
6964e9b655 ⬆️ Bump nonebot-plugin-orm from 0.7.1 to 0.7.2 (#298)
Bumps [nonebot-plugin-orm](https://github.com/nonebot/plugin-orm) from 0.7.1 to 0.7.2.
- [Release notes](https://github.com/nonebot/plugin-orm/releases)
- [Commits](https://github.com/nonebot/plugin-orm/compare/v0.7.1...v0.7.2)

---
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-04-29 23:56:55 +08:00
dependabot[bot]
7a032bf947 ⬆️ Bump nonebot-adapter-kaiheila from 0.3.3 to 0.3.4 (#299)
Bumps [nonebot-adapter-kaiheila](https://github.com/Tian-que/nonebot-adapter-kaiheila) from 0.3.3 to 0.3.4.
- [Release notes](https://github.com/Tian-que/nonebot-adapter-kaiheila/releases)
- [Commits](https://github.com/Tian-que/nonebot-adapter-kaiheila/compare/v0.3.3...v0.3.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-29 23:56:31 +08:00
dependabot[bot]
9a91e5ef5b ⬆️ Bump nonebot-plugin-alconna from 0.44.0 to 0.45.0 (#297)
Bumps [nonebot-plugin-alconna](https://github.com/nonebot/plugin-alconna) from 0.44.0 to 0.45.0.
- [Release notes](https://github.com/nonebot/plugin-alconna/releases)
- [Commits](https://github.com/nonebot/plugin-alconna/compare/v0.44.0...v0.45.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-04-29 23:56:20 +08:00
dependabot[bot]
5b58697fce ⬆️ Bump nonebot-adapter-satori from 0.11.0 to 0.11.2 (#296)
Bumps [nonebot-adapter-satori](https://github.com/nonebot/adapter-satori) from 0.11.0 to 0.11.2.
- [Release notes](https://github.com/nonebot/adapter-satori/releases)
- [Commits](https://github.com/nonebot/adapter-satori/compare/v0.11.0...v0.11.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-28 15:38:57 +08:00
dependabot[bot]
b14cebe832 ⬆️ Bump ruff from 0.4.1 to 0.4.2 (#295)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.4.1 to 0.4.2.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.4.1...v0.4.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-28 15:38:48 +08:00
dependabot[bot]
4306195ee5 ⬆️ Bump nonebot-plugin-alconna from 0.43.0 to 0.44.0 (#294)
Bumps [nonebot-plugin-alconna](https://github.com/nonebot/plugin-alconna) from 0.43.0 to 0.44.0.
- [Release notes](https://github.com/nonebot/plugin-alconna/releases)
- [Commits](https://github.com/nonebot/plugin-alconna/compare/v0.43.0...v0.44.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-04-28 15:38:39 +08:00
dependabot[bot]
ac9c6e79d9 ⬆️ Bump nonebot-adapter-satori from 0.10.5 to 0.11.0 (#293)
Bumps [nonebot-adapter-satori](https://github.com/nonebot/adapter-satori) from 0.10.5 to 0.11.0.
- [Release notes](https://github.com/nonebot/adapter-satori/releases)
- [Commits](https://github.com/nonebot/adapter-satori/compare/v0.10.5...v0.11.0)

---
updated-dependencies:
- dependency-name: nonebot-adapter-satori
  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-04-25 15:53:32 +08:00
dependabot[bot]
ed035c65c1 ⬆️ Bump mypy from 1.9.0 to 1.10.0 (#292)
Bumps [mypy](https://github.com/python/mypy) from 1.9.0 to 1.10.0.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/1.9.0...v1.10.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-25 02:29:20 +08:00
dc8bc9b306 🚨 添加一些type: ignore) 2024-04-24 17:56:24 +08:00
454dd57007 🐛 mode写错了 2024-04-24 17:55:17 +08:00
b396a6d450 存储历史IO Rank数据至本地 2024-04-24 17:28:40 +08:00
7f584a46eb 添加依赖 zstandard 2024-04-24 16:56:44 +08:00
27518c0408 适配 Pydantic V2 2024-04-24 16:49:01 +08:00
dependabot[bot]
d2a3801dac ⬆️ Bump nonebot-plugin-alconna from 0.42.5 to 0.43.0 (#290)
Bumps [nonebot-plugin-alconna](https://github.com/nonebot/plugin-alconna) from 0.42.5 to 0.43.0.
- [Release notes](https://github.com/nonebot/plugin-alconna/releases)
- [Commits](https://github.com/nonebot/plugin-alconna/compare/v0.42.5...v0.43.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-04-24 00:53:44 +08:00
563564ac8d 适配 Pydantic V2 2024-04-24 00:52:32 +08:00
87c87ad231 🗃️ 重命名字段 2024-04-24 00:52:31 +08:00
30515d1907 🚨 ruff auto fix 2024-04-23 22:52:04 +08:00
dependabot[bot]
bd0a8ea447 ⬆️ Bump nonebot-plugin-alconna from 0.42.4 to 0.42.5 (#289)
Bumps [nonebot-plugin-alconna](https://github.com/nonebot/plugin-alconna) from 0.42.4 to 0.42.5.
- [Release notes](https://github.com/nonebot/plugin-alconna/releases)
- [Commits](https://github.com/nonebot/plugin-alconna/compare/v0.42.4...v0.42.5)

---
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-04-23 22:28:43 +08:00
dependabot[bot]
1db1e6dbba ⬆️ Bump nonebot-adapter-satori from 0.10.4 to 0.10.5 (#288)
Bumps [nonebot-adapter-satori](https://github.com/nonebot/adapter-satori) from 0.10.4 to 0.10.5.
- [Release notes](https://github.com/nonebot/adapter-satori/releases)
- [Commits](https://github.com/nonebot/adapter-satori/compare/v0.10.4...v0.10.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-23 22:28:21 +08:00
dependabot[bot]
9040aa9fba ⬆️ Bump ruff from 0.3.7 to 0.4.1 (#287)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.3.7 to 0.4.1.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.3.7...v0.4.1)

---
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-04-23 22:27:59 +08:00
呵呵です
3a5f1eb266 🔖 1.0.0.a15 2024-04-17 14:11:45 +08:00
MianSoft
43e927430a 👽️ 修改茶服api地址 (#286) 2024-04-17 14:09:59 +08:00
e1b0918a52 🔖 1.0.0.a14 2024-04-15 17:42:31 +08:00
c86b2eb31b ⬆️ 更新依赖 2024-04-15 17:41:39 +08:00
dependabot[bot]
47b3f3e881 ⬆️ Bump nonebot-plugin-alconna from 0.37.0 to 0.38.2 (#268)
Bumps [nonebot-plugin-alconna](https://github.com/nonebot/plugin-alconna) from 0.37.0 to 0.38.2.
- [Release notes](https://github.com/nonebot/plugin-alconna/releases)
- [Commits](https://github.com/nonebot/plugin-alconna/compare/v0.37.0...v0.38.2)

---
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-03-14 08:53:59 +08:00
dependabot[bot]
7caee587b4 ⬆️ Bump pandas from 2.2.0 to 2.2.1 (#266)
Bumps [pandas](https://github.com/pandas-dev/pandas) from 2.2.0 to 2.2.1.
- [Release notes](https://github.com/pandas-dev/pandas/releases)
- [Commits](https://github.com/pandas-dev/pandas/compare/v2.2.0...v2.2.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-14 08:53:44 +08:00
dependabot[bot]
28ae564e59 ⬆️ Bump nonebot-plugin-orm from 0.6.3 to 0.7.1 (#264)
Bumps [nonebot-plugin-orm](https://github.com/nonebot/plugin-orm) from 0.6.3 to 0.7.1.
- [Release notes](https://github.com/nonebot/plugin-orm/releases)
- [Commits](https://github.com/nonebot/plugin-orm/compare/v0.6.3...v0.7.1)

---
updated-dependencies:
- dependency-name: nonebot-plugin-orm
  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-03-04 14:06:04 +08:00
dependabot[bot]
90dee8402d ⬆️ Bump httpx from 0.26.0 to 0.27.0 (#263)
Bumps [httpx](https://github.com/encode/httpx) from 0.26.0 to 0.27.0.
- [Release notes](https://github.com/encode/httpx/releases)
- [Changelog](https://github.com/encode/httpx/blob/master/CHANGELOG.md)
- [Commits](https://github.com/encode/httpx/compare/0.26.0...0.27.0)

---
updated-dependencies:
- dependency-name: httpx
  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-03-04 12:02:53 +08:00
dependabot[bot]
8b560e55cb ⬆️ Bump pandas-stubs from 2.1.4.231227 to 2.2.0.240218 (#259)
Bumps [pandas-stubs](https://github.com/pandas-dev/pandas-stubs) from 2.1.4.231227 to 2.2.0.240218.
- [Changelog](https://github.com/pandas-dev/pandas-stubs/blob/main/docs/release_procedure.md)
- [Commits](https://github.com/pandas-dev/pandas-stubs/compare/v2.1.4.231227...v2.2.0.240218)

---
updated-dependencies:
- dependency-name: pandas-stubs
  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-03-04 11:53:08 +08:00
dependabot[bot]
3080531503 ⬆️ Bump nonebot-adapter-satori from 0.9.1 to 0.9.3 (#258)
Bumps [nonebot-adapter-satori](https://github.com/nonebot/adapter-satori) from 0.9.1 to 0.9.3.
- [Release notes](https://github.com/nonebot/adapter-satori/releases)
- [Commits](https://github.com/nonebot/adapter-satori/compare/v0.9.1...v0.9.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-04 11:52:54 +08:00
dependabot[bot]
fae0088533 ⬆️ Bump ruff from 0.2.1 to 0.3.0 (#265)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.2.1 to 0.3.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/v0.2.1...v0.3.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-03-04 11:48:09 +08:00
dependabot[bot]
db9286a369 ⬆️ Bump nonebot-adapter-onebot from 2.4.0 to 2.4.1 (#257)
Bumps [nonebot-adapter-onebot](https://github.com/nonebot/adapter-onebot) from 2.4.0 to 2.4.1.
- [Release notes](https://github.com/nonebot/adapter-onebot/releases)
- [Commits](https://github.com/nonebot/adapter-onebot/compare/v2.4.0...v2.4.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-17 22:14:59 +08:00
dependabot[bot]
420fb29318 ⬆️ Bump nonebot-adapter-kaiheila from 0.3.0 to 0.3.1 (#256)
Bumps [nonebot-adapter-kaiheila](https://github.com/Tian-que/nonebot-adapter-kaiheila) from 0.3.0 to 0.3.1.
- [Release notes](https://github.com/Tian-que/nonebot-adapter-kaiheila/releases)
- [Commits](https://github.com/Tian-que/nonebot-adapter-kaiheila/compare/v0.03...v0.3.1)

---
updated-dependencies:
- dependency-name: nonebot-adapter-kaiheila
  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-02-17 21:31:20 +08:00
dependabot[bot]
433a6edd3b ⬆️ Bump nonebot-adapter-satori from 0.9.0 to 0.9.1 (#255)
Bumps [nonebot-adapter-satori](https://github.com/nonebot/adapter-satori) from 0.9.0 to 0.9.1.
- [Release notes](https://github.com/nonebot/adapter-satori/releases)
- [Commits](https://github.com/nonebot/adapter-satori/compare/v0.9.0...v0.9.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-17 20:56:27 +08:00
dependabot[bot]
fa81231f78 ⬆️ Bump nonebot-plugin-alconna from 0.36.2 to 0.37.0 (#253)
Bumps [nonebot-plugin-alconna](https://github.com/nonebot/plugin-alconna) from 0.36.2 to 0.37.0.
- [Release notes](https://github.com/nonebot/plugin-alconna/releases)
- [Commits](https://github.com/nonebot/plugin-alconna/compare/v0.36.2...v0.37.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-02-17 19:22:12 +08:00
dependabot[bot]
c474cf0af2 ⬆️ Bump nonebot-plugin-apscheduler from 0.3.0 to 0.4.0 (#252)
Bumps [nonebot-plugin-apscheduler](https://github.com/nonebot/plugin-apscheduler) from 0.3.0 to 0.4.0.
- [Release notes](https://github.com/nonebot/plugin-apscheduler/releases)
- [Commits](https://github.com/nonebot/plugin-apscheduler/compare/v0.3.0...v0.4.0)

---
updated-dependencies:
- dependency-name: nonebot-plugin-apscheduler
  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-02-17 10:09:27 +08:00
dependabot[bot]
e38eb5cdff ⬆️ Bump types-lxml from 2023.10.21 to 2024.2.9 (#251)
Bumps [types-lxml](https://github.com/abelcheung/types-lxml) from 2023.10.21 to 2024.2.9.
- [Release notes](https://github.com/abelcheung/types-lxml/releases)
- [Commits](https://github.com/abelcheung/types-lxml/compare/2023.10.21...2024.02.09)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-14 16:39:41 +08:00
dependabot[bot]
7bacf89840 ⬆️ Bump nonebot-adapter-onebot from 2.3.1 to 2.4.0 (#254)
Bumps [nonebot-adapter-onebot](https://github.com/nonebot/adapter-onebot) from 2.3.1 to 2.4.0.
- [Release notes](https://github.com/nonebot/adapter-onebot/releases)
- [Commits](https://github.com/nonebot/adapter-onebot/compare/v2.3.1...v2.4.0)

---
updated-dependencies:
- dependency-name: nonebot-adapter-onebot
  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-02-14 14:44:14 +08:00
dependabot[bot]
4622e90995 ⬆️ Bump nonebot-adapter-satori from 0.8.1 to 0.9.0 (#250)
Bumps [nonebot-adapter-satori](https://github.com/nonebot/adapter-satori) from 0.8.1 to 0.9.0.
- [Release notes](https://github.com/nonebot/adapter-satori/releases)
- [Commits](https://github.com/nonebot/adapter-satori/compare/v0.8.1...v0.9.0)

---
updated-dependencies:
- dependency-name: nonebot-adapter-satori
  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-02-12 22:42:50 +08:00
dependabot[bot]
fa8c2b11e6 ⬆️ Bump nonebot-plugin-localstore from 0.5.1 to 0.6.0 (#249)
Bumps [nonebot-plugin-localstore](https://github.com/nonebot/plugin-localstore) from 0.5.1 to 0.6.0.
- [Release notes](https://github.com/nonebot/plugin-localstore/releases)
- [Commits](https://github.com/nonebot/plugin-localstore/compare/v0.5.1...v0.6.0)

---
updated-dependencies:
- dependency-name: nonebot-plugin-localstore
  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-02-12 19:54:44 +08:00
dependabot[bot]
2123b747af ⬆️ Bump playwright from 1.41.1 to 1.41.2 (#246)
Bumps [playwright](https://github.com/Microsoft/playwright-python) from 1.41.1 to 1.41.2.
- [Release notes](https://github.com/Microsoft/playwright-python/releases)
- [Commits](https://github.com/Microsoft/playwright-python/compare/v1.41.1...v1.41.2)

---
updated-dependencies:
- dependency-name: playwright
  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-02-12 08:38:07 +08:00
dependabot[bot]
e65233d09f ⬆️ Bump viztracer from 0.16.1 to 0.16.2 (#245)
Bumps [viztracer](https://github.com/gaogaotiantian/viztracer) from 0.16.1 to 0.16.2.
- [Release notes](https://github.com/gaogaotiantian/viztracer/releases)
- [Commits](https://github.com/gaogaotiantian/viztracer/compare/0.16.1...0.16.2)

---
updated-dependencies:
- dependency-name: viztracer
  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-02-12 08:03:30 +08:00
dependabot[bot]
7e81bf6b8b ⬆️ Bump ruff from 0.2.0 to 0.2.1 (#243)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.2.0 to 0.2.1.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.2.0...v0.2.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-12 07:41:37 +08:00
dependabot[bot]
c4614aa006 ⬆️ Bump nonebot2 from 2.1.3 to 2.2.0 (#248)
Bumps [nonebot2](https://github.com/nonebot/nonebot2) from 2.1.3 to 2.2.0.
- [Release notes](https://github.com/nonebot/nonebot2/releases)
- [Changelog](https://github.com/nonebot/nonebot2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nonebot/nonebot2/compare/v2.1.3...v2.2.0)

---
updated-dependencies:
- dependency-name: nonebot2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-12 07:30:30 +08:00
dependabot[bot]
79a657b9f5 ⬆️ Bump playwright from 1.40.0 to 1.41.1 (#237)
Bumps [playwright](https://github.com/Microsoft/playwright-python) from 1.40.0 to 1.41.1.
- [Release notes](https://github.com/Microsoft/playwright-python/releases)
- [Commits](https://github.com/Microsoft/playwright-python/compare/v1.40.0...v1.41.1)

---
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-02-06 07:07:21 +08:00
dependabot[bot]
0164f29c1e ⬆️ Bump pandas from 2.1.4 to 2.2.0 (#235)
Bumps [pandas](https://github.com/pandas-dev/pandas) from 2.1.4 to 2.2.0.
- [Release notes](https://github.com/pandas-dev/pandas/releases)
- [Commits](https://github.com/pandas-dev/pandas/compare/v2.1.4...v2.2.0)

---
updated-dependencies:
- dependency-name: pandas
  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-02-06 07:00:41 +08:00
dependabot[bot]
8db56366df ⬆️ Bump nonebot-plugin-alconna from 0.35.1 to 0.36.2 (#239)
Bumps [nonebot-plugin-alconna](https://github.com/nonebot/plugin-alconna) from 0.35.1 to 0.36.2.
- [Release notes](https://github.com/nonebot/plugin-alconna/releases)
- [Commits](https://github.com/nonebot/plugin-alconna/compare/v0.35.1...v0.36.2)

---
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-02-06 07:00:12 +08:00
dependabot[bot]
de0a1e4c73 ⬆️ Bump nonebot-plugin-orm from 0.6.2 to 0.6.3 (#233)
Bumps [nonebot-plugin-orm](https://github.com/nonebot/plugin-orm) from 0.6.2 to 0.6.3.
- [Release notes](https://github.com/nonebot/plugin-orm/releases)
- [Commits](https://github.com/nonebot/plugin-orm/compare/v0.6.2...v0.6.3)

---
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-02-06 05:52:35 +08:00
dependabot[bot]
3670ce7221 ⬆️ Bump ruff from 0.1.13 to 0.2.0 (#240)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.1.13 to 0.2.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/v0.1.13...v0.2.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-02-06 05:51:32 +08:00
dependabot[bot]
101ed737ab ⬆️ Bump fastapi from 0.103.0 to 0.109.1 (#241)
Bumps [fastapi](https://github.com/tiangolo/fastapi) from 0.103.0 to 0.109.1.
- [Release notes](https://github.com/tiangolo/fastapi/releases)
- [Commits](https://github.com/tiangolo/fastapi/compare/0.103.0...0.109.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-06 05:51:11 +08:00
呵呵です
1611bf47fa 🔖 1.0.0.a13 2024-01-15 18:51:38 +08:00
dependabot[bot]
e084cdb145 ⬆️ Bump lxml from 5.0.0 to 5.1.0 (#230)
Bumps [lxml](https://github.com/lxml/lxml) from 5.0.0 to 5.1.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.0.0...lxml-5.1.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-01-15 18:51:05 +08:00
呵呵です
27258ab744 👽️ 修改茶服api地址 2024-01-15 18:40:20 +08:00
dependabot[bot]
07324825e6 ⬆️ Bump ruff from 0.1.11 to 0.1.13 (#231)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.1.11 to 0.1.13.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.1.11...v0.1.13)

---
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-01-15 18:35:27 +08:00
dependabot[bot]
472becdfe0 ⬆️ Bump types-aiofiles from 23.2.0.0 to 23.2.0.20240106 (#229)
Bumps [types-aiofiles](https://github.com/python/typeshed) from 23.2.0.0 to 23.2.0.20240106.
- [Commits](https://github.com/python/typeshed/commits)

---
updated-dependencies:
- dependency-name: types-aiofiles
  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-01-15 12:15:09 +08:00
dependabot[bot]
bc87e4b16d ⬆️ Bump ruff from 0.1.9 to 0.1.11 (#228)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.1.9 to 0.1.11.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.1.9...v0.1.11)

---
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-01-04 06:21:11 +08:00
dependabot[bot]
28e2a46303 ⬆️ Bump nonebot-plugin-orm from 0.6.0 to 0.6.2 (#227)
Bumps [nonebot-plugin-orm](https://github.com/nonebot/plugin-orm) from 0.6.0 to 0.6.2.
- [Release notes](https://github.com/nonebot/plugin-orm/releases)
- [Commits](https://github.com/nonebot/plugin-orm/compare/v0.6.0...v0.6.2)

---
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-01-04 06:20:58 +08:00
1324015d58 🔖 1.0.0.a12 2024-01-03 09:52:25 +08:00
e6eae023e7 🐛 修复茶服pb数据处理错误的bug 2024-01-03 09:51:56 +08:00
67cfb07246 🔖 1.0.0.a11 2024-01-03 09:37:36 +08:00
12145a614f ⬇️ 错误的使用了Python3.11的新特性 2024-01-03 09:37:10 +08:00
0b07882a16 🐛 修复事件没有正确结束的bug 2024-01-03 09:27:26 +08:00
呵呵です
9073bf5d0b 🔖 1.0.0.a10 2024-01-03 09:02:22 +08:00
dependabot[bot]
f4dd5fe76f ⬆️ Bump nonebot-plugin-alconna from 0.34.1 to 0.35.1 (#226) 2024-01-03 09:02:20 +08:00
dependabot[bot]
1f44fc9884 ⬆️ Bump nonebot2 from 2.1.2 to 2.1.3 (#225) 2024-01-03 09:02:18 +08:00
dependabot[bot]
44dee7f200 ⬆️ Bump types-ujson from 5.8.0.1 to 5.9.0.0 (#224)
Bumps [types-ujson](https://github.com/python/typeshed) from 5.8.0.1 to 5.9.0.0.
- [Commits](https://github.com/python/typeshed/commits)

---
updated-dependencies:
- dependency-name: types-ujson
  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-01-03 09:02:14 +08:00
dependabot[bot]
dc5ade6ffc ⬆️ Bump pandas from 2.1.3 to 2.1.4 (#223)
Bumps [pandas](https://github.com/pandas-dev/pandas) from 2.1.3 to 2.1.4.
- [Release notes](https://github.com/pandas-dev/pandas/releases)
- [Commits](https://github.com/pandas-dev/pandas/compare/v2.1.3...v2.1.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-03 09:02:12 +08:00
dependabot[bot]
05ce329976 ⬆️ Bump pandas-stubs from 2.1.1.230928 to 2.1.4.231227 (#222)
Bumps [pandas-stubs](https://github.com/pandas-dev/pandas-stubs) from 2.1.1.230928 to 2.1.4.231227.
- [Changelog](https://github.com/pandas-dev/pandas-stubs/blob/main/docs/release_procedure.md)
- [Commits](https://github.com/pandas-dev/pandas-stubs/compare/v2.1.1.230928...v2.1.4.231227)

---
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-01-03 09:02:07 +08:00
dependabot[bot]
43cabf2135 ⬆️ Bump nonebot-adapter-discord from 0.1.2 to 0.1.3 (#218) 2024-01-03 09:02:06 +08:00
dependabot[bot]
6767136850 ⬆️ Bump lxml from 4.9.3 to 5.0.0 (#221) 2024-01-03 09:02:06 +08:00
dependabot[bot]
65999b4625 ⬆️ Bump nonebot-adapter-satori from 0.8.0 to 0.8.1 (#217) 2024-01-03 09:02:06 +08:00
dependabot[bot]
9fde62ac9e ⬆️ Bump ujson from 5.8.0 to 5.9.0 (#219) 2024-01-03 09:02:05 +08:00
dependabot[bot]
c74d8b70aa ⬆️ Bump ruff from 0.1.6 to 0.1.9 (#220)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.1.6 to 0.1.9.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.1.6...v0.1.9)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-03 09:02:05 +08:00
dependabot[bot]
0e29b38f9d ⬆️ Bump playwright from 1.39.0 to 1.40.0 (#205) 2024-01-03 09:01:45 +08:00
dependabot[bot]
d040c7dca2 ⬆️ Bump viztracer from 0.16.0 to 0.16.1 (#211) 2024-01-03 09:00:06 +08:00
dependabot[bot]
68ace3a715 ⬆️ Bump httpx from 0.25.1 to 0.26.0 (#214) 2024-01-03 09:00:00 +08:00
dependabot[bot]
e63ac69e0f ⬆️ Bump mypy from 1.7.0 to 1.8.0 (#215) 2024-01-03 08:59:18 +08:00
4afda62782 添加状态码检查 2023-12-30 06:52:45 +08:00
呵呵です
abf4410a00 👽️ 适配 茶服 新赛季 (#216)
* 👽️ 适配 茶服 新赛季

* ✏️ 少个-

*  添加开发依赖 nonebot-adapter-kaiheila

*  适配 kook 茶服查target

* 🐛 修复 onebotv11 查自己 找不到用户的bug

* 🐛 修复 茶服 查绑定 找不到用户的bug

*  kook 茶服查target 添加后备方案

*  添加开发依赖 nonebot-adapter-discord

*  适配 discord 茶服查target
2023-12-30 06:43:06 +08:00
88c2915251 🐛 修复 pydantic model 不能被正确反序列化的bug 2023-11-29 11:43:00 +08:00
546369241a 添加冗余 platform 字段 2023-11-29 11:41:48 +08:00
d59bccbd4d 细化异常 2023-11-29 11:29:46 +08:00
75a6989a7f 使用上下文管理器管理页面 2023-11-29 11:00:55 +08:00
ad635bd37d 🎨 修改错误处理逻辑 2023-11-29 10:59:58 +08:00
呵呵です
b6d63c9e7f 🐛 修复 io record 解析错误的bug (#207) 2023-11-23 20:07:57 +08:00
805da8cd36 🔖 1.0.0.a9 2023-11-22 18:34:07 +08:00
4a13d7807a 🐛 修复计算时间时区不正确的bug 2023-11-22 18:33:42 +08:00
7bbdeacc5e 🔖 1.0.0.a8 2023-11-22 16:11:57 +08:00
dependabot[bot]
782792e455 ⬆️ Bump nonebot-plugin-orm from 0.5.1 to 0.6.0 (#203) 2023-11-22 08:11:15 +00:00
dependabot[bot]
bd10549b4c ⬆️ Bump ruff from 0.1.5 to 0.1.6 (#202) 2023-11-22 08:02:43 +00:00
dependabot[bot]
035e6d4782 ⬆️ Bump nonebot-plugin-alconna from 0.33.3 to 0.33.6 (#201) 2023-11-22 08:02:33 +00:00
003e6619d8 iorank 指令不再去尝试更新数据 2023-11-22 15:58:55 +08:00
c0fa92df30 🚨 fix Incompatible overrides 2023-11-22 15:57:04 +08:00
7cdb0f3547 为 IO Rank 添加重试机制 2023-11-22 15:49:33 +08:00
b773fb44a1 ️ 为 IO 添加缓存 2023-11-22 13:22:18 +08:00
c75c6b73bd 🙈 更新.gitignore 2023-11-22 13:02:17 +08:00
67782c3156 添加依赖 aiocache 2023-11-22 13:01:41 +08:00
1e02858913 💥 🗃️ 将 pydantic 模型序列化后再存数据库 2023-11-21 20:47:56 +08:00
60605d0dca 🐛 修复 IO Z段位 不显示glicko和rd的bug 2023-11-21 13:58:39 +08:00
0d589450bd 将处理过程中的 dataclass 换成 pydantic 2023-11-21 00:50:32 +08:00
dependabot[bot]
2f144acf0c ⬆️ Bump nonebot-adapter-satori from 0.7.0 to 0.8.0 (#200)
Bumps [nonebot-adapter-satori](https://github.com/nonebot/adapter-satori) from 0.7.0 to 0.8.0.
- [Release notes](https://github.com/nonebot/adapter-satori/releases)
- [Commits](https://github.com/nonebot/adapter-satori/compare/v0.7.0...v0.8.0)

---
updated-dependencies:
- dependency-name: nonebot-adapter-satori
  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>
2023-11-17 01:49:57 +08:00
87e6a544a2 🔖 1.0.0.a7 2023-11-16 21:34:41 +08:00
74db1931fd 🐛 修复在多个无效参数时 Alconna 自动回复的bug 2023-11-16 21:33:24 +08:00
1ca6d1f86a 使用 zoneinfo 处理时区,并优化数据库查询逻辑 2023-11-16 16:27:06 +08:00
dependabot[bot]
7361789245 ⬆️ Bump nonebot-plugin-orm from 0.5.0 to 0.5.1 (#199) 2023-11-15 15:44:10 +00:00
fe69d8d2fe 🔖 1.0.0.a6 2023-11-15 14:37:41 +08:00
2737119865 🐛 修复 茶服 命令参数设置错误的bug 2023-11-15 14:36:54 +08:00
34a654b5df 🐛 修复只输入主命令时不发送帮助提示的bug 2023-11-15 14:34:27 +08:00
f9f39618a1 💚 修复Release CI 2023-11-15 14:02:22 +08:00
81a3c9cb79 🔖 1.0.0.a5 2023-11-15 11:44:23 +08:00
4a15c45e0a 💚 修复Release CI 2023-11-15 11:44:23 +08:00
e90ad53ee6 🐛 修复在事件响应器异常退出后 Recorder 继续执行的bug 2023-11-15 11:37:12 +08:00
0c968be163 避免 Alconna 在 ParamsUnmatched 时自动回复
🎨 将通用 handle 封装一下
2023-11-15 11:04:09 +08:00
bfadac4f79 添加配置项 请求超时时间 2023-11-15 00:43:12 +08:00
89f09cd66c 🔥 删除配置项 db_url 2023-11-15 00:37:04 +08:00
777703362e 🎨 先断开连接再删文件 2023-11-15 00:28:54 +08:00
ea5308877c 🎨 🔥 去除一个没什么用的函数 2023-11-15 00:26:55 +08:00
3cc93925a6 🐛 修复迁移旧数据库时拿错config字段的bug 2023-11-15 00:14:03 +08:00
e0bd0a9252 🐛 修复 io user info 解析错误的bug 2023-11-15 00:07:05 +08:00
d31ce48a18 💚 修复Release CI 2023-11-14 13:15:51 +08:00
7da38e0346 🔖 Release 1.0.0.a4 2023-11-14 13:11:46 +08:00
呵呵です
84368a16c3 👷 更改 dependabot 推送分支 2023-11-14 13:44:58 +08:00
6a10ede5ba 👷 更新Release CI 2023-11-14 12:56:06 +08:00
4c205e516f 🔥 去除命令解析失败时发送提醒 2023-11-14 12:54:42 +08:00
c1feccd608 🔖 1.0.0.a3 2023-11-14 09:07:08 +08:00
f27d7b4440 🐛 修复 io record 解析错误的bug 2023-11-14 09:02:22 +08:00
7fa498de48 👽️ io record 添加一个pentas字段 2023-11-14 09:00:35 +08:00
ff7c5847a3 🐛 修复 io record 解析错误的bug 2023-11-14 08:59:44 +08:00
39 changed files with 2379 additions and 1424 deletions

View File

@@ -7,6 +7,6 @@ version: 2
updates: updates:
- package-ecosystem: "pip" # See documentation for possible values - package-ecosystem: "pip" # See documentation for possible values
directory: "/" # Location of package manifests directory: "/" # Location of package manifests
target-branch: "dev" target-branch: "main"
schedule: schedule:
interval: "daily" interval: "daily"

View File

@@ -8,18 +8,42 @@ on:
jobs: jobs:
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
id-token: write
contents: write
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Install poetry
run: pipx install poetry
shell: bash
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
with: with:
python-version: '3.11' python-version: '3.11'
- name: Install Poetry cache: "poetry"
- run: poetry install
shell: bash shell: bash
- name: Get Version
id: version
run: | run: |
pip install poetry echo "VERSION=$(poetry version -s)" >> $GITHUB_OUTPUT
- name: Build echo "TAG_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
shell: bash echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
run: |
poetry install - name: Check Version
poetry env use python if: steps.version.outputs.VERSION != steps.version.outputs.TAG_VERSION
poetry publish --build -u ${{ secrets.USERNAME }} -p ${{ secrets.PASSWORD }} -n run: exit 1
- name: Build Package
run: poetry build
- name: Publish Package to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
- name: Publish Package to GitHub Release
run: gh release create ${{ steps.version.outputs.TAG_NAME }} dist/*.tar.gz dist/*.whl -t "🔖 ${{ steps.version.outputs.TAG_NAME }}" --generate-notes
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

2
.gitignore vendored
View File

@@ -5,6 +5,6 @@ Untitled*
*copy* *copy*
.vscode .vscode
*dev* *dev*
*cache* *_cache*
*backup* *backup*
*.pyc *.pyc

View File

@@ -11,4 +11,4 @@ CACHE_PATH: Path = get_cache_dir('nonebot_plugin_tetris_stats')
class Config(BaseModel): class Config(BaseModel):
"""配置类""" """配置类"""
db_url: str = 'sqlite://data/nonebot_plugin_tetris_stats/data.db' tetris_req_timeout: float = 30.0

View File

@@ -0,0 +1,51 @@
"""Rename field
迁移 ID: 09d4bb60160d
父迁移: b9d65badc713
创建时间: 2024-04-23 23:42:04.541672
"""
from __future__ import annotations
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = '09d4bb60160d'
down_revision: str | Sequence[str] | None = 'b9d65badc713'
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_iorank', schema=None) as batch_op:
batch_op.alter_column('create_time', new_column_name='update_time', existing_type=sa.DateTime())
batch_op.drop_index('ix_nonebot_plugin_tetris_stats_iorank_create_time')
op.create_index(
batch_op.f('ix_nonebot_plugin_tetris_stats_iorank_update_time'),
'nonebot_plugin_tetris_stats_iorank',
['update_time'],
unique=False,
)
# ### end Alembic commands ###
def downgrade(name: str = '') -> None:
if name:
return
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('nonebot_plugin_tetris_stats_iorank', schema=None) as batch_op:
batch_op.alter_column('update_time', new_column_name='create_time')
batch_op.drop_index(batch_op.f('ix_nonebot_plugin_tetris_stats_iorank_update_time'))
op.create_index(
'ix_nonebot_plugin_tetris_stats_iorank_create_time',
'nonebot_plugin_tetris_stats_iorank',
['create_time'],
unique=False,
)
# ### end Alembic commands ###

View File

@@ -0,0 +1,43 @@
"""add field
迁移 ID: 0d50142b780f
父迁移: 09d4bb60160d
创建时间: 2024-04-24 14:55:08.064098
"""
from __future__ import annotations
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = '0d50142b780f'
down_revision: str | Sequence[str] | None = '09d4bb60160d'
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_iorank', schema=None) as batch_op:
batch_op.add_column(sa.Column('file_hash', sa.String(length=128), nullable=True))
batch_op.create_index(
batch_op.f('ix_nonebot_plugin_tetris_stats_iorank_file_hash'), ['file_hash'], unique=False
)
# ### end Alembic commands ###
def downgrade(name: str = '') -> None:
if name:
return
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('nonebot_plugin_tetris_stats_iorank', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_nonebot_plugin_tetris_stats_iorank_file_hash'))
batch_op.drop_column('file_hash')
# ### end Alembic commands ###

View File

@@ -0,0 +1,65 @@
"""Add redundant platform field
迁移 ID: 6c3206f90cc3
父迁移: 9f6582279ce2
创建时间: 2023-11-26 20:15:56.033892
"""
from __future__ import annotations
from collections.abc import Sequence
from alembic import op
from sqlalchemy.ext.automap import automap_base
from sqlalchemy.orm import Session
from ujson import dumps, loads
revision: str = '6c3206f90cc3'
down_revision: str | Sequence[str] | None = '9f6582279ce2'
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade(name: str = '') -> None:
if name:
return
Base = automap_base() # noqa: N806
connection = op.get_bind()
Base.prepare(autoload_with=connection)
HistoricalData = Base.classes.nonebot_plugin_tetris_stats_historicaldata # noqa: N806
with Session(connection) as session:
for row in session.query(HistoricalData):
platform = row.game_platform
game_user = loads(row.game_user)
processed_data = loads(row.processed_data)
game_user['platform'] = platform
processed_data['platform'] = platform
row.game_user = dumps(game_user)
row.processed_data = dumps(processed_data)
session.add(row)
session.commit()
def downgrade(name: str = '') -> None:
if name:
return
Base = automap_base() # noqa: N806
connection = op.get_bind()
Base.prepare(autoload_with=connection)
HistoricalData = Base.classes.nonebot_plugin_tetris_stats_historicaldata # noqa: N806
with Session(connection) as session:
for row in session.query(HistoricalData):
game_user = loads(row.game_user)
processed_data = loads(row.processed_data)
game_user.pop('platform', None)
processed_data.pop('platform', None)
row.game_user = dumps(game_user)
row.processed_data = dumps(processed_data)
session.add(row)
session.commit()

View File

@@ -64,7 +64,7 @@ def upgrade(name: str = '') -> None:
if name: if name:
return return
try: try:
db_path = Path(config.db_path) db_path = Path(config.db_url)
except AttributeError: except AttributeError:
db_path = Path('data/nonebot_plugin_tetris_stats/data.db') db_path = Path('data/nonebot_plugin_tetris_stats/data.db')
if db_path.exists() is False: if db_path.exists() is False:
@@ -84,7 +84,7 @@ def upgrade(name: str = '') -> None:
raise RuntimeError('nonebot_plugin_tetris_stats: 请先安装 0.4.4 版本完成迁移之后再升级') raise RuntimeError('nonebot_plugin_tetris_stats: 请先安装 0.4.4 版本完成迁移之后再升级')
logger.info('nonebot_plugin_tetris_stats: 发现来自老版本的数据, 正在迁移...') logger.info('nonebot_plugin_tetris_stats: 发现来自老版本的数据, 正在迁移...')
migrate_old_data(connection) migrate_old_data(connection)
db_path.unlink() db_path.unlink()
def downgrade(name: str = '') -> None: def downgrade(name: str = '') -> None:

View File

@@ -0,0 +1,112 @@
"""Recreate HistoricalData
迁移 ID: 9f6582279ce2
父迁移: 9cd1647db502
创建时间: 2023-11-21 08:35:50.393246
"""
from __future__ import annotations
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import sqlite
from nonebot_plugin_tetris_stats.db.models import PydanticType
revision: str = '9f6582279ce2'
down_revision: str | Sequence[str] | None = '9cd1647db502'
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_historicaldata', schema=None) as batch_op:
batch_op.drop_index('ix_nonebot_plugin_tetris_stats_historicaldata_command_type')
batch_op.drop_index('ix_nonebot_plugin_tetris_stats_historicaldata_game_platform')
batch_op.drop_index('ix_nonebot_plugin_tetris_stats_historicaldata_source_account')
batch_op.drop_index('ix_nonebot_plugin_tetris_stats_historicaldata_source_type')
op.drop_table('nonebot_plugin_tetris_stats_historicaldata')
op.create_table(
'nonebot_plugin_tetris_stats_historicaldata',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('trigger_time', sa.DateTime(), nullable=False),
sa.Column('bot_platform', sa.String(length=32), nullable=True),
sa.Column('bot_account', sa.String(), nullable=True),
sa.Column('source_type', sa.String(length=32), nullable=True),
sa.Column('source_account', sa.String(), nullable=True),
sa.Column('message', sa.PickleType(), nullable=True),
sa.Column('game_platform', sa.String(length=32), nullable=False),
sa.Column('command_type', sa.String(length=16), nullable=False),
sa.Column('command_args', sa.JSON(), nullable=False),
sa.Column('game_user', PydanticType(list), nullable=False),
sa.Column('processed_data', PydanticType(list), nullable=False),
sa.Column('finish_time', sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('pk_nonebot_plugin_tetris_stats_historicaldata')),
)
with op.batch_alter_table('nonebot_plugin_tetris_stats_historicaldata', schema=None) as batch_op:
batch_op.create_index(
batch_op.f('ix_nonebot_plugin_tetris_stats_historicaldata_command_type'), ['command_type'], unique=False
)
batch_op.create_index(
batch_op.f('ix_nonebot_plugin_tetris_stats_historicaldata_game_platform'), ['game_platform'], unique=False
)
batch_op.create_index(
batch_op.f('ix_nonebot_plugin_tetris_stats_historicaldata_source_account'), ['source_account'], unique=False
)
batch_op.create_index(
batch_op.f('ix_nonebot_plugin_tetris_stats_historicaldata_source_type'), ['source_type'], unique=False
)
# ### end Alembic commands ###
def downgrade(name: str = '') -> None:
if name:
return
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('nonebot_plugin_tetris_stats_historicaldata', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_nonebot_plugin_tetris_stats_historicaldata_source_type'))
batch_op.drop_index(batch_op.f('ix_nonebot_plugin_tetris_stats_historicaldata_source_account'))
batch_op.drop_index(batch_op.f('ix_nonebot_plugin_tetris_stats_historicaldata_game_platform'))
batch_op.drop_index(batch_op.f('ix_nonebot_plugin_tetris_stats_historicaldata_command_type'))
op.drop_table('nonebot_plugin_tetris_stats_historicaldata')
op.create_table(
'nonebot_plugin_tetris_stats_historicaldata',
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('trigger_time', sa.DATETIME(), nullable=False),
sa.Column('bot_platform', sa.VARCHAR(length=32), nullable=True),
sa.Column('bot_account', sa.VARCHAR(), nullable=True),
sa.Column('source_type', sa.VARCHAR(length=32), nullable=True),
sa.Column('source_account', sa.VARCHAR(), nullable=True),
sa.Column('message', sa.BLOB(), nullable=True),
sa.Column('game_platform', sa.VARCHAR(length=32), nullable=False),
sa.Column('command_type', sa.VARCHAR(length=16), nullable=False),
sa.Column('command_args', sqlite.JSON(), nullable=False),
sa.Column('game_user', sa.BLOB(), nullable=False),
sa.Column('processed_data', sa.BLOB(), nullable=False),
sa.Column('finish_time', sa.DATETIME(), nullable=False),
sa.PrimaryKeyConstraint('id', name='pk_nonebot_plugin_tetris_stats_historicaldata'),
)
with op.batch_alter_table('nonebot_plugin_tetris_stats_historicaldata', schema=None) as batch_op:
batch_op.create_index(
'ix_nonebot_plugin_tetris_stats_historicaldata_source_type', ['source_type'], unique=False
)
batch_op.create_index(
'ix_nonebot_plugin_tetris_stats_historicaldata_source_account', ['source_account'], unique=False
)
batch_op.create_index(
'ix_nonebot_plugin_tetris_stats_historicaldata_game_platform', ['game_platform'], unique=False
)
batch_op.create_index(
'ix_nonebot_plugin_tetris_stats_historicaldata_command_type', ['command_type'], unique=False
)
# ### end Alembic commands ###

View File

@@ -0,0 +1,38 @@
"""Del old TOS bind data
迁移 ID: b9d65badc713
父迁移: 6c3206f90cc3
创建时间: 2023-12-30 00:27:40.991704
"""
from __future__ import annotations
from collections.abc import Sequence
from alembic import op
from sqlalchemy.ext.automap import automap_base
from sqlalchemy.orm import Session
revision: str = 'b9d65badc713'
down_revision: str | Sequence[str] | None = '6c3206f90cc3'
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade(name: str = '') -> None:
if name:
return
Base = automap_base() # noqa: N806
connection = op.get_bind()
Base.prepare(autoload_with=connection)
Bind = Base.classes.nonebot_plugin_tetris_stats_bind # noqa: N806
with Session(connection) as session:
session.query(Bind).filter(Bind.game_platform == 'TOS').delete()
session.commit()
def downgrade(name: str = '') -> None:
if name:
return

View File

@@ -1,14 +1,41 @@
from collections.abc import Callable, Sequence
from datetime import datetime from datetime import datetime
from typing import Any
from nonebot.adapters import Message from nonebot.adapters import Message
from nonebot_plugin_orm import Model from nonebot_plugin_orm import Model
from sqlalchemy import JSON, DateTime, PickleType, String from pydantic import BaseModel, ValidationError
from sqlalchemy import JSON, DateTime, Dialect, PickleType, String, TypeDecorator
from sqlalchemy.orm import Mapped, MappedAsDataclass, mapped_column from sqlalchemy.orm import Mapped, MappedAsDataclass, mapped_column
from ..game_data_processor import ProcessedData, User from ..game_data_processor.schemas import BaseProcessedData, BaseUser
from ..utils.typing import CommandType, GameType from ..utils.typing import CommandType, GameType
class PydanticType(TypeDecorator):
impl = JSON
def __init__(self, get_model: Callable[[], Sequence[type[BaseModel]]], *args: Any, **kwargs: Any): # noqa: ANN401
self.get_model = get_model
super().__init__(*args, **kwargs)
def process_bind_param(self, value: Any | None, dialect: Dialect) -> str: # noqa: ANN401
# 将 Pydantic 模型实例转换为 JSON
if isinstance(value, tuple(self.get_model())):
return value.json() # type: ignore[union-attr]
raise TypeError
def process_result_value(self, value: Any | None, dialect: Dialect) -> BaseModel: # noqa: ANN401
# 将 JSON 转换回 Pydantic 模型实例
if isinstance(value, str | bytes):
for i in self.get_model():
try:
return i.model_validate_json(value)
except ValidationError: # noqa: PERF203
...
raise TypeError
class Bind(MappedAsDataclass, Model): class Bind(MappedAsDataclass, Model):
id: Mapped[int] = mapped_column(init=False, primary_key=True) id: Mapped[int] = mapped_column(init=False, primary_key=True)
chat_platform: Mapped[str] = mapped_column(String(32), index=True) chat_platform: Mapped[str] = mapped_column(String(32), index=True)
@@ -28,6 +55,8 @@ class HistoricalData(MappedAsDataclass, Model):
game_platform: Mapped[GameType] = mapped_column(String(32), index=True, init=False) game_platform: Mapped[GameType] = mapped_column(String(32), index=True, init=False)
command_type: Mapped[CommandType] = mapped_column(String(16), index=True, init=False) command_type: Mapped[CommandType] = mapped_column(String(16), index=True, init=False)
command_args: Mapped[list[str]] = mapped_column(JSON, init=False) command_args: Mapped[list[str]] = mapped_column(JSON, init=False)
game_user: Mapped[User] = mapped_column(PickleType, init=False) game_user: Mapped[BaseUser] = mapped_column(PydanticType(get_model=BaseUser.__subclasses__), init=False)
processed_data: Mapped[ProcessedData] = mapped_column(PickleType, init=False) processed_data: Mapped[BaseProcessedData] = mapped_column(
PydanticType(get_model=BaseProcessedData.__subclasses__), init=False
)
finish_time: Mapped[datetime] = mapped_column(DateTime, init=False) finish_time: Mapped[datetime] = mapped_column(DateTime, init=False)

View File

@@ -1,26 +1,18 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from dataclasses import dataclass from datetime import datetime, timezone
from datetime import UTC, datetime from typing import Any
from nonebot.matcher import Matcher
from nonebot_plugin_alconna import AlcMatches, AlconnaMatcher
from ..utils.exception import MessageFormatError
from ..utils.recorder import Recorder
from ..utils.typing import CommandType, GameType from ..utils.typing import CommandType, GameType
from .schemas import BaseProcessedData as ProcessedData
from .schemas import BaseRawResponse as RawResponse
from .schemas import BaseUser as User
UTC = timezone.utc
@dataclass
class User:
"""游戏用户"""
@dataclass
class RawResponse:
"""原始请求数据"""
@dataclass
class ProcessedData:
"""处理/验证后的数据"""
from ..utils.recorder import Recorder # noqa: E402 避免循环导入
class Processor(ABC): class Processor(ABC):
@@ -65,6 +57,9 @@ class Processor(ABC):
def __del__(self) -> None: def __del__(self) -> None:
finish_time = datetime.now(tz=UTC) finish_time = datetime.now(tz=UTC)
if Recorder.is_error_event(self.event_id):
Recorder.del_error_event(self.event_id)
return
historical_data = Recorder.get_historical_data(self.event_id) historical_data = Recorder.get_historical_data(self.event_id)
historical_data.game_platform = self.game_platform historical_data.game_platform = self.game_platform
historical_data.command_type = self.command_type historical_data.command_type = self.command_type
@@ -75,6 +70,24 @@ class Processor(ABC):
Recorder.update_historical_data(self.event_id, historical_data) Recorder.update_historical_data(self.event_id, historical_data)
def add_default_handlers(matcher: type[AlconnaMatcher]) -> None:
@matcher.handle()
async def _(matcher: Matcher, account: MessageFormatError):
await matcher.finish(str(account))
@matcher.handle()
async def _(matcher: Matcher, matches: AlcMatches):
if matches.head_matched and matches.options != {} or matches.main_args == {}:
await matcher.finish(
(f'{matches.error_info!r}\n' if matches.error_info is not None else '')
+ f'输入"{matches.header_result} --help"查看帮助'
)
@matcher.handle()
async def _(matcher: Matcher, other: Any): # noqa: ANN401
await matcher.finish()
from . import ( # noqa: F401, E402 from . import ( # noqa: F401, E402
io_data_processor, io_data_processor,
top_data_processor, top_data_processor,

View File

@@ -1,23 +1,27 @@
from datetime import timedelta from datetime import datetime, timedelta, timezone
from zoneinfo import ZoneInfo
from arclet.alconna import Alconna, Arg, ArgFlag, Args, CommandMeta, Option from arclet.alconna import Alconna, AllParam, Arg, ArgFlag, Args, CommandMeta, Option
from nonebot.adapters import Bot, Event from nonebot.adapters import Bot, Event
from nonebot.matcher import Matcher from nonebot.matcher import Matcher
from nonebot_plugin_alconna import AlcMatches, At, on_alconna from nonebot_plugin_alconna import At, on_alconna
from nonebot_plugin_orm import get_session from nonebot_plugin_orm import get_session
from sqlalchemy import select from sqlalchemy import func, select
from ...db import query_bind_info from ...db import query_bind_info
from ...utils.exception import MessageFormatError, NeedCatchError from ...utils.exception import HandleNotFinishedError, NeedCatchError
from ...utils.metrics import get_metrics from ...utils.metrics import get_metrics
from ...utils.platform import get_platform from ...utils.platform import get_platform
from ...utils.typing import Me from ...utils.typing import Me
from .. import add_default_handlers
from ..constant import BIND_COMMAND, QUERY_COMMAND from ..constant import BIND_COMMAND, QUERY_COMMAND
from .constant import GAME_TYPE from .constant import GAME_TYPE
from .model import IORank from .model import IORank
from .processor import Processor, User, check_rank_data, identify_user_info from .processor import Processor, User, identify_user_info
from .typing import Rank from .typing import Rank
UTC = timezone.utc
alc = on_alconna( alc = on_alconna(
Alconna( Alconna(
'io', 'io',
@@ -65,6 +69,7 @@ alc = on_alconna(
dest='rank', dest='rank',
help_text='查询 IO 段位信息', help_text='查询 IO 段位信息',
), ),
Arg('other', AllParam, flags=[ArgFlag.HIDDEN, ArgFlag.OPTIONAL]),
meta=CommandMeta( meta=CommandMeta(
description='查询 TETR.IO 的信息', description='查询 TETR.IO 的信息',
example='io绑定scdhh\nio查我\niorankx', example='io绑定scdhh\nio查我\niorankx',
@@ -90,7 +95,8 @@ async def _(bot: Bot, event: Event, matcher: Matcher, account: User):
try: try:
await matcher.finish(await proc.handle_bind(platform=get_platform(bot), account=event.get_user_id())) await matcher.finish(await proc.handle_bind(platform=get_platform(bot), account=event.get_user_id()))
except NeedCatchError as e: except NeedCatchError as e:
await matcher.finish(str(e)) await matcher.send(str(e))
raise HandleNotFinishedError from e
@alc.assign('query') @alc.assign('query')
@@ -113,7 +119,8 @@ async def _(bot: Bot, event: Event, matcher: Matcher, target: At | Me):
try: try:
await matcher.finish(message + await proc.handle_query()) await matcher.finish(message + await proc.handle_query())
except NeedCatchError as e: except NeedCatchError as e:
await matcher.finish(str(e)) await matcher.send(str(e))
raise HandleNotFinishedError from e
@alc.assign('query') @alc.assign('query')
@@ -126,25 +133,37 @@ async def _(event: Event, matcher: Matcher, account: User):
try: try:
await matcher.finish(await proc.handle_query()) await matcher.finish(await proc.handle_query())
except NeedCatchError as e: except NeedCatchError as e:
await matcher.finish(str(e)) await matcher.send(str(e))
raise HandleNotFinishedError from e
@alc.assign('rank') @alc.assign('rank')
async def _(event: Event, matcher: Matcher, rank: Rank): async def _(matcher: Matcher, rank: Rank):
if rank == 'z': if rank == 'z':
await matcher.finish('暂不支持查询未知段位') await matcher.finish('暂不支持查询未知段位')
try:
await check_rank_data()
except NeedCatchError as e:
await matcher.finish(str(f'段位信息获取失败\n{e}'))
async with get_session() as session: async with get_session() as session:
data = ( latest_data = (
await session.scalars(select(IORank).where(IORank.rank == rank).order_by(IORank.id.desc()).limit(5)) await session.scalars(select(IORank).where(IORank.rank == rank).order_by(IORank.id.desc()).limit(1))
).all() ).one()
latest_data = data[0] compare_data = (
message = f'{rank.upper()} 段 分数线 {latest_data.tr_line:.2f} TR, {latest_data.player_count} 名玩家\n' await session.scalars(
if len(data) > 1: select(IORank)
message += f'对比 {(latest_data.create_time-data[-1].create_time).total_seconds()/3600:.2f} 小时前趋势: {f"{difference:.2f}" if (difference:=latest_data.tr_line-data[-1].tr_line) > 0 else f"{-difference:.2f}" if difference < 0 else ""}' .where(IORank.rank == rank)
.order_by(
func.abs(
func.julianday(IORank.update_time)
- func.julianday(latest_data.update_time - timedelta(hours=24))
)
)
.limit(1)
)
).one()
message = ''
if (datetime.now(UTC) - latest_data.update_time.replace(tzinfo=UTC)) > timedelta(hours=7):
message += 'Warning: 数据超过7小时未更新, 请联系Bot主人查看后台\n'
message += f'{rank.upper()} 段 分数线 {latest_data.tr_line:.2f} TR, {latest_data.player_count} 名玩家\n'
if compare_data.id != latest_data.id:
message += f'对比 {(latest_data.update_time-compare_data.update_time).total_seconds()/3600:.2f} 小时前趋势: {f"{difference:.2f}" if (difference:=latest_data.tr_line-compare_data.tr_line) > 0 else f"{-difference:.2f}" if difference < 0 else ""}'
else: else:
message += '暂无对比数据' message += '暂无对比数据'
avg = get_metrics(pps=latest_data.avg_pps, apm=latest_data.avg_apm, vs=latest_data.avg_vs) avg = get_metrics(pps=latest_data.avg_pps, apm=latest_data.avg_apm, vs=latest_data.avg_vs)
@@ -169,19 +188,9 @@ async def _(event: Event, matcher: Matcher, rank: Rank):
f'APM: {latest_data.high_apm[1]} By: {latest_data.high_apm[0]["name"].upper()}\n' f'APM: {latest_data.high_apm[1]} By: {latest_data.high_apm[0]["name"].upper()}\n'
f'ADPM: {max_vs.adpm} ( {max_vs.vs}vs ) By: {latest_data.high_vs[0]["name"].upper()}\n' f'ADPM: {max_vs.adpm} ( {max_vs.vs}vs ) By: {latest_data.high_vs[0]["name"].upper()}\n'
'\n' '\n'
f'数据更新时间: {(latest_data.create_time+timedelta(hours=8)).strftime("%Y-%m-%d %H:%M:%S")}' f'数据更新时间: {latest_data.update_time.replace(tzinfo=UTC).astimezone(ZoneInfo("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")}'
) )
await matcher.finish(message) await matcher.finish(message)
@alc.handle() add_default_handlers(alc)
async def _(matcher: Matcher, account: MessageFormatError):
await matcher.finish(str(account))
@alc.handle()
async def _(matcher: Matcher, matches: AlcMatches):
if matches.head_matched:
await matcher.finish(
f'{matches.error_info!r}\n' if matches.error_info is not None else '' + '输入"io --help"查看帮助'
)

View File

@@ -0,0 +1,30 @@
from datetime import datetime, timezone
from aiocache import Cache as ACache # type: ignore[import-untyped]
from nonebot.compat import type_validate_json
from nonebot.log import logger
from ...utils.request import Request
from .schemas.base import FailedModel, SuccessModel
UTC = timezone.utc
class Cache:
cache = ACache(ACache.MEMORY)
@classmethod
async def get(cls, url: str) -> bytes:
cached_data = await cls.cache.get(url)
if cached_data is None:
response_data = await Request.request(url)
parsed_data: SuccessModel | FailedModel = type_validate_json(SuccessModel | FailedModel, response_data) # type: ignore[arg-type]
if isinstance(parsed_data, SuccessModel):
await cls.cache.add(
url,
response_data,
(parsed_data.cache.cached_until - datetime.now(UTC)).total_seconds(),
)
return response_data
logger.debug(f'{url}: Cache hit!')
return cached_data

View File

@@ -1,7 +1,8 @@
from ...utils.typing import GameType from typing import Literal
from .typing import Rank from .typing import Rank
GAME_TYPE: GameType = 'IO' GAME_TYPE: Literal['IO'] = 'IO'
BASE_URL = 'https://ch.tetr.io/api/' BASE_URL = 'https://ch.tetr.io/api/'
RANK_PERCENTILE: dict[Rank, float] = { RANK_PERCENTILE: dict[Rank, float] = {
'x': 1, 'x': 1,

View File

@@ -1,4 +1,4 @@
from datetime import UTC, datetime from datetime import datetime, timezone
from nonebot_plugin_orm import Model from nonebot_plugin_orm import Model
from sqlalchemy import JSON, DateTime, String from sqlalchemy import JSON, DateTime, String
@@ -6,6 +6,8 @@ from sqlalchemy.orm import Mapped, MappedAsDataclass, mapped_column
from .typing import Rank from .typing import Rank
UTC = timezone.utc
class IORank(MappedAsDataclass, Model): class IORank(MappedAsDataclass, Model):
id: Mapped[int] = mapped_column(init=False, primary_key=True) id: Mapped[int] = mapped_column(init=False, primary_key=True)
@@ -21,9 +23,8 @@ class IORank(MappedAsDataclass, Model):
high_pps: Mapped[tuple[dict[str, str], float]] = mapped_column(JSON) high_pps: Mapped[tuple[dict[str, str], float]] = mapped_column(JSON)
high_apm: 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) high_vs: Mapped[tuple[dict[str, str], float]] = mapped_column(JSON)
create_time: Mapped[datetime] = mapped_column( update_time: Mapped[datetime] = mapped_column(
DateTime, DateTime,
default=lambda: datetime.now(tz=UTC),
index=True, index=True,
init=False,
) )
file_hash: Mapped[str | None] = mapped_column(String(128), index=True)

View File

@@ -1,63 +1,48 @@
from collections import defaultdict from collections import defaultdict
from collections.abc import Callable from collections.abc import Callable
from dataclasses import asdict, dataclass from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta from hashlib import sha512
from math import floor from math import floor
from re import match from re import match
from statistics import mean from statistics import mean
from typing import Literal
from aiofiles import open
from nonebot import get_driver from nonebot import get_driver
from nonebot.compat import type_validate_json
from nonebot.utils import run_sync
from nonebot_plugin_apscheduler import scheduler # type: ignore[import-untyped] from nonebot_plugin_apscheduler import scheduler # type: ignore[import-untyped]
from nonebot_plugin_localstore import get_data_file # type: ignore[import-untyped]
from nonebot_plugin_orm import get_session from nonebot_plugin_orm import get_session
from pydantic import parse_raw_as
from sqlalchemy import select from sqlalchemy import select
from zstandard import ZstdCompressor
from ...db import create_or_update_bind from ...db import create_or_update_bind
from ...utils.exception import MessageFormatError, RequestError, WhatTheFuckError from ...utils.exception import MessageFormatError, RequestError, WhatTheFuckError
from ...utils.request import Request, splice_url from ...utils.request import splice_url
from ...utils.typing import GameType from ...utils.retry import retry
from .. import ProcessedData as ProcessedDataMeta
from .. import Processor as ProcessorMeta from .. import Processor as ProcessorMeta
from .. import RawResponse as RawResponseMeta from .cache import Cache
from .. import User as UserMeta
from .constant import BASE_URL, GAME_TYPE, RANK_PERCENTILE from .constant import BASE_URL, GAME_TYPE, RANK_PERCENTILE
from .model import IORank from .model import IORank
from .schemas.league_all import FailedModel as LeagueAllFailed from .schemas.league_all import FailedModel as LeagueAllFailed
from .schemas.league_all import LeagueAll from .schemas.league_all import LeagueAll
from .schemas.league_all import ValidUser as LeagueAllUser from .schemas.league_all import ValidUser as LeagueAllUser
from .schemas.response import ProcessedData, RawResponse
from .schemas.user import User
from .schemas.user_info import FailedModel as InfoFailed from .schemas.user_info import FailedModel as InfoFailed
from .schemas.user_info import ( from .schemas.user_info import NeverPlayedLeague, NeverRatedLeague, UserInfo
NeverPlayedLeague,
NeverRatedLeague,
UserInfo,
)
from .schemas.user_info import SuccessModel as InfoSuccess from .schemas.user_info import SuccessModel as InfoSuccess
from .schemas.user_records import FailedModel as RecordsFailed from .schemas.user_records import FailedModel as RecordsFailed
from .schemas.user_records import SoloRecord, UserRecords from .schemas.user_records import SoloRecord, UserRecords
from .schemas.user_records import SuccessModel as RecordsSuccess from .schemas.user_records import SuccessModel as RecordsSuccess
from .typing import Rank from .typing import Rank
UTC = timezone.utc
driver = get_driver() driver = get_driver()
@dataclass
class User(UserMeta):
ID: str | None = None
name: str | None = None
@dataclass
class RawResponse(RawResponseMeta):
user_info: bytes | None = None
user_records: bytes | None = None
@dataclass
class ProcessedData(ProcessedDataMeta):
user_info: InfoSuccess | None = None
user_records: RecordsSuccess | None = None
def identify_user_info(info: str) -> User | MessageFormatError: def identify_user_info(info: str) -> User | MessageFormatError:
if match(r'^[a-f0-9]{24}$', info): if match(r'^[a-f0-9]{24}$', info):
return User(ID=info) return User(ID=info)
@@ -77,7 +62,7 @@ class Processor(ProcessorMeta):
self.processed_data = ProcessedData() self.processed_data = ProcessedData()
@property @property
def game_platform(self) -> GameType: def game_platform(self) -> Literal['IO']:
return GAME_TYPE return GAME_TYPE
async def handle_bind(self, platform: str, account: str) -> str: async def handle_bind(self, platform: str, account: str) -> str:
@@ -113,10 +98,10 @@ class Processor(ProcessorMeta):
async def get_user_info(self) -> InfoSuccess: async def get_user_info(self) -> InfoSuccess:
"""获取用户数据""" """获取用户数据"""
if self.processed_data.user_info is None: if self.processed_data.user_info is None:
self.raw_response.user_info = await Request.request( self.raw_response.user_info = await Cache.get(
splice_url([BASE_URL, 'users/', f'{self.user.ID or self.user.name}']) splice_url([BASE_URL, 'users/', f'{self.user.ID or self.user.name}'])
) )
user_info: UserInfo = parse_raw_as(UserInfo, self.raw_response.user_info) # type: ignore[arg-type] user_info: UserInfo = type_validate_json(UserInfo, self.raw_response.user_info) # type: ignore[arg-type]
if isinstance(user_info, InfoFailed): if isinstance(user_info, InfoFailed):
raise RequestError(f'用户信息请求错误:\n{user_info.error}') raise RequestError(f'用户信息请求错误:\n{user_info.error}')
self.processed_data.user_info = user_info self.processed_data.user_info = user_info
@@ -125,20 +110,10 @@ class Processor(ProcessorMeta):
async def get_user_records(self) -> RecordsSuccess: async def get_user_records(self) -> RecordsSuccess:
"""获取Solo数据""" """获取Solo数据"""
if self.processed_data.user_records is None: if self.processed_data.user_records is None:
self.raw_response.user_records = await Request.request( self.raw_response.user_records = await Cache.get(
splice_url( splice_url([BASE_URL, 'users/', f'{self.user.ID or self.user.name}/', 'records'])
[
BASE_URL,
'users/',
f'{self.user.ID or self.user.name}/',
'records',
]
)
)
user_records: UserRecords = parse_raw_as(
UserRecords, # type: ignore[arg-type]
self.raw_response.user_records,
) )
user_records: UserRecords = type_validate_json(UserRecords, self.raw_response.user_records) # type: ignore[arg-type]
if isinstance(user_records, RecordsFailed): if isinstance(user_records, RecordsFailed):
raise RequestError(f'用户Solo数据请求错误:\n{user_records.error}') raise RequestError(f'用户Solo数据请求错误:\n{user_records.error}')
self.processed_data.user_records = user_records self.processed_data.user_records = user_records
@@ -155,12 +130,13 @@ class Processor(ProcessorMeta):
else: else:
if isinstance(league, NeverRatedLeague): if isinstance(league, NeverRatedLeague):
ret_message += f'用户 {user_name} 暂未完成定级赛, 最近十场的数据:' ret_message += f'用户 {user_name} 暂未完成定级赛, 最近十场的数据:'
elif league.rank == 'z':
ret_message += f'用户 {user_name} 暂无段位, {round(league.rating,2)} TR'
else: else:
ret_message += ( if league.rank == 'z':
f'{league.rank.upper()}用户 {user_name} {round(league.rating,2)} TR (#{league.standing})' ret_message += f'用户 {user_name} 暂无段位, {round(league.rating,2)} TR'
) else:
ret_message += (
f'{league.rank.upper()} 段用户 {user_name} {round(league.rating,2)} TR (#{league.standing})'
)
ret_message += f', 段位分 {round(league.glicko,2)}±{round(league.rd,2)}, 最近十场的数据:' ret_message += f', 段位分 {round(league.glicko,2)}±{round(league.rd,2)}, 最近十场的数据:'
lpm = league.pps * 24 lpm = league.pps * 24
ret_message += f"\nL'PM: {round(lpm, 2)} ( {league.pps} pps )" ret_message += f"\nL'PM: {round(lpm, 2)} ( {league.pps} pps )"
@@ -185,13 +161,14 @@ class Processor(ProcessorMeta):
@scheduler.scheduled_job('cron', hour='0,6,12,18', minute=0) @scheduler.scheduled_job('cron', hour='0,6,12,18', minute=0)
@retry(exception_type=RequestError, delay=timedelta(minutes=15))
async def get_io_rank_data() -> None: async def get_io_rank_data() -> None:
league_all: LeagueAll = parse_raw_as( league_all: LeagueAll = type_validate_json(
LeagueAll, # type: ignore[arg-type] LeagueAll, # type: ignore[arg-type]
await Request.request(splice_url([BASE_URL, 'users/lists/league/all'])), (data := await Cache.get(splice_url([BASE_URL, 'users/lists/league/all']))),
) )
if isinstance(league_all, LeagueAllFailed): if isinstance(league_all, LeagueAllFailed):
raise RequestError(f'用户Solo数据请求错误:\n{league_all.error}') raise RequestError(f'排行榜数据请求错误:\n{league_all.error}')
def pps(user: LeagueAllUser) -> float: def pps(user: LeagueAllUser) -> float:
return user.league.pps return user.league.pps
@@ -214,7 +191,11 @@ async def get_io_rank_data() -> None:
sort: Callable[[list[LeagueAllUser], Callable[[LeagueAllUser], float]], LeagueAllUser], sort: Callable[[list[LeagueAllUser], Callable[[LeagueAllUser], float]], LeagueAllUser],
) -> tuple[dict[str, str], float]: ) -> tuple[dict[str, str], float]:
user = sort(users, field) user = sort(users, field)
return asdict(User(ID=user.id, name=user.username)), field(user) return User(ID=user.id, name=user.username).dict(), field(user)
data_hash: str | None = await run_sync((await run_sync(sha512)(data)).hexdigest)()
async with open(get_data_file('nonebot_plugin_tetris_stats', f'{data_hash}.json.zst'), mode='wb') as file:
await file.write(await run_sync(ZstdCompressor(level=12, threads=-1).compress)(data))
users = [i for i in league_all.data.users if isinstance(i, LeagueAllUser)] users = [i for i in league_all.data.users if isinstance(i, LeagueAllUser)]
rank_to_users: defaultdict[Rank, list[LeagueAllUser]] = defaultdict(list) rank_to_users: defaultdict[Rank, list[LeagueAllUser]] = defaultdict(list)
@@ -239,6 +220,8 @@ async def get_io_rank_data() -> None:
high_pps=(build_extremes_data(rank_users, pps, _max)), high_pps=(build_extremes_data(rank_users, pps, _max)),
high_apm=(build_extremes_data(rank_users, apm, _max)), high_apm=(build_extremes_data(rank_users, apm, _max)),
high_vs=(build_extremes_data(rank_users, vs, _max)), high_vs=(build_extremes_data(rank_users, vs, _max)),
update_time=league_all.cache.cached_at,
file_hash=data_hash,
) )
) )
async with get_session() as session: async with get_session() as session:
@@ -247,8 +230,8 @@ async def get_io_rank_data() -> None:
@driver.on_startup @driver.on_startup
async def check_rank_data() -> None: async def _() -> None:
async with get_session() as session: async with get_session() as session:
latest_time = await session.scalar(select(IORank.create_time).order_by(IORank.id.desc()).limit(1)) latest_time = await session.scalar(select(IORank.update_time).order_by(IORank.id.desc()).limit(1))
if latest_time is None or datetime.now(tz=UTC) - latest_time.replace(tzinfo=UTC) > timedelta(hours=6): if latest_time is None or datetime.now(tz=UTC) - latest_time.replace(tzinfo=UTC) > timedelta(hours=6):
await get_io_rank_data() await get_io_rank_data()

View File

@@ -28,20 +28,20 @@ class SuccessModel(BaseSuccessModel):
league: League league: League
supporter: bool supporter: bool
verified: bool verified: bool
country: str | None country: str | None = None
class InvalidUser(BaseModel): class InvalidUser(BaseModel):
class League(BaseModel): class League(BaseModel):
gamesplayed: int gamesplayed: int
gameswon: int gameswon: int
rating: float rating: float
glicko: float | None glicko: float | None = None
rd: float | None rd: float | None = None
rank: Rank rank: Rank
bestrank: Rank bestrank: Rank
apm: float | None apm: float | None = None
pps: float | None pps: float | None = None
vs: float | None vs: float | None = None
decaying: bool decaying: bool
id: str = Field(..., alias='_id') id: str = Field(..., alias='_id')

View File

@@ -0,0 +1,21 @@
from typing import Literal
from ... import ProcessedData as ProcessedDataMeta
from ... import RawResponse as RawResponseMeta
from ..constant import GAME_TYPE
from .user_info import SuccessModel as InfoSuccess
from .user_records import SuccessModel as RecordsSuccess
class RawResponse(RawResponseMeta):
platform: Literal['IO'] = GAME_TYPE
user_info: bytes | None = None
user_records: bytes | None = None
class ProcessedData(ProcessedDataMeta):
platform: Literal['IO'] = GAME_TYPE
user_info: InfoSuccess | None = None
user_records: RecordsSuccess | None = None

View File

@@ -0,0 +1,17 @@
from typing import Literal
from ...schemas import BaseUser
from ..constant import GAME_TYPE
class User(BaseUser):
platform: Literal['IO'] = GAME_TYPE
ID: str | None = None
name: str | None = None
@property
def unique_identifier(self) -> str:
if self.ID is None:
raise ValueError('不完整的User!')
return self.ID

View File

@@ -14,7 +14,7 @@ class SuccessModel(BaseSuccessModel):
class Badge(BaseModel): class Badge(BaseModel):
id: str id: str
label: str label: str
ts: datetime | None ts: datetime | None = None
class NeverPlayedLeague(BaseModel): class NeverPlayedLeague(BaseModel):
gamesplayed: Literal[0] gamesplayed: Literal[0]
@@ -29,9 +29,9 @@ class SuccessModel(BaseSuccessModel):
prev_at: Literal[-1] prev_at: Literal[-1]
percentile: Literal[-1] percentile: Literal[-1]
percentile_rank: Literal['z'] percentile_rank: Literal['z']
apm: None apm: None = Field(None)
pps: None pps: None = Field(None)
vs: None vs: None = Field(None)
decaying: bool decaying: bool
class NeverRatedLeague(BaseModel): class NeverRatedLeague(BaseModel):
@@ -60,8 +60,8 @@ class SuccessModel(BaseSuccessModel):
bestrank: Rank bestrank: Rank
standing: int standing: int
standing_local: int standing_local: int
next_rank: Rank | None next_rank: Rank | None = None
prev_rank: Rank | None prev_rank: Rank | None = None
next_at: int next_at: int
prev_at: int prev_at: int
percentile: float percentile: float
@@ -70,7 +70,7 @@ class SuccessModel(BaseSuccessModel):
rd: float rd: float
apm: float apm: float
pps: float pps: float
vs: float | None vs: float | None = None
decaying: bool decaying: bool
class Connections(BaseModel): class Connections(BaseModel):
@@ -78,41 +78,41 @@ class SuccessModel(BaseSuccessModel):
id: str id: str
username: str username: str
discord: Discord | None discord: Discord | None = None
class Distinguishment(BaseModel): class Distinguishment(BaseModel):
type: str # noqa: A003 type: str
id: str = Field(..., alias='_id') id: str = Field(..., alias='_id')
username: str username: str
role: Literal['anon', 'user', 'bot', 'halfmod', 'mod', 'admin', 'sysop', 'banned'] role: Literal['anon', 'user', 'bot', 'halfmod', 'mod', 'admin', 'sysop', 'banned']
ts: datetime | None ts: datetime | None = None
botmaster: str | None botmaster: str | None = None
badges: list[Badge] badges: list[Badge]
xp: float xp: float
gamesplayed: int gamesplayed: int
gameswon: int gameswon: int
gametime: float gametime: float
country: str | None country: str | None = None
badstanding: bool | None badstanding: bool | None = None
supporter: bool | None # osk说是必有, 但实际上不是 fk osk supporter: bool | None = None # osk说是必有, 但实际上不是 fk osk
supporter_tier: int supporter_tier: int
verified: bool verified: bool
league: NeverPlayedLeague | NeverRatedLeague | RatedLeague league: NeverPlayedLeague | NeverRatedLeague | RatedLeague
avatar_revision: int | None avatar_revision: int | None = None
"""This user's avatar ID. Get their avatar at """This user's avatar ID. Get their avatar at
https://tetr.io/user-content/avatars/{ USERID }.jpg?rv={ AVATAR_REVISION }""" https://tetr.io/user-content/avatars/{ USERID }.jpg?rv={ AVATAR_REVISION }"""
banner_revision: int | None banner_revision: int | None = None
"""This user's banner ID. Get their banner at """This user's banner ID. Get their banner at
https://tetr.io/user-content/banners/{ USERID }.jpg?rv={ BANNER_REVISION } https://tetr.io/user-content/banners/{ USERID }.jpg?rv={ BANNER_REVISION }
Ignore this field if the user is not a supporter.""" Ignore this field if the user is not a supporter."""
bio: str | None bio: str | None = None
connections: Connections connections: Connections
friend_count: int friend_count: int | None = None
distinguishment: Distinguishment | None distinguishment: Distinguishment | None = None
user: User user: User

View File

@@ -12,13 +12,14 @@ class EndContext(BaseModel):
zero: bool zero: bool
locked: bool locked: bool
prev: int prev: int
frameoffset: int frameoffset: int | None = None
class Clears(BaseModel): class Clears(BaseModel):
singles: int singles: int
doubles: int doubles: int
triples: int triples: int
quads: int quads: int
pentas: int | None = None
realtspins: int realtspins: int
minitspins: int minitspins: int
minitspinsingles: int minitspinsingles: int
@@ -32,7 +33,7 @@ class EndContext(BaseModel):
class Garbage(BaseModel): class Garbage(BaseModel):
sent: int sent: int
received: int received: int
attack: int attack: int | None = None
cleared: int cleared: int
class Finesse(BaseModel): class Finesse(BaseModel):
@@ -45,18 +46,18 @@ class EndContext(BaseModel):
level_lines: int level_lines: int
level_lines_needed: int level_lines_needed: int
inputs: int inputs: int
holds: int holds: int | None = None
time: Time time: Time
score: int score: int
zenlevel: int zenlevel: int | None = None
zenprogress: int zenprogress: int | None = None
level: int level: int
combo: int combo: int
currentcombopower: int | None # WTF currentcombopower: int | None = None # WTF
topcombo: int topcombo: int
btb: int btb: int
topbtb: int topbtb: int
currentbtbchainpower: int | None # WTF * 2 currentbtbchainpower: int | None = None # WTF * 2
tspins: int tspins: int
piecesplaced: int piecesplaced: int
clears: Clears clears: Clears
@@ -78,7 +79,7 @@ class BaseModeRecord(BaseModel):
replayid: str replayid: str
user: User user: User
ts: datetime ts: datetime
ismulti: bool | None ismulti: bool | None = None
endcontext: EndContext endcontext: EndContext
class MultiRecord(BaseModel): class MultiRecord(BaseModel):
@@ -91,21 +92,19 @@ class BaseModeRecord(BaseModel):
replayid: str replayid: str
user: User user: User
ts: datetime ts: datetime
ismulti: bool | None ismulti: bool | None = None
endcontext: list[EndContext] endcontext: list[EndContext]
record: SoloRecord | MultiRecord | None record: SoloRecord | MultiRecord | None = None
rank: int | None rank: int | None = None
class SuccessModel(BaseSuccessModel): class SuccessModel(BaseSuccessModel):
class Data(BaseModel): class Data(BaseModel):
class Records(BaseModel): class Records(BaseModel):
class Sprint(BaseModeRecord): class Sprint(BaseModeRecord): ...
...
class Blitz(BaseModeRecord): class Blitz(BaseModeRecord): ...
...
sprint: Sprint = Field(..., alias='40l') sprint: Sprint = Field(..., alias='40l')
blitz: Blitz blitz: Blitz

View File

@@ -0,0 +1,31 @@
from abc import ABC, abstractmethod
from pydantic import BaseModel
from ..utils.typing import GameType
class Base(BaseModel):
platform: GameType
class BaseUser(ABC, Base):
"""游戏用户"""
def __eq__(self, __value: object) -> bool:
if isinstance(__value, BaseUser):
return self.unique_identifier == __value.unique_identifier
return False
@property
@abstractmethod
def unique_identifier(self) -> str:
raise NotImplementedError
class BaseRawResponse(Base):
"""原始请求数据"""
class BaseProcessedData(Base):
"""处理/验证后的数据"""

View File

@@ -1,13 +1,14 @@
from arclet.alconna import Alconna, Arg, ArgFlag, Args, CommandMeta, Option from arclet.alconna import Alconna, AllParam, Arg, ArgFlag, Args, CommandMeta, Option
from nonebot.adapters import Bot, Event from nonebot.adapters import Bot, Event
from nonebot.matcher import Matcher from nonebot.matcher import Matcher
from nonebot_plugin_alconna import AlcMatches, At, on_alconna from nonebot_plugin_alconna import At, on_alconna
from nonebot_plugin_orm import get_session from nonebot_plugin_orm import get_session
from ...db import query_bind_info from ...db import query_bind_info
from ...utils.exception import MessageFormatError, NeedCatchError from ...utils.exception import HandleNotFinishedError, NeedCatchError
from ...utils.platform import get_platform from ...utils.platform import get_platform
from ...utils.typing import Me from ...utils.typing import Me
from .. import add_default_handlers
from ..constant import BIND_COMMAND, QUERY_COMMAND from ..constant import BIND_COMMAND, QUERY_COMMAND
from .constant import GAME_TYPE from .constant import GAME_TYPE
from .processor import Processor, User, identify_user_info from .processor import Processor, User, identify_user_info
@@ -51,6 +52,7 @@ alc = on_alconna(
dest='query', dest='query',
help_text='查询 TOP 游戏信息', help_text='查询 TOP 游戏信息',
), ),
Arg('other', AllParam, flags=[ArgFlag.HIDDEN, ArgFlag.OPTIONAL]),
meta=CommandMeta( meta=CommandMeta(
description='查询 TetrisOnline波兰服 的信息', description='查询 TetrisOnline波兰服 的信息',
example='top绑定scdhh\ntop查我', example='top绑定scdhh\ntop查我',
@@ -74,7 +76,8 @@ async def _(bot: Bot, event: Event, matcher: Matcher, account: User):
try: try:
await matcher.finish(await proc.handle_bind(platform=get_platform(bot), account=event.get_user_id())) await matcher.finish(await proc.handle_bind(platform=get_platform(bot), account=event.get_user_id()))
except NeedCatchError as e: except NeedCatchError as e:
await matcher.finish(str(e)) await matcher.send(str(e))
raise HandleNotFinishedError from e
@alc.assign('query') @alc.assign('query')
@@ -97,7 +100,8 @@ async def _(bot: Bot, event: Event, matcher: Matcher, target: At | Me):
try: try:
await matcher.finish(message + await proc.handle_query()) await matcher.finish(message + await proc.handle_query())
except NeedCatchError as e: except NeedCatchError as e:
await matcher.finish(str(e)) await matcher.send(str(e))
raise HandleNotFinishedError from e
@alc.assign('query') @alc.assign('query')
@@ -110,17 +114,8 @@ async def _(event: Event, matcher: Matcher, account: User):
try: try:
await matcher.finish(await proc.handle_query()) await matcher.finish(await proc.handle_query())
except NeedCatchError as e: except NeedCatchError as e:
await matcher.finish(str(e)) await matcher.send(str(e))
raise HandleNotFinishedError from e
@alc.handle() add_default_handlers(alc)
async def _(matcher: Matcher, account: MessageFormatError):
await matcher.finish(str(account))
@alc.handle()
async def _(matcher: Matcher, matches: AlcMatches):
if matches.head_matched:
await matcher.finish(
f'{matches.error_info!r}\n' if matches.error_info is not None else '' + '输入"top --help"查看帮助'
)

View File

@@ -1,4 +1,4 @@
from ...utils.typing import GameType from typing import Literal
GAME_TYPE: GameType = 'TOP' GAME_TYPE: Literal['TOP'] = 'TOP'
BASE_URL = 'http://tetrisonline.pl/top/' BASE_URL = 'http://tetrisonline.pl/top/'

View File

@@ -2,7 +2,7 @@ from contextlib import suppress
from dataclasses import dataclass from dataclasses import dataclass
from io import StringIO from io import StringIO
from re import match from re import match
from typing import NoReturn from typing import Literal
from urllib.parse import urlencode from urllib.parse import urlencode
from lxml import etree from lxml import etree
@@ -12,27 +12,20 @@ from pandas import read_html
from ...db import create_or_update_bind from ...db import create_or_update_bind
from ...utils.exception import MessageFormatError, RequestError from ...utils.exception import MessageFormatError, RequestError
from ...utils.request import Request, splice_url from ...utils.request import Request, splice_url
from ...utils.typing import GameType
from .. import ProcessedData as ProcessedDataMeta
from .. import Processor as ProcessorMeta from .. import Processor as ProcessorMeta
from .. import RawResponse as RawResponseMeta from ..schemas import BaseUser
from .. import User as UserMeta
from .constant import BASE_URL, GAME_TYPE from .constant import BASE_URL, GAME_TYPE
from .schemas.response import ProcessedData, RawResponse
@dataclass class User(BaseUser):
class User(UserMeta): platform: Literal['TOP'] = GAME_TYPE
name: str name: str
@property
@dataclass def unique_identifier(self) -> str:
class RawResponse(RawResponseMeta): return self.name
user_profile: bytes | None = None
@dataclass
class ProcessedData(ProcessedDataMeta):
user_profile: str | None = None
@dataclass @dataclass
@@ -64,7 +57,7 @@ class Processor(ProcessorMeta):
self.processed_data = ProcessedData() self.processed_data = ProcessedData()
@property @property
def game_platform(self) -> GameType: def game_platform(self) -> Literal['TOP']:
return GAME_TYPE return GAME_TYPE
async def handle_bind(self, platform: str, account: str) -> str: async def handle_bind(self, platform: str, account: str) -> str:
@@ -94,10 +87,9 @@ class Processor(ProcessorMeta):
self.processed_data.user_profile = self.raw_response.user_profile.decode() self.processed_data.user_profile = self.raw_response.user_profile.decode()
return self.processed_data.user_profile return self.processed_data.user_profile
async def check_user(self) -> None | NoReturn: async def check_user(self) -> None:
if 'user not found!' in await self.get_user_profile(): if 'user not found!' in await self.get_user_profile():
raise RequestError('用户不存在!') raise RequestError('用户不存在!')
return None
async def get_user_name(self) -> str: async def get_user_name(self) -> str:
"""获取用户名""" """获取用户名"""

View File

@@ -0,0 +1,16 @@
from typing import Literal
from ...schemas import BaseProcessedData, BaseRawResponse
from ..constant import GAME_TYPE
class RawResponse(BaseRawResponse):
platform: Literal['TOP'] = GAME_TYPE
user_profile: bytes | None = None
class ProcessedData(BaseProcessedData):
platform: Literal['TOP'] = GAME_TYPE
user_profile: str | None = None

View File

@@ -1,13 +1,16 @@
from arclet.alconna import Alconna, Arg, ArgFlag, Args, CommandMeta, Option from typing import NoReturn
from arclet.alconna import Alconna, AllParam, Arg, ArgFlag, Args, CommandMeta, Option
from nonebot.adapters import Bot, Event from nonebot.adapters import Bot, Event
from nonebot.matcher import Matcher from nonebot.matcher import Matcher
from nonebot_plugin_alconna import AlcMatches, At, on_alconna from nonebot_plugin_alconna import At, on_alconna
from nonebot_plugin_orm import get_session from nonebot_plugin_orm import get_session
from ...db import query_bind_info from ...db import query_bind_info
from ...utils.exception import MessageFormatError, NeedCatchError from ...utils.exception import HandleNotFinishedError, NeedCatchError, RequestError
from ...utils.platform import get_platform from ...utils.platform import get_platform
from ...utils.typing import Me from ...utils.typing import Me
from .. import add_default_handlers
from ..constant import BIND_COMMAND, QUERY_COMMAND from ..constant import BIND_COMMAND, QUERY_COMMAND
from .constant import GAME_TYPE from .constant import GAME_TYPE
from .processor import Processor, User, identify_user_info from .processor import Processor, User, identify_user_info
@@ -52,6 +55,7 @@ alc = on_alconna(
dest='query', dest='query',
help_text='查询 茶服 游戏信息', help_text='查询 茶服 游戏信息',
), ),
Arg('other', AllParam, flags=[ArgFlag.HIDDEN, ArgFlag.OPTIONAL]),
meta=CommandMeta( meta=CommandMeta(
description='查询 TetrisOnline茶服 的信息', description='查询 TetrisOnline茶服 的信息',
example='茶服查我', example='茶服查我',
@@ -64,27 +68,63 @@ alc = on_alconna(
aliases={'tos', 'TOS'}, aliases={'tos', 'TOS'},
) )
try:
from nonebot.adapters.onebot.v11 import GROUP, MessageEvent
from nonebot.adapters.onebot.v11 import Bot as OB11Bot
@alc.assign('bind') async def finish_special_query(matcher: Matcher, proc: Processor) -> NoReturn:
async def _(event: MessageEvent, matcher: Matcher): try:
await matcher.finish('QQ 平台无需绑定') await matcher.finish(await proc.handle_query())
except NeedCatchError as e:
if isinstance(e, RequestError) and '未找到此用户' in e.message:
matcher.skip()
await matcher.send(str(e))
raise HandleNotFinishedError from e
try:
from nonebot.adapters.onebot.v11 import GROUP as OB11GROUP
from nonebot.adapters.onebot.v11 import Bot as OB11Bot
from nonebot.adapters.onebot.v11 import MessageEvent as OB11MessageEvent
@alc.assign('query') @alc.assign('query')
async def _(bot: OB11Bot, event: MessageEvent, matcher: Matcher, target: At | Me): async def _(bot: OB11Bot, event: OB11MessageEvent, matcher: Matcher, target: At | Me):
if event.is_tome() and await GROUP(bot, event): if event.is_tome() and await OB11GROUP(bot, event):
await matcher.finish('不能查询bot的信息') await matcher.finish('不能查询bot的信息')
proc = Processor( proc = Processor(
event_id=id(event), event_id=id(event),
user=User(teaid=target.target if isinstance(target, At) else event.get_user_id()), user=User(teaid=f'onebot-{target.target}' if isinstance(target, At) else f'onebot-{event.get_user_id()}'),
command_args=[], command_args=[],
) )
try: await finish_special_query(matcher, proc)
await matcher.finish(await proc.handle_query())
except NeedCatchError as e: except ImportError:
await matcher.finish(str(e)) pass
try:
from nonebot.adapters.kaiheila.event import MessageEvent as KookMessageEvent
@alc.assign('query')
async def _(event: KookMessageEvent, matcher: Matcher, target: At | Me):
proc = Processor(
event_id=id(event),
user=User(teaid=f'kook-{target.target}' if isinstance(target, At) else f'kook-{event.get_user_id()}'),
command_args=[],
)
await finish_special_query(matcher, proc)
except ImportError:
pass
try:
from nonebot.adapters.discord import MessageEvent as DiscordMessageEvent
@alc.assign('query')
async def _(event: DiscordMessageEvent, matcher: Matcher, target: At | Me):
proc = Processor(
event_id=id(event),
user=User(teaid=f'discord-{target.target}' if isinstance(target, At) else f'discord-{event.get_user_id()}'),
command_args=[],
)
await finish_special_query(matcher, proc)
except ImportError: except ImportError:
pass pass
@@ -99,7 +139,8 @@ async def _(bot: Bot, event: Event, matcher: Matcher, account: User):
try: try:
await matcher.finish(await proc.handle_bind(platform=get_platform(bot), account=event.get_user_id())) await matcher.finish(await proc.handle_bind(platform=get_platform(bot), account=event.get_user_id()))
except NeedCatchError as e: except NeedCatchError as e:
await matcher.finish(str(e)) await matcher.send(str(e))
raise HandleNotFinishedError from e
@alc.assign('query') @alc.assign('query')
@@ -116,13 +157,14 @@ async def _(bot: Bot, event: Event, matcher: Matcher, target: At | Me):
message = '* 由于无法验证绑定信息, 不能保证查询到的用户为本人\n' message = '* 由于无法验证绑定信息, 不能保证查询到的用户为本人\n'
proc = Processor( proc = Processor(
event_id=id(event), event_id=id(event),
user=User(name=bind.game_account), user=User(teaid=bind.game_account),
command_args=[], command_args=[],
) )
try: try:
await matcher.finish(message + await proc.handle_query()) await matcher.finish(message + await proc.handle_query())
except NeedCatchError as e: except NeedCatchError as e:
await matcher.finish(str(e)) await matcher.send(str(e))
raise HandleNotFinishedError from e
@alc.assign('query') @alc.assign('query')
@@ -135,17 +177,8 @@ async def _(event: Event, matcher: Matcher, account: User):
try: try:
await matcher.finish(await proc.handle_query()) await matcher.finish(await proc.handle_query())
except NeedCatchError as e: except NeedCatchError as e:
await matcher.finish(str(e)) await matcher.send(str(e))
raise HandleNotFinishedError from e
@alc.handle() add_default_handlers(alc)
async def _(matcher: Matcher, account: MessageFormatError):
await matcher.finish(str(account))
@alc.handle()
async def _(matcher: Matcher, matches: AlcMatches):
if matches.head_matched:
await matcher.finish(
f'{matches.error_info!r}\n' if matches.error_info is not None else '' + '输入"茶服 --help"查看帮助'
)

View File

@@ -1,4 +1,10 @@
from ...utils.typing import GameType from typing import Literal
GAME_TYPE: GameType = 'TOS' GAME_TYPE: Literal['TOS'] = 'TOS'
BASE_URL = 'https://teatube.cn:8888/' BASE_URL = {
'https://teatube.cn:8888/',
'http://cafuuchino1.studio26f.org:19970',
'http://cafuuchino2.studio26f.org:19970',
'http://cafuuchino3.studio26f.org:19970',
'http://cafuuchino4.studio26f.org:19970',
}

View File

@@ -1,41 +1,35 @@
from dataclasses import dataclass from dataclasses import dataclass
from re import match from re import match
from typing import Any from typing import Literal
from urllib.parse import urlencode from urllib.parse import urlencode
from httpx import TimeoutException
from nonebot.compat import type_validate_json
from nonebot_plugin_orm import get_session from nonebot_plugin_orm import get_session
from pydantic import parse_raw_as
from ...db import create_or_update_bind from ...db import create_or_update_bind
from ...utils.exception import MessageFormatError, RequestError from ...utils.exception import MessageFormatError, RequestError
from ...utils.request import Request, splice_url from ...utils.request import Request, splice_url
from ...utils.typing import GameType
from .. import ProcessedData as ProcessedDataMeta
from .. import Processor as ProcessorMeta from .. import Processor as ProcessorMeta
from .. import RawResponse as RawResponseMeta from ..schemas import BaseUser
from .. import User as UserMeta
from .constant import BASE_URL, GAME_TYPE from .constant import BASE_URL, GAME_TYPE
from .schemas.response import ProcessedData, RawResponse
from .schemas.user_info import SuccessModel as InfoSuccess from .schemas.user_info import SuccessModel as InfoSuccess
from .schemas.user_info import UserInfo from .schemas.user_info import UserInfo
from .schemas.user_profile import UserProfile from .schemas.user_profile import UserProfile
@dataclass class User(BaseUser):
class User(UserMeta): platform: Literal['TOS'] = GAME_TYPE
teaid: str | None = None teaid: str | None = None
name: str | None = None name: str | None = None
@property
@dataclass def unique_identifier(self) -> str:
class RawResponse(RawResponseMeta): if self.teaid is None:
user_profile: dict[frozenset[tuple[str, Any]], bytes] raise ValueError('不完整的User!')
user_info: bytes | None = None return self.teaid
@dataclass
class ProcessedData(ProcessedDataMeta):
user_profile: dict[frozenset[tuple[str, Any]], UserProfile]
user_info: InfoSuccess | None = None
@dataclass @dataclass
@@ -60,7 +54,7 @@ def identify_user_info(info: str) -> User | MessageFormatError:
and 2 <= len(info) <= 18 # noqa: PLR2004 and 2 <= len(info) <= 18 # noqa: PLR2004
): ):
return User(name=info) return User(name=info)
if info.isdigit(): if info.startswith(('onebot-', 'qqguild-', 'kook-', 'discord-')) and info.split('-', maxsplit=1)[1].isdigit():
return User(teaid=info) return User(teaid=info)
return MessageFormatError('用户名/QQ号不合法') return MessageFormatError('用户名/QQ号不合法')
@@ -76,22 +70,20 @@ class Processor(ProcessorMeta):
self.processed_data = ProcessedData(user_profile={}) self.processed_data = ProcessedData(user_profile={})
@property @property
def game_platform(self) -> GameType: def game_platform(self) -> Literal['TOS']:
return GAME_TYPE return GAME_TYPE
async def handle_bind(self, platform: str, account: str) -> str: async def handle_bind(self, platform: str, account: str) -> str:
"""处理绑定消息""" """处理绑定消息"""
self.command_type = 'bind' self.command_type = 'bind'
await self.get_user() await self.get_user()
if self.user.name is None:
raise # FIXME: 不知道怎么才能把这类型给变过来了
async with get_session() as session: async with get_session() as session:
return await create_or_update_bind( return await create_or_update_bind(
session=session, session=session,
chat_platform=platform, chat_platform=platform,
chat_account=account, chat_account=account,
game_platform=GAME_TYPE, game_platform=GAME_TYPE,
game_account=self.user.name, game_account=self.user.unique_identifier,
) )
async def handle_query(self) -> str: async def handle_query(self) -> str:
@@ -113,45 +105,60 @@ class Processor(ProcessorMeta):
"""获取用户信息""" """获取用户信息"""
if self.processed_data.user_info is None: if self.processed_data.user_info is None:
if self.user.teaid is not None: if self.user.teaid is not None:
url = splice_url( url = [
[ splice_url(
BASE_URL, [
'getTeaIdInfo', i,
f'?{urlencode({"teaId":self.user.teaid})}', 'getTeaIdInfo',
] f'?{urlencode({"teaId":self.user.teaid})}',
) ]
)
for i in BASE_URL
]
else: else:
url = splice_url( url = [
[ splice_url(
BASE_URL, [
'getUsernameInfo', i,
f'?{urlencode({"username":self.user.name})}', 'getUsernameInfo',
] f'?{urlencode({"username":self.user.name})}',
) ]
self.raw_response.user_info = await Request.request(url) )
user_info: UserInfo = parse_raw_as(UserInfo, self.raw_response.user_info) # type: ignore[arg-type] for i in BASE_URL
]
self.raw_response.user_info = await Request.failover_request(
url, failover_code=[502], failover_exc=(TimeoutException,)
)
user_info: UserInfo = type_validate_json(UserInfo, self.raw_response.user_info) # type: ignore[arg-type]
if not isinstance(user_info, InfoSuccess): if not isinstance(user_info, InfoSuccess):
raise RequestError(f'用户信息请求错误:\n{user_info.error}') raise RequestError(f'用户信息请求错误:\n{user_info.error}')
self.processed_data.user_info = user_info self.processed_data.user_info = user_info
return self.processed_data.user_info return self.processed_data.user_info
async def get_user_profile(self, other_parameter: dict[str, Any] | None = None) -> UserProfile: async def get_user_profile(self, other_parameter: dict[str, str | bytes] | None = None) -> UserProfile:
"""获取用户数据""" """获取用户数据"""
if other_parameter is None: if other_parameter is None:
other_parameter = {} other_parameter = {}
fset = frozenset(other_parameter.items()) params = urlencode(dict(sorted(other_parameter.items())))
if self.processed_data.user_profile.get(fset) is None: if self.processed_data.user_profile.get(params) is None:
self.raw_response.user_profile[fset] = await Request.request( self.raw_response.user_profile[params] = await Request.failover_request(
splice_url( [
[ splice_url(
BASE_URL, [
'getProfile', i,
f'?{urlencode({"id":self.user.teaid or self.user.name},**other_parameter)}', 'getProfile',
] f'?{urlencode({"id":self.user.teaid or self.user.name,**other_parameter})}',
) ]
)
for i in BASE_URL
],
failover_code=[502],
failover_exc=(TimeoutException,),
) )
self.processed_data.user_profile[fset] = UserProfile.parse_raw(self.raw_response.user_profile[fset]) self.processed_data.user_profile[params] = UserProfile.model_validate_json(
return self.processed_data.user_profile[fset] self.raw_response.user_profile[params]
)
return self.processed_data.user_profile[params]
async def get_game_data(self) -> GameData | None: async def get_game_data(self) -> GameData | None:
"""获取游戏数据""" """获取游戏数据"""
@@ -211,11 +218,7 @@ class Processor(ProcessorMeta):
message += f"\nL'PM: {game_data.lpm} ( {game_data.pps} pps )" message += f"\nL'PM: {game_data.lpm} ( {game_data.pps} pps )"
message += f'\nAPM: {game_data.apm} ( x{game_data.apl} )' message += f'\nAPM: {game_data.apm} ( x{game_data.apl} )'
message += f'\nADPM: {game_data.adpm} ( x{game_data.adpl} ) ( {game_data.vs}vs )' message += f'\nADPM: {game_data.adpm} ( x{game_data.adpl} ) ( {game_data.vs}vs )'
message += ( message += f'\n40L: {float(user_info.pb_sprint)/1000:.2f}s' if user_info.pb_sprint != '2147483647' else ''
f'\n40L: {float(user_info.pb_sprint)/1000:.2f}s' message += f'\nMarathon: {user_info.pb_marathon}' if user_info.pb_marathon != '0' else ''
if user_info.pb_sprint != 2147483647 # noqa: PLR2004 message += f'\nChallenge: {user_info.pb_challenge}' if user_info.pb_challenge != '0' else ''
else ''
)
message += f'\nMarathon: {user_info.pb_marathon}' if user_info.pb_marathon != 0 else ''
message += f'\nChallenge: {user_info.pb_challenge}' if user_info.pb_challenge != 0 else ''
return message return message

View File

@@ -0,0 +1,20 @@
from typing import Literal
from ...schemas import BaseProcessedData, BaseRawResponse
from ..constant import GAME_TYPE
from .user_info import SuccessModel as InfoSuccess
from .user_profile import UserProfile
class RawResponse(BaseRawResponse):
platform: Literal['TOS'] = GAME_TYPE
user_profile: dict[str, bytes]
user_info: bytes | None = None
class ProcessedData(BaseProcessedData):
platform: Literal['TOS'] = GAME_TYPE
user_profile: dict[str, UserProfile]
user_info: InfoSuccess | None = None

View File

@@ -1,6 +1,7 @@
import sys import sys
from os import environ from os import environ
from platform import system from platform import system
from re import sub
from nonebot import get_driver from nonebot import get_driver
from nonebot.log import logger from nonebot.log import logger
@@ -33,12 +34,12 @@ class BrowserManager:
raise ImportError('加载失败, Windows 必须设置 FASTAPI_RELOAD=false 才能正常运行 playwright') raise ImportError('加载失败, Windows 必须设置 FASTAPI_RELOAD=false 才能正常运行 playwright')
logger.info('开始 安装/更新 playwright 浏览器') logger.info('开始 安装/更新 playwright 浏览器')
environ['PLAYWRIGHT_DOWNLOAD_HOST'] = 'https://npmmirror.com/mirrors/playwright/' environ['PLAYWRIGHT_DOWNLOAD_HOST'] = 'https://npmmirror.com/mirrors/playwright/'
if cls._handle_error(cls._call_playwright(['', 'install', 'firefox'])): if cls._call_playwright(['', 'install', 'firefox']):
logger.success('安装/更新 playwright 浏览器成功') logger.success('安装/更新 playwright 浏览器成功')
else: else:
logger.warning('playwright 浏览器 安装/更新 失败, 尝试使用原始仓库下载') logger.warning('playwright 浏览器 安装/更新 失败, 尝试使用原始仓库下载')
del environ['PLAYWRIGHT_DOWNLOAD_HOST'] del environ['PLAYWRIGHT_DOWNLOAD_HOST']
if cls._handle_error(cls._call_playwright(['', 'install', 'firefox'])): if cls._call_playwright(['', 'install', 'firefox']):
logger.success('安装/更新 playwright 浏览器成功') logger.success('安装/更新 playwright 浏览器成功')
else: else:
logger.error('安装/更新 playwright 浏览器失败') logger.error('安装/更新 playwright 浏览器失败')
@@ -52,26 +53,20 @@ class BrowserManager:
logger.success('playwright 启动成功') logger.success('playwright 启动成功')
@classmethod @classmethod
def _call_playwright(cls, argv: list[str]) -> BaseException: def _call_playwright(cls, argv: list[str]) -> bool:
"""等价于调用 playwright 的命令行程序""" """等价于调用 playwright 的命令行程序"""
argv_backup = sys.argv.copy() argv_backup = sys.argv.copy()
from re import sub
sys.argv[0] = sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) sys.argv[0] = sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.argv = argv sys.argv = argv
try: try:
main() main()
except BaseException as e: # noqa: BLE001 不在这里处理 playwright 的异常 except SystemExit as e:
return e return e.code == 0
except BaseException: # noqa: BLE001
return False
finally: finally:
sys.argv = argv_backup sys.argv = argv_backup
return SystemExit(0) return True
@classmethod
def _handle_error(cls, error: BaseException) -> bool:
if isinstance(error, SystemExit) and error.code == 0:
return True
return False
@classmethod @classmethod
async def _start_browser(cls) -> Browser: async def _start_browser(cls) -> Browser:

View File

@@ -15,21 +15,25 @@ class NeedCatchError(TetrisStatsError):
"""需要被捕获的异常基类""" """需要被捕获的异常基类"""
class DoNotCatchError(TetrisStatsError):
"""不应该被捕获的异常基类"""
class RequestError(NeedCatchError): class RequestError(NeedCatchError):
"""请求错误""" """请求错误"""
def __init__(self, message: str = '', *, status_code: int | None = None):
super().__init__(message)
self.status_code = status_code
class MessageFormatError(NeedCatchError): class MessageFormatError(NeedCatchError):
"""用户发送的消息格式不正确""" """用户发送的消息格式不正确"""
class DatabaseVersionError(DoNotCatchError): class DoNotCatchError(TetrisStatsError):
"""数据库版本错误""" """不应该被捕获的异常基类"""
class WhatTheFuckError(DoNotCatchError): class WhatTheFuckError(DoNotCatchError):
"""用于表示不应该出现的情况 (""" """用于表示不应该出现的情况 ("""
class HandleNotFinishedError(DoNotCatchError):
"""任务没有正常完成处理的错误"""

View File

@@ -144,7 +144,7 @@ class TetrisMetricsProWithLPMADPM(TetrisMetricsBasicWithLPM, TetrisMetricsBaseWi
@overload @overload
def get_metrics( # noqa: PLR0913 def get_metrics(
*, *,
pps: Number, pps: Number,
lpm: None = None, lpm: None = None,
@@ -157,7 +157,7 @@ def get_metrics( # noqa: PLR0913
@overload @overload
def get_metrics( # noqa: PLR0913 def get_metrics(
*, *,
pps: None = None, pps: None = None,
lpm: Number, lpm: Number,
@@ -170,7 +170,7 @@ def get_metrics( # noqa: PLR0913
@overload @overload
def get_metrics( # noqa: PLR0913 def get_metrics(
*, *,
pps: None = None, pps: None = None,
lpm: None = None, lpm: None = None,
@@ -183,7 +183,7 @@ def get_metrics( # noqa: PLR0913
@overload @overload
def get_metrics( # noqa: PLR0913 def get_metrics(
*, *,
pps: None = None, pps: None = None,
lpm: None = None, lpm: None = None,
@@ -196,7 +196,7 @@ def get_metrics( # noqa: PLR0913
@overload @overload
def get_metrics( # noqa: PLR0913 def get_metrics(
*, *,
pps: Number, pps: Number,
lpm: None = None, lpm: None = None,
@@ -209,7 +209,7 @@ def get_metrics( # noqa: PLR0913
@overload @overload
def get_metrics( # noqa: PLR0913 def get_metrics(
*, *,
pps: None = None, pps: None = None,
lpm: Number, lpm: Number,
@@ -222,7 +222,7 @@ def get_metrics( # noqa: PLR0913
@overload @overload
def get_metrics( # noqa: PLR0913 def get_metrics(
*, *,
pps: Number, pps: Number,
lpm: None = None, lpm: None = None,
@@ -235,7 +235,7 @@ def get_metrics( # noqa: PLR0913
@overload @overload
def get_metrics( # noqa: PLR0913 def get_metrics(
*, *,
pps: Number, pps: Number,
lpm: None = None, lpm: None = None,
@@ -248,7 +248,7 @@ def get_metrics( # noqa: PLR0913
@overload @overload
def get_metrics( # noqa: PLR0913 def get_metrics(
*, *,
pps: None = None, pps: None = None,
lpm: Number, lpm: Number,
@@ -261,7 +261,7 @@ def get_metrics( # noqa: PLR0913
@overload @overload
def get_metrics( # noqa: PLR0913 def get_metrics(
*, *,
pps: None = None, pps: None = None,
lpm: Number, lpm: Number,

View File

@@ -1,4 +1,4 @@
from datetime import UTC, datetime from datetime import datetime, timezone
from typing import ClassVar from typing import ClassVar
from nonebot import get_driver, get_plugin from nonebot import get_driver, get_plugin
@@ -9,12 +9,15 @@ from nonebot_plugin_orm import get_session
from ..db.models import HistoricalData from ..db.models import HistoricalData
UTC = timezone.utc
driver = get_driver() driver = get_driver()
class Recorder: class Recorder:
matchers: ClassVar[set[type[Matcher]]] = set() matchers: ClassVar[set[type[Matcher]]] = set()
historical_data: ClassVar[dict[int, tuple[HistoricalData, bool]]] = {} historical_data: ClassVar[dict[int, tuple[HistoricalData, bool]]] = {}
error_event: ClassVar[set[int]] = set()
@classmethod @classmethod
def create_historical_data(cls, event_id: int, historical_data: HistoricalData) -> None: def create_historical_data(cls, event_id: int, historical_data: HistoricalData) -> None:
@@ -32,17 +35,27 @@ class Recorder:
@classmethod @classmethod
async def save_historical_data(cls, event_id: int) -> None: async def save_historical_data(cls, event_id: int) -> None:
if event_id not in cls.historical_data: historical_data, completed = cls.del_historical_data(event_id)
raise KeyError
historical_data, completed = cls.historical_data.pop(event_id)
if completed: if completed:
async with get_session() as session: async with get_session() as session:
session.add(historical_data) session.add(historical_data)
await session.commit() await session.commit()
@classmethod @classmethod
def del_historical_data(cls, event_id: int) -> None: def del_historical_data(cls, event_id: int) -> tuple[HistoricalData, bool]:
cls.historical_data.pop(event_id) return cls.historical_data.pop(event_id)
@classmethod
def add_error_event(cls, event_id: int) -> None:
cls.error_event.add(event_id)
@classmethod
def del_error_event(cls, event_id: int) -> None:
cls.error_event.remove(event_id)
@classmethod
def is_error_event(cls, event_id: int) -> bool:
return event_id in cls.error_event
@driver.on_startup @driver.on_startup
@@ -73,7 +86,9 @@ def _(bot: Bot, event: Event, matcher: Matcher):
@run_postprocessor @run_postprocessor
async def _(event: Event, matcher: Matcher, exception: Exception | None): async def _(event: Event, matcher: Matcher, exception: Exception | None):
if isinstance(matcher, tuple(Recorder.matchers)): if isinstance(matcher, tuple(Recorder.matchers)):
event_id = id(event)
if exception is not None: if exception is not None:
Recorder.del_historical_data(id(event)) Recorder.add_error_event(event_id)
Recorder.del_historical_data(event_id)
else: else:
await Recorder.save_historical_data(id(event)) await Recorder.save_historical_data(event_id)

View File

@@ -1,17 +1,20 @@
from collections.abc import Sequence
from http import HTTPStatus
from urllib.parse import urljoin, urlparse from urllib.parse import urljoin, urlparse
from aiofiles import open from aiofiles import open
from httpx import AsyncClient, HTTPError from httpx import AsyncClient, HTTPError
from nonebot import get_driver from nonebot import get_driver, get_plugin_config
from nonebot.log import logger from nonebot.log import logger
from playwright.async_api import Response from playwright.async_api import Response
from ujson import JSONDecodeError, dumps, loads from ujson import JSONDecodeError, dumps, loads
from ..config.config import CACHE_PATH from ..config.config import CACHE_PATH, Config
from .browser import BrowserManager from .browser import BrowserManager
from .exception import RequestError from .exception import RequestError
driver = get_driver() driver = get_driver()
config = get_plugin_config(Config)
@driver.on_startup @driver.on_startup
@@ -37,7 +40,7 @@ def splice_url(url_list: list[str]) -> str:
class Request: class Request:
"""网络请求相关类""" """网络请求相关类"""
_CACHE_FILE = CACHE_PATH.joinpath('cloudflare_cache.json') _CACHE_FILE = CACHE_PATH / 'cloudflare_cache.json'
_headers: dict | None = None _headers: dict | None = None
_cookies: dict | None = None _cookies: dict | None = None
@@ -45,36 +48,31 @@ class Request:
async def _anti_cloudflare(cls, url: str) -> bytes: async def _anti_cloudflare(cls, url: str) -> bytes:
"""用firefox硬穿五秒盾""" """用firefox硬穿五秒盾"""
browser = await BrowserManager.get_browser() browser = await BrowserManager.get_browser()
context = await browser.new_context() async with await browser.new_context() as context, await context.new_page() as page:
page = await context.new_page() response = await page.goto(url)
response = await page.goto(url) attempts = 0
attempts = 0 while attempts < 60: # noqa: PLR2004
while attempts < 60: # noqa: PLR2004 attempts += 1
attempts += 1 text = await page.locator('body').text_content()
text = await page.locator('body').text_content() if text is None:
if text is None: await page.wait_for_timeout(1000)
await page.wait_for_timeout(1000) continue
continue if await page.title() == 'Please Wait... | Cloudflare':
if await page.title() == 'Please Wait... | Cloudflare': logger.warning('疑似触发了 Cloudflare 的验证码')
logger.warning('疑似触发了 Cloudflare 的验证码') break
break
try:
loads(text)
except JSONDecodeError:
await page.wait_for_timeout(1000)
else:
if not isinstance(response, Response):
raise RequestError('api请求失败')
cls._headers = await response.request.all_headers()
try: try:
cls._cookies = {i['name']: i['value'] for i in await context.cookies()} loads(text)
except KeyError: except JSONDecodeError:
cls._cookies = None await page.wait_for_timeout(1000)
await page.close() else:
await context.close() if not isinstance(response, Response):
return await response.body() raise RequestError('api请求失败')
await page.close() cls._headers = await response.request.all_headers()
await context.close() try:
cls._cookies = {i['name']: i['value'] for i in await context.cookies()}
except KeyError:
cls._cookies = None
return await response.body()
raise RequestError('绕过五秒盾失败') raise RequestError('绕过五秒盾失败')
@classmethod @classmethod
@@ -115,14 +113,49 @@ 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) as session: async with AsyncClient(cookies=cls._cookies, timeout=config.tetris_req_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:
raise RequestError(
f'请求错误 code: {response.status_code} {HTTPStatus(response.status_code).phrase}\n{response.text}',
status_code=response.status_code,
)
if is_json: if is_json:
loads(response.content) loads(response.content)
return response.content return response.content
except HTTPError as e: except HTTPError as e:
raise RequestError(f'请求错误\n{e!r}') from e raise RequestError(f'请求错误 \n{e!r}') from e
except JSONDecodeError: except JSONDecodeError:
if urlparse(url).netloc.lower().endswith('tetr.io'): if urlparse(url).netloc.lower().endswith('tetr.io'):
return await cls._anti_cloudflare(url) return await cls._anti_cloudflare(url)
raise raise
@classmethod
async def failover_request(
cls,
urls: Sequence[str],
*,
failover_code: Sequence[int],
failover_exc: tuple[type[BaseException], ...],
is_json: bool = True,
) -> bytes:
error_list: list[RequestError] = []
for i in urls:
logger.debug(f'尝试请求 {i}')
try:
return await cls.request(i, is_json=is_json)
except RequestError as e:
if e.status_code in failover_code: # 如果状态码在 failover_code 中, 则继续尝试下一个URL
error_list.append(e)
continue
# 如果状态码不在故障转移列表中, 则查找异常栈, 如果异常栈内有 failover_exc 内的异常类型, 则继续尝试下一个URL
tb = e.__traceback__
while tb is not None:
if isinstance(tb.tb_frame.f_locals.get('exc_value'), failover_exc):
error_list.append(e)
break
tb = tb.tb_next
else:
raise
continue
raise RequestError(f'所有地址皆不可用\n{error_list!r}')

View File

@@ -0,0 +1,37 @@
from asyncio import sleep
from collections.abc import Awaitable, Callable
from datetime import timedelta
from functools import wraps
from typing import TypeVar, cast
from nonebot.log import logger
T = TypeVar('T')
def retry(
max_attempts: int = 3,
exception_type: type[BaseException] | tuple[type[BaseException], ...] = Exception,
delay: timedelta | None = None,
) -> Callable[[Callable[..., Awaitable[T]]], Callable[..., Awaitable[T]]]:
def decorator(func: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]:
@wraps(func)
async def wrapper(*args, **kwargs) -> T: # noqa: ANN002, ANN003
attempts = 0
while attempts < max_attempts + 1:
try:
return await func(*args, **kwargs)
except exception_type as e: # noqa: PERF203
logger.exception(e)
attempts += 1
if attempts <= max_attempts:
if delay is not None:
await sleep(delay.total_seconds())
logger.debug(f'Retrying: {func.__name__} ({attempts}/{max_attempts})')
continue
raise
raise RuntimeError('Unexpectedly reached the end of the retry loop')
return cast(Callable[..., Awaitable[T]], wrapper)
return decorator

2385
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = 'nonebot-plugin-tetris-stats' name = 'nonebot-plugin-tetris-stats'
version = '1.0.0.a2' version = '1.0.0.a16'
description = '一款基于 NoneBot2 的用于查询 Tetris 相关游戏数据的插件' description = '一款基于 NoneBot2 的用于查询 Tetris 相关游戏数据的插件'
authors = ['scdhh <wallfjjd@gmail.com>'] authors = ['scdhh <wallfjjd@gmail.com>']
readme = 'README.md' readme = 'README.md'
@@ -10,33 +10,37 @@ license = 'AGPL-3.0'
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = '^3.10' python = '^3.10'
nonebot2 = '^2.0.0-beta.3' nonebot2 = '^2.2.0'
lxml = '^4.9.1' lxml = '^5.1.0'
pandas = '>=1.4.3,<3.0.0' pandas = '>=1.4.3,<3.0.0'
playwright = '^1.24.1' playwright = '^1.41.2'
ujson = '^5.4.0' ujson = '^5.9.0'
aiofiles = "^23.2.1" aiofiles = "^23.2.1"
nonebot-plugin-orm = ">=0.1.1,<0.6.0" nonebot-plugin-orm = ">=0.1.1,<0.8.0"
nonebot-plugin-localstore = "^0.5.1" nonebot-plugin-localstore = "^0.6.0"
httpx = "^0.25.0" httpx = "^0.27.0"
nonebot-plugin-alconna = ">=0.30,<0.34" nonebot-plugin-alconna = ">=0.40"
nonebot-plugin-apscheduler = "^0.3.0" nonebot-plugin-apscheduler = "^0.4.0"
aiocache = "^0.12.2"
zstandard = "^0.22.0"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
mypy = '>=0.991,<1.8' mypy = '>=1.9'
types-ujson = '^5.7.0' types-ujson = '^5.9.0'
pandas-stubs = '>=1.5.2,<3.0.0' pandas-stubs = '>=1.5.2,<3.0.0'
ruff = '>=0.0.239,<0.1.6' ruff = '>=0.3.0'
types-aiofiles = "^23.2.0.0" types-aiofiles = "^23.2.0.20240106"
nonebot2 = { extras = ["fastapi"], version = "^2.1.1" } nonebot2 = { extras = ["fastapi"], version = "^2.2.0" }
types-lxml = "^2023.3.28" types-lxml = "^2024.2.9"
nonebot-plugin-orm = { extras = ["default"], version = ">=0.3,<0.6" } nonebot-plugin-orm = { extras = ["default"], version = ">=0.3,<0.8" }
nonebot-adapter-onebot = "^2.3.1" nonebot-adapter-onebot = "^2.4.1"
nonebot-adapter-satori = "^0.7.0" nonebot-adapter-satori = "^0.11.3"
nonebot-adapter-kaiheila = "^0.3.4"
nonebot-adapter-discord = "^0.1.3"
[tool.poetry.group.debug.dependencies] [tool.poetry.group.debug.dependencies]
objprint = '^0.2.2' objprint = '^0.2.2'
viztracer = "^0.16.0" viztracer = "^0.16.2"
[build-system] [build-system]
requires = ['poetry-core>=1.0.0'] requires = ['poetry-core>=1.0.0']