mirror of
https://github.com/A-Minos/nonebot-plugin-tetris-stats.git
synced 2026-03-05 05:36:54 +08:00
🔖 Release 1.0.0.a1 (#80)
* 使用 `pathlib` 替代 `os` * 防止建立多个数据库连接对象 * 调整数据库结构 # 破坏性更新 * 格式化代码 * ➖ 去除依赖Brotli ➖ 去除开发依赖autopep8 pylint ➕ 添加开发依赖ruff black 🔥 删除.pylintrc 🎨 使用black格式化代码 * 📝 一些很赞的小牌子 * ✏️ 修正`config`变量名 * 🐛 修复OperationalError语法错误 * ➕ 添加 debug 依赖 objprint * 🚧 数据记录器demo * 🔥 这个init好像没什么用( * 💡 ✏️ 修改错误的注释 * 📝 🍱 添加一个logo * 📝 添加logo的悬浮提示 * 🙈 更新 .gitignore * 🚨 消除了一些 init 文件中的错误警告 * ♻️ 💩 重构 IO 的 processor 模块 🐛 修复了 bind user_id 不能正确处理的bug 🎨 使用一些自定义类型和基于异常的编程( * 🐛 忘记写try了 * 👷 将 Release CI 切换到 Python 3.11 版本 * 🎨 修改 Exception 类的变量名 * 🎨 修改捕获的 aiohttp 的错误类型 * 🎨 将 AsyncCallable 放进 typing 模块 * 🏗️ 将 recorder 装饰器中执行函数的部分放在 collector 函数中 🚧 完善数据收集部分 * 🚧 receive 记录添加 message_id 以辅助消息上下文识别 * ➕ 添加依赖 tortoise-orm * ♻️ 🗃️ 将数据库操作替换成 tortoise-orm * 🎨 显式传递 locals 字典 * 🎨 将装饰器封装到类里 * 🐛 忘记 exec 需要拿变量了 * 🗃️ 微调数据类型 * 🗃️ 调整数据库索引 * Bump playwright from 1.29.0 to 1.30.0 (#72) * Bump ujson from 5.6.0 to 5.7.0 (#69) * Bump pandas-stubs from 1.5.2.221213 to 1.5.2.230105 (#57) * Bump types-ujson from 5.6.0.0 to 5.7.0.0 (#68) * ✨ 存储命令历史 #58 * Bump nonebot2 from 2.0.0rc2 to 2.0.0rc3 (#73) * Bump nonebot-adapter-onebot from 2.2.0 to 2.2.1 (#74) * Bump tortoise-orm from 0.19.2 to 0.19.3 (#75) * Bump black from 23.1a1 to 23.1.0 (#77) * Bump pandas from 1.5.2 to 1.5.3 (#76) * ⬆️ 更新 ruff * 🔧 启用更多的检查规则 * 🎨 使用单引号编写配置文件 * 💡 为 ignore 添加注释 * 🎨 使用 ruff 规范化引号 * 🔧 启用 PEP8 命名规范检查 * 🚨 添加一些 noqa( * 💡 添加和修改了一些注释 * 🔊 添加一条日志 * 🎨 🚨 使用 replace 替换 strip * 🎨 🚨 规范化命名 * 🎨 格式化代码 * 🔊 修改日志等级 * 🎨 去除重复的 get_driver() 调用 * ✨ 自动安装 playwright 浏览器 close #71 * 🙈 更新 gitignore * ✨ 将所有 playwright 相关整合进 BrowserManager 类 * 📝 更换开源许可证 (#78) * 📝 更新 README * Bump pandas-stubs from 1.5.2.230105 to 1.5.3.230203 (#79) * ➕ 添加依赖 nonebot-plugin-datastore * ✨ 使用 nonebot_plugin_datastore 提供的路径存储缓存 * Bump nonebot-plugin-datastore from 0.5.7 to 0.5.8 (#81) * ⬆️ Bump aiohttp from 3.8.3 to 3.8.4 (#82) Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.8.3 to 3.8.4. - [Release notes](https://github.com/aio-libs/aiohttp/releases) - [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst) - [Commits](https://github.com/aio-libs/aiohttp/compare/v3.8.3...v3.8.4) --- updated-dependencies: - dependency-name: aiohttp 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> * ⬆️ Bump pandas-stubs from 1.5.3.230203 to 1.5.3.230214 (#84) Bumps [pandas-stubs](https://github.com/pandas-dev/pandas-stubs) from 1.5.3.230203 to 1.5.3.230214. - [Release notes](https://github.com/pandas-dev/pandas-stubs/releases) - [Changelog](https://github.com/pandas-dev/pandas-stubs/blob/main/docs/release_procedure.md) - [Commits](https://github.com/pandas-dev/pandas-stubs/compare/v1.5.3.230203...v1.5.3.230214) --- 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> * ⬆️ Bump ruff from 0.0.239 to 0.0.253 (#90) * ⬆️ Bump playwright from 1.30.0 to 1.31.1 (#89) * ⬆️ Bump types-ujson from 5.7.0.0 to 5.7.0.1 (#86) * ⬆️ Bump pandas-stubs from 1.5.3.230214 to 1.5.3.230227 (#88) * ⬆️ Bump ruff from 0.0.253 to 0.0.254 (#91) * ⬆️ Bump pandas-stubs from 1.5.3.230227 to 1.5.3.230304 (#92) * ⬆️ Bump mypy from 0.991 to 1.0.1 (#93) * ⬆️ Bump mypy from 1.0.1 to 1.1.1 (#94) * ⬆️ Bump ruff from 0.0.254 to 0.0.284 (#139) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.0.254 to 0.0.284. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/BREAKING_CHANGES.md) - [Commits](https://github.com/astral-sh/ruff/compare/v0.0.254...v0.0.284) --- 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> * ⬆️ Bump nonebot-adapter-onebot from 2.2.1 to 2.2.4 (#137) Bumps [nonebot-adapter-onebot](https://github.com/nonebot/adapter-onebot) from 2.2.1 to 2.2.4. - [Release notes](https://github.com/nonebot/adapter-onebot/releases) - [Commits](https://github.com/nonebot/adapter-onebot/compare/v2.2.1...v2.2.4) --- updated-dependencies: - dependency-name: nonebot-adapter-onebot 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> * ⬆️ Bump playwright from 1.31.1 to 1.36.0 (#133) Bumps [playwright](https://github.com/Microsoft/playwright-python) from 1.31.1 to 1.36.0. - [Release notes](https://github.com/Microsoft/playwright-python/releases) - [Commits](https://github.com/Microsoft/playwright-python/compare/v1.31.1...v1.36.0) --- updated-dependencies: - dependency-name: playwright dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * ⬆️ Bump pandas-stubs from 1.5.3.230304 to 2.0.2.230605 (#124) * ⬆️ Bump mypy from 1.1.1 to 1.5.1 (#144) Bumps [mypy](https://github.com/python/mypy) from 1.1.1 to 1.5.1. - [Commits](https://github.com/python/mypy/compare/v1.1.1...v1.5.1) --- 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> * ⬆️ Bump types-ujson from 5.7.0.1 to 5.8.0.1 (#142) Bumps [types-ujson](https://github.com/python/typeshed) from 5.7.0.1 to 5.8.0.1. - [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> * ⬆️ Bump nonebot2 from 2.0.0rc3 to 2.0.1 (#141) Bumps [nonebot2](https://github.com/nonebot/nonebot2) from 2.0.0rc3 to 2.0.1. - [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.0.0rc3...v2.0.1) --- updated-dependencies: - dependency-name: nonebot2 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> * ⬆️ Bump lxml from 4.9.2 to 4.9.3 (#140) Bumps [lxml](https://github.com/lxml/lxml) from 4.9.2 to 4.9.3. - [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-4.9.2...lxml-4.9.3) --- updated-dependencies: - dependency-name: lxml 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> * ⬆️ Bump black from 23.1.0 to 23.9.1 (#148) Bumps [black](https://github.com/psf/black) from 23.1.0 to 23.9.1. - [Release notes](https://github.com/psf/black/releases) - [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) - [Commits](https://github.com/psf/black/compare/23.1.0...23.9.1) --- updated-dependencies: - dependency-name: black 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> * ⬆️ Bump pandas-stubs from 2.0.2.230605 to 2.0.3.230814 (#149) Bumps [pandas-stubs](https://github.com/pandas-dev/pandas-stubs) from 2.0.2.230605 to 2.0.3.230814. - [Changelog](https://github.com/pandas-dev/pandas-stubs/blob/main/docs/release_procedure.md) - [Commits](https://github.com/pandas-dev/pandas-stubs/compare/v2.0.2.230605...v2.0.3.230814) --- 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> * ⬆️ Bump pandas from 1.5.3 to 2.1.1 (#152) Bumps [pandas](https://github.com/pandas-dev/pandas) from 1.5.3 to 2.1.1. - [Release notes](https://github.com/pandas-dev/pandas/releases) - [Commits](https://github.com/pandas-dev/pandas/compare/v1.5.3...v2.1.1) --- updated-dependencies: - dependency-name: pandas dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * ⬆️ Bump aiohttp from 3.8.4 to 3.8.5 (#155) Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.8.4 to 3.8.5. - [Release notes](https://github.com/aio-libs/aiohttp/releases) - [Changelog](https://github.com/aio-libs/aiohttp/blob/v3.8.5/CHANGES.rst) - [Commits](https://github.com/aio-libs/aiohttp/compare/v3.8.4...v3.8.5) --- updated-dependencies: - dependency-name: aiohttp 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> * ⬆️ Bump ruff from 0.0.284 to 0.0.291 (#154) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.0.284 to 0.0.291. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/BREAKING_CHANGES.md) - [Commits](https://github.com/astral-sh/ruff/compare/v0.0.284...v0.0.291) --- 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> * ⬆️ Bump playwright from 1.36.0 to 1.38.0 (#153) Bumps [playwright](https://github.com/Microsoft/playwright-python) from 1.36.0 to 1.38.0. - [Release notes](https://github.com/Microsoft/playwright-python/releases) - [Commits](https://github.com/Microsoft/playwright-python/compare/v1.36.0...v1.38.0) --- updated-dependencies: - dependency-name: playwright dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * ⬆️ Bump nonebot-plugin-datastore from 0.5.8 to 1.1.2 (#151) Bumps [nonebot-plugin-datastore](https://github.com/he0119/nonebot-plugin-datastore) from 0.5.8 to 1.1.2. - [Release notes](https://github.com/he0119/nonebot-plugin-datastore/releases) - [Changelog](https://github.com/he0119/nonebot-plugin-datastore/blob/main/CHANGELOG.md) - [Commits](https://github.com/he0119/nonebot-plugin-datastore/compare/v0.5.8...v1.1.2) --- updated-dependencies: - dependency-name: nonebot-plugin-datastore dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * ⬆️ Bump nonebot2 from 2.0.1 to 2.1.0 (#156) Bumps [nonebot2](https://github.com/nonebot/nonebot2) from 2.0.1 to 2.1.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.0.1...v2.1.0) --- updated-dependencies: - dependency-name: nonebot2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * ⬆️ Bump ujson from 5.7.0 to 5.8.0 (#157) Bumps [ujson](https://github.com/ultrajson/ultrajson) from 5.7.0 to 5.8.0. - [Release notes](https://github.com/ultrajson/ultrajson/releases) - [Commits](https://github.com/ultrajson/ultrajson/compare/5.7.0...5.8.0) --- updated-dependencies: - dependency-name: ujson 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> * ⚡️ 移除无意义的async * 🎨 修正 type hint * ➕ 添加依赖 aiofiles * 🎨 将文件读写操作换成 aiofiles * ⚡️ 移除无意义的async * 🎨 重命名一些函数 * 🎨 去除不需要的转换 * ⬆️ Bump pandas-stubs from 2.0.3.230814 to 2.1.1.230928 (#158) Bumps [pandas-stubs](https://github.com/pandas-dev/pandas-stubs) from 2.0.3.230814 to 2.1.1.230928. - [Changelog](https://github.com/pandas-dev/pandas-stubs/blob/main/docs/release_procedure.md) - [Commits](https://github.com/pandas-dev/pandas-stubs/compare/v2.0.3.230814...v2.1.1.230928) --- 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> * ⬆️ Bump nonebot2 from 2.1.0 to 2.1.1 (#159) Bumps [nonebot2](https://github.com/nonebot/nonebot2) from 2.1.0 to 2.1.1. - [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.0...v2.1.1) --- updated-dependencies: - dependency-name: nonebot2 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> * ➕ 添加依赖 nonebot-plugin-orm ➕ 添加依赖 nonebot-plugin-localstore ➖ 移除依赖 nonebot-plugin-datastore * 📌 取消 python 最高版本限制 * ⬆️ Bump objprint from 0.2.2 to 0.2.3 (#161) Bumps [objprint](https://github.com/gaogaotiantian/objprint) from 0.2.2 to 0.2.3. - [Release notes](https://github.com/gaogaotiantian/objprint/releases) - [Commits](https://github.com/gaogaotiantian/objprint/compare/0.2.2...0.2.3) --- updated-dependencies: - dependency-name: objprint 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> * ⬆️ Bump ruff from 0.0.291 to 0.0.292 (#160) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.0.291 to 0.0.292. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/BREAKING_CHANGES.md) - [Commits](https://github.com/astral-sh/ruff/compare/v0.0.291...v0.0.292) --- 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> * ⬆️ Bump mypy from 1.5.1 to 1.6.0 (#163) Bumps [mypy](https://github.com/python/mypy) from 1.5.1 to 1.6.0. - [Commits](https://github.com/python/mypy/compare/v1.5.1...v1.6.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> * ➕ 添加依赖 httpx * ⬆️ Bump nonebot-plugin-orm from 0.1.1 to 0.2.1 (#166) Bumps [nonebot-plugin-orm](https://github.com/nonebot/plugin-orm) from 0.1.1 to 0.2.1. - [Release notes](https://github.com/nonebot/plugin-orm/releases) - [Commits](https://github.com/nonebot/plugin-orm/compare/v0.1.1...v0.2.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> * ➕ 添加开发依赖 nonebot2 * 🎨 改为直接使用 nonebot_plugin_localstore 提供缓存路径 * 🐛 忘记 require * 🐛 顺序错了 * 🗃️ 使用 nb orm * 🏗️ 再次重构 IO 模块 * 🐛 忘记 push 这个了 * 🏗️ 将 request 改成通用的 * ⬆️ Bump nonebot-plugin-orm from 0.2.1 to 0.2.2 (#167) Bumps [nonebot-plugin-orm](https://github.com/nonebot/plugin-orm) from 0.2.1 to 0.2.2. - [Release notes](https://github.com/nonebot/plugin-orm/releases) - [Commits](https://github.com/nonebot/plugin-orm/compare/v0.2.1...v0.2.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> * ⬆️ Bump ruff from 0.0.292 to 0.1.0 (#168) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.0.292 to 0.1.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.0.292...v0.1.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> * ⬆️ Bump nonebot-adapter-onebot from 2.3.0 to 2.3.1 (#165) Bumps [nonebot-adapter-onebot](https://github.com/nonebot/adapter-onebot) from 2.3.0 to 2.3.1. - [Release notes](https://github.com/nonebot/adapter-onebot/releases) - [Commits](https://github.com/nonebot/adapter-onebot/compare/v2.3.0...v2.3.1) --- updated-dependencies: - dependency-name: nonebot-adapter-onebot 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> * 🏗️ 重构 TOS 模块 * 🩹 补充返回值类型标注 * 🐛 写错变量了 * 🐛 缺个 else * 🐛 忘记声明变量 * 🐛 忘记初始化变量 * 🎨 去除不需要的 else * 🎨 去除不需要的判断 * 🎨 减少一次函数调用 * 🐛 写错命令了 * ⬆️ Bump nonebot-plugin-orm from 0.2.2 to 0.2.3 (#170) Bumps [nonebot-plugin-orm](https://github.com/nonebot/plugin-orm) from 0.2.2 to 0.2.3. - [Release notes](https://github.com/nonebot/plugin-orm/releases) - [Commits](https://github.com/nonebot/plugin-orm/compare/v0.2.2...v0.2.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> * ⬆️ Bump black from 23.9.1 to 23.10.0 (#171) Bumps [black](https://github.com/psf/black) from 23.9.1 to 23.10.0. - [Release notes](https://github.com/psf/black/releases) - [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) - [Commits](https://github.com/psf/black/compare/23.9.1...23.10.0) --- updated-dependencies: - dependency-name: black 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> * ⬆️ Bump mypy from 1.6.0 to 1.6.1 (#172) Bumps [mypy](https://github.com/python/mypy) from 1.6.0 to 1.6.1. - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.6.0...v1.6.1) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * ⬆️ Bump playwright from 1.38.0 to 1.39.0 (#169) Bumps [playwright](https://github.com/Microsoft/playwright-python) from 1.38.0 to 1.39.0. - [Release notes](https://github.com/Microsoft/playwright-python/releases) - [Commits](https://github.com/Microsoft/playwright-python/compare/v1.38.0...v1.39.0) --- updated-dependencies: - dependency-name: playwright dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * ➖ 去除开发依赖 lxml-stubs ➕ 添加开发依赖 types-lxml * 🏗️ 重构 TOP 模块 * 🐛 忘记传参了 * 🐛 忘记判断有没有绑定了 * 🎨 忘记用封好的函数了 * 📝 把 wakatime 的小牌牌放上去 并且格式化 * ⬆️ Bump ruff from 0.1.0 to 0.1.1 (#174) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.1.0 to 0.1.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.1.0...v0.1.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> * ⬆️ Bump nonebot-plugin-orm from 0.2.3 to 0.2.4 (#173) Bumps [nonebot-plugin-orm](https://github.com/nonebot/plugin-orm) from 0.2.3 to 0.2.4. - [Release notes](https://github.com/nonebot/plugin-orm/releases) - [Commits](https://github.com/nonebot/plugin-orm/compare/v0.2.3...v0.2.4) --- 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> * 🏗️ 把 config 从 utils 里拿出来 * 🔥 orm 搞错用法了 * 🗃️ 创建新的迁移脚本 * ✨ 添加插件元数据 * 🐛 导包顺序又错了 * ➕ 添加依赖 nonebot-adapter-qq * ➕ 添加依赖 nonebot-plugin-alconna * ➖ 移除依赖 tortoise-orm * ⬆️ Bump nonebot-plugin-orm from 0.2.4 to 0.3.0 (#175) * ⬆️ Bump types-lxml from 2023.3.28 to 2023.10.21 (#176) * ⬆️ Bump black from 23.10.0 to 23.10.1 (#177) * ⬆️ Bump ruff from 0.1.1 to 0.1.2 (#178) * ➕ 添加debug依赖 viztracer * ➕ 添加开发依赖 nonebot-plugin-orm[default] * ✨ IO 基础查询功能适配所有平台 * ⬆️ Bump nonebot-plugin-alconna from 0.30.3 to 0.30.6 (#179) Bumps [nonebot-plugin-alconna](https://github.com/nonebot/plugin-alconna) from 0.30.3 to 0.30.6. - [Release notes](https://github.com/nonebot/plugin-alconna/releases) - [Commits](https://github.com/nonebot/plugin-alconna/compare/v0.30.3...v0.30.6) --- 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> * 🩹 防止在使用其他命令的时候意外触发 * 🚧 暂时去除记录器 待重构 * 🐛 修正输出信息格式 * ✨ TOP 基础查询功能适配所有平台 * ⬆️ Bump ruff from 0.1.2 to 0.1.3 (#180) * ⬆️ Bump nonebot-plugin-alconna from 0.30.6 to 0.30.7 (#181) * ✨ TOS 基础查询功能适配所有平台 * 🐛 修复一些输出消息问题 * 🔥 删除一些不需要的常量 * 🔥 把手搓的解析器爆了 * 🏷️ 添加一个不知道有什么用的类型注释 * 🎨 从 black 迁移 到 ruff format ➖ 删除开发依赖 black * ✨ 启用 pyupgrade 规则 * ✨ 启用 flake8-2020 规则 * ✨ 启用 flake8-annotations 规则 * ✨ 启用 flake8-async 规则 * ✨ 启用 flake8-bandit 规则 * ✨ 启用 flake8-blind-except 规则 * ✨ 启用 flake8-boolean-trap 规则 * ✨ 启用 flake8-builtins 规则 * ✨ 启用 flake8-datetimez 规则 * ✨ 启用 flake8-future-annotations 规则 * ✨ 启用 flake8-implicit-str-concat 规则 * ✨ 启用 flake8-pie 规则 * ✨ 启用 flake8-print 规则 * ✨ 启用 flake8-raise 规则 * ✨ 启用 flake8-return 规则 * ✨ 启用 flake8-simplify 规则 * ✨ 启用 flake8-use-pathlib 规则 * ✨ 启用 pandas-vet 规则 * ✨ 启用 tryceratops 规则 * ✨ 启用 flynt 规则 * ✨ 启用 Perflint 规则 * ⬆️ Bump nonebot-plugin-alconna from 0.30.7 to 0.31.0 (#183) Bumps [nonebot-plugin-alconna](https://github.com/nonebot/plugin-alconna) from 0.30.7 to 0.31.0. - [Release notes](https://github.com/nonebot/plugin-alconna/releases) - [Commits](https://github.com/nonebot/plugin-alconna/compare/v0.30.7...v0.31.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> * ⬆️ Bump nonebot-plugin-orm from 0.3.0 to 0.4.0 (#182) * ⬆️ Bump nonebot-plugin-orm from 0.4.0 to 0.4.1 (#186) Bumps [nonebot-plugin-orm](https://github.com/nonebot/plugin-orm) from 0.4.0 to 0.4.1. - [Release notes](https://github.com/nonebot/plugin-orm/releases) - [Commits](https://github.com/nonebot/plugin-orm/compare/v0.4.0...v0.4.1) --- 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> * ⬆️ Bump nonebot2 from 2.1.1 to 2.1.2 (#185) Bumps [nonebot2](https://github.com/nonebot/nonebot2) from 2.1.1 to 2.1.2. - [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.1...v2.1.2) --- updated-dependencies: - dependency-name: nonebot2 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> * ⬆️ Bump nonebot-plugin-alconna from 0.31.0 to 0.31.3 (#187) Bumps [nonebot-plugin-alconna](https://github.com/nonebot/plugin-alconna) from 0.31.0 to 0.31.3. - [Release notes](https://github.com/nonebot/plugin-alconna/releases) - [Commits](https://github.com/nonebot/plugin-alconna/compare/v0.31.0...v0.31.3) --- updated-dependencies: - dependency-name: nonebot-plugin-alconna dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * 🐛 修复拼接 url 的 bug * ⬆️ Bump nonebot-plugin-orm from 0.4.1 to 0.5.0 (#189) Bumps [nonebot-plugin-orm](https://github.com/nonebot/plugin-orm) from 0.4.1 to 0.5.0. - [Release notes](https://github.com/nonebot/plugin-orm/releases) - [Commits](https://github.com/nonebot/plugin-orm/compare/v0.4.1...v0.5.0) --- 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> * ⬆️ Bump nonebot-plugin-alconna from 0.31.3 to 0.31.7 (#191) Bumps [nonebot-plugin-alconna](https://github.com/nonebot/plugin-alconna) from 0.31.3 to 0.31.7. - [Release notes](https://github.com/nonebot/plugin-alconna/releases) - [Commits](https://github.com/nonebot/plugin-alconna/compare/v0.31.3...v0.31.7) --- 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> * ⬆️ Bump ruff from 0.1.3 to 0.1.4 (#190) * ⬆️ Bump httpx from 0.25.0 to 0.25.1 (#188) * ✨ 绑定适配所有平台 * ⬆️ Bump nonebot-plugin-alconna from 0.31.7 to 0.32.0 (#192) Bumps [nonebot-plugin-alconna](https://github.com/nonebot/plugin-alconna) from 0.31.7 to 0.32.0. - [Release notes](https://github.com/nonebot/plugin-alconna/releases) - [Commits](https://github.com/nonebot/plugin-alconna/compare/v0.31.7...v0.32.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> * ♻️ 重构 recorder * ➕ 添加依赖 nonebot-plugin-apscheduler * ⬆️ Bump mypy from 1.6.1 to 1.7.0 (#194) * ⬆️ Bump ruff from 0.1.4 to 0.1.5 (#193) * ✨ 将数据库内存储时间时区切换为UTC * ✨ 添加 iorank 指令 * 🎨 将行长限制改为120 * 🥚 :fkosk: * 🗃️ 迁移旧版本 sqlite 中的数据 * 🚨 添加 type: ignore * ✨ 更新 PluginMetadata * ➖ 移除所有 nonebot-adapter 依赖 * ➖ 移除依赖 aiohttp * ➖ 移除依赖 asyncio (为什么会有这个) * ➕ 添加开发依赖 nonebot-adapter-onebot * ➕ 添加开发依赖 nonebot-adapter-satori * 📝 更新 readme * 🔖 1.0.0.a1 * 🔒️ 修复 Incomplete URL substring sanitization * 🔒️ 修复 Incomplete URL substring sanitization --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
@@ -1 +1,22 @@
|
||||
from . import game_data_processor
|
||||
from nonebot import require
|
||||
from nonebot.plugin import PluginMetadata
|
||||
|
||||
require('nonebot_plugin_localstore')
|
||||
require('nonebot_plugin_orm')
|
||||
require('nonebot_plugin_alconna')
|
||||
require('nonebot_plugin_apscheduler')
|
||||
|
||||
from .config.config import migrations # noqa: E402
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name='Tetris Stats',
|
||||
description='一个用于查询 Tetris 相关游戏玩家数据的插件',
|
||||
usage='发送 {游戏名} --help 查询使用方法',
|
||||
type='application',
|
||||
homepage='https://github.com/shoucandanghehe/nonebot-plugin-tetris-stats',
|
||||
extra={
|
||||
'orm_version_location': migrations,
|
||||
},
|
||||
)
|
||||
|
||||
from . import game_data_processor # noqa: F401, E402
|
||||
|
||||
14
nonebot_plugin_tetris_stats/config/config.py
Normal file
14
nonebot_plugin_tetris_stats/config/config.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from pathlib import Path
|
||||
|
||||
from nonebot_plugin_localstore import get_cache_dir # type: ignore[import-untyped]
|
||||
from pydantic import BaseModel
|
||||
|
||||
from . import migrations # noqa: F401
|
||||
|
||||
CACHE_PATH: Path = get_cache_dir('nonebot_plugin_tetris_stats')
|
||||
|
||||
|
||||
class Config(BaseModel):
|
||||
"""配置类"""
|
||||
|
||||
db_url: str = 'sqlite://data/nonebot_plugin_tetris_stats/data.db'
|
||||
@@ -0,0 +1,139 @@
|
||||
"""init db
|
||||
|
||||
迁移 ID: 9866f53ce44f
|
||||
父迁移:
|
||||
创建时间: 2023-11-11 16:24:11.826667
|
||||
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = '9866f53ce44f'
|
||||
down_revision: str | Sequence[str] | None = None
|
||||
branch_labels: str | Sequence[str] | None = ('nonebot_plugin_tetris_stats',)
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade(name: str = '') -> None:
|
||||
if name:
|
||||
return
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
'nonebot_plugin_tetris_stats_bind',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('chat_platform', sa.String(length=32), nullable=False),
|
||||
sa.Column('chat_account', sa.String(), nullable=False),
|
||||
sa.Column('game_platform', sa.String(length=32), nullable=False),
|
||||
sa.Column('game_account', sa.String(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id', name=op.f('pk_nonebot_plugin_tetris_stats_bind')),
|
||||
)
|
||||
with op.batch_alter_table('nonebot_plugin_tetris_stats_bind', schema=None) as batch_op:
|
||||
batch_op.create_index(
|
||||
batch_op.f('ix_nonebot_plugin_tetris_stats_bind_chat_account'),
|
||||
['chat_account'],
|
||||
unique=False,
|
||||
)
|
||||
batch_op.create_index(
|
||||
batch_op.f('ix_nonebot_plugin_tetris_stats_bind_chat_platform'),
|
||||
['chat_platform'],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
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', sa.PickleType(), nullable=False),
|
||||
sa.Column('processed_data', sa.PickleType(), 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,
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
'nonebot_plugin_tetris_stats_iorank',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('rank', sa.String(length=2), nullable=False),
|
||||
sa.Column('tr_line', sa.Float(), nullable=False),
|
||||
sa.Column('player_count', sa.Integer(), nullable=False),
|
||||
sa.Column('low_pps', sa.JSON(), nullable=False),
|
||||
sa.Column('low_apm', sa.JSON(), nullable=False),
|
||||
sa.Column('low_vs', sa.JSON(), nullable=False),
|
||||
sa.Column('avg_pps', sa.Float(), nullable=False),
|
||||
sa.Column('avg_apm', sa.Float(), nullable=False),
|
||||
sa.Column('avg_vs', sa.Float(), nullable=False),
|
||||
sa.Column('high_pps', sa.JSON(), nullable=False),
|
||||
sa.Column('high_apm', sa.JSON(), nullable=False),
|
||||
sa.Column('high_vs', sa.JSON(), nullable=False),
|
||||
sa.Column('create_time', sa.DateTime(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id', name=op.f('pk_nonebot_plugin_tetris_stats_iorank')),
|
||||
)
|
||||
with op.batch_alter_table('nonebot_plugin_tetris_stats_iorank', schema=None) as batch_op:
|
||||
batch_op.create_index(
|
||||
batch_op.f('ix_nonebot_plugin_tetris_stats_iorank_create_time'),
|
||||
['create_time'],
|
||||
unique=False,
|
||||
)
|
||||
batch_op.create_index(
|
||||
batch_op.f('ix_nonebot_plugin_tetris_stats_iorank_rank'),
|
||||
['rank'],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade(name: str = '') -> None:
|
||||
if name:
|
||||
return
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('nonebot_plugin_tetris_stats_iorank', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_nonebot_plugin_tetris_stats_iorank_rank'))
|
||||
batch_op.drop_index(batch_op.f('ix_nonebot_plugin_tetris_stats_iorank_create_time'))
|
||||
|
||||
op.drop_table('nonebot_plugin_tetris_stats_iorank')
|
||||
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')
|
||||
with op.batch_alter_table('nonebot_plugin_tetris_stats_bind', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_nonebot_plugin_tetris_stats_bind_chat_platform'))
|
||||
batch_op.drop_index(batch_op.f('ix_nonebot_plugin_tetris_stats_bind_chat_account'))
|
||||
|
||||
op.drop_table('nonebot_plugin_tetris_stats_bind')
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,92 @@
|
||||
"""Merge old db
|
||||
|
||||
迁移 ID: 9cd1647db502
|
||||
父迁移: 9866f53ce44f
|
||||
创建时间: 2023-11-11 16:51:30.718277
|
||||
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from pathlib import Path
|
||||
from shutil import copyfile
|
||||
|
||||
from alembic import op
|
||||
from nonebot import get_driver
|
||||
from nonebot.log import logger
|
||||
from sqlalchemy import Connection, create_engine, inspect, text
|
||||
from sqlalchemy.ext.automap import automap_base
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
revision: str = '9cd1647db502'
|
||||
down_revision: str | Sequence[str] | None = '9866f53ce44f'
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
driver = get_driver()
|
||||
config = driver.config
|
||||
|
||||
|
||||
def migrate_old_data(connection: Connection) -> None:
|
||||
Base = automap_base() # noqa: N806
|
||||
Base.prepare(autoload_with=op.get_bind())
|
||||
Bind = Base.classes.nonebot_plugin_tetris_stats_bind # noqa: N806
|
||||
|
||||
def non_empty(obj: str) -> bool:
|
||||
if obj != '' and not obj.isspace():
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_int(obj: int | str) -> bool:
|
||||
if isinstance(obj, int) or obj.isdigit():
|
||||
return True
|
||||
return False
|
||||
|
||||
bind_list = [
|
||||
Bind(chat_platform='OneBot V11', chat_account=int(row.QQ), game_platform='IO', game_account=row.USER)
|
||||
for row in connection.execute(text('select QQ, USER from IOBIND;'))
|
||||
if is_int(row.QQ) and non_empty(row.USER)
|
||||
]
|
||||
bind_list.extend(
|
||||
[
|
||||
Bind(chat_platform='OneBot V11', chat_account=int(row.QQ), game_platform='TOP', game_account=row.USER)
|
||||
for row in connection.execute(text('select QQ, USER from TOPBIND;'))
|
||||
if is_int(row.QQ) and non_empty(row.USER)
|
||||
]
|
||||
)
|
||||
with Session(op.get_bind()) as session:
|
||||
session.add_all(bind_list)
|
||||
session.commit()
|
||||
logger.success('nonebot_plugin_tetris_stats: 迁移完成')
|
||||
|
||||
|
||||
def upgrade(name: str = '') -> None:
|
||||
if name:
|
||||
return
|
||||
try:
|
||||
db_path = Path(config.db_path)
|
||||
except AttributeError:
|
||||
db_path = Path('data/nonebot_plugin_tetris_stats/data.db')
|
||||
if db_path.exists() is False:
|
||||
logger.warning('nonebot_plugin_tetris_stats: 未发现老版本的数据')
|
||||
logger.success('nonebot_plugin_tetris_stats: 跳过迁移')
|
||||
return
|
||||
copyfile(db_path, db_path.parent / 'data.db.bak')
|
||||
engine = create_engine(f'sqlite:///{db_path.absolute()!s}')
|
||||
with engine.connect() as connection:
|
||||
tables = inspect(connection).get_table_names()
|
||||
if 'IOBIND' not in tables or 'TOPBIND' not in tables:
|
||||
logger.warning('nonebot_plugin_tetris_stats: 未发现老版本的数据')
|
||||
logger.success('nonebot_plugin_tetris_stats: 跳过迁移')
|
||||
return
|
||||
if 'IORANK' not in tables:
|
||||
logger.warning('nonebot_plugin_tetris_stats: 发现过早版本的数据, 请先更新到 0.4.4 版本')
|
||||
raise RuntimeError('nonebot_plugin_tetris_stats: 请先安装 0.4.4 版本完成迁移之后再升级')
|
||||
logger.info('nonebot_plugin_tetris_stats: 发现来自老版本的数据, 正在迁移...')
|
||||
migrate_old_data(connection)
|
||||
db_path.unlink()
|
||||
|
||||
|
||||
def downgrade(name: str = '') -> None:
|
||||
if name:
|
||||
return
|
||||
50
nonebot_plugin_tetris_stats/db/__init__.py
Normal file
50
nonebot_plugin_tetris_stats/db/__init__.py
Normal file
@@ -0,0 +1,50 @@
|
||||
from nonebot_plugin_orm import AsyncSession
|
||||
from sqlalchemy import select
|
||||
|
||||
from ..utils.typing import GameType
|
||||
from .models import Bind
|
||||
|
||||
|
||||
async def query_bind_info(
|
||||
session: AsyncSession,
|
||||
chat_platform: str,
|
||||
chat_account: str,
|
||||
game_platform: GameType,
|
||||
) -> Bind | None:
|
||||
return (
|
||||
await session.scalars(
|
||||
select(Bind)
|
||||
.where(Bind.chat_platform == chat_platform)
|
||||
.where(Bind.chat_account == chat_account)
|
||||
.where(Bind.game_platform == game_platform)
|
||||
)
|
||||
).one_or_none()
|
||||
|
||||
|
||||
async def create_or_update_bind(
|
||||
session: AsyncSession,
|
||||
chat_platform: str,
|
||||
chat_account: str,
|
||||
game_platform: GameType,
|
||||
game_account: str,
|
||||
) -> str:
|
||||
bind = await query_bind_info(
|
||||
session=session,
|
||||
chat_platform=chat_platform,
|
||||
chat_account=chat_account,
|
||||
game_platform=game_platform,
|
||||
)
|
||||
if bind is None:
|
||||
bind = Bind(
|
||||
chat_platform=chat_platform,
|
||||
chat_account=chat_account,
|
||||
game_platform=game_platform,
|
||||
game_account=game_account,
|
||||
)
|
||||
session.add(bind)
|
||||
message = '绑定成功'
|
||||
else:
|
||||
bind.game_account = game_account
|
||||
message = '更新绑定成功'
|
||||
await session.commit()
|
||||
return message
|
||||
33
nonebot_plugin_tetris_stats/db/models.py
Normal file
33
nonebot_plugin_tetris_stats/db/models.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from datetime import datetime
|
||||
|
||||
from nonebot.adapters import Message
|
||||
from nonebot_plugin_orm import Model
|
||||
from sqlalchemy import JSON, DateTime, PickleType, String
|
||||
from sqlalchemy.orm import Mapped, MappedAsDataclass, mapped_column
|
||||
|
||||
from ..game_data_processor import ProcessedData, User
|
||||
from ..utils.typing import CommandType, GameType
|
||||
|
||||
|
||||
class Bind(MappedAsDataclass, Model):
|
||||
id: Mapped[int] = mapped_column(init=False, primary_key=True)
|
||||
chat_platform: Mapped[str] = mapped_column(String(32), index=True)
|
||||
chat_account: Mapped[str] = mapped_column(index=True)
|
||||
game_platform: Mapped[GameType] = mapped_column(String(32))
|
||||
game_account: Mapped[str]
|
||||
|
||||
|
||||
class HistoricalData(MappedAsDataclass, Model):
|
||||
id: Mapped[int] = mapped_column(init=False, primary_key=True)
|
||||
trigger_time: Mapped[datetime] = mapped_column(DateTime)
|
||||
bot_platform: Mapped[str | None] = mapped_column(String(32))
|
||||
bot_account: Mapped[str | None]
|
||||
source_type: Mapped[str | None] = mapped_column(String(32), index=True)
|
||||
source_account: Mapped[str | None] = mapped_column(index=True)
|
||||
message: Mapped[Message | None] = mapped_column(PickleType)
|
||||
game_platform: Mapped[GameType] = mapped_column(String(32), index=True, init=False)
|
||||
command_type: Mapped[CommandType] = mapped_column(String(16), index=True, init=False)
|
||||
command_args: Mapped[list[str]] = mapped_column(JSON, init=False)
|
||||
game_user: Mapped[User] = mapped_column(PickleType, init=False)
|
||||
processed_data: Mapped[ProcessedData] = mapped_column(PickleType, init=False)
|
||||
finish_time: Mapped[datetime] = mapped_column(DateTime, init=False)
|
||||
@@ -1 +1,82 @@
|
||||
from . import io_data_processor, top_data_processor, tos_data_processor
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from ..utils.typing import CommandType, GameType
|
||||
|
||||
|
||||
@dataclass
|
||||
class User:
|
||||
"""游戏用户"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class RawResponse:
|
||||
"""原始请求数据"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProcessedData:
|
||||
"""处理/验证后的数据"""
|
||||
|
||||
|
||||
from ..utils.recorder import Recorder # noqa: E402 避免循环导入
|
||||
|
||||
|
||||
class Processor(ABC):
|
||||
event_id: int
|
||||
command_type: CommandType
|
||||
command_args: list[str]
|
||||
user: User
|
||||
raw_response: RawResponse
|
||||
processed_data: ProcessedData
|
||||
|
||||
@abstractmethod
|
||||
def __init__(
|
||||
self,
|
||||
event_id: int,
|
||||
user: User,
|
||||
command_args: list[str],
|
||||
) -> None:
|
||||
self.event_id = event_id
|
||||
self.user = user
|
||||
self.command_args = command_args
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def game_platform(self) -> GameType:
|
||||
"""游戏平台"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
async def handle_bind(self, platform: str, account: str) -> str:
|
||||
"""处理绑定消息"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
async def handle_query(self) -> str:
|
||||
"""处理查询消息"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
async def generate_message(self) -> str:
|
||||
"""生成消息"""
|
||||
raise NotImplementedError
|
||||
|
||||
def __del__(self) -> None:
|
||||
finish_time = datetime.now(tz=UTC)
|
||||
historical_data = Recorder.get_historical_data(self.event_id)
|
||||
historical_data.game_platform = self.game_platform
|
||||
historical_data.command_type = self.command_type
|
||||
historical_data.command_args = self.command_args
|
||||
historical_data.game_user = self.user
|
||||
historical_data.processed_data = self.processed_data
|
||||
historical_data.finish_time = finish_time
|
||||
Recorder.update_historical_data(self.event_id, historical_data)
|
||||
|
||||
|
||||
from . import ( # noqa: F401, E402
|
||||
io_data_processor,
|
||||
top_data_processor,
|
||||
tos_data_processor,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
BIND_COMMAND: list[str] = ['绑定', 'bind']
|
||||
QUERY_COMMAND: list[str] = ['查', '查询', 'query', 'stats']
|
||||
@@ -1,40 +1,187 @@
|
||||
from re import I
|
||||
from datetime import timedelta
|
||||
|
||||
from nonebot import on_regex
|
||||
from nonebot.adapters.onebot.v11 import GROUP, MessageEvent
|
||||
from arclet.alconna import Alconna, Arg, ArgFlag, Args, CommandMeta, Option
|
||||
from nonebot.adapters import Bot, Event
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot_plugin_alconna import AlcMatches, At, on_alconna
|
||||
from nonebot_plugin_orm import get_session
|
||||
from sqlalchemy import select
|
||||
|
||||
from .processor import Processor
|
||||
from ...db import query_bind_info
|
||||
from ...utils.exception import MessageFormatError, NeedCatchError
|
||||
from ...utils.metrics import get_metrics
|
||||
from ...utils.platform import get_platform
|
||||
from ...utils.typing import Me
|
||||
from ..constant import BIND_COMMAND, QUERY_COMMAND
|
||||
from .constant import GAME_TYPE
|
||||
from .model import IORank
|
||||
from .processor import Processor, User, check_rank_data, identify_user_info
|
||||
from .typing import Rank
|
||||
|
||||
IOBind = on_regex(pattern=r'^io绑定|^iobind', flags=I, permission=GROUP)
|
||||
IOStats = on_regex(pattern=r'^io查|^iostats', flags=I, permission=GROUP)
|
||||
IORank = on_regex(pattern=r'^io段位|^iorank', flags=I, permission=GROUP)
|
||||
alc = on_alconna(
|
||||
Alconna(
|
||||
'io',
|
||||
Option(
|
||||
BIND_COMMAND[0],
|
||||
Args(
|
||||
Arg(
|
||||
'account',
|
||||
identify_user_info,
|
||||
notice='IO 用户名 / ID',
|
||||
flags=[ArgFlag.HIDDEN],
|
||||
)
|
||||
),
|
||||
alias=BIND_COMMAND[1:],
|
||||
compact=True,
|
||||
dest='bind',
|
||||
help_text='绑定 IO 账号',
|
||||
),
|
||||
Option(
|
||||
QUERY_COMMAND[0],
|
||||
Args(
|
||||
Arg(
|
||||
'target',
|
||||
At | Me,
|
||||
notice='@想要查询的人 | 自己',
|
||||
flags=[ArgFlag.HIDDEN, ArgFlag.OPTIONAL],
|
||||
),
|
||||
Arg(
|
||||
'account',
|
||||
identify_user_info,
|
||||
notice='IO 用户名 / ID',
|
||||
flags=[ArgFlag.HIDDEN, ArgFlag.OPTIONAL],
|
||||
),
|
||||
),
|
||||
alias=QUERY_COMMAND[1:],
|
||||
compact=True,
|
||||
dest='query',
|
||||
help_text='查询 IO 游戏信息',
|
||||
),
|
||||
Option(
|
||||
'rank',
|
||||
Args(Arg('rank', Rank, notice='IO 段位')),
|
||||
alias={'Rank', 'RANK', '段位'},
|
||||
compact=True,
|
||||
dest='rank',
|
||||
help_text='查询 IO 段位信息',
|
||||
),
|
||||
meta=CommandMeta(
|
||||
description='查询 TETR.IO 的信息',
|
||||
example='io绑定scdhh\nio查我\niorankx',
|
||||
compact=True,
|
||||
fuzzy_match=True,
|
||||
),
|
||||
),
|
||||
skip_for_unmatch=False,
|
||||
auto_send_output=True,
|
||||
aliases={'IO'},
|
||||
)
|
||||
|
||||
@IOBind.handle()
|
||||
async def _(event: MessageEvent, matcher: Matcher):
|
||||
await matcher.finish(
|
||||
await Processor.handle_bind(
|
||||
message=event.raw_message,
|
||||
qq_number=event.sender.user_id
|
||||
)
|
||||
alc.shortcut('fkosk', {'command': 'io查', 'args': ['我']})
|
||||
|
||||
|
||||
@alc.assign('bind')
|
||||
async def _(bot: Bot, event: Event, matcher: Matcher, account: User):
|
||||
proc = Processor(
|
||||
event_id=id(event),
|
||||
user=account,
|
||||
command_args=[],
|
||||
)
|
||||
try:
|
||||
await matcher.finish(await proc.handle_bind(platform=get_platform(bot), account=event.get_user_id()))
|
||||
except NeedCatchError as e:
|
||||
await matcher.finish(str(e))
|
||||
|
||||
|
||||
@IOStats.handle()
|
||||
async def _(event: MessageEvent, matcher: Matcher):
|
||||
if event.is_tome():
|
||||
await matcher.finish('不能查询bot的信息')
|
||||
await matcher.finish(
|
||||
await Processor.handle_query(
|
||||
message=event.raw_message,
|
||||
qq_number=event.sender.user_id
|
||||
@alc.assign('query')
|
||||
async def _(bot: Bot, event: Event, matcher: Matcher, target: At | Me):
|
||||
async with get_session() as session:
|
||||
bind = await query_bind_info(
|
||||
session=session,
|
||||
chat_platform=get_platform(bot),
|
||||
chat_account=(target.target if isinstance(target, At) else event.get_user_id()),
|
||||
game_platform=GAME_TYPE,
|
||||
)
|
||||
if bind is None:
|
||||
await matcher.finish('未查询到绑定信息')
|
||||
message = '* 由于无法验证绑定信息, 不能保证查询到的用户为本人\n'
|
||||
proc = Processor(
|
||||
event_id=id(event),
|
||||
user=User(ID=bind.game_account),
|
||||
command_args=[],
|
||||
)
|
||||
try:
|
||||
await matcher.finish(message + await proc.handle_query())
|
||||
except NeedCatchError as e:
|
||||
await matcher.finish(str(e))
|
||||
|
||||
@IORank.handle()
|
||||
async def _(event: MessageEvent, matcher: Matcher):
|
||||
await matcher.finish(
|
||||
await Processor.handle_rank(
|
||||
message=event.raw_message
|
||||
)
|
||||
|
||||
@alc.assign('query')
|
||||
async def _(event: Event, matcher: Matcher, account: User):
|
||||
proc = Processor(
|
||||
event_id=id(event),
|
||||
user=account,
|
||||
command_args=[],
|
||||
)
|
||||
try:
|
||||
await matcher.finish(await proc.handle_query())
|
||||
except NeedCatchError as e:
|
||||
await matcher.finish(str(e))
|
||||
|
||||
|
||||
@alc.assign('rank')
|
||||
async def _(event: Event, matcher: Matcher, rank: Rank):
|
||||
if rank == 'z':
|
||||
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:
|
||||
data = (
|
||||
await session.scalars(select(IORank).where(IORank.rank == rank).order_by(IORank.id.desc()).limit(5))
|
||||
).all()
|
||||
latest_data = data[0]
|
||||
message = f'{rank.upper()} 段 分数线 {latest_data.tr_line:.2f} TR, {latest_data.player_count} 名玩家\n'
|
||||
if len(data) > 1:
|
||||
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 "→"}'
|
||||
else:
|
||||
message += '暂无对比数据'
|
||||
avg = get_metrics(pps=latest_data.avg_pps, apm=latest_data.avg_apm, vs=latest_data.avg_vs)
|
||||
low_pps = get_metrics(pps=latest_data.low_pps[1])
|
||||
low_vs = get_metrics(vs=latest_data.low_vs[1])
|
||||
max_pps = get_metrics(pps=latest_data.high_pps[1])
|
||||
max_vs = get_metrics(vs=latest_data.high_vs[1])
|
||||
message += (
|
||||
'\n'
|
||||
'平均数据:\n'
|
||||
f"L'PM: {avg.lpm} ( {avg.pps} pps )\n"
|
||||
f'APM: {avg.apm} ( x{avg.apl} )\n'
|
||||
f'ADPM: {avg.adpm} ( x{avg.adpl} ) ( {avg.vs}vs )\n'
|
||||
'\n'
|
||||
'最低数据:\n'
|
||||
f"L'PM: {low_pps.lpm} ( {low_pps.pps} pps ) By: {latest_data.low_pps[0]['name'].upper()}\n"
|
||||
f'APM: {latest_data.low_apm[1]} By: {latest_data.low_apm[0]["name"].upper()}\n'
|
||||
f'ADPM: {low_vs.adpm} ( {low_vs.vs}vs ) By: {latest_data.low_vs[0]["name"].upper()}\n'
|
||||
'\n'
|
||||
'最高数据:\n'
|
||||
f"L'PM: {max_pps.lpm} ( {max_pps.pps} pps ) By: {latest_data.high_pps[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'
|
||||
'\n'
|
||||
f'数据更新时间: {(latest_data.create_time+timedelta(hours=8)).strftime("%Y-%m-%d %H:%M:%S")}'
|
||||
)
|
||||
await matcher.finish(message)
|
||||
|
||||
|
||||
@alc.handle()
|
||||
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"查看帮助'
|
||||
)
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
from ...utils.typing import GameType
|
||||
from .typing import Rank
|
||||
|
||||
GAME_TYPE: GameType = 'IO'
|
||||
BASE_URL = 'https://ch.tetr.io/api/'
|
||||
RANK_PERCENTILE: dict[Rank, float] = {
|
||||
'x': 1,
|
||||
'u': 5,
|
||||
'ss': 11,
|
||||
's+': 17,
|
||||
's': 23,
|
||||
's-': 30,
|
||||
'a+': 38,
|
||||
'a': 46,
|
||||
'a-': 54,
|
||||
'b+': 62,
|
||||
'b': 70,
|
||||
'b-': 78,
|
||||
'c+': 84,
|
||||
'c': 90,
|
||||
'c-': 95,
|
||||
'd+': 97.5,
|
||||
'd': 100,
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from nonebot_plugin_orm import Model
|
||||
from sqlalchemy import JSON, DateTime, String
|
||||
from sqlalchemy.orm import Mapped, MappedAsDataclass, mapped_column
|
||||
|
||||
from .typing import Rank
|
||||
|
||||
|
||||
class IORank(MappedAsDataclass, Model):
|
||||
id: Mapped[int] = mapped_column(init=False, primary_key=True)
|
||||
rank: Mapped[Rank] = mapped_column(String(2), index=True)
|
||||
tr_line: Mapped[float]
|
||||
player_count: Mapped[int]
|
||||
low_pps: Mapped[tuple[dict[str, str], float]] = mapped_column(JSON)
|
||||
low_apm: Mapped[tuple[dict[str, str], float]] = mapped_column(JSON)
|
||||
low_vs: Mapped[tuple[dict[str, str], float]] = mapped_column(JSON)
|
||||
avg_pps: Mapped[float]
|
||||
avg_apm: Mapped[float]
|
||||
avg_vs: Mapped[float]
|
||||
high_pps: Mapped[tuple[dict[str, str], float]] = mapped_column(JSON)
|
||||
high_apm: Mapped[tuple[dict[str, str], float]] = mapped_column(JSON)
|
||||
high_vs: Mapped[tuple[dict[str, str], float]] = mapped_column(JSON)
|
||||
create_time: Mapped[datetime] = mapped_column(
|
||||
DateTime,
|
||||
default=lambda: datetime.now(tz=UTC),
|
||||
index=True,
|
||||
init=False,
|
||||
)
|
||||
@@ -1,300 +1,254 @@
|
||||
import math
|
||||
from asyncio import gather
|
||||
from typing import Any
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable
|
||||
from dataclasses import asdict, dataclass
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from math import floor
|
||||
from re import match
|
||||
from statistics import mean
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot import get_driver
|
||||
from nonebot_plugin_apscheduler import scheduler # type: ignore[import-untyped]
|
||||
from nonebot_plugin_orm import get_session
|
||||
from pydantic import parse_raw_as
|
||||
from sqlalchemy import select
|
||||
|
||||
from ...utils.database import DataBase
|
||||
from ...utils.message_analyzer import (
|
||||
handle_bind_message,
|
||||
handle_rank_message,
|
||||
handle_stats_query_message,
|
||||
from ...db import create_or_update_bind
|
||||
from ...utils.exception import MessageFormatError, RequestError, WhatTheFuckError
|
||||
from ...utils.request import Request, splice_url
|
||||
from ...utils.typing import GameType
|
||||
from .. import ProcessedData as ProcessedDataMeta
|
||||
from .. import Processor as ProcessorMeta
|
||||
from .. import RawResponse as RawResponseMeta
|
||||
from .. import User as UserMeta
|
||||
from .constant import BASE_URL, GAME_TYPE, RANK_PERCENTILE
|
||||
from .model import IORank
|
||||
from .schemas.league_all import FailedModel as LeagueAllFailed
|
||||
from .schemas.league_all import LeagueAll
|
||||
from .schemas.league_all import User as LeagueAllUser
|
||||
from .schemas.user_info import FailedModel as InfoFailed
|
||||
from .schemas.user_info import (
|
||||
NeverPlayedLeague,
|
||||
NeverRatedLeague,
|
||||
UserInfo,
|
||||
)
|
||||
from .request import Request
|
||||
from .schemas.user_info import SuccessModel as InfoSuccess
|
||||
from .schemas.user_records import FailedModel as RecordsFailed
|
||||
from .schemas.user_records import SoloRecord, UserRecords
|
||||
from .schemas.user_records import SuccessModel as RecordsSuccess
|
||||
from .typing import Rank
|
||||
|
||||
driver = get_driver()
|
||||
|
||||
|
||||
class Processor:
|
||||
@classmethod
|
||||
async def handle_bind(cls, message: str, qq_number: int | None) -> str:
|
||||
@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:
|
||||
if match(r'^[a-f0-9]{24}$', info):
|
||||
return User(ID=info)
|
||||
if match(r'^[a-zA-Z0-9_-]{3,16}$', info):
|
||||
return User(name=info.lower())
|
||||
return MessageFormatError('用户名/ID不合法')
|
||||
|
||||
|
||||
class Processor(ProcessorMeta):
|
||||
user: User
|
||||
raw_response: RawResponse
|
||||
processed_data: ProcessedData
|
||||
|
||||
def __init__(self, event_id: int, user: User, command_args: list[str]) -> None:
|
||||
super().__init__(event_id, user, command_args)
|
||||
self.raw_response = RawResponse()
|
||||
self.processed_data = ProcessedData()
|
||||
|
||||
@property
|
||||
def game_platform(self) -> GameType:
|
||||
return GAME_TYPE
|
||||
|
||||
async def handle_bind(self, platform: str, account: str) -> str:
|
||||
"""处理绑定消息"""
|
||||
decoded_message = await handle_bind_message(message=message, game_type='IO')
|
||||
if decoded_message[0] is None:
|
||||
return decoded_message[1][0]
|
||||
if decoded_message[0] == 'ID':
|
||||
user_id_stats = await cls.check_user_id(decoded_message[1][1])
|
||||
if user_id_stats[0] is False:
|
||||
return user_id_stats[1]
|
||||
user_id = decoded_message[1][1]
|
||||
if qq_number is None: # 理论上是不会有None出现的, ide快乐行属于是(
|
||||
logger.error('获取QQ号失败')
|
||||
return '获取QQ号失败'
|
||||
return await DataBase.write_bind_info(
|
||||
qq_number=qq_number, user=user_id, game_type='IO'
|
||||
self.command_type = 'bind'
|
||||
await self.get_user()
|
||||
if self.user.ID is None:
|
||||
raise # FIXME: 不知道怎么才能把这类型给变过来了
|
||||
async with get_session() as session:
|
||||
return await create_or_update_bind(
|
||||
session=session,
|
||||
chat_platform=platform,
|
||||
chat_account=account,
|
||||
game_platform=GAME_TYPE,
|
||||
game_account=self.user.ID,
|
||||
)
|
||||
elif decoded_message[0] == 'Name':
|
||||
user_data = await cls.get_user_data(user_name=decoded_message[1][1])
|
||||
if user_data[0] is False:
|
||||
return '用户信息请求失败'
|
||||
if user_data[1] is False:
|
||||
return f'用户信息请求错误:\n{user_data[2]["error"]}'
|
||||
user_id = await cls.get_user_id(user_data[2])
|
||||
if qq_number is None: # 理论上是不会有None出现的, ide快乐行属于是(
|
||||
logger.error('获取QQ号失败')
|
||||
return '获取QQ号失败'
|
||||
return await DataBase.write_bind_info(
|
||||
qq_number=qq_number, user=user_id, game_type='IO'
|
||||
)
|
||||
logger.error('预期外行为, 请上报GitHub')
|
||||
return '出现预期外行为, 请查看后台信息'
|
||||
|
||||
@classmethod
|
||||
async def handle_rank(cls, message: str):
|
||||
query_rank = await handle_rank_message(message)
|
||||
rank_info = await DataBase.query_rank_info_today(rank=query_rank.lower())
|
||||
|
||||
if rank_info is None:
|
||||
ranks_percentiles = {
|
||||
'x': 1,
|
||||
'u': 5,
|
||||
'ss': 11,
|
||||
's+': 17,
|
||||
's': 23,
|
||||
's-': 30,
|
||||
'a+': 38,
|
||||
'a': 46,
|
||||
'a-': 54,
|
||||
'b+': 62,
|
||||
'b': 70,
|
||||
'b-': 78,
|
||||
'c+': 84,
|
||||
'c': 90,
|
||||
'c-': 95,
|
||||
'd+': 97.5,
|
||||
'd': 100,
|
||||
}
|
||||
|
||||
if query_rank.lower() not in (i for i in ranks_percentiles.keys()):
|
||||
return '未知段位'
|
||||
|
||||
result = await Request.request(
|
||||
'https://ch.tetr.io/api/users/lists/league/all'
|
||||
)
|
||||
users: list = result[2]['data']['users']
|
||||
|
||||
def avg(
|
||||
rank_users: list, column: str, playercount: int | None = None
|
||||
) -> float:
|
||||
return sum(i['league'][column] for i in rank_users) / (
|
||||
playercount or len(rank_users)
|
||||
)
|
||||
|
||||
for rank, percentile in ranks_percentiles.items():
|
||||
offset = math.floor((percentile / 100) * len(users)) - 1
|
||||
tr = users[offset]['league']['rating']
|
||||
|
||||
rank_users = list(filter(lambda x: x['league']['rank'] == rank, users))
|
||||
playercount = len(rank_users)
|
||||
|
||||
avg_apm = avg(rank_users, 'apm', playercount)
|
||||
avg_pps = avg(rank_users, 'pps', playercount)
|
||||
avg_vs = avg(rank_users, 'vs', playercount)
|
||||
|
||||
await DataBase.write_rank_info_today(
|
||||
rank=rank,
|
||||
trline=tr,
|
||||
playercount=playercount,
|
||||
avgapm=avg_apm,
|
||||
avgpps=avg_pps,
|
||||
avgvs=avg_vs,
|
||||
)
|
||||
|
||||
return await Processor.handle_rank(message=message)
|
||||
else:
|
||||
avg_apm = round(rank_info[3], 2)
|
||||
avg_pps = round(rank_info[4], 2)
|
||||
avg_vs = round(rank_info[5], 2)
|
||||
avg_lpm = round((avg_pps * 24), 2)
|
||||
avg_apl = round((avg_apm / avg_lpm), 2)
|
||||
avg_adpm = round((avg_vs * 0.6), 2)
|
||||
avg_adpl = round((avg_adpm / avg_lpm), 2)
|
||||
|
||||
message = f'{query_rank.upper()} 段, 分数线 {round(rank_info[1], 2)} TR, {rank_info[2]} 名玩家'
|
||||
message += f'\n对比昨日趋势: {rank_info[0]}'
|
||||
message += '\n平均数据: '
|
||||
message += f"\nL'PM: {avg_lpm} ( {avg_pps} pps )"
|
||||
message += f'\nAPM: {avg_apm} ( x{avg_apl} )'
|
||||
message += f'\nADPM: {avg_adpm} ( x{avg_adpl} ) ( {avg_vs}vs )'
|
||||
message += '\n'
|
||||
message += f'\n数据更新时间: {rank_info[6]}'
|
||||
|
||||
return message
|
||||
|
||||
@classmethod
|
||||
async def handle_query(cls, message: str, qq_number: int | None):
|
||||
async def handle_query(self) -> str:
|
||||
"""处理查询消息"""
|
||||
decoded_message = await handle_stats_query_message(
|
||||
message=message, game_type='IO'
|
||||
)
|
||||
if decoded_message[0] is None:
|
||||
return decoded_message[1][0]
|
||||
if decoded_message[0] == 'AT': # 在入口处判断是否@bot本身
|
||||
bind_info = await DataBase.query_bind_info(
|
||||
qq_number=decoded_message[1][1], game_type='IO'
|
||||
)
|
||||
if bind_info is None:
|
||||
return '未查询到绑定信息'
|
||||
return f'* 由于无法验证绑定信息, 不能保证查询到的用户为本人\n{await Processor.generate_message(user_id=bind_info)}'
|
||||
if decoded_message[0] == 'ME':
|
||||
if qq_number is None:
|
||||
logger.error('获取QQ号失败')
|
||||
return '获取QQ号失败, 请联系bot主人'
|
||||
bind_info = await DataBase.query_bind_info(
|
||||
qq_number=qq_number, game_type='IO'
|
||||
)
|
||||
if bind_info is None:
|
||||
return '未查询到绑定信息'
|
||||
return f'* 由于无法验证绑定信息, 不能保证查询到的用户为本人\n{await Processor.generate_message(user_id=bind_info)}'
|
||||
if decoded_message[0] == 'ID':
|
||||
return await Processor.generate_message(user_id=decoded_message[1][1])
|
||||
if decoded_message[0] == 'Name':
|
||||
return await Processor.generate_message(user_name=decoded_message[1][1])
|
||||
self.command_type = 'query'
|
||||
await self.get_user()
|
||||
return await self.generate_message()
|
||||
|
||||
@classmethod
|
||||
async def get_user_data(
|
||||
cls, user_name: str | None = None, user_id: str | None = None
|
||||
) -> tuple[bool, bool, dict[str, Any]]:
|
||||
async def get_user(self) -> None:
|
||||
"""
|
||||
用于获取 UserName 和 UserID 的函数
|
||||
"""
|
||||
if self.user.name is None:
|
||||
self.user.name = (await self.get_user_info()).data.user.username
|
||||
if self.user.ID is None:
|
||||
self.user.ID = (await self.get_user_info()).data.user.id
|
||||
|
||||
async def get_user_info(self) -> InfoSuccess:
|
||||
"""获取用户数据"""
|
||||
if user_name is not None and user_id is None:
|
||||
user_data_url = f'https://ch.tetr.io/api/users/{user_name}'
|
||||
elif user_name is None and user_id is not None:
|
||||
user_data_url = f'https://ch.tetr.io/api/users/{user_id}'
|
||||
else:
|
||||
raise ValueError('预期外行为, 请上报GitHub')
|
||||
return await Request.request(user_data_url)
|
||||
if self.processed_data.user_info is None:
|
||||
self.raw_response.user_info = await Request.request(
|
||||
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]
|
||||
if isinstance(user_info, InfoFailed):
|
||||
raise RequestError(f'用户信息请求错误:\n{user_info.error}')
|
||||
self.processed_data.user_info = user_info
|
||||
return self.processed_data.user_info
|
||||
|
||||
@classmethod
|
||||
async def get_solo_data(
|
||||
cls, user_name: str | None = None, user_id: str | None = None
|
||||
) -> tuple[bool, bool, dict[str, Any]]:
|
||||
async def get_user_records(self) -> RecordsSuccess:
|
||||
"""获取Solo数据"""
|
||||
if user_name is not None and user_id is None:
|
||||
user_solo_url = f'https://ch.tetr.io/api/users/{user_name}/records'
|
||||
elif user_name is None and user_id is not None:
|
||||
user_solo_url = f'https://ch.tetr.io/api/users/{user_id}/records'
|
||||
else:
|
||||
raise ValueError('预期外行为, 请上报GitHub')
|
||||
return await Request.request(user_solo_url)
|
||||
|
||||
@classmethod
|
||||
async def get_user_id(cls, user_data: dict) -> str:
|
||||
"""获取用户ID"""
|
||||
return user_data['data']['user']['_id']
|
||||
|
||||
@classmethod
|
||||
async def check_user_id(cls, user_id: str) -> tuple[bool, str]:
|
||||
"""检查用户ID是否有效 返回值为tuple[bool, message]"""
|
||||
user_data = await cls.get_user_data(user_id=user_id)
|
||||
if user_data[0] is False:
|
||||
return False, '用户信息请求失败'
|
||||
if user_data[1] is False:
|
||||
return False, f'用户信息请求错误:\n{user_data[2]["error"]}'
|
||||
if user_id == user_data[2]['data']['user']['_id']:
|
||||
return True, ''
|
||||
raise ValueError('服务器返回的userID和用户提供的不一致, 这种情况理论上不应该发生, 以防万一还是写一下(x')
|
||||
|
||||
@classmethod
|
||||
async def get_league_stats(cls, user_data: dict) -> dict[str, Any]:
|
||||
"""获取排位统计数据"""
|
||||
league = user_data['data']['user']['league']
|
||||
league_stats: dict[str, Any] = {}
|
||||
if league['gamesplayed'] != 0:
|
||||
league_stats['PPS'] = league['pps']
|
||||
league_stats['APM'] = league['apm']
|
||||
league_stats['VS'] = 0 if league['vs'] is None else league['vs']
|
||||
league_stats['Rank'] = (
|
||||
'Z' if league['rank'] == 'z' else league['rank'].upper()
|
||||
)
|
||||
if league['rating'] == -1:
|
||||
league_stats['Rank'] = None
|
||||
else:
|
||||
league_stats['Rating'] = round(league['rating'], 2)
|
||||
league_stats['Glicko'] = round(league['glicko'], 2)
|
||||
league_stats['RD'] = round(league['rd'], 2)
|
||||
league_stats['Standing'] = league['standing']
|
||||
league_stats['LPM'] = round((league['pps'] * 24), 2)
|
||||
league_stats['APL'] = round((league_stats['APM'] / league_stats['LPM']), 2)
|
||||
league_stats['ADPM'] = round((league_stats['VS'] * 0.6), 2)
|
||||
league_stats['ADPL'] = round(
|
||||
(league_stats['ADPM'] / league_stats['LPM']), 2
|
||||
)
|
||||
return league_stats
|
||||
|
||||
@classmethod
|
||||
async def get_sprint_stats(cls, solo_data: dict) -> dict[str, Any]:
|
||||
"""获取40L统计数据"""
|
||||
sprint_stats = {}
|
||||
solo = solo_data['data']['records']['40l']
|
||||
if solo['record'] is not None:
|
||||
sprint_stats['Time'] = round(
|
||||
solo['record']['endcontext']['finalTime'] / 1000, 2
|
||||
)
|
||||
if solo['rank'] is not None:
|
||||
sprint_stats['Rank'] = solo['rank']
|
||||
return sprint_stats
|
||||
|
||||
@classmethod
|
||||
async def get_blitz_stats(cls, solo_data: dict) -> dict[str, Any]:
|
||||
"""获取Blitz统计数据"""
|
||||
blitz_stats = {}
|
||||
blitz = solo_data['data']['records']['blitz']
|
||||
if blitz['record'] is not None:
|
||||
blitz_stats['Score'] = blitz['record']['endcontext']['score']
|
||||
if blitz['rank'] is not None:
|
||||
blitz_stats['Rank'] = blitz['rank']
|
||||
return blitz_stats
|
||||
|
||||
@classmethod
|
||||
async def generate_message(
|
||||
cls, user_name: str | None = None, user_id: str | None = None
|
||||
) -> str:
|
||||
"""生成消息"""
|
||||
user_data, solo_data = await gather(
|
||||
cls.get_user_data(user_name=user_name, user_id=user_id),
|
||||
cls.get_solo_data(user_name=user_name, user_id=user_id),
|
||||
)
|
||||
if user_data[0] is False:
|
||||
return '用户信息请求失败'
|
||||
if user_data[1] is False:
|
||||
return f'用户信息请求错误:\n{user_data[2]["error"]}'
|
||||
user_name = user_data[2]['data']['user']['username'].upper()
|
||||
league_stats = await cls.get_league_stats(user_data[2])
|
||||
message = ''
|
||||
if not league_stats:
|
||||
message += f'用户 {user_name} 没有排位统计数据'
|
||||
else:
|
||||
if league_stats['Rank'] is None:
|
||||
message += f'用户 {user_name} 暂未完成定级赛, 最近十场的数据:'
|
||||
else:
|
||||
if league_stats['Rank'] == 'Z':
|
||||
message += f'用户 {user_name} 暂无段位, {league_stats["Rating"]} TR'
|
||||
else:
|
||||
message += f'{league_stats["Rank"]} 段用户 {user_name} {league_stats["Rating"]} TR (#{league_stats["Standing"]})'
|
||||
message += (
|
||||
f', 段位分 {league_stats["Glicko"]}±{league_stats["RD"]}, 最近十场的数据:'
|
||||
if self.processed_data.user_records is None:
|
||||
self.raw_response.user_records = await Request.request(
|
||||
splice_url(
|
||||
[
|
||||
BASE_URL,
|
||||
'users/',
|
||||
f'{self.user.ID or self.user.name}/',
|
||||
'records',
|
||||
]
|
||||
)
|
||||
message += f'\nL\'PM: {league_stats["LPM"]} ( {league_stats["PPS"]} pps )'
|
||||
message += f'\nAPM: {league_stats["APM"]} ( x{league_stats["APL"]} )'
|
||||
if league_stats['VS'] != 0:
|
||||
message += f'\nADPM: {league_stats["ADPM"]} ( x{league_stats["ADPL"]} ) ( {league_stats["VS"]}vs )'
|
||||
if solo_data[0] is False:
|
||||
return f'{message}\nSolo统计数据请求失败'
|
||||
if solo_data[1] is False:
|
||||
return f'{message}\nSolo统计数据请求错误:\n{solo_data[2]["error"]}'
|
||||
sprint_stats, blitz_stats = await gather(
|
||||
cls.get_sprint_stats(solo_data[2]), cls.get_blitz_stats(solo_data[2])
|
||||
)
|
||||
user_records: UserRecords = parse_raw_as(
|
||||
UserRecords, # type: ignore[arg-type]
|
||||
self.raw_response.user_records,
|
||||
)
|
||||
if isinstance(user_records, RecordsFailed):
|
||||
raise RequestError(f'用户Solo数据请求错误:\n{user_records.error}')
|
||||
self.processed_data.user_records = user_records
|
||||
return self.processed_data.user_records
|
||||
|
||||
async def generate_message(self) -> str:
|
||||
"""生成消息"""
|
||||
user_info = await self.get_user_info()
|
||||
user_name = user_info.data.user.username.upper()
|
||||
league = user_info.data.user.league
|
||||
ret_message = ''
|
||||
if isinstance(league, NeverPlayedLeague):
|
||||
ret_message += f'用户 {user_name} 没有排位统计数据'
|
||||
else:
|
||||
if isinstance(league, NeverRatedLeague):
|
||||
ret_message += f'用户 {user_name} 暂未完成定级赛, 最近十场的数据:'
|
||||
elif league.rank == 'z':
|
||||
ret_message += f'用户 {user_name} 暂无段位, {round(league.rating,2)} TR'
|
||||
else:
|
||||
ret_message += (
|
||||
f'{league.rank.upper()} 段用户 {user_name} {round(league.rating,2)} TR (#{league.standing})'
|
||||
)
|
||||
ret_message += f', 段位分 {round(league.glicko,2)}±{round(league.rd,2)}, 最近十场的数据:'
|
||||
lpm = league.pps * 24
|
||||
ret_message += f"\nL'PM: {round(lpm, 2)} ( {league.pps} pps )"
|
||||
ret_message += f'\nAPM: {league.apm} ( x{round(league.apm/(league.pps*24),2)} )'
|
||||
if league.vs is not None:
|
||||
adpm = league.vs * 0.6
|
||||
ret_message += f'\nADPM: {round(adpm,2)} ( x{round(adpm/lpm,2)} ) ( {league.vs}vs )'
|
||||
user_records = await self.get_user_records()
|
||||
sprint = user_records.data.records.sprint
|
||||
if sprint.record is not None:
|
||||
if not isinstance(sprint.record, SoloRecord):
|
||||
raise WhatTheFuckError('40L记录不是单人记录')
|
||||
ret_message += f'\n40L: {round(sprint.record.endcontext.final_time/1000,2)}s'
|
||||
ret_message += f' ( #{sprint.rank} )' if sprint.rank is not None else ''
|
||||
blitz = user_records.data.records.blitz
|
||||
if blitz.record is not None:
|
||||
if not isinstance(blitz.record, SoloRecord):
|
||||
raise WhatTheFuckError('Blitz记录不是单人记录')
|
||||
ret_message += f'\nBlitz: {blitz.record.endcontext.score}'
|
||||
ret_message += f' ( #{blitz.rank} )' if blitz.rank is not None else ''
|
||||
return ret_message
|
||||
|
||||
|
||||
@scheduler.scheduled_job('cron', hour='0,6,12,18', minute=0)
|
||||
async def get_io_rank_data() -> None:
|
||||
league_all: LeagueAll = parse_raw_as(
|
||||
LeagueAll, # type: ignore[arg-type]
|
||||
await Request.request(splice_url([BASE_URL, 'users/lists/league/all'])),
|
||||
)
|
||||
if isinstance(league_all, LeagueAllFailed):
|
||||
raise RequestError(f'用户Solo数据请求错误:\n{league_all.error}')
|
||||
|
||||
def pps(user: LeagueAllUser) -> float:
|
||||
return user.league.pps
|
||||
|
||||
def apm(user: LeagueAllUser) -> float:
|
||||
return user.league.apm
|
||||
|
||||
def vs(user: LeagueAllUser) -> float:
|
||||
return user.league.vs
|
||||
|
||||
def _min(users: list[LeagueAllUser], field: Callable[[LeagueAllUser], float]) -> LeagueAllUser:
|
||||
return min(users, key=field)
|
||||
|
||||
def _max(users: list[LeagueAllUser], field: Callable[[LeagueAllUser], float]) -> LeagueAllUser:
|
||||
return max(users, key=field)
|
||||
|
||||
def build_extremes_data(
|
||||
users: list[LeagueAllUser],
|
||||
field: Callable[[LeagueAllUser], float],
|
||||
sort: Callable[[list[LeagueAllUser], Callable[[LeagueAllUser], float]], LeagueAllUser],
|
||||
) -> tuple[dict[str, str], float]:
|
||||
user = sort(users, field)
|
||||
return asdict(User(ID=user.id, name=user.username)), field(user)
|
||||
|
||||
users = league_all.data.users
|
||||
rank_to_users: defaultdict[Rank, list[LeagueAllUser]] = defaultdict(list)
|
||||
for i in users:
|
||||
rank_to_users[i.league.rank].append(i)
|
||||
rank_info: list[IORank] = []
|
||||
for rank, percentile in RANK_PERCENTILE.items():
|
||||
offset = floor((percentile / 100) * len(users)) - 1
|
||||
tr_line = users[offset].league.rating
|
||||
rank_users = rank_to_users[rank]
|
||||
rank_info.append(
|
||||
IORank(
|
||||
rank=rank,
|
||||
tr_line=tr_line,
|
||||
player_count=len(rank_users),
|
||||
low_pps=(build_extremes_data(rank_users, pps, _min)),
|
||||
low_apm=(build_extremes_data(rank_users, apm, _min)),
|
||||
low_vs=(build_extremes_data(rank_users, vs, _min)),
|
||||
avg_pps=mean({i.league.pps for i in rank_users}),
|
||||
avg_apm=mean({i.league.apm for i in rank_users}),
|
||||
avg_vs=mean({i.league.vs for i in rank_users}),
|
||||
high_pps=(build_extremes_data(rank_users, pps, _max)),
|
||||
high_apm=(build_extremes_data(rank_users, apm, _max)),
|
||||
high_vs=(build_extremes_data(rank_users, vs, _max)),
|
||||
)
|
||||
)
|
||||
message += f'\n40L: {sprint_stats["Time"]}s' if 'Time' in sprint_stats else ''
|
||||
message += f' ( #{sprint_stats["Rank"]} )' if 'Rank' in sprint_stats else ''
|
||||
message += f'\nBlitz: {blitz_stats["Score"]}' if 'Score' in blitz_stats else ''
|
||||
message += f' ( #{blitz_stats["Rank"]} )' if 'Rank' in blitz_stats else ''
|
||||
return message
|
||||
async with get_session() as session:
|
||||
session.add_all(rank_info)
|
||||
await session.commit()
|
||||
|
||||
|
||||
@driver.on_startup
|
||||
async def check_rank_data() -> None:
|
||||
async with get_session() as session:
|
||||
latest_time = await session.scalar(select(IORank.create_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):
|
||||
await get_io_rank_data()
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from httpx import AsyncClient, HTTPError
|
||||
from nonebot import get_driver
|
||||
from nonebot.log import logger
|
||||
from playwright.async_api import Browser, Response, async_playwright
|
||||
from ujson import JSONDecodeError, dumps, loads
|
||||
|
||||
from ...utils.config import Config
|
||||
|
||||
driver = get_driver()
|
||||
|
||||
config = Config.parse_obj(get_driver().config)
|
||||
|
||||
|
||||
@driver.on_startup
|
||||
async def _():
|
||||
await Request.init_cache()
|
||||
await Request.read_cache()
|
||||
|
||||
|
||||
@driver.on_shutdown
|
||||
async def _():
|
||||
await Request.close_browser()
|
||||
await Request.write_cache()
|
||||
|
||||
|
||||
class Request:
|
||||
"""网络请求相关类"""
|
||||
|
||||
_browser: Browser | None = None
|
||||
_headers: dict | None = None
|
||||
_cookies: dict | None = None
|
||||
|
||||
@classmethod
|
||||
async def _init_playwright(cls) -> Browser:
|
||||
"""初始化playwright"""
|
||||
playwright = await async_playwright().start()
|
||||
cls._browser = await playwright.firefox.launch()
|
||||
return cls._browser
|
||||
|
||||
@classmethod
|
||||
async def _get_browser(cls) -> Browser:
|
||||
"""获取浏览器对象"""
|
||||
return cls._browser or await cls._init_playwright()
|
||||
|
||||
@classmethod
|
||||
async def _anti_cloudflare(cls, url: str) -> tuple[bool, bool, dict[str, Any]]:
|
||||
"""用firefox硬穿五秒盾"""
|
||||
browser = await cls._get_browser()
|
||||
context = await browser.new_context()
|
||||
page = await context.new_page()
|
||||
response = await page.goto(url)
|
||||
attempts = 0
|
||||
while attempts < 60:
|
||||
attempts += 1
|
||||
text = await page.locator("body").text_content()
|
||||
if text is None:
|
||||
await page.wait_for_timeout(1000)
|
||||
continue
|
||||
if await page.title() == "Please Wait... | Cloudflare":
|
||||
# TODO 有无人来做一个过验证码(
|
||||
break
|
||||
try:
|
||||
data = loads(text)
|
||||
except JSONDecodeError:
|
||||
await page.wait_for_timeout(1000)
|
||||
else:
|
||||
assert isinstance(response, Response)
|
||||
cls._headers = await response.request.all_headers()
|
||||
try:
|
||||
cls._cookies = {
|
||||
i["name"]: i["value"] for i in await context.cookies()
|
||||
}
|
||||
except KeyError:
|
||||
cls._cookies = None
|
||||
await page.close()
|
||||
await context.close()
|
||||
return True, data["success"], data
|
||||
await page.close()
|
||||
await context.close()
|
||||
return True, False, {"error": "绕过五秒盾失败"}
|
||||
|
||||
@classmethod
|
||||
async def init_cache(cls) -> None:
|
||||
"""初始化缓存文件"""
|
||||
if not os.path.exists(os.path.dirname(config.cache_path)):
|
||||
os.makedirs(os.path.dirname(config.cache_path))
|
||||
if not os.path.exists(config.cache_path):
|
||||
with open(file=config.cache_path, mode="w", encoding="UTF-8") as file:
|
||||
file.write(dumps({"headers": cls._headers, "cookies": cls._cookies}))
|
||||
|
||||
@classmethod
|
||||
async def read_cache(cls) -> None:
|
||||
"""读取缓存文件"""
|
||||
try:
|
||||
with open(file=config.cache_path, mode="r", encoding="UTF-8") as file:
|
||||
json = loads(file.read())
|
||||
cls._headers = json["headers"]
|
||||
cls._cookies = json["cookies"]
|
||||
except FileNotFoundError:
|
||||
await cls.init_cache()
|
||||
except PermissionError:
|
||||
os.remove(config.cache_path)
|
||||
await cls.init_cache()
|
||||
except JSONDecodeError:
|
||||
os.remove(config.cache_path)
|
||||
await cls.init_cache()
|
||||
|
||||
@classmethod
|
||||
async def write_cache(cls) -> None:
|
||||
"""写入缓存文件"""
|
||||
try:
|
||||
with open(file=config.cache_path, mode="r+", encoding="UTF-8") as file:
|
||||
file.write(dumps({"headers": cls._headers, "cookies": cls._cookies}))
|
||||
except FileNotFoundError:
|
||||
await cls.init_cache()
|
||||
except PermissionError:
|
||||
os.remove(config.cache_path)
|
||||
await cls.init_cache()
|
||||
except JSONDecodeError:
|
||||
os.remove(config.cache_path)
|
||||
await cls.init_cache()
|
||||
|
||||
@classmethod
|
||||
async def request(cls, url: str) -> tuple[bool, bool, dict[str, Any]]:
|
||||
"""请求api"""
|
||||
try:
|
||||
async with AsyncClient(cookies=cls._cookies) as session:
|
||||
response = await session.get(url, headers=cls._headers)
|
||||
data = loads(response.content)
|
||||
return True, data["success"], data
|
||||
except HTTPError as error:
|
||||
logger.error(f"请求错误\n{error}")
|
||||
return False, False, {}
|
||||
except JSONDecodeError:
|
||||
return await cls._anti_cloudflare(url)
|
||||
|
||||
@classmethod
|
||||
async def close_browser(cls) -> None:
|
||||
"""关闭浏览器对象"""
|
||||
if isinstance(cls._browser, Browser):
|
||||
await cls._browser.close()
|
||||
@@ -0,0 +1,20 @@
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Cache(BaseModel):
|
||||
status: str
|
||||
cached_at: datetime
|
||||
cached_until: datetime
|
||||
|
||||
|
||||
class SuccessModel(BaseModel):
|
||||
success: Literal[True]
|
||||
cache: Cache
|
||||
|
||||
|
||||
class FailedModel(BaseModel):
|
||||
success: Literal[False]
|
||||
error: str
|
||||
@@ -0,0 +1,39 @@
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from ..typing import Rank
|
||||
from .base import FailedModel
|
||||
from .base import SuccessModel as BaseSuccessModel
|
||||
|
||||
|
||||
class SuccessModel(BaseSuccessModel):
|
||||
class Data(BaseModel):
|
||||
class User(BaseModel):
|
||||
class League(BaseModel):
|
||||
gamesplayed: int
|
||||
gameswon: int
|
||||
rating: float
|
||||
glicko: float
|
||||
rd: float
|
||||
rank: Rank
|
||||
bestrank: Rank
|
||||
apm: float
|
||||
pps: float
|
||||
vs: float
|
||||
decaying: bool
|
||||
|
||||
id: str = Field(..., alias='_id')
|
||||
username: str
|
||||
role: str
|
||||
xp: float
|
||||
league: League
|
||||
supporter: bool
|
||||
verified: bool
|
||||
country: str | None
|
||||
|
||||
users: list[User]
|
||||
|
||||
data: Data
|
||||
|
||||
|
||||
LeagueAll = SuccessModel | FailedModel
|
||||
User = SuccessModel.Data.User
|
||||
@@ -0,0 +1,125 @@
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from ..typing import Rank
|
||||
from .base import FailedModel
|
||||
from .base import SuccessModel as BaseSuccessModel
|
||||
|
||||
|
||||
class SuccessModel(BaseSuccessModel):
|
||||
class Data(BaseModel):
|
||||
class User(BaseModel):
|
||||
class Badge(BaseModel):
|
||||
id: str
|
||||
label: str
|
||||
ts: datetime | None
|
||||
|
||||
class NeverPlayedLeague(BaseModel):
|
||||
gamesplayed: Literal[0]
|
||||
gameswon: Literal[0]
|
||||
rating: Literal[-1]
|
||||
rank: Literal['z']
|
||||
standing: Literal[-1]
|
||||
standing_local: Literal[-1]
|
||||
next_rank: None
|
||||
prev_rank: None
|
||||
next_at: Literal[-1]
|
||||
prev_at: Literal[-1]
|
||||
percentile: Literal[-1]
|
||||
percentile_rank: Literal['z']
|
||||
apm: None
|
||||
pps: None
|
||||
vs: None
|
||||
decaying: bool
|
||||
|
||||
class NeverRatedLeague(BaseModel):
|
||||
gamesplayed: Literal[1, 2, 3, 4, 5, 6, 7, 8, 9]
|
||||
gameswon: int
|
||||
rating: Literal[-1]
|
||||
rank: Literal['z']
|
||||
standing: Literal[-1]
|
||||
standing_local: Literal[-1]
|
||||
next_rank: None
|
||||
prev_rank: None
|
||||
next_at: Literal[-1]
|
||||
prev_at: Literal[-1]
|
||||
percentile: Literal[-1]
|
||||
percentile_rank: Literal['z']
|
||||
apm: float
|
||||
pps: float
|
||||
vs: float
|
||||
decaying: bool
|
||||
|
||||
class RatedLeague(BaseModel):
|
||||
gamesplayed: int
|
||||
gameswon: int
|
||||
rating: float
|
||||
rank: Rank
|
||||
bestrank: Rank
|
||||
standing: int
|
||||
standing_local: int
|
||||
next_rank: Rank | None
|
||||
prev_rank: Rank | None
|
||||
next_at: int
|
||||
prev_at: int
|
||||
percentile: float
|
||||
percentile_rank: str
|
||||
glicko: float
|
||||
rd: float
|
||||
apm: float
|
||||
pps: float
|
||||
vs: float | None
|
||||
decaying: bool
|
||||
|
||||
class Connections(BaseModel):
|
||||
class Discord(BaseModel):
|
||||
id: str
|
||||
username: str
|
||||
|
||||
discord: Discord | None
|
||||
|
||||
class Distinguishment(BaseModel):
|
||||
type: str # noqa: A003
|
||||
|
||||
id: str = Field(..., alias='_id')
|
||||
username: str
|
||||
role: Literal['anon', 'user', 'bot', 'halfmod', 'mod', 'admin', 'sysop', 'banned']
|
||||
ts: datetime | None
|
||||
botmaster: str | None
|
||||
badges: list[Badge]
|
||||
xp: float
|
||||
gamesplayed: int
|
||||
gameswon: int
|
||||
gametime: float
|
||||
country: str | None
|
||||
badstanding: bool | None
|
||||
supporter: bool | None # osk说是必有, 但实际上不是 fk osk
|
||||
supporter_tier: int
|
||||
verified: bool
|
||||
league: NeverPlayedLeague | NeverRatedLeague | RatedLeague
|
||||
avatar_revision: int | None
|
||||
"""This user's avatar ID. Get their avatar at
|
||||
|
||||
https://tetr.io/user-content/avatars/{ USERID }.jpg?rv={ AVATAR_REVISION }"""
|
||||
banner_revision: int | None
|
||||
"""This user's banner ID. Get their banner at
|
||||
|
||||
https://tetr.io/user-content/banners/{ USERID }.jpg?rv={ BANNER_REVISION }
|
||||
|
||||
Ignore this field if the user is not a supporter."""
|
||||
bio: str | None
|
||||
connections: Connections
|
||||
friend_count: int
|
||||
distinguishment: Distinguishment | None
|
||||
|
||||
user: User
|
||||
|
||||
data: Data
|
||||
|
||||
|
||||
NeverPlayedLeague = SuccessModel.Data.User.NeverPlayedLeague
|
||||
NeverRatedLeague = SuccessModel.Data.User.NeverRatedLeague
|
||||
RatedLeague = SuccessModel.Data.User.RatedLeague
|
||||
UserInfo = SuccessModel | FailedModel
|
||||
@@ -0,0 +1,125 @@
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from .base import FailedModel
|
||||
from .base import SuccessModel as BaseSuccessModel
|
||||
|
||||
|
||||
class EndContext(BaseModel):
|
||||
class Time(BaseModel):
|
||||
start: int
|
||||
zero: bool
|
||||
locked: bool
|
||||
prev: int
|
||||
frameoffset: int
|
||||
|
||||
class Clears(BaseModel):
|
||||
singles: int
|
||||
doubles: int
|
||||
triples: int
|
||||
quads: int
|
||||
realtspins: int
|
||||
minitspins: int
|
||||
minitspinsingles: int
|
||||
tspinsingles: int
|
||||
minitspindoubles: int
|
||||
tspindoubles: int
|
||||
tspintriples: int
|
||||
tspinquads: int
|
||||
allclear: int
|
||||
|
||||
class Garbage(BaseModel):
|
||||
sent: int
|
||||
received: int
|
||||
attack: int
|
||||
cleared: int
|
||||
|
||||
class Finesse(BaseModel):
|
||||
combo: int
|
||||
faults: int
|
||||
perfectpieces: int
|
||||
|
||||
seed: int
|
||||
lines: int
|
||||
level_lines: int
|
||||
level_lines_needed: int
|
||||
inputs: int
|
||||
holds: int
|
||||
time: Time
|
||||
score: int
|
||||
zenlevel: int
|
||||
zenprogress: int
|
||||
level: int
|
||||
combo: int
|
||||
currentcombopower: int # WTF
|
||||
topcombo: int
|
||||
btb: int
|
||||
topbtb: int
|
||||
currentbtbchainpower: int | None # WTF * 2 40l 里有 但是 blitz 没有
|
||||
tspins: int
|
||||
piecesplaced: int
|
||||
clears: Clears
|
||||
garbage: Garbage
|
||||
kills: int
|
||||
finesse: Finesse
|
||||
final_time: float = Field(..., alias='finalTime')
|
||||
gametype: str
|
||||
|
||||
|
||||
class BaseModeRecord(BaseModel):
|
||||
class SoloRecord(BaseModel):
|
||||
class User(BaseModel):
|
||||
id: str = Field(..., alias='_id')
|
||||
username: str
|
||||
|
||||
id: str = Field(..., alias='_id')
|
||||
stream: str
|
||||
replayid: str
|
||||
user: User
|
||||
ts: datetime
|
||||
ismulti: bool | None
|
||||
endcontext: EndContext
|
||||
|
||||
class MultiRecord(BaseModel):
|
||||
class User(BaseModel):
|
||||
id: str = Field(..., alias='_id')
|
||||
username: str
|
||||
|
||||
id: str = Field(..., alias='_id')
|
||||
stream: str
|
||||
replayid: str
|
||||
user: User
|
||||
ts: datetime
|
||||
ismulti: bool | None
|
||||
endcontext: list[EndContext]
|
||||
|
||||
record: SoloRecord | MultiRecord | None
|
||||
rank: int | None
|
||||
|
||||
|
||||
class SuccessModel(BaseSuccessModel):
|
||||
class Data(BaseModel):
|
||||
class Records(BaseModel):
|
||||
class Sprint(BaseModeRecord):
|
||||
...
|
||||
|
||||
class Blitz(BaseModeRecord):
|
||||
...
|
||||
|
||||
sprint: Sprint = Field(..., alias='40l')
|
||||
blitz: Blitz
|
||||
|
||||
class Zen(BaseModel):
|
||||
level: int
|
||||
score: int
|
||||
|
||||
records: Records
|
||||
zen: Zen
|
||||
|
||||
data: Data
|
||||
|
||||
|
||||
SoloRecord = BaseModeRecord.SoloRecord
|
||||
MultiRecord = BaseModeRecord.MultiRecord
|
||||
UserRecords = SuccessModel | FailedModel
|
||||
@@ -0,0 +1,22 @@
|
||||
from typing import Literal
|
||||
|
||||
Rank = Literal[
|
||||
'x',
|
||||
'u',
|
||||
'ss',
|
||||
's+',
|
||||
's',
|
||||
's-',
|
||||
'a+',
|
||||
'a',
|
||||
'a-',
|
||||
'b+',
|
||||
'b',
|
||||
'b-',
|
||||
'c+',
|
||||
'c',
|
||||
'c-',
|
||||
'd+',
|
||||
'd',
|
||||
'z', # 未定级
|
||||
]
|
||||
@@ -1,196 +0,0 @@
|
||||
from asyncio import gather
|
||||
from re import I
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
from lxml import etree
|
||||
from nonebot import on_regex
|
||||
from nonebot.adapters.onebot.v11 import GROUP, MessageEvent
|
||||
from nonebot.log import logger
|
||||
from nonebot.matcher import Matcher
|
||||
from pandas import read_html
|
||||
|
||||
from ..utils.database import DataBase
|
||||
from ..utils.message_analyzer import (
|
||||
handle_bind_message,
|
||||
handle_stats_query_message
|
||||
)
|
||||
|
||||
TOPBind = on_regex(pattern=r'^top绑定|^topbind', flags=I, permission=GROUP)
|
||||
TopStats = on_regex(pattern=r'^top查|^topstats', flags=I, permission=GROUP)
|
||||
|
||||
|
||||
@TOPBind.handle()
|
||||
async def _(event: MessageEvent, matcher: Matcher):
|
||||
decoded_message = await handle_bind_message(message=event.raw_message, game_type='TOP')
|
||||
if decoded_message[0] is None:
|
||||
await matcher.finish(decoded_message[1][0])
|
||||
elif decoded_message[0] == 'Name':
|
||||
user_data = await get_user_data(decoded_message[1][1])
|
||||
if user_data[0] is False:
|
||||
await matcher.finish('用户信息请求失败')
|
||||
else:
|
||||
if await check_user(user_data[1]) is False:
|
||||
await matcher.finish('用户不存在')
|
||||
user_name = await get_user_name(user_data[1])
|
||||
if event.sender.user_id is None: # 理论上是不会有None出现的, ide快乐行属于是(
|
||||
logger.error('获取QQ号失败')
|
||||
await matcher.finish('获取QQ号失败')
|
||||
await matcher.finish(
|
||||
await DataBase.write_bind_info(
|
||||
qq_number=event.sender.user_id,
|
||||
user=user_name,
|
||||
game_type='TOP'
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@TopStats.handle()
|
||||
async def _(event: MessageEvent, matcher: Matcher):
|
||||
decoded_message = await handle_stats_query_message(message=event.raw_message, game_type='TOP')
|
||||
if decoded_message[0] is None:
|
||||
await matcher.finish(decoded_message[1][0])
|
||||
elif decoded_message[0] == 'AT':
|
||||
if event.is_tome() is True:
|
||||
await matcher.finish('不能查询bot的信息')
|
||||
bind_info = await DataBase.query_bind_info(qq_number=decoded_message[1][1], game_type='TOP')
|
||||
if bind_info is None:
|
||||
message = '未查询到绑定信息'
|
||||
else:
|
||||
message = (f'* 由于无法验证绑定信息, 不能保证查询到的用户为本人\n{await generate_message(bind_info)}')
|
||||
elif decoded_message[0] == 'ME':
|
||||
if event.sender.user_id is None:
|
||||
logger.error('获取QQ号失败')
|
||||
await matcher.finish('获取QQ号失败, 请联系bot主人')
|
||||
bind_info = await DataBase.query_bind_info(qq_number=event.sender.user_id, game_type='TOP')
|
||||
if bind_info is None:
|
||||
message = '未查询到绑定信息'
|
||||
else:
|
||||
message = (f'* 由于无法验证绑定信息, 不能保证查询到的用户为本人\n{await generate_message(bind_info)}')
|
||||
elif decoded_message[0] == 'Name':
|
||||
message = await generate_message(decoded_message[1][1])
|
||||
else:
|
||||
raise ValueError('预期外行为, 请上报GitHub')
|
||||
await matcher.finish(message)
|
||||
|
||||
|
||||
async def get_user_data(user_name: str) -> tuple[bool, str]:
|
||||
'''获取用户信息'''
|
||||
url = f'http://tetrisonline.pl/top/profile.php?user={user_name}'
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url) as resp:
|
||||
return True, await resp.text()
|
||||
except aiohttp.client_exceptions.ClientConnectorError as error: # type: ignore
|
||||
logger.error(error)
|
||||
return False, ''
|
||||
|
||||
|
||||
async def check_user(user_data: str) -> bool:
|
||||
'''如果用户存在返回True, 如果用户不存在返回False'''
|
||||
return user_data.find('user not found!') == -1
|
||||
|
||||
|
||||
async def get_user_name(user_data: str) -> str:
|
||||
'''获取用户名'''
|
||||
data = etree.HTML(user_data).xpath('//div[@class="mycontent"]/h1/text()')
|
||||
if isinstance(data, list):
|
||||
return str(data[0]).replace('\'s profile', '')
|
||||
raise TypeError('预期外行为, 请上报GitHub')
|
||||
|
||||
|
||||
async def get_game_stats(user_data: str) -> dict[str, dict[str, Any]]:
|
||||
'''获取游戏统计数据'''
|
||||
game_stats: dict[str, Any] = {'24H': {}, 'All': {}}
|
||||
html = etree.HTML(user_data)
|
||||
data = html.xpath('//div[@class="mycontent"]//text()')
|
||||
if isinstance(data, list):
|
||||
for i in data:
|
||||
if not isinstance(i, str):
|
||||
i = str(i)
|
||||
i = i.strip()
|
||||
if i.startswith('lpm:'):
|
||||
game_stats['24H']['LPM'] = i.replace('lpm:', '').strip()
|
||||
if i.startswith('apm:'):
|
||||
game_stats['24H']['APM'] = i.replace('apm:', '').strip()
|
||||
if 'LPM' in game_stats['24H'] and 'APM' in game_stats['24H']:
|
||||
break
|
||||
else:
|
||||
raise TypeError('预期外行为, 请上报GitHub')
|
||||
# 如果没有24H统计数据
|
||||
if game_stats['24H'].get('LPM') in [None, ''] or game_stats['24H'].get('APM') in [None, '']:
|
||||
game_stats['24H'].pop('LPM', None)
|
||||
game_stats['24H'].pop('APM', None)
|
||||
else:
|
||||
game_stats['24H']['PPS'] = round(
|
||||
float(game_stats['24H']['LPM']) / 24, 2)
|
||||
game_stats['24H']['APL'] = round(
|
||||
float(game_stats['24H']['APM']) / float(game_stats['24H']['LPM']), 2)
|
||||
game_stats['24H']['LPM'] = round(float(game_stats['24H']['LPM']), 2)
|
||||
game_stats['24H']['APM'] = round(float(game_stats['24H']['APM']), 2)
|
||||
table = html.xpath('//table')
|
||||
if isinstance(table, list):
|
||||
if isinstance(table[0], etree._Element):
|
||||
stats_table = etree.tostring(table[0], encoding='utf-8').decode()
|
||||
df = read_html(stats_table, encoding='utf-8', header=0)[0]
|
||||
results = df.T.to_dict().values()
|
||||
if results:
|
||||
game_stats['All']['LPM'] = 0
|
||||
game_stats['All']['APM'] = 0
|
||||
for i in results:
|
||||
if isinstance(i, dict):
|
||||
game_stats['All']['LPM'] += i['lpm']
|
||||
game_stats['All']['APM'] += i['apm']
|
||||
game_stats['All']['LPM'] = game_stats['All']['LPM'] / \
|
||||
len(results)
|
||||
game_stats['All']['APM'] = game_stats['All']['APM'] / \
|
||||
len(results)
|
||||
game_stats['All']['PPS'] = round(
|
||||
game_stats['All']['LPM'] / 24, 2)
|
||||
game_stats['All']['APL'] = round(
|
||||
float(game_stats['All']['APM']) / float(game_stats['All']['LPM']), 2)
|
||||
game_stats['All']['LPM'] = round(
|
||||
float(game_stats['All']['LPM']), 2)
|
||||
game_stats['All']['APM'] = round(
|
||||
float(game_stats['All']['APM']), 2)
|
||||
else:
|
||||
raise TypeError('预期外行为, 请上报GitHub')
|
||||
return game_stats
|
||||
|
||||
|
||||
async def generate_message(user_name: str) -> str:
|
||||
'''生成消息'''
|
||||
user_data = await get_user_data(user_name)
|
||||
if user_data[0] is False:
|
||||
return '用户信息请求失败'
|
||||
if await check_user(user_data[1]) is False:
|
||||
return '用户不存在'
|
||||
user_name, game_stats = await gather(
|
||||
get_user_name(user_data[1]),
|
||||
get_game_stats(user_data[1])
|
||||
)
|
||||
message = ''
|
||||
if game_stats['24H'] and game_stats['All']:
|
||||
message += f'用户 {user_name} 24小时内统计数据为: '
|
||||
message += f'\nL\'PM: {game_stats["24H"]["LPM"]} ( {game_stats["24H"]["PPS"]} pps )'
|
||||
message += f'\nAPM: {game_stats["24H"]["APM"]} ( x{game_stats["24H"]["APL"]} )'
|
||||
message += '\n历史统计数据为: '
|
||||
message += f'\nL\'PM: {game_stats["All"]["LPM"]} ( {game_stats["All"]["PPS"]} pps )'
|
||||
message += f'\nAPM: {game_stats["All"]["APM"]} ( x{game_stats["All"]["APL"]} )'
|
||||
elif game_stats['24H'] and not game_stats['All']:
|
||||
message += f'用户 {user_name} 24小时内统计数据为: '
|
||||
message += f'\nL\'PM: {game_stats["24H"]["LPM"]} ( {game_stats["24H"]["PPS"]} pps )'
|
||||
message += f'\nAPM: {game_stats["24H"]["APM"]} ( x{game_stats["24H"]["APL"]} )'
|
||||
message += '\n暂无历史统计数据'
|
||||
message += '\n( 这理论上不该存在, 如果你看到了, 请联系bot主人查看后台'
|
||||
logger.error(f'老实说这个不算Error, 但是理论上不应该有, 如果你看到了这条日志, 我希望你能来Github发个issue(\
|
||||
user_name: {user_name}\
|
||||
user_data: {user_data}\
|
||||
game_stats: {game_stats}')
|
||||
elif not game_stats['24H'] and game_stats['All']:
|
||||
message += f'用户 {user_name} 暂无24小时内统计数据, 历史统计数据为: '
|
||||
message += f'\nL\'PM: {game_stats["All"]["LPM"]} ( {game_stats["All"]["PPS"]} pps )'
|
||||
message += f'\nAPM: {game_stats["All"]["APM"]} ( x{game_stats["All"]["APL"]} )'
|
||||
else:
|
||||
message += f'用户 {user_name} 暂无24小时内统计数据, 暂无历史统计数据'
|
||||
return message
|
||||
@@ -0,0 +1,126 @@
|
||||
from arclet.alconna import Alconna, Arg, ArgFlag, Args, CommandMeta, Option
|
||||
from nonebot.adapters import Bot, Event
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot_plugin_alconna import AlcMatches, At, on_alconna
|
||||
from nonebot_plugin_orm import get_session
|
||||
|
||||
from ...db import query_bind_info
|
||||
from ...utils.exception import MessageFormatError, NeedCatchError
|
||||
from ...utils.platform import get_platform
|
||||
from ...utils.typing import Me
|
||||
from ..constant import BIND_COMMAND, QUERY_COMMAND
|
||||
from .constant import GAME_TYPE
|
||||
from .processor import Processor, User, identify_user_info
|
||||
|
||||
alc = on_alconna(
|
||||
Alconna(
|
||||
'top',
|
||||
Option(
|
||||
BIND_COMMAND[0],
|
||||
Args(
|
||||
Arg(
|
||||
'account',
|
||||
identify_user_info,
|
||||
notice='TOP 用户名',
|
||||
flags=[ArgFlag.HIDDEN],
|
||||
)
|
||||
),
|
||||
alias=BIND_COMMAND[1:],
|
||||
compact=True,
|
||||
dest='bind',
|
||||
help_text='绑定 TOP 账号',
|
||||
),
|
||||
Option(
|
||||
QUERY_COMMAND[0],
|
||||
Args(
|
||||
Arg(
|
||||
'target',
|
||||
At | Me,
|
||||
notice='@想要查询的人 | 自己',
|
||||
flags=[ArgFlag.HIDDEN, ArgFlag.OPTIONAL],
|
||||
),
|
||||
Arg(
|
||||
'account',
|
||||
identify_user_info | Me | At,
|
||||
notice='TOP 用户名',
|
||||
flags=[ArgFlag.HIDDEN, ArgFlag.OPTIONAL],
|
||||
),
|
||||
),
|
||||
alias=QUERY_COMMAND[1:],
|
||||
compact=True,
|
||||
dest='query',
|
||||
help_text='查询 TOP 游戏信息',
|
||||
),
|
||||
meta=CommandMeta(
|
||||
description='查询 TetrisOnline波兰服 的信息',
|
||||
example='top绑定scdhh\ntop查我',
|
||||
compact=True,
|
||||
fuzzy_match=True,
|
||||
),
|
||||
),
|
||||
skip_for_unmatch=False,
|
||||
auto_send_output=True,
|
||||
aliases={'TOP'},
|
||||
)
|
||||
|
||||
|
||||
@alc.assign('bind')
|
||||
async def _(bot: Bot, event: Event, matcher: Matcher, account: User):
|
||||
proc = Processor(
|
||||
event_id=id(event),
|
||||
user=account,
|
||||
command_args=[],
|
||||
)
|
||||
try:
|
||||
await matcher.finish(await proc.handle_bind(platform=get_platform(bot), account=event.get_user_id()))
|
||||
except NeedCatchError as e:
|
||||
await matcher.finish(str(e))
|
||||
|
||||
|
||||
@alc.assign('query')
|
||||
async def _(bot: Bot, event: Event, matcher: Matcher, target: At | Me):
|
||||
async with get_session() as session:
|
||||
bind = await query_bind_info(
|
||||
session=session,
|
||||
chat_platform=get_platform(bot),
|
||||
chat_account=(target.target if isinstance(target, At) else event.get_user_id()),
|
||||
game_platform=GAME_TYPE,
|
||||
)
|
||||
if bind is None:
|
||||
await matcher.finish('未查询到绑定信息')
|
||||
message = '* 由于无法验证绑定信息, 不能保证查询到的用户为本人\n'
|
||||
proc = Processor(
|
||||
event_id=id(event),
|
||||
user=User(name=bind.game_account),
|
||||
command_args=[],
|
||||
)
|
||||
try:
|
||||
await matcher.finish(message + await proc.handle_query())
|
||||
except NeedCatchError as e:
|
||||
await matcher.finish(str(e))
|
||||
|
||||
|
||||
@alc.assign('query')
|
||||
async def _(event: Event, matcher: Matcher, account: User):
|
||||
proc = Processor(
|
||||
event_id=id(event),
|
||||
user=account,
|
||||
command_args=[],
|
||||
)
|
||||
try:
|
||||
await matcher.finish(await proc.handle_query())
|
||||
except NeedCatchError as e:
|
||||
await matcher.finish(str(e))
|
||||
|
||||
|
||||
@alc.handle()
|
||||
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"查看帮助'
|
||||
)
|
||||
@@ -0,0 +1,4 @@
|
||||
from ...utils.typing import GameType
|
||||
|
||||
GAME_TYPE: GameType = 'TOP'
|
||||
BASE_URL = 'http://tetrisonline.pl/top/'
|
||||
@@ -0,0 +1,142 @@
|
||||
from contextlib import suppress
|
||||
from dataclasses import dataclass
|
||||
from io import StringIO
|
||||
from re import match
|
||||
from typing import NoReturn
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from lxml import etree
|
||||
from nonebot_plugin_orm import get_session
|
||||
from pandas import read_html
|
||||
|
||||
from ...db import create_or_update_bind
|
||||
from ...utils.exception import MessageFormatError, RequestError
|
||||
from ...utils.request import Request, splice_url
|
||||
from ...utils.typing import GameType
|
||||
from .. import ProcessedData as ProcessedDataMeta
|
||||
from .. import Processor as ProcessorMeta
|
||||
from .. import RawResponse as RawResponseMeta
|
||||
from .. import User as UserMeta
|
||||
from .constant import BASE_URL, GAME_TYPE
|
||||
|
||||
|
||||
@dataclass
|
||||
class User(UserMeta):
|
||||
name: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class RawResponse(RawResponseMeta):
|
||||
user_profile: bytes | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProcessedData(ProcessedDataMeta):
|
||||
user_profile: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class Data:
|
||||
lpm: float
|
||||
apm: float
|
||||
|
||||
|
||||
@dataclass
|
||||
class GameData:
|
||||
day: Data | None
|
||||
total: Data | None
|
||||
|
||||
|
||||
def identify_user_info(info: str) -> User | MessageFormatError:
|
||||
if match(r'^[a-zA-Z0-9_]{1,16}$', info):
|
||||
return User(name=info)
|
||||
return MessageFormatError('用户名不合法')
|
||||
|
||||
|
||||
class Processor(ProcessorMeta):
|
||||
user: User
|
||||
raw_response: RawResponse
|
||||
processed_data: ProcessedData
|
||||
|
||||
def __init__(self, event_id: int, user: User, command_args: list[str]) -> None:
|
||||
super().__init__(event_id, user, command_args)
|
||||
self.raw_response = RawResponse()
|
||||
self.processed_data = ProcessedData()
|
||||
|
||||
@property
|
||||
def game_platform(self) -> GameType:
|
||||
return GAME_TYPE
|
||||
|
||||
async def handle_bind(self, platform: str, account: str) -> str:
|
||||
"""处理绑定消息"""
|
||||
self.command_type = 'bind'
|
||||
await self.check_user()
|
||||
async with get_session() as session:
|
||||
return await create_or_update_bind(
|
||||
session=session,
|
||||
chat_platform=platform,
|
||||
chat_account=account,
|
||||
game_platform=GAME_TYPE,
|
||||
game_account=self.user.name,
|
||||
)
|
||||
|
||||
async def handle_query(self) -> str:
|
||||
"""处理查询消息"""
|
||||
self.command_type = 'query'
|
||||
await self.check_user()
|
||||
return await self.generate_message()
|
||||
|
||||
async def get_user_profile(self) -> str:
|
||||
"""获取用户信息"""
|
||||
if self.processed_data.user_profile is None:
|
||||
url = splice_url([BASE_URL, 'profile.php', f'?{urlencode({"user":self.user.name})}'])
|
||||
self.raw_response.user_profile = await Request.request(url, is_json=False)
|
||||
self.processed_data.user_profile = self.raw_response.user_profile.decode()
|
||||
return self.processed_data.user_profile
|
||||
|
||||
async def check_user(self) -> None | NoReturn:
|
||||
if 'user not found!' in await self.get_user_profile():
|
||||
raise RequestError('用户不存在!')
|
||||
return None
|
||||
|
||||
async def get_user_name(self) -> str:
|
||||
"""获取用户名"""
|
||||
data = etree.HTML(await self.get_user_profile()).xpath('//div[@class="mycontent"]/h1/text()')
|
||||
return data[0].replace("'s profile", '')
|
||||
|
||||
async def get_game_data(self) -> GameData:
|
||||
"""获取游戏统计数据"""
|
||||
html = etree.HTML(await self.get_user_profile())
|
||||
day = None
|
||||
with suppress(ValueError):
|
||||
day = Data(
|
||||
lpm=float(str(html.xpath('//div[@class="mycontent"]/text()[3]')[0]).replace('lpm:', '').strip()),
|
||||
apm=float(str(html.xpath('//div[@class="mycontent"]/text()[4]')[0]).replace('apm:', '').strip()),
|
||||
)
|
||||
table = StringIO(
|
||||
etree.tostring(
|
||||
html.xpath('//div[@class="mycontent"]/table[@class="mytable"]')[0],
|
||||
encoding='utf-8',
|
||||
).decode()
|
||||
)
|
||||
dataframe = read_html(table, encoding='utf-8', header=0)[0]
|
||||
total = Data(lpm=dataframe['lpm'].mean(), apm=dataframe['apm'].mean()) if len(dataframe) != 0 else None
|
||||
return GameData(day=day, total=total)
|
||||
|
||||
async def generate_message(self) -> str:
|
||||
"""生成消息"""
|
||||
game_data = await self.get_game_data()
|
||||
message = ''
|
||||
if game_data.day is not None:
|
||||
message += f'用户 {self.user.name} 24小时内统计数据为: '
|
||||
message += f"\nL'PM: {round(game_data.day.lpm,2)} ( {round(game_data.day.lpm/24,2)} pps )"
|
||||
message += f'\nAPM: {round(game_data.day.apm,2)} ( x{round(game_data.day.apm/game_data.day.lpm,2)} )'
|
||||
else:
|
||||
message += f'用户 {self.user.name} 暂无24小时内统计数据'
|
||||
if game_data.total is not None:
|
||||
message += '\n历史统计数据为: '
|
||||
message += f"\nL'PM: {round(game_data.total.lpm,2)} ( {round(game_data.total.lpm/24,2)} pps )"
|
||||
message += f'\nAPM: {round(game_data.total.apm,2)} ( x{round(game_data.total.apm/game_data.total.lpm,2)} )'
|
||||
else:
|
||||
message += '\n暂无历史统计数据'
|
||||
return message
|
||||
@@ -1,179 +0,0 @@
|
||||
from asyncio import gather
|
||||
from re import I
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
from nonebot import on_regex
|
||||
from nonebot.adapters.onebot.v11 import GROUP, MessageEvent
|
||||
from nonebot.log import logger
|
||||
from nonebot.matcher import Matcher
|
||||
|
||||
from ..utils.message_analyzer import handle_stats_query_message
|
||||
|
||||
TOSStats = on_regex(
|
||||
pattern=r'^tos查|^tostats|^tosstats|^茶服查|^茶服stats',
|
||||
flags=I,
|
||||
permission=GROUP
|
||||
)
|
||||
|
||||
|
||||
@TOSStats.handle()
|
||||
async def _(event: MessageEvent, matcher: Matcher):
|
||||
decoded_message = await handle_stats_query_message(message=event.raw_message, game_type='TOS')
|
||||
if decoded_message[0] is None:
|
||||
await matcher.finish(decoded_message[1][0])
|
||||
elif decoded_message[0] == 'AT' or decoded_message[0] == 'QQ':
|
||||
if decoded_message[1][1] == event.self_id:
|
||||
await matcher.finish('不能查询bot的信息')
|
||||
message = await generate_message(tea_id=decoded_message[1][1])
|
||||
elif decoded_message[0] == 'ME':
|
||||
message = await generate_message(tea_id=event.sender.user_id)
|
||||
elif decoded_message[0] == 'Name':
|
||||
message = await generate_message(user_name=decoded_message[1][1])
|
||||
else:
|
||||
raise ValueError('预期外行为, 请上报GitHub')
|
||||
await matcher.finish(message)
|
||||
|
||||
|
||||
async def request(url: str) -> tuple[bool, bool, dict[str, Any]]:
|
||||
'''请求api'''
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url) as resp:
|
||||
data = await resp.json()
|
||||
return True, data['success'], data
|
||||
except aiohttp.client_exceptions.ClientConnectorError as error: # type: ignore
|
||||
logger.error(f'请求错误\n{error}')
|
||||
return False, False, {}
|
||||
|
||||
|
||||
async def get_user_info(
|
||||
user_name: str | None = None,
|
||||
tea_id: int | None = None
|
||||
) -> tuple[bool, bool, dict[str, Any]]:
|
||||
'''获取用户信息'''
|
||||
if user_name is not None and tea_id is None:
|
||||
user_data_url = f'https://teatube.cn:8888/getUsernameInfo?username={user_name}'
|
||||
elif user_name is None and tea_id is not None:
|
||||
user_data_url = f'https://teatube.cn:8888/getTeaIdInfo?teaId={tea_id}'
|
||||
else:
|
||||
raise ValueError('预期外行为, 请上报GitHub')
|
||||
return await request(user_data_url)
|
||||
|
||||
|
||||
async def get_user_data(
|
||||
user_name: str | None = None,
|
||||
tea_id: int | None = None,
|
||||
other_parameter: str = ''
|
||||
) -> tuple[bool, bool, dict[str, Any]]:
|
||||
'''获取用户数据'''
|
||||
if user_name is not None and tea_id is None:
|
||||
user_data_url = f'https://teatube.cn:8888/getProfile?id={user_name}{other_parameter}'
|
||||
elif user_name is None and tea_id is not None:
|
||||
user_data_url = f'https://teatube.cn:8888/getProfile?id={tea_id}{other_parameter}'
|
||||
else:
|
||||
raise ValueError('预期外行为, 请上报GitHub')
|
||||
return await request(user_data_url)
|
||||
|
||||
|
||||
async def get_rank_stats(user_info: dict) -> dict[str, float]:
|
||||
'''获取Rank数据'''
|
||||
data = user_info['data']
|
||||
rank_stats = {}
|
||||
if int(data['rankedGames']) != 0:
|
||||
rank_stats['Rating'] = round(float(data['ratingNow']), 2)
|
||||
rank_stats['RD'] = round(float(data['rdNow']), 2)
|
||||
rank_stats['Vol'] = round(float(data['volNow']), 3)
|
||||
return rank_stats
|
||||
|
||||
|
||||
async def get_game_data(user_data: dict) -> dict[str, int | float]:
|
||||
'''获取游戏数据'''
|
||||
game_data: dict[str, int | float] = {}
|
||||
if user_data['data'] != []:
|
||||
weighted_total_lpm = weighted_total_apm = weighted_total_adpm = total_time = num = 0
|
||||
for i in user_data['data']:
|
||||
# 排除单人局和时间为0的游戏
|
||||
if i['num_players'] == 1 or i['time'] == 0:
|
||||
continue
|
||||
# 茶:不计算没挖掘的局, 即使apm和lpm也如此
|
||||
if i['dig'] is None:
|
||||
continue
|
||||
# 加权计算
|
||||
time = i['time'] / 1000
|
||||
lpm = 24 * (i['pieces'] / time)
|
||||
apm = (i['attack'] / time) * 60
|
||||
adpm = ((i['attack'] + i['dig']) / time) * 60
|
||||
weighted_total_lpm += lpm * time
|
||||
weighted_total_apm += apm * time
|
||||
weighted_total_adpm += adpm * time
|
||||
total_time += time
|
||||
num += 1
|
||||
if num == 50:
|
||||
break
|
||||
if num > 0:
|
||||
game_data['NUM'] = num
|
||||
game_data['LPM'] = round((weighted_total_lpm / total_time), 2)
|
||||
game_data['APM'] = round((weighted_total_apm / total_time), 2)
|
||||
game_data['ADPM'] = round((weighted_total_adpm / total_time), 2)
|
||||
game_data['PPS'] = round((game_data['LPM'] / 24), 2)
|
||||
game_data['APL'] = round((game_data['APM'] / game_data['LPM']), 2)
|
||||
game_data['ADPL'] = round(
|
||||
(game_data['ADPM'] / game_data['LPM']), 2)
|
||||
game_data['VS'] = round((game_data['ADPM'] / 60 * 100), 2)
|
||||
# TODO: 如果有效局数不满50, 没有无dig信息的局, 且userData['data']内有50个局, 则继续往前获取信息
|
||||
return game_data
|
||||
|
||||
|
||||
async def get_pb_data(user_info: dict) -> dict[str, float | str]:
|
||||
'''获取PB数据'''
|
||||
pb_data: dict[str, float | str] = {}
|
||||
data = user_info['data']
|
||||
if int(data['PBSprint']) != 2147483647:
|
||||
pb_data['Sprint'] = round(float(data['PBSprint']) / 1000, 2)
|
||||
if int(data['PBMarathon']) != 0:
|
||||
pb_data['Marathon'] = data['PBMarathon']
|
||||
if int(data['PBChallenge']) != 0:
|
||||
pb_data['Challenge'] = data['PBChallenge']
|
||||
return pb_data
|
||||
|
||||
|
||||
async def generate_message(
|
||||
user_name: str | None = None,
|
||||
tea_id: int | None = None
|
||||
) -> str:
|
||||
'''生成消息'''
|
||||
user_info, user_data = await gather(
|
||||
get_user_info(user_name=user_name, tea_id=tea_id),
|
||||
get_user_data(user_name=user_name, tea_id=tea_id)
|
||||
)
|
||||
if user_info[0] is False:
|
||||
return '用户信息请求失败'
|
||||
if user_info[1] is False:
|
||||
return f'用户信息请求错误:\n{user_info[2]["error"]}'
|
||||
rank_stats, pb_data = await gather(
|
||||
get_rank_stats(user_info[2]),
|
||||
get_pb_data(user_info[2])
|
||||
)
|
||||
message = f'用户 {user_info[2]["data"]["name"]} ({user_info[2]["data"]["teaId"]}) '
|
||||
if not rank_stats:
|
||||
message += '暂无段位统计数据'
|
||||
else:
|
||||
message += f', 段位分 {rank_stats["Rating"]}±{rank_stats["RD"]} ({rank_stats["Vol"]}) '
|
||||
if user_data[0] is False:
|
||||
message = f'{message.rstrip()}\n游戏数据请求失败'
|
||||
elif user_data[1] is False:
|
||||
message = f'{message.rstrip()}\n游戏数据请求错误:\n{user_data[2]["error"]}'
|
||||
else:
|
||||
game_data = await get_game_data(user_data[2])
|
||||
if not game_data:
|
||||
message += ', 暂无游戏数据'
|
||||
else:
|
||||
message += f', 最近 {game_data["NUM"]} 局数据'
|
||||
message += f'\nL\'PM: {game_data["LPM"]} ( {game_data["PPS"]} pps )'
|
||||
message += f'\nAPM:{game_data["APM"]} ( x{game_data["APL"]} )'
|
||||
message += f'\nADPM:{game_data["ADPM"]} ( x{game_data["ADPL"]} ) ( {game_data["VS"]}vs )'
|
||||
message += f'\n40L: {pb_data["Sprint"]}s' if 'Sprint' in pb_data else ''
|
||||
message += f'\nMarathon: {pb_data["Marathon"]}' if 'Marathon' in pb_data else ''
|
||||
message += f'\nChallenge: {pb_data["Challenge"]}' if 'Challenge' in pb_data else ''
|
||||
return message
|
||||
@@ -0,0 +1,151 @@
|
||||
from arclet.alconna import Alconna, Arg, ArgFlag, Args, CommandMeta, Option
|
||||
from nonebot.adapters import Bot, Event
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot_plugin_alconna import AlcMatches, At, on_alconna
|
||||
from nonebot_plugin_orm import get_session
|
||||
|
||||
from ...db import query_bind_info
|
||||
from ...utils.exception import MessageFormatError, NeedCatchError
|
||||
from ...utils.platform import get_platform
|
||||
from ...utils.typing import Me
|
||||
from ..constant import BIND_COMMAND, QUERY_COMMAND
|
||||
from .constant import GAME_TYPE
|
||||
from .processor import Processor, User, identify_user_info
|
||||
|
||||
alc = on_alconna(
|
||||
Alconna(
|
||||
'茶服',
|
||||
Option(
|
||||
BIND_COMMAND[0],
|
||||
Args(
|
||||
Arg(
|
||||
'account',
|
||||
identify_user_info,
|
||||
notice='茶服 用户名 / TeaID',
|
||||
flags=[ArgFlag.HIDDEN],
|
||||
)
|
||||
),
|
||||
alias=BIND_COMMAND[1:],
|
||||
compact=True,
|
||||
dest='bind',
|
||||
help_text='绑定 茶服 账号',
|
||||
),
|
||||
Option(
|
||||
QUERY_COMMAND[0],
|
||||
Args(
|
||||
Arg(
|
||||
'target',
|
||||
At | Me,
|
||||
notice='@想要查询的人 | 自己',
|
||||
flags=[ArgFlag.HIDDEN, ArgFlag.OPTIONAL],
|
||||
),
|
||||
Arg(
|
||||
'account',
|
||||
identify_user_info,
|
||||
notice='茶服 用户名 / TeaID',
|
||||
flags=[ArgFlag.HIDDEN, ArgFlag.OPTIONAL],
|
||||
),
|
||||
# 如果放在一个 Union Args 里, 验证顺序不能保证, 可能出错
|
||||
),
|
||||
alias=QUERY_COMMAND[1:],
|
||||
compact=True,
|
||||
dest='query',
|
||||
help_text='查询 茶服 游戏信息',
|
||||
),
|
||||
meta=CommandMeta(
|
||||
description='查询 TetrisOnline茶服 的信息',
|
||||
example='茶服查我',
|
||||
compact=True,
|
||||
fuzzy_match=True,
|
||||
),
|
||||
),
|
||||
skip_for_unmatch=False,
|
||||
auto_send_output=True,
|
||||
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 _(event: MessageEvent, matcher: Matcher):
|
||||
await matcher.finish('QQ 平台无需绑定')
|
||||
|
||||
@alc.assign('query')
|
||||
async def _(bot: OB11Bot, event: MessageEvent, matcher: Matcher, target: At | Me):
|
||||
if event.is_tome() and await GROUP(bot, event):
|
||||
await matcher.finish('不能查询bot的信息')
|
||||
proc = Processor(
|
||||
event_id=id(event),
|
||||
user=User(teaid=target.target if isinstance(target, At) else event.get_user_id()),
|
||||
command_args=[],
|
||||
)
|
||||
try:
|
||||
await matcher.finish(await proc.handle_query())
|
||||
except NeedCatchError as e:
|
||||
await matcher.finish(str(e))
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
@alc.assign('bind')
|
||||
async def _(bot: Bot, event: Event, matcher: Matcher, account: User):
|
||||
proc = Processor(
|
||||
event_id=id(event),
|
||||
user=account,
|
||||
command_args=[],
|
||||
)
|
||||
try:
|
||||
await matcher.finish(await proc.handle_bind(platform=get_platform(bot), account=event.get_user_id()))
|
||||
except NeedCatchError as e:
|
||||
await matcher.finish(str(e))
|
||||
|
||||
|
||||
@alc.assign('query')
|
||||
async def _(bot: Bot, event: Event, matcher: Matcher, target: At | Me):
|
||||
async with get_session() as session:
|
||||
bind = await query_bind_info(
|
||||
session=session,
|
||||
chat_platform=get_platform(bot),
|
||||
chat_account=(target.target if isinstance(target, At) else event.get_user_id()),
|
||||
game_platform=GAME_TYPE,
|
||||
)
|
||||
if bind is None:
|
||||
await matcher.finish('未查询到绑定信息')
|
||||
message = '* 由于无法验证绑定信息, 不能保证查询到的用户为本人\n'
|
||||
proc = Processor(
|
||||
event_id=id(event),
|
||||
user=User(name=bind.game_account),
|
||||
command_args=[],
|
||||
)
|
||||
try:
|
||||
await matcher.finish(message + await proc.handle_query())
|
||||
except NeedCatchError as e:
|
||||
await matcher.finish(str(e))
|
||||
|
||||
|
||||
@alc.assign('query')
|
||||
async def _(event: Event, matcher: Matcher, account: User):
|
||||
proc = Processor(
|
||||
event_id=id(event),
|
||||
user=account,
|
||||
command_args=[],
|
||||
)
|
||||
try:
|
||||
await matcher.finish(await proc.handle_query())
|
||||
except NeedCatchError as e:
|
||||
await matcher.finish(str(e))
|
||||
|
||||
|
||||
@alc.handle()
|
||||
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"查看帮助'
|
||||
)
|
||||
@@ -0,0 +1,4 @@
|
||||
from ...utils.typing import GameType
|
||||
|
||||
GAME_TYPE: GameType = 'TOS'
|
||||
BASE_URL = 'https://teatube.cn:8888/'
|
||||
@@ -0,0 +1,221 @@
|
||||
from dataclasses import dataclass
|
||||
from re import match
|
||||
from typing import Any
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from nonebot_plugin_orm import get_session
|
||||
from pydantic import parse_raw_as
|
||||
|
||||
from ...db import create_or_update_bind
|
||||
from ...utils.exception import MessageFormatError, RequestError
|
||||
from ...utils.request import Request, splice_url
|
||||
from ...utils.typing import GameType
|
||||
from .. import ProcessedData as ProcessedDataMeta
|
||||
from .. import Processor as ProcessorMeta
|
||||
from .. import RawResponse as RawResponseMeta
|
||||
from .. import User as UserMeta
|
||||
from .constant import BASE_URL, GAME_TYPE
|
||||
from .schemas.user_info import SuccessModel as InfoSuccess
|
||||
from .schemas.user_info import UserInfo
|
||||
from .schemas.user_profile import UserProfile
|
||||
|
||||
|
||||
@dataclass
|
||||
class User(UserMeta):
|
||||
teaid: str | None = None
|
||||
name: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class RawResponse(RawResponseMeta):
|
||||
user_profile: dict[frozenset[tuple[str, Any]], bytes]
|
||||
user_info: bytes | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProcessedData(ProcessedDataMeta):
|
||||
user_profile: dict[frozenset[tuple[str, Any]], UserProfile]
|
||||
user_info: InfoSuccess | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class GameData:
|
||||
num: int
|
||||
pps: float
|
||||
lpm: float
|
||||
apm: float
|
||||
adpm: float
|
||||
apl: float
|
||||
adpl: float
|
||||
vs: float
|
||||
|
||||
|
||||
def identify_user_info(info: str) -> User | MessageFormatError:
|
||||
if (
|
||||
match(
|
||||
r'^(?!\.)(?!com[0-9]$)(?!con$)(?!lpt[0-9]$)(?!nul$)(?!prn$)[^\-][^\+][^\|\*\?\\\s\!:<>/$"]*[^\.\|\*\?\\\s\!:<>/$"]+$',
|
||||
info,
|
||||
)
|
||||
and info.isdigit() is False
|
||||
and 2 <= len(info) <= 18 # noqa: PLR2004
|
||||
):
|
||||
return User(name=info)
|
||||
if info.isdigit():
|
||||
return User(teaid=info)
|
||||
return MessageFormatError('用户名/QQ号不合法')
|
||||
|
||||
|
||||
class Processor(ProcessorMeta):
|
||||
user: User
|
||||
raw_response: RawResponse
|
||||
processed_data: ProcessedData
|
||||
|
||||
def __init__(self, event_id: int, user: User, command_args: list[str]) -> None:
|
||||
super().__init__(event_id, user, command_args)
|
||||
self.raw_response = RawResponse(user_profile={})
|
||||
self.processed_data = ProcessedData(user_profile={})
|
||||
|
||||
@property
|
||||
def game_platform(self) -> GameType:
|
||||
return GAME_TYPE
|
||||
|
||||
async def handle_bind(self, platform: str, account: str) -> str:
|
||||
"""处理绑定消息"""
|
||||
self.command_type = 'bind'
|
||||
await self.get_user()
|
||||
if self.user.name is None:
|
||||
raise # FIXME: 不知道怎么才能把这类型给变过来了
|
||||
async with get_session() as session:
|
||||
return await create_or_update_bind(
|
||||
session=session,
|
||||
chat_platform=platform,
|
||||
chat_account=account,
|
||||
game_platform=GAME_TYPE,
|
||||
game_account=self.user.name,
|
||||
)
|
||||
|
||||
async def handle_query(self) -> str:
|
||||
"""处理查询消息"""
|
||||
self.command_type = 'query'
|
||||
await self.get_user()
|
||||
return await self.generate_message()
|
||||
|
||||
async def get_user(self) -> None:
|
||||
"""
|
||||
用于获取 UserName 和 UserID 的函数
|
||||
"""
|
||||
if self.user.name is None:
|
||||
self.user.name = (await self.get_user_info()).data.name
|
||||
if self.user.teaid is None:
|
||||
self.user.teaid = (await self.get_user_info()).data.teaid
|
||||
|
||||
async def get_user_info(self) -> InfoSuccess:
|
||||
"""获取用户信息"""
|
||||
if self.processed_data.user_info is None:
|
||||
if self.user.teaid is not None:
|
||||
url = splice_url(
|
||||
[
|
||||
BASE_URL,
|
||||
'getTeaIdInfo',
|
||||
f'?{urlencode({"teaId":self.user.teaid})}',
|
||||
]
|
||||
)
|
||||
else:
|
||||
url = splice_url(
|
||||
[
|
||||
BASE_URL,
|
||||
'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]
|
||||
if not isinstance(user_info, InfoSuccess):
|
||||
raise RequestError(f'用户信息请求错误:\n{user_info.error}')
|
||||
self.processed_data.user_info = user_info
|
||||
return self.processed_data.user_info
|
||||
|
||||
async def get_user_profile(self, other_parameter: dict[str, Any] | None = None) -> UserProfile:
|
||||
"""获取用户数据"""
|
||||
if other_parameter is None:
|
||||
other_parameter = {}
|
||||
fset = frozenset(other_parameter.items())
|
||||
if self.processed_data.user_profile.get(fset) is None:
|
||||
self.raw_response.user_profile[fset] = await Request.request(
|
||||
splice_url(
|
||||
[
|
||||
BASE_URL,
|
||||
'getProfile',
|
||||
f'?{urlencode({"id":self.user.teaid or self.user.name},**other_parameter)}',
|
||||
]
|
||||
)
|
||||
)
|
||||
self.processed_data.user_profile[fset] = UserProfile.parse_raw(self.raw_response.user_profile[fset])
|
||||
return self.processed_data.user_profile[fset]
|
||||
|
||||
async def get_game_data(self) -> GameData | None:
|
||||
"""获取游戏数据"""
|
||||
user_profile = await self.get_user_profile()
|
||||
if user_profile.data == []:
|
||||
return None
|
||||
weighted_total_lpm = weighted_total_apm = weighted_total_adpm = 0.0
|
||||
total_time = 0.0
|
||||
num = 0
|
||||
for i in user_profile.data:
|
||||
# 排除单人局和时间为0的游戏
|
||||
# 茶: 不计算没挖掘的局, 即使apm和lpm也如此
|
||||
if i.num_players == 1 or i.time == 0 or i.dig is None:
|
||||
continue
|
||||
# 加权计算
|
||||
time = i.time / 1000
|
||||
lpm = 24 * (i.pieces / time)
|
||||
apm = (i.attack / time) * 60
|
||||
adpm = ((i.attack + i.dig) / time) * 60
|
||||
weighted_total_lpm += lpm * time
|
||||
weighted_total_apm += apm * time
|
||||
weighted_total_adpm += adpm * time
|
||||
total_time += time
|
||||
num += 1
|
||||
if num == 50: # noqa: PLR2004 # TODO: 将查询局数作为可选命令参数
|
||||
break
|
||||
if num == 0:
|
||||
return None
|
||||
# TODO: 如果有效局数不满50, 没有无dig信息的局, 且userData['data']内有50个局, 则继续往前获取信息
|
||||
lpm = weighted_total_lpm / total_time
|
||||
apm = weighted_total_apm / total_time
|
||||
adpm = weighted_total_adpm / total_time
|
||||
return GameData(
|
||||
num=num,
|
||||
pps=round(lpm / 24, 2),
|
||||
lpm=round(lpm, 2),
|
||||
apm=round(apm, 2),
|
||||
adpm=round(adpm, 2),
|
||||
apl=round((apm / lpm), 2),
|
||||
adpl=round((adpm / lpm), 2),
|
||||
vs=round((adpm / 60 * 100), 2),
|
||||
)
|
||||
|
||||
async def generate_message(self) -> str:
|
||||
"""生成消息"""
|
||||
user_info = (await self.get_user_info()).data
|
||||
message = f'用户 {user_info.name} ({user_info.teaid}) '
|
||||
if user_info.ranked_games == '0':
|
||||
message += '暂无段位统计数据'
|
||||
else:
|
||||
message += f', 段位分 {round(float(user_info.rating_now),2)}±{round(float(user_info.rd_now),2)} ({round(float(user_info.vol_now),2)}) '
|
||||
game_data = await self.get_game_data()
|
||||
if game_data is None:
|
||||
message += ', 暂无游戏数据'
|
||||
else:
|
||||
message += f', 最近 {game_data.num} 局数据'
|
||||
message += f"\nL'PM: {game_data.lpm} ( {game_data.pps} pps )"
|
||||
message += f'\nAPM: {game_data.apm} ( x{game_data.apl} )'
|
||||
message += f'\nADPM: {game_data.adpm} ( x{game_data.adpl} ) ( {game_data.vs}vs )'
|
||||
message += (
|
||||
f'\n40L: {float(user_info.pb_sprint)/1000:.2f}s'
|
||||
if user_info.pb_sprint != 2147483647 # noqa: PLR2004
|
||||
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
|
||||
@@ -0,0 +1,86 @@
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class SuccessModel(BaseModel):
|
||||
class Data(BaseModel):
|
||||
class PeriodMatch(BaseModel):
|
||||
name: str
|
||||
teaid: str = Field(..., alias='teaId')
|
||||
rating: str
|
||||
rd: str
|
||||
start_time: datetime = Field(..., alias='startTime')
|
||||
end_time: datetime = Field(..., alias='endTime')
|
||||
win: str
|
||||
lose: str
|
||||
score: str
|
||||
|
||||
class UserDataTotalItem(BaseModel):
|
||||
time_map: str = Field(..., alias='timeMap')
|
||||
pieces_map: str = Field(..., alias='piecesMap')
|
||||
clear_lines_map: str = Field(..., alias='clearLinesMap')
|
||||
attacks_map: str = Field(..., alias='attacksMap')
|
||||
dig_map: str = Field(..., alias='digMap')
|
||||
send_map: str = Field(..., alias='sendMap')
|
||||
rise_map: str = Field(..., alias='riseMap')
|
||||
offset_map: str = Field(..., alias='offsetMap')
|
||||
receive_map: str = Field(..., alias='receiveMap')
|
||||
games_map: str = Field(..., alias='gamesMap')
|
||||
tetris_map: str = Field(..., alias='tetrisMap')
|
||||
combo_map: str = Field(..., alias='comboMap')
|
||||
tspin_map: str = Field(..., alias='tspinMap')
|
||||
b2b_map: str = Field(..., alias='b2bMap')
|
||||
perfect_clear_map: str = Field(..., alias='perfectClearMap')
|
||||
time_no_map: str = Field(..., alias='timeNoMap')
|
||||
pieces_no_map: str = Field(..., alias='piecesNoMap')
|
||||
clear_lines_no_map: str = Field(..., alias='clearLinesNoMap')
|
||||
attacks_no_map: str = Field(..., alias='attacksNoMap')
|
||||
dig_no_map: str = Field(..., alias='digNoMap')
|
||||
send_no_map: str = Field(..., alias='sendNoMap')
|
||||
rise_no_map: str = Field(..., alias='riseNoMap')
|
||||
offset_no_map: str = Field(..., alias='offsetNoMap')
|
||||
receive_no_map: str = Field(..., alias='receiveNoMap')
|
||||
games_no_map: str = Field(..., alias='gamesNoMap')
|
||||
tetris_no_map: str = Field(..., alias='tetrisNoMap')
|
||||
combo_no_map: str = Field(..., alias='comboNoMap')
|
||||
tspin_no_map: str = Field(..., alias='tspinNoMap')
|
||||
b2b_no_map: str = Field(..., alias='b2bNoMap')
|
||||
perfect_clear_no_map: str = Field(..., alias='perfectClearNoMap')
|
||||
|
||||
teaid: str = Field(..., alias='teaId')
|
||||
name: str
|
||||
total_exp: str = Field(..., alias='totalExp')
|
||||
ranking: str
|
||||
ranked_games: str = Field(..., alias='rankedGames')
|
||||
rating_now: str = Field(..., alias='ratingNow')
|
||||
rd_now: str = Field(..., alias='rdNow')
|
||||
vol_now: str = Field(..., alias='volNow')
|
||||
rating_last: str = Field(..., alias='ratingLast')
|
||||
rd_last: str = Field(..., alias='rdLast')
|
||||
vol_last: str = Field(..., alias='volLast')
|
||||
period_matches: list[PeriodMatch] = Field(..., alias='periodMatches')
|
||||
user_data_total: list[UserDataTotalItem] = Field(..., alias='userDataTotal')
|
||||
ranking_items: str = Field(..., alias='rankingItems')
|
||||
ranking_game_items: str = Field(..., alias='rankingGameItems')
|
||||
training_level: str = Field(..., alias='trainingLevel')
|
||||
training_wins: str = Field(..., alias='trainingWins')
|
||||
pb_sprint: str = Field(..., alias='PBSprint')
|
||||
pb_marathon: str = Field(..., alias='PBMarathon')
|
||||
pb_challenge: str = Field(..., alias='PBChallenge')
|
||||
register_date: datetime = Field(..., alias='registerDate')
|
||||
last_login_date: datetime = Field(..., alias='lastLoginDate')
|
||||
|
||||
code: int
|
||||
success: Literal[True]
|
||||
data: Data
|
||||
|
||||
|
||||
class FailedModel(BaseModel):
|
||||
code: int
|
||||
success: Literal[False]
|
||||
error: str
|
||||
|
||||
|
||||
UserInfo = SuccessModel | FailedModel
|
||||
@@ -0,0 +1,33 @@
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class UserProfile(BaseModel):
|
||||
class Data(BaseModel):
|
||||
idmultiplayergameresult: int
|
||||
iduser: str
|
||||
teaid: str
|
||||
time: int
|
||||
clear_lines: int
|
||||
attack: int
|
||||
send: int
|
||||
offset: int
|
||||
receive: int
|
||||
rise: int
|
||||
dig: int
|
||||
pieces: int
|
||||
max_combo: int
|
||||
pc_count: int
|
||||
place: int
|
||||
num_players: int
|
||||
fumen_code: Literal['0', '1'] # wtf
|
||||
rule_set: str
|
||||
garbage: str
|
||||
idmultiplayergame: int
|
||||
datetime: datetime
|
||||
|
||||
code: int
|
||||
success: bool
|
||||
data: list[Data]
|
||||
@@ -1 +0,0 @@
|
||||
from . import config, database, message_analyzer
|
||||
92
nonebot_plugin_tetris_stats/utils/browser.py
Normal file
92
nonebot_plugin_tetris_stats/utils/browser.py
Normal file
@@ -0,0 +1,92 @@
|
||||
import sys
|
||||
from os import environ
|
||||
from platform import system
|
||||
|
||||
from nonebot import get_driver
|
||||
from nonebot.log import logger
|
||||
from playwright.__main__ import main
|
||||
from playwright.async_api import Browser, async_playwright
|
||||
|
||||
driver = get_driver()
|
||||
|
||||
global_config = driver.config
|
||||
|
||||
|
||||
@driver.on_startup
|
||||
async def _():
|
||||
await BrowserManager._init_playwright()
|
||||
|
||||
|
||||
@driver.on_shutdown
|
||||
async def _():
|
||||
await BrowserManager._close_browser()
|
||||
|
||||
|
||||
class BrowserManager:
|
||||
"""浏览器管理类"""
|
||||
|
||||
_browser: Browser | None = None
|
||||
|
||||
@classmethod
|
||||
async def _init_playwright(cls) -> None:
|
||||
if system() == 'Windows' and getattr(global_config, 'fastapi_reload', False):
|
||||
raise ImportError('加载失败, Windows 必须设置 FASTAPI_RELOAD=false 才能正常运行 playwright')
|
||||
logger.info('开始 安装/更新 playwright 浏览器')
|
||||
environ['PLAYWRIGHT_DOWNLOAD_HOST'] = 'https://npmmirror.com/mirrors/playwright/'
|
||||
if cls._handle_error(cls._call_playwright(['', 'install', 'firefox'])):
|
||||
logger.success('安装/更新 playwright 浏览器成功')
|
||||
else:
|
||||
logger.warning('playwright 浏览器 安装/更新 失败, 尝试使用原始仓库下载')
|
||||
del environ['PLAYWRIGHT_DOWNLOAD_HOST']
|
||||
if cls._handle_error(cls._call_playwright(['', 'install', 'firefox'])):
|
||||
logger.success('安装/更新 playwright 浏览器成功')
|
||||
else:
|
||||
logger.error('安装/更新 playwright 浏览器失败')
|
||||
try:
|
||||
await cls._start_browser()
|
||||
except BaseException as e: # noqa: BLE001 不知道会有什么异常, 交给用户解决
|
||||
raise ImportError(
|
||||
'playwright 启动失败, 请尝试在命令行运行 playwright install-deps firefox, 如果仍然启动失败, 请参考上面的报错👆'
|
||||
) from e
|
||||
else:
|
||||
logger.success('playwright 启动成功')
|
||||
|
||||
@classmethod
|
||||
def _call_playwright(cls, argv: list[str]) -> BaseException:
|
||||
"""等价于调用 playwright 的命令行程序"""
|
||||
argv_backup = sys.argv.copy()
|
||||
from re import sub
|
||||
|
||||
sys.argv[0] = sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.argv = argv
|
||||
try:
|
||||
main()
|
||||
except BaseException as e: # noqa: BLE001 不在这里处理 playwright 的异常
|
||||
return e
|
||||
finally:
|
||||
sys.argv = argv_backup
|
||||
return SystemExit(0)
|
||||
|
||||
@classmethod
|
||||
def _handle_error(cls, error: BaseException) -> bool:
|
||||
if isinstance(error, SystemExit) and error.code == 0:
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
async def _start_browser(cls) -> Browser:
|
||||
"""启动浏览器实例"""
|
||||
playwright = await async_playwright().start()
|
||||
cls._browser = await playwright.firefox.launch()
|
||||
return cls._browser
|
||||
|
||||
@classmethod
|
||||
async def get_browser(cls) -> Browser:
|
||||
"""获取浏览器实例"""
|
||||
return cls._browser or await cls._start_browser()
|
||||
|
||||
@classmethod
|
||||
async def _close_browser(cls) -> None:
|
||||
"""关闭浏览器实例"""
|
||||
if isinstance(cls._browser, Browser):
|
||||
await cls._browser.close()
|
||||
@@ -1,7 +0,0 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Config(BaseModel):
|
||||
'''配置类'''
|
||||
cache_path: str = 'cache/nonebot_plugin_tetris_stats/cache'
|
||||
db_path: str = 'data/nonebot_plugin_tetris_stats/data.db'
|
||||
@@ -1,141 +0,0 @@
|
||||
import datetime
|
||||
import os
|
||||
from asyncio import gather
|
||||
from sqlite3 import Connection, connect
|
||||
|
||||
from nonebot import get_driver
|
||||
from nonebot.log import logger
|
||||
|
||||
from .config import Config
|
||||
|
||||
driver = get_driver()
|
||||
|
||||
config = Config.parse_obj(get_driver().config)
|
||||
|
||||
|
||||
@driver.on_startup
|
||||
async def _():
|
||||
await DataBase.init_db()
|
||||
|
||||
|
||||
@driver.on_shutdown
|
||||
async def _():
|
||||
await DataBase.close_db()
|
||||
|
||||
|
||||
class DataBase():
|
||||
'''数据库交互类'''
|
||||
_db: Connection | None = None
|
||||
|
||||
@classmethod
|
||||
async def init_db(cls) -> Connection:
|
||||
'''初始化数据库'''
|
||||
if not os.path.exists(os.path.dirname(config.db_path)):
|
||||
os.makedirs(os.path.dirname(config.db_path))
|
||||
cls._db = connect(config.db_path)
|
||||
cursor = cls._db.cursor()
|
||||
cursor.execute('''CREATE TABLE IF NOT EXISTS IOBIND
|
||||
(QQ INTEGER NOT NULL,
|
||||
USER TEXT NOT NULL)''')
|
||||
cursor.execute('''CREATE TABLE IF NOT EXISTS TOPBIND
|
||||
(QQ INTEGER NOT NULL,
|
||||
USER TEXT NOT NULL)''')
|
||||
cursor.execute('''CREATE TABLE IF NOT EXISTS IORANK
|
||||
(RANK VARCHAR(2) NOT NULL,
|
||||
TRENDING CHAR(1) NOT NULL,
|
||||
TRLINE FLOAT NOT NULL,
|
||||
PLAYERCOUNT INTEGER NOT NULL,
|
||||
AVGAPM FLOAT NOT NULL,
|
||||
AVGPPS FLOAT NOT NULL,
|
||||
ARGVS FLOAT NOT NULL,
|
||||
DATE TEXT NOT NULL)''')
|
||||
cls._db.commit()
|
||||
logger.info('数据库初始化完成')
|
||||
return cls._db
|
||||
|
||||
@classmethod
|
||||
async def query_rank_info_today(cls, rank: str) -> list | None:
|
||||
'''查询段位信息'''
|
||||
db = await cls._get_db()
|
||||
cursor = db.cursor()
|
||||
cursor.execute('''SELECT TRENDING, TRLINE, PLAYERCOUNT, AVGAPM, AVGPPS, ARGVS, DATE
|
||||
FROM IORANK
|
||||
WHERE RANK = ? AND DATE = ?''', (rank, datetime.date.today()))
|
||||
result = cursor.fetchone()
|
||||
|
||||
if result is None:
|
||||
return None
|
||||
|
||||
return list(result)
|
||||
|
||||
@classmethod
|
||||
async def write_rank_info_today(cls, rank: str, trline: int, playercount: int, avgapm: float, avgpps: float, avgvs: float):
|
||||
'''写入段位信息'''
|
||||
|
||||
db = await cls._get_db()
|
||||
cursor = db.cursor()
|
||||
cursor.execute('''SELECT TRLINE
|
||||
FROM IORANK
|
||||
WHERE RANK = ? AND DATE = ?''', (rank, datetime.date.today() - datetime.timedelta(days=1)))
|
||||
|
||||
result = cursor.fetchone()
|
||||
|
||||
if result is None:
|
||||
trending = '?'
|
||||
elif trline > result[0]:
|
||||
trending = '↑'
|
||||
elif trline == result[0]:
|
||||
trending = '-'
|
||||
elif trline < result[0]:
|
||||
trending = '↓'
|
||||
|
||||
cursor.execute('''INSERT INTO IORANK
|
||||
(RANK, TRENDING, TRLINE, PLAYERCOUNT, AVGAPM, AVGPPS, ARGVS, DATE)
|
||||
VALUES (?,?,?,?,?,?,?,?)''',
|
||||
(rank, trending, trline, playercount, avgapm, avgpps, avgvs, datetime.date.today()))
|
||||
|
||||
db.commit()
|
||||
|
||||
@classmethod
|
||||
async def _get_db(cls) -> Connection:
|
||||
'''获取数据库对象'''
|
||||
return cls._db or await cls.init_db()
|
||||
|
||||
@classmethod
|
||||
async def query_bind_info(cls, qq_number: str | int, game_type: str) -> str | None:
|
||||
'''查询绑定信息'''
|
||||
db = await cls._get_db()
|
||||
cursor = db.cursor()
|
||||
cursor.execute(
|
||||
f'SELECT USER FROM {game_type}BIND WHERE QQ = {qq_number}')
|
||||
user = cursor.fetchone()
|
||||
if user is None:
|
||||
return None
|
||||
return user[0]
|
||||
|
||||
@classmethod
|
||||
async def write_bind_info(cls, qq_number: str | int, user: str, game_type: str) -> str:
|
||||
'''写入绑定信息'''
|
||||
bind_info, db = await gather(
|
||||
cls.query_bind_info(qq_number=qq_number, game_type=game_type),
|
||||
cls._get_db()
|
||||
)
|
||||
cursor = db.cursor()
|
||||
if bind_info is not None:
|
||||
cursor.execute(
|
||||
f'UPDATE {game_type}BIND SET USER = ? WHERE QQ = ?', (user, qq_number))
|
||||
message = '更新成功'
|
||||
elif bind_info is None:
|
||||
cursor.execute(
|
||||
f'INSERT INTO {game_type}BIND (QQ, USER) VALUES (?, ?)', (qq_number, user))
|
||||
message = '绑定成功'
|
||||
else:
|
||||
raise ValueError('预期外行为, 请上报GitHub')
|
||||
db.commit()
|
||||
return message
|
||||
|
||||
@classmethod
|
||||
async def close_db(cls) -> None:
|
||||
'''关闭数据库对象'''
|
||||
if isinstance(cls._db, Connection):
|
||||
cls._db.close()
|
||||
35
nonebot_plugin_tetris_stats/utils/exception.py
Normal file
35
nonebot_plugin_tetris_stats/utils/exception.py
Normal file
@@ -0,0 +1,35 @@
|
||||
class TetrisStatsError(Exception):
|
||||
"""所有 TetrisStats 发生的异常基类"""
|
||||
|
||||
def __init__(self, message: str = ''):
|
||||
self.message = message
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.message
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.message
|
||||
|
||||
|
||||
class NeedCatchError(TetrisStatsError):
|
||||
"""需要被捕获的异常基类"""
|
||||
|
||||
|
||||
class DoNotCatchError(TetrisStatsError):
|
||||
"""不应该被捕获的异常基类"""
|
||||
|
||||
|
||||
class RequestError(NeedCatchError):
|
||||
"""请求错误"""
|
||||
|
||||
|
||||
class MessageFormatError(NeedCatchError):
|
||||
"""用户发送的消息格式不正确"""
|
||||
|
||||
|
||||
class DatabaseVersionError(DoNotCatchError):
|
||||
"""数据库版本错误"""
|
||||
|
||||
|
||||
class WhatTheFuckError(DoNotCatchError):
|
||||
"""用于表示不应该出现的情况 ("""
|
||||
@@ -1,91 +0,0 @@
|
||||
from re import match, sub
|
||||
|
||||
|
||||
async def handle_bind_message(message: str, game_type: str) -> tuple[str | None, tuple]:
|
||||
'''返回值为tuple[gameType, tuple[message, user]]'''
|
||||
_cmd_aliases = {'IO': ['io绑定', 'iobind'],
|
||||
'TOP': ['top绑定', 'topbind']}
|
||||
# 剔除命令前缀
|
||||
for i in _cmd_aliases[game_type]:
|
||||
if match(rf'(?i){i}', message):
|
||||
message = sub(rf'(?i){i}', '', message)
|
||||
message = message.strip()
|
||||
break
|
||||
else:
|
||||
raise ValueError('预期外行为, 请上报GitHub')
|
||||
if message == '' or message.isspace():
|
||||
return None, ('用户名为空', None)
|
||||
return await check_name(message, game_type)
|
||||
|
||||
|
||||
async def handle_stats_query_message(message: str, game_type: str) -> tuple[str | None, tuple]:
|
||||
'''返回值为tuple[gameType, tuple[message, user]]'''
|
||||
_cmd_aliases = {'IO': ['io查', 'iostats'],
|
||||
'TOS': ['tos查', 'tostats', 'tosstats', '茶服查', '茶服stats'],
|
||||
'TOP': ['top查', 'topstats']}
|
||||
_me = [
|
||||
'我', '自己', '我等', '卑人', '愚', '老身', '爷', '老娘', '本姑娘', '本大爷', '鄙人', '寡人',
|
||||
'小生', '贫僧', '本人', '孤', '吾', '俺', '咱', '私', 'me', '洒家', '在下', '偶', '人家',
|
||||
'本小姐', '老夫', '老子', '朕', '本尊', '僕', '拙者', '妾', '儂', '自分', '吾輩', '我輩', '某',
|
||||
'己等', '俺等', '此方', '哥', '姐', '劳资', '本宝宝', '余', '本喵', 'watashi', 'i', 'myself',
|
||||
'self', 'oneself'
|
||||
]
|
||||
# 剔除命令前缀
|
||||
for i in _cmd_aliases[game_type]:
|
||||
if match(rf'(?i){i}', message):
|
||||
message = sub(rf'(?i){i}', '', message)
|
||||
message = message.strip()
|
||||
break
|
||||
if message == '' or message.isspace():
|
||||
return None, ('用户名为空', None)
|
||||
if message.startswith('[CQ:at,qq='):
|
||||
try:
|
||||
user = int(str(message).split('[CQ:at,qq=')[1].split(']')[0])
|
||||
except ValueError:
|
||||
return None, ('QQ号码不合法', None)
|
||||
else:
|
||||
return 'AT', (None, user)
|
||||
elif message in _me:
|
||||
# 会不会有人叫本姑娘 本大爷这种或许可以成为id的名字呢
|
||||
# TODO: 在判断是否可能是查自己的情况的时候 也去判断是否能成立为一个UserName
|
||||
return 'ME', (None, None)
|
||||
else:
|
||||
return await check_name(message, game_type)
|
||||
|
||||
|
||||
async def handle_rank_message(message: str) -> str:
|
||||
_cmd_aliases = ['io段位', 'iorank']
|
||||
# 剔除命令前缀
|
||||
for i in _cmd_aliases:
|
||||
if match(rf'(?i){i}', message):
|
||||
message = sub(rf'(?i){i}', '', message)
|
||||
message = message.strip()
|
||||
break
|
||||
else:
|
||||
raise ValueError('预期外行为, 请上报GitHub')
|
||||
return message
|
||||
|
||||
|
||||
async def check_name(name: str, game_type: str) -> tuple[str | None, tuple]:
|
||||
'''返回值为tuple[gameType, tuple[message, user]]'''
|
||||
if game_type == 'IO':
|
||||
if match(r'^[a-f0-9]{24}$', name):
|
||||
return 'ID', (None, name)
|
||||
if match(r'^[a-zA-Z0-9_-]{3,16}$', name):
|
||||
return 'Name', (None, name.lower())
|
||||
return None, ('用户名不合法', None)
|
||||
if game_type == 'TOP':
|
||||
if match(r'^[a-zA-Z0-9_]{1,16}$', name):
|
||||
return 'Name', (None, name)
|
||||
return None, ('用户名不合法', None)
|
||||
if game_type == 'TOS':
|
||||
if (match(r'^(?!\.)(?!com[0-9]$)(?!con$)(?!lpt[0-9]$)(?!nul$)(?!prn$)[^\-][^\+][^\|\*\?\\\s\!:<>/$"]*[^\.\|\*\?\\\s\!:<>/$"]+$', name)
|
||||
and name.isdigit() is False
|
||||
and 2 <= len(name) <= 18):
|
||||
# 虽然我也不想这么长 但是似乎确实得这么长
|
||||
# TODO 简化正则表达式
|
||||
return 'Name', (None, name)
|
||||
if name.isdigit() is True:
|
||||
return 'QQ', (None, name)
|
||||
return None, ('用户名不合法', None)
|
||||
return None, ('游戏类型错误', None)
|
||||
322
nonebot_plugin_tetris_stats/utils/metrics.py
Normal file
322
nonebot_plugin_tetris_stats/utils/metrics.py
Normal file
@@ -0,0 +1,322 @@
|
||||
from typing import overload
|
||||
|
||||
from .typing import Number
|
||||
|
||||
|
||||
class TetrisMetricsBaseWithPPS:
|
||||
def __init__(self, pps: Number, precision: int) -> None:
|
||||
self._pps = pps
|
||||
self.precision = precision
|
||||
|
||||
@property
|
||||
def _lpm(self) -> Number:
|
||||
return self._pps * 24
|
||||
|
||||
@property
|
||||
def lpm(self) -> Number:
|
||||
return round(self._lpm, self.precision)
|
||||
|
||||
@property
|
||||
def pps(self) -> Number:
|
||||
return round(self._pps, self.precision)
|
||||
|
||||
|
||||
class TetrisMetricsBaseWithLPM:
|
||||
def __init__(self, lpm: Number, precision: int) -> None:
|
||||
self._lpm = lpm
|
||||
self.precision = precision
|
||||
|
||||
@property
|
||||
def lpm(self) -> Number:
|
||||
return round(self._lpm, self.precision)
|
||||
|
||||
@property
|
||||
def _pps(self) -> Number:
|
||||
return self._lpm / 24
|
||||
|
||||
@property
|
||||
def pps(self) -> Number:
|
||||
return round(self._pps, self.precision)
|
||||
|
||||
|
||||
class TetrisMetricsBaseWithVS:
|
||||
def __init__(self, vs: Number, precision: int) -> None:
|
||||
self._vs = vs
|
||||
self.precision = precision
|
||||
|
||||
@property
|
||||
def vs(self) -> Number:
|
||||
return round(self._vs, self.precision)
|
||||
|
||||
@property
|
||||
def _adpm(self) -> Number:
|
||||
return self._vs * 0.6
|
||||
|
||||
@property
|
||||
def adpm(self) -> Number:
|
||||
return round(self._adpm, self.precision)
|
||||
|
||||
|
||||
class TetrisMetricsBaseWithADPM:
|
||||
def __init__(self, adpm: Number, precision: int) -> None:
|
||||
self._adpm = adpm
|
||||
self.precision = precision
|
||||
|
||||
@property
|
||||
def _vs(self) -> Number:
|
||||
return self._adpm / 0.6
|
||||
|
||||
@property
|
||||
def vs(self) -> Number:
|
||||
return round(self._vs, self.precision)
|
||||
|
||||
@property
|
||||
def adpm(self) -> Number:
|
||||
return round(self._adpm, self.precision)
|
||||
|
||||
|
||||
class TetrisMetricsBasicWithPPS(TetrisMetricsBaseWithPPS):
|
||||
def __init__(self, pps: Number, apm: Number, precision: int) -> None:
|
||||
super().__init__(pps=pps, precision=precision)
|
||||
self._apm = apm
|
||||
|
||||
@property
|
||||
def apm(self) -> Number:
|
||||
return round(self._apm, self.precision)
|
||||
|
||||
@property
|
||||
def apl(self) -> Number:
|
||||
return round(self._apm / self._lpm, self.precision)
|
||||
|
||||
|
||||
class TetrisMetricsBasicWithLPM(TetrisMetricsBaseWithLPM):
|
||||
def __init__(self, lpm: Number, apm: Number, precision: int):
|
||||
super().__init__(lpm=lpm, precision=precision)
|
||||
self._apm = apm
|
||||
|
||||
@property
|
||||
def apm(self) -> Number:
|
||||
return round(self._apm, self.precision)
|
||||
|
||||
@property
|
||||
def apl(self) -> Number:
|
||||
return round(self._apm / self._lpm, self.precision)
|
||||
|
||||
|
||||
class TetrisMetricsProWithPPSVS(TetrisMetricsBasicWithPPS, TetrisMetricsBaseWithVS):
|
||||
def __init__(self, pps: Number, apm: Number, vs: Number, precision: int) -> None:
|
||||
super().__init__(pps=pps, apm=apm, precision=precision)
|
||||
super(TetrisMetricsBaseWithPPS, self).__init__(vs=vs, precision=precision)
|
||||
|
||||
@property
|
||||
def adpl(self) -> Number:
|
||||
return round(self._adpm / self._lpm, self.precision)
|
||||
|
||||
|
||||
class TetrisMetricsProWithPPSADPM(TetrisMetricsBasicWithPPS, TetrisMetricsBaseWithADPM):
|
||||
def __init__(self, pps: Number, apm: Number, adpm: Number, precision: int) -> None:
|
||||
super().__init__(pps=pps, apm=apm, precision=precision)
|
||||
super(TetrisMetricsBaseWithPPS, self).__init__(adpm=adpm, precision=precision)
|
||||
|
||||
@property
|
||||
def adpl(self) -> Number:
|
||||
return round(self._adpm / self._lpm, self.precision)
|
||||
|
||||
|
||||
class TetrisMetricsProWithLPMVS(TetrisMetricsBasicWithLPM, TetrisMetricsBaseWithVS):
|
||||
def __init__(self, lpm: Number, apm: Number, vs: Number, precision: int) -> None:
|
||||
super().__init__(lpm=lpm, apm=apm, precision=precision)
|
||||
super(TetrisMetricsBaseWithLPM, self).__init__(vs=vs, precision=precision)
|
||||
|
||||
@property
|
||||
def adpl(self) -> Number:
|
||||
return round(self._adpm / self._lpm, self.precision)
|
||||
|
||||
|
||||
class TetrisMetricsProWithLPMADPM(TetrisMetricsBasicWithLPM, TetrisMetricsBaseWithADPM):
|
||||
def __init__(self, lpm: Number, apm: Number, adpm: Number, precision: int) -> None:
|
||||
super().__init__(lpm=lpm, apm=apm, precision=precision)
|
||||
super(TetrisMetricsBaseWithLPM, self).__init__(adpm=adpm, precision=precision)
|
||||
|
||||
@property
|
||||
def adpl(self) -> Number:
|
||||
return round(self._adpm / self._lpm, self.precision)
|
||||
|
||||
|
||||
@overload
|
||||
def get_metrics( # noqa: PLR0913
|
||||
*,
|
||||
pps: Number,
|
||||
lpm: None = None,
|
||||
apm: None = None,
|
||||
vs: None = None,
|
||||
adpm: None = None,
|
||||
precision: int = 2,
|
||||
) -> TetrisMetricsBaseWithPPS:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def get_metrics( # noqa: PLR0913
|
||||
*,
|
||||
pps: None = None,
|
||||
lpm: Number,
|
||||
apm: None = None,
|
||||
vs: None = None,
|
||||
adpm: None = None,
|
||||
precision: int = 2,
|
||||
) -> TetrisMetricsBaseWithLPM:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def get_metrics( # noqa: PLR0913
|
||||
*,
|
||||
pps: None = None,
|
||||
lpm: None = None,
|
||||
apm: None = None,
|
||||
vs: Number,
|
||||
adpm: None = None,
|
||||
precision: int = 2,
|
||||
) -> TetrisMetricsBaseWithVS:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def get_metrics( # noqa: PLR0913
|
||||
*,
|
||||
pps: None = None,
|
||||
lpm: None = None,
|
||||
apm: None = None,
|
||||
vs: None = None,
|
||||
adpm: Number,
|
||||
precision: int = 2,
|
||||
) -> TetrisMetricsBaseWithADPM:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def get_metrics( # noqa: PLR0913
|
||||
*,
|
||||
pps: Number,
|
||||
lpm: None = None,
|
||||
apm: Number,
|
||||
vs: None = None,
|
||||
adpm: None = None,
|
||||
precision: int = 2,
|
||||
) -> TetrisMetricsBasicWithPPS:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def get_metrics( # noqa: PLR0913
|
||||
*,
|
||||
pps: None = None,
|
||||
lpm: Number,
|
||||
apm: Number,
|
||||
vs: None = None,
|
||||
adpm: None = None,
|
||||
precision: int = 2,
|
||||
) -> TetrisMetricsBasicWithLPM:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def get_metrics( # noqa: PLR0913
|
||||
*,
|
||||
pps: Number,
|
||||
lpm: None = None,
|
||||
apm: Number,
|
||||
vs: Number,
|
||||
adpm: None = None,
|
||||
precision: int = 2,
|
||||
) -> TetrisMetricsProWithPPSVS:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def get_metrics( # noqa: PLR0913
|
||||
*,
|
||||
pps: Number,
|
||||
lpm: None = None,
|
||||
apm: Number,
|
||||
vs: None = None,
|
||||
adpm: Number,
|
||||
precision: int = 2,
|
||||
) -> TetrisMetricsProWithPPSADPM:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def get_metrics( # noqa: PLR0913
|
||||
*,
|
||||
pps: None = None,
|
||||
lpm: Number,
|
||||
apm: Number,
|
||||
vs: Number,
|
||||
adpm: None = None,
|
||||
precision: int = 2,
|
||||
) -> TetrisMetricsProWithLPMVS:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def get_metrics( # noqa: PLR0913
|
||||
*,
|
||||
pps: None = None,
|
||||
lpm: Number,
|
||||
apm: Number,
|
||||
vs: None = None,
|
||||
adpm: Number,
|
||||
precision: int = 2,
|
||||
) -> TetrisMetricsProWithLPMADPM:
|
||||
...
|
||||
|
||||
|
||||
def get_metrics( # noqa: PLR0911, PLR0912, PLR0913
|
||||
*,
|
||||
pps: Number | None = None,
|
||||
lpm: Number | None = None,
|
||||
apm: Number | None = None,
|
||||
vs: Number | None = None,
|
||||
adpm: Number | None = None,
|
||||
precision: int = 2,
|
||||
) -> (
|
||||
TetrisMetricsBaseWithPPS
|
||||
| TetrisMetricsBaseWithLPM
|
||||
| TetrisMetricsBaseWithVS
|
||||
| TetrisMetricsBaseWithADPM
|
||||
| TetrisMetricsBasicWithPPS
|
||||
| TetrisMetricsBasicWithLPM
|
||||
| TetrisMetricsProWithPPSVS
|
||||
| TetrisMetricsProWithLPMVS
|
||||
| TetrisMetricsProWithPPSADPM
|
||||
| TetrisMetricsProWithLPMADPM
|
||||
):
|
||||
if apm is None:
|
||||
if pps is not None:
|
||||
return TetrisMetricsBaseWithPPS(pps, precision=precision)
|
||||
if lpm is not None:
|
||||
return TetrisMetricsBaseWithLPM(lpm, precision=precision)
|
||||
if vs is not None:
|
||||
return TetrisMetricsBaseWithVS(vs, precision=precision)
|
||||
if adpm is not None:
|
||||
return TetrisMetricsBaseWithADPM(adpm, precision=precision)
|
||||
elif vs is None and adpm is None:
|
||||
if pps is not None:
|
||||
return TetrisMetricsBasicWithPPS(pps, apm, precision=precision)
|
||||
if lpm is not None:
|
||||
return TetrisMetricsBasicWithLPM(lpm, apm, precision=precision)
|
||||
else:
|
||||
if vs is not None:
|
||||
if pps is not None:
|
||||
return TetrisMetricsProWithPPSVS(pps, apm, vs, precision=precision)
|
||||
if lpm is not None:
|
||||
return TetrisMetricsProWithLPMVS(lpm, apm, vs, precision=precision)
|
||||
if adpm is not None:
|
||||
if pps is not None:
|
||||
return TetrisMetricsProWithPPSADPM(pps, apm, adpm, precision=precision)
|
||||
if lpm is not None:
|
||||
return TetrisMetricsProWithLPMADPM(lpm, apm, adpm, precision=precision)
|
||||
|
||||
raise TypeError
|
||||
19
nonebot_plugin_tetris_stats/utils/platform.py
Normal file
19
nonebot_plugin_tetris_stats/utils/platform.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from nonebot.adapters import Bot
|
||||
|
||||
|
||||
def get_platform(bot: Bot) -> str:
|
||||
try:
|
||||
from nonebot.adapters.onebot.v12 import Bot as OB12Bot
|
||||
|
||||
if isinstance(bot, OB12Bot):
|
||||
return bot.platform
|
||||
except ImportError:
|
||||
pass
|
||||
try:
|
||||
from nonebot.adapters.satori import Bot as SaBot
|
||||
|
||||
if isinstance(bot, SaBot):
|
||||
return bot.platform
|
||||
except ImportError:
|
||||
pass
|
||||
return bot.type
|
||||
79
nonebot_plugin_tetris_stats/utils/recorder.py
Normal file
79
nonebot_plugin_tetris_stats/utils/recorder.py
Normal file
@@ -0,0 +1,79 @@
|
||||
from datetime import UTC, datetime
|
||||
from typing import ClassVar
|
||||
|
||||
from nonebot import get_driver, get_plugin
|
||||
from nonebot.adapters import Bot, Event
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot.message import run_postprocessor, run_preprocessor
|
||||
from nonebot_plugin_orm import get_session
|
||||
|
||||
from ..db.models import HistoricalData
|
||||
|
||||
driver = get_driver()
|
||||
|
||||
|
||||
class Recorder:
|
||||
matchers: ClassVar[set[type[Matcher]]] = set()
|
||||
historical_data: ClassVar[dict[int, tuple[HistoricalData, bool]]] = {}
|
||||
|
||||
@classmethod
|
||||
def create_historical_data(cls, event_id: int, historical_data: HistoricalData) -> None:
|
||||
cls.historical_data[event_id] = (historical_data, False)
|
||||
|
||||
@classmethod
|
||||
def update_historical_data(cls, event_id: int, historical_data: HistoricalData) -> None:
|
||||
if event_id not in cls.historical_data:
|
||||
raise KeyError
|
||||
cls.historical_data[event_id] = (historical_data, True)
|
||||
|
||||
@classmethod
|
||||
def get_historical_data(cls, event_id: int) -> HistoricalData:
|
||||
return cls.historical_data[event_id][0]
|
||||
|
||||
@classmethod
|
||||
async def save_historical_data(cls, event_id: int) -> None:
|
||||
if event_id not in cls.historical_data:
|
||||
raise KeyError
|
||||
historical_data, completed = cls.historical_data.pop(event_id)
|
||||
if completed:
|
||||
async with get_session() as session:
|
||||
session.add(historical_data)
|
||||
await session.commit()
|
||||
|
||||
@classmethod
|
||||
def del_historical_data(cls, event_id: int) -> None:
|
||||
cls.historical_data.pop(event_id)
|
||||
|
||||
|
||||
@driver.on_startup
|
||||
def _():
|
||||
plugin = get_plugin('nonebot_plugin_tetris_stats')
|
||||
if plugin is not None:
|
||||
Recorder.matchers = plugin.matcher
|
||||
else:
|
||||
raise RuntimeError('获取不到自身插件对象')
|
||||
|
||||
|
||||
@run_preprocessor
|
||||
def _(bot: Bot, event: Event, matcher: Matcher):
|
||||
if isinstance(matcher, tuple(Recorder.matchers)):
|
||||
Recorder.create_historical_data(
|
||||
event_id=id(event),
|
||||
historical_data=HistoricalData(
|
||||
trigger_time=datetime.now(tz=UTC),
|
||||
bot_platform=bot.type,
|
||||
bot_account=bot.self_id,
|
||||
source_type=event.get_type(),
|
||||
source_account=event.get_session_id(),
|
||||
message=event.get_message(),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@run_postprocessor
|
||||
async def _(event: Event, matcher: Matcher, exception: Exception | None):
|
||||
if isinstance(matcher, tuple(Recorder.matchers)):
|
||||
if exception is not None:
|
||||
Recorder.del_historical_data(id(event))
|
||||
else:
|
||||
await Recorder.save_historical_data(id(event))
|
||||
128
nonebot_plugin_tetris_stats/utils/request.py
Normal file
128
nonebot_plugin_tetris_stats/utils/request.py
Normal file
@@ -0,0 +1,128 @@
|
||||
from urllib.parse import urljoin, urlparse
|
||||
|
||||
from aiofiles import open
|
||||
from httpx import AsyncClient, HTTPError
|
||||
from nonebot import get_driver
|
||||
from nonebot.log import logger
|
||||
from playwright.async_api import Response
|
||||
from ujson import JSONDecodeError, dumps, loads
|
||||
|
||||
from ..config.config import CACHE_PATH
|
||||
from .browser import BrowserManager
|
||||
from .exception import RequestError
|
||||
|
||||
driver = get_driver()
|
||||
|
||||
|
||||
@driver.on_startup
|
||||
async def _():
|
||||
await Request._init_cache()
|
||||
await Request._read_cache()
|
||||
|
||||
|
||||
@driver.on_shutdown
|
||||
async def _():
|
||||
await Request._write_cache()
|
||||
|
||||
|
||||
def splice_url(url_list: list[str]) -> str:
|
||||
url = ''
|
||||
if len(url_list):
|
||||
url = url_list.pop(0)
|
||||
for i in url_list:
|
||||
url = urljoin(url, i)
|
||||
return url
|
||||
|
||||
|
||||
class Request:
|
||||
"""网络请求相关类"""
|
||||
|
||||
_CACHE_FILE = CACHE_PATH.joinpath('cloudflare_cache.json')
|
||||
_headers: dict | None = None
|
||||
_cookies: dict | None = None
|
||||
|
||||
@classmethod
|
||||
async def _anti_cloudflare(cls, url: str) -> bytes:
|
||||
"""用firefox硬穿五秒盾"""
|
||||
browser = await BrowserManager.get_browser()
|
||||
context = await browser.new_context()
|
||||
page = await context.new_page()
|
||||
response = await page.goto(url)
|
||||
attempts = 0
|
||||
while attempts < 60: # noqa: PLR2004
|
||||
attempts += 1
|
||||
text = await page.locator('body').text_content()
|
||||
if text is None:
|
||||
await page.wait_for_timeout(1000)
|
||||
continue
|
||||
if await page.title() == 'Please Wait... | Cloudflare':
|
||||
logger.warning('疑似触发了 Cloudflare 的验证码')
|
||||
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:
|
||||
cls._cookies = {i['name']: i['value'] for i in await context.cookies()}
|
||||
except KeyError:
|
||||
cls._cookies = None
|
||||
await page.close()
|
||||
await context.close()
|
||||
return await response.body()
|
||||
await page.close()
|
||||
await context.close()
|
||||
raise RequestError('绕过五秒盾失败')
|
||||
|
||||
@classmethod
|
||||
async def _init_cache(cls) -> None:
|
||||
"""初始化缓存文件"""
|
||||
if not cls._CACHE_FILE.exists():
|
||||
async with open(file=cls._CACHE_FILE, mode='w', encoding='UTF-8') as file:
|
||||
await file.write(dumps({'headers': cls._headers, 'cookies': cls._cookies}))
|
||||
|
||||
@classmethod
|
||||
async def _read_cache(cls) -> None:
|
||||
"""读取缓存文件"""
|
||||
try:
|
||||
async with open(file=cls._CACHE_FILE, mode='r', encoding='UTF-8') as file:
|
||||
json = loads(await file.read())
|
||||
except FileNotFoundError:
|
||||
await cls._init_cache()
|
||||
except (PermissionError, JSONDecodeError):
|
||||
cls._CACHE_FILE.unlink()
|
||||
await cls._init_cache()
|
||||
else:
|
||||
cls._headers = json['headers']
|
||||
cls._cookies = json['cookies']
|
||||
|
||||
@classmethod
|
||||
async def _write_cache(cls) -> None:
|
||||
"""写入缓存文件"""
|
||||
try:
|
||||
async with open(file=cls._CACHE_FILE, mode='r+', encoding='UTF-8') as file:
|
||||
await file.write(dumps({'headers': cls._headers, 'cookies': cls._cookies}))
|
||||
except FileNotFoundError:
|
||||
await cls._init_cache()
|
||||
except (PermissionError, JSONDecodeError):
|
||||
cls._CACHE_FILE.unlink()
|
||||
await cls._init_cache()
|
||||
|
||||
@classmethod
|
||||
async def request(cls, url: str, *, is_json: bool = True) -> bytes:
|
||||
"""请求api"""
|
||||
try:
|
||||
async with AsyncClient(cookies=cls._cookies) as session:
|
||||
response = await session.get(url, headers=cls._headers)
|
||||
if is_json:
|
||||
loads(response.content)
|
||||
return response.content
|
||||
except HTTPError as e:
|
||||
raise RequestError(f'请求错误\n{e!r}') from e
|
||||
except JSONDecodeError:
|
||||
if urlparse(url).netloc.lower().endswith('tetr.io'):
|
||||
return await cls._anti_cloudflare(url)
|
||||
raise
|
||||
61
nonebot_plugin_tetris_stats/utils/typing.py
Normal file
61
nonebot_plugin_tetris_stats/utils/typing.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any, Literal
|
||||
|
||||
Number = int | float
|
||||
GameType = Literal['IO', 'TOP', 'TOS']
|
||||
CommandType = Literal['bind', 'query']
|
||||
AsyncCallable = Callable[..., Awaitable[Any]]
|
||||
Me = Literal[
|
||||
'我',
|
||||
'自己',
|
||||
'我等',
|
||||
'卑人',
|
||||
'愚',
|
||||
'老身',
|
||||
'爷',
|
||||
'老娘',
|
||||
'本姑娘',
|
||||
'本大爷',
|
||||
'鄙人',
|
||||
'寡人',
|
||||
'小生',
|
||||
'贫僧',
|
||||
'本人',
|
||||
'孤',
|
||||
'吾',
|
||||
'俺',
|
||||
'咱',
|
||||
'私',
|
||||
'me',
|
||||
'洒家',
|
||||
'在下',
|
||||
'偶',
|
||||
'人家',
|
||||
'本小姐',
|
||||
'老夫',
|
||||
'老子',
|
||||
'朕',
|
||||
'本尊',
|
||||
'僕',
|
||||
'拙者',
|
||||
'妾',
|
||||
'儂',
|
||||
'自分',
|
||||
'吾輩',
|
||||
'我輩',
|
||||
'某',
|
||||
'己等',
|
||||
'俺等',
|
||||
'此方',
|
||||
'哥',
|
||||
'姐',
|
||||
'劳资',
|
||||
'本宝宝',
|
||||
'余',
|
||||
'本喵',
|
||||
'watashi',
|
||||
'i',
|
||||
'myself',
|
||||
'self',
|
||||
'oneself',
|
||||
]
|
||||
3
nonebot_plugin_tetris_stats/version.py
Normal file
3
nonebot_plugin_tetris_stats/version.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from importlib.metadata import version
|
||||
|
||||
__version__ = version('nonebot_plugin_tetris_stats')
|
||||
Reference in New Issue
Block a user