重构webui适配 (#1801)

* ♻️ 使用Uninfo重构PlatformUtils基础方法

* 🩹 优化插件加载与模块格式转换逻辑

* 🚑 修复商店道具无法使用

* 🚑 修复道具无法正常使用

* 🔧 增加Bot状态管理及模块禁用功能

* 🎨  优化Web UI代码结构,修改target方法

* 🚨 auto fix by pre-commit hooks

* 🎨 添加菜单API及优化异常处理

* 🐛 优化菜单API及模型结构,修复WebUi插件列表Api

* 📝 更新仓库readme

* 🚨 add mdlint file

* 📝 Add help chapter.

* 🐛 修复优化AuthChecker逻辑

* 🐛 优化数据库API,移除冗余导入及修正SQL_DICT引用

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: BalconyJH <balconyjh@gmail.com>
This commit is contained in:
HibiKier 2024-12-25 12:03:49 +08:00 committed by GitHub
parent ebf05fd884
commit 35014e4048
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 1939 additions and 1754 deletions

4
.markdownlint.yaml Normal file
View File

@ -0,0 +1,4 @@
MD013: false
MD024: # 重复标题
siblings_only: true
MD033: false # 允许 html

830
README.md
View File

@ -1,21 +1,43 @@
<!-- markdownlint-disable MD033 MD041 -->
<div align=center> <div align=center>
<img width="250" height="312" src="https://github.com/HibiKier/zhenxun_bot/blob/main/docs_image/tt.jpg"/> <img width="250" height="312" src=./docs_image/tt.jpg alt="zhenxun_bot"/>
</div> </div>
<div align=center> <div align=center>
<a href="./LICENSE">
![python](https://img.shields.io/badge/python-v3.9%2B-blue) <img src="https://img.shields.io/badge/license-AGPL3.0-FE7D37" alt="license">
![nonebot](https://img.shields.io/badge/nonebot-v2.1.3-yellow) </a>
![onebot](https://img.shields.io/badge/onebot-v11-black) <a href="https://www.python.org">
<img src="https://img.shields.io/badge/Python-3.10%20%7C%203.11%20%7C%203.12-blue" alt="python">
</a>
<a href="https://nonebot.dev/">
<img src="https://img.shields.io/badge/nonebot-v2.1.3-EA5252" alt="nonebot">
</a>
<a href="https://onebot.dev/">
<img src="https://img.shields.io/badge/OneBot-v11-black?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAAIVBMVEUAAAAAAAADAwMHBwceHh4UFBQNDQ0ZGRkoKCgvLy8iIiLWSdWYAAAAAXRSTlMAQObYZgAAAQVJREFUSMftlM0RgjAQhV+0ATYK6i1Xb+iMd0qgBEqgBEuwBOxU2QDKsjvojQPvkJ/ZL5sXkgWrFirK4MibYUdE3OR2nEpuKz1/q8CdNxNQgthZCXYVLjyoDQftaKuniHHWRnPh2GCUetR2/9HsMAXyUT4/3UHwtQT2AggSCGKeSAsFnxBIOuAggdh3AKTL7pDuCyABcMb0aQP7aM4AnAbc/wHwA5D2wDHTTe56gIIOUA/4YYV2e1sg713PXdZJAuncdZMAGkAukU9OAn40O849+0ornPwT93rphWF0mgAbauUrEOthlX8Zu7P5A6kZyKCJy75hhw1Mgr9RAUvX7A3csGqZegEdniCx30c3agAAAABJRU5ErkJggg==" alt="onebot">
</a>
<a href="https://onebot.dev/">
<img src="https://img.shields.io/badge/OneBot-v12-black?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAAIVBMVEUAAAAAAAADAwMHBwceHh4UFBQNDQ0ZGRkoKCgvLy8iIiLWSdWYAAAAAXRSTlMAQObYZgAAAQVJREFUSMftlM0RgjAQhV+0ATYK6i1Xb+iMd0qgBEqgBEuwBOxU2QDKsjvojQPvkJ/ZL5sXkgWrFirK4MibYUdE3OR2nEpuKz1/q8CdNxNQgthZCXYVLjyoDQftaKuniHHWRnPh2GCUetR2/9HsMAXyUT4/3UHwtQT2AggSCGKeSAsFnxBIOuAggdh3AKTL7pDuCyABcMb0aQP7aM4AnAbc/wHwA5D2wDHTTe56gIIOUA/4YYV2e1sg713PXdZJAuncdZMAGkAukU9OAn40O849+0ornPwT93rphWF0mgAbauUrEOthlX8Zu7P5A6kZyKCJy75hhw1Mgr9RAUvX7A3csGqZegEdniCx30c3agAAAABJRU5ErkJggg==" alt="onebot">
</a>
<a href="https://bot.q.qq.com/wiki/">
<img src="https://img.shields.io/badge/QQ-Bot-lightgrey?style=social&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMTIuODIgMTMwLjg5Ij48ZyBkYXRhLW5hbWU9IuWbvuWxgiAyIj48ZyBkYXRhLW5hbWU9IuWbvuWxgiAxIj48cGF0aCBkPSJNNTUuNjMgMTMwLjhjLTcgMC0xMy45LjA4LTIwLjg2IDAtMTkuMTUtLjI1LTMxLjcxLTExLjQtMzQuMjItMzAuMy00LjA3LTMwLjY2IDE0LjkzLTU5LjIgNDQuODMtNjYuNjQgMi0uNTEgNS4yMS0uMzEgNS4yMS0xLjYzIDAtMi4xMy4xNC0yLjEzLjE0LTUuNTcgMC0uODktMS4zLTEuNDYtMi4yMi0yLjMxLTYuNzMtNi4yMy03LjY3LTEzLjQxLTEtMjAuMTggNS40LTUuNTIgMTEuODctNS40IDE3LjgtLjU5IDYuNDkgNS4yNiA2LjMxIDEzLjA4LS44NiAyMS0uNjguNzQtMS43OCAxLjYtMS43OCAyLjY3djQuMjFjMCAxLjM1IDIuMiAxLjYyIDQuNzkgMi4zNSAzMS4wOSA4LjY1IDQ4LjE3IDM0LjEzIDQ1IDY2LjM3LTEuNzYgMTguMTUtMTQuNTYgMzAuMjMtMzIuNyAzMC42My04LjAyLjE5LTE2LjA3LS4wMS0yNC4xMy0uMDF6IiBmaWxsPSIjMDI5OWZlIi8+PHBhdGggZD0iTTMxLjQ2IDExOC4zOGMtMTAuNS0uNjktMTYuOC02Ljg2LTE4LjM4LTE3LjI3LTMtMTkuNDIgMi43OC0zNS44NiAxOC40Ni00Ny44MyAxNC4xNi0xMC44IDI5Ljg3LTEyIDQ1LjM4LTMuMTkgMTcuMjUgOS44NCAyNC41OSAyNS44MSAyNCA0NS4yOS0uNDkgMTUuOS04LjQyIDIzLjE0LTI0LjM4IDIzLjUtNi41OS4xNC0xMy4xOSAwLTE5Ljc5IDAiIGZpbGw9IiNmZWZlZmUiLz48cGF0aCBkPSJNNDYuMDUgNzkuNThjLjA5IDUgLjIzIDkuODItNyA5Ljc3LTcuODItLjA2LTYuMS01LjY5LTYuMjQtMTAuMTktLjE1LTQuODItLjczLTEwIDYuNzMtOS44NHM2LjM3IDUuNTUgNi41MSAxMC4yNnoiIGZpbGw9IiMxMDlmZmUiLz48cGF0aCBkPSJNODAuMjcgNzkuMjdjLS41MyAzLjkxIDEuNzUgOS42NC01Ljg4IDEwLTcuNDcuMzctNi44MS00LjgyLTYuNjEtOS41LjItNC4zMi0xLjgzLTEwIDUuNzgtMTAuNDJzNi41OSA0Ljg5IDYuNzEgOS45MnoiIGZpbGw9IiMwODljZmUiLz48L2c+PC9nPjwvc3ZnPg==" alt="QQ">
</a>
<a href="https://github.com/psf/black">
<img src="https://img.shields.io/badge/code%20style-black-000000.svg?logo=python&logoColor=edb641" alt="black">
</a>
<a href="https://github.com/Microsoft/pyright">
<img src="https://img.shields.io/badge/types-pyright-797952.svg?logo=python&logoColor=edb641" alt="pyright">
</a>
<a href="https://github.com/astral-sh/ruff">
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json" alt="ruff">
</a>
</div> </div>
<div align=center> <div align=center>
[![license](https://img.shields.io/badge/license-AGPL3.0-FE7D37)](https://github.com/HibiKier/zhenxun_bot/blob/main/LICENSE) [![tencent-qq](https://img.shields.io/badge/%E7%BE%A4-是真寻酱哒-red?style=logo=tencent-qq)](https://qm.qq.com/q/mRNtLSl6uc)
[![tencent-qq](https://img.shields.io/badge/%E7%BE%A4-是真寻酱哒-red?style=logo=tencent-qq)](https://jq.qq.com/?_wv=1027&k=u8PgBkMZ)
[![tencent-qq](https://img.shields.io/badge/%E7%BE%A4-真寻的技术群-c73e7e?style=logo=tencent-qq)](https://qm.qq.com/q/YYYt5rkMYc) [![tencent-qq](https://img.shields.io/badge/%E7%BE%A4-真寻的技术群-c73e7e?style=logo=tencent-qq)](https://qm.qq.com/q/YYYt5rkMYc)
</div> </div>
@ -36,7 +58,7 @@
“真寻是<strong>[椛椛](https://github.com/FloatTech/ZeroBot-Plugin)</strong>的好朋友!” “真寻是<strong>[椛椛](https://github.com/FloatTech/ZeroBot-Plugin)</strong>的好朋友!”
:tada:喜欢真寻,于是真寻就来了!:tada: 🎉喜欢真寻,于是真寻就来了!🎉
本项目符合 [OneBot](https://github.com/howmanybots/onebot) 标准,可基于以下项目与机器人框架/平台进行交互 本项目符合 [OneBot](https://github.com/howmanybots/onebot) 标准,可基于以下项目与机器人框架/平台进行交互
@ -50,28 +72,27 @@
<div align=center> <div align=center>
![Star Trend](https://api.star-history.com/svg?repos=HibiKier/zhenxun_bot&type=Timeline) <img src="https://api.star-history.com/svg?repos=HibiKier/zhenxun_bot&type=Timeline" alt="Star Trend" width="800" />
</div> </div>
## 真寻觉得你需要帮助 ## 🤝 帮助页面
<div align=center> <details>
<summary>点击展开查看图片</summary>
<img width="300" height="auto" src="./docs_image/zhenxun_help.png" alt="zhenxun_help"/>
<img width="300" height="auto" src="./docs_image/html_help.png" alt="html_help"/>
<img width="300" height="auto" src="./docs_image/help.png" alt="help"/>
</details>
<img width="350" height="350" src="https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs_image/help.png"/> ## 📦 这是一份扩展
<img width="250" height="500" src="https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs_image/html_help.png"/>
<img width="180" height="450" src="https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs_image/zhenxun_help.png"/>
</div>
## 这是一份扩展
### 1. 体验一下? ### 1. 体验一下?
这是一个免费的,版本为 dev 的 zhenxun你可以通过 [napcat](https://github.com/NapNeko/NapCatQQ) 或 [拉格朗日](https://github.com/LagrangeDev/Lagrange.Core) 以及 [matcha](https://github.com/A-kirami/matcha) 等直接连接用于体验与测试 这是一个免费的,版本为 dev 的 zhenxun你可以通过 [napcat](https://github.com/NapNeko/NapCatQQ) 或 [拉格朗日](https://github.com/LagrangeDev/Lagrange.Core) 以及 [matcha](https://github.com/A-kirami/matcha) 等直接连接用于体验与测试
(球球了测试君!) (球球了测试君!)
``` ```text
Url: ws://test.zhenxun.org:8080/onebot/v11/ws Url: ws://test.zhenxun.org:8080/onebot/v11/ws
AccessToken: PUBLIC_ZHENXUN_TEST AccessToken: PUBLIC_ZHENXUN_TEST
@ -89,39 +110,23 @@ AccessToken: PUBLIC_ZHENXUN_TEST
| [插件库](https://github.com/zhenxun-org/zhenxun_bot_plugins) | 插件 | [zhenxun-org](https://github.com/zhenxun-org) | 原 plugins 文件夹插件 | | [插件库](https://github.com/zhenxun-org/zhenxun_bot_plugins) | 插件 | [zhenxun-org](https://github.com/zhenxun-org) | 原 plugins 文件夹插件 |
| [插件索引库](https://github.com/zhenxun-org/zhenxun_bot_plugins_index) | 插件 | [zhenxun-org](https://github.com/zhenxun-org) | 扩展插件索引库 | | [插件索引库](https://github.com/zhenxun-org/zhenxun_bot_plugins_index) | 插件 | [zhenxun-org](https://github.com/zhenxun-org) | 扩展插件索引库 |
| [一键安装](https://github.com/soloxiaoye2022/zhenxun_bot-deploy) | 安装 | [soloxiaoye2022](https://github.com/soloxiaoye2022) | 第三方 | | [一键安装](https://github.com/soloxiaoye2022/zhenxun_bot-deploy) | 安装 | [soloxiaoye2022](https://github.com/soloxiaoye2022) | 第三方 |
| [WebUi](https://github.com/HibiKier/zhenxun_bot_webui) | 管理 | [hibikier](https://github.com/HibiKier) | 基于真寻 WebApi 的 webui 实现 | | [WebUi](https://github.com/HibiKier/zhenxun_bot_webui) | 管理 | [hibikier](https://github.com/HibiKier) | 基于真寻 WebApi 的 webui 实现 [预览](#-webui界面展示) |
| [安卓 app(WebUi)](https://github.com/YuS1aN/zhenxun_bot_android_ui) | 安装 | [YuS1aN](https://github.com/YuS1aN) | 第三方 | | [安卓 app(WebUi)](https://github.com/YuS1aN/zhenxun_bot_android_ui) | 安装 | [YuS1aN](https://github.com/YuS1aN) | 第三方 |
<details>
<summary> <strong> WebUI </strong>后台示例图 </summary>
![x](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs_image/webui00.png)
![x](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs_image/webui01.png)
![x](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs_image/webui02.png)
![x](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs_image/webui03.png)
![x](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs_image/webui04.png)
![x](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs_image/webui05.png)
![x](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs_image/webui06.png)
![x](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs_image/webui07.png)
</details>
<br/>
</div> </div>
## ~~来点优点?~~ 可爱难道还不够吗 ## 🥰 ~~来点优点?~~ 可爱难道还不够吗
- 实现了许多功能,且提供了大量功能管理命令,进行了多平台适配,兼容 nb2 商店插件 - 实现了许多功能,且提供了大量功能管理命令,进行了多平台适配,兼容 nb2 商店插件
- 拥有完善可用的 webui - 拥有完善可用的 webui
- 通过 Config 配置项将所有插件配置统计保存至 config.yaml利于统一用户修改 - 通过 Config 配置项将所有插件配置统计保存至 config.yaml利于统一用户修改
- 方便增删插件,原生 nonebot2 matcher不需要额外修改仅仅通过简单的配置属性就可以生成`帮助图片`和`帮助信息` - 方便增删插件,原生 nonebot2 matcher不需要额外修改仅仅通过简单的配置属性就可以生成`帮助图片`和`帮助信息`
- 提供了 cd阻塞每日次数等限制仅仅通过简单的属性就可以生成一个限制例如`PluginCdBlock` 等 - 提供了 cd阻塞每日次数等限制仅仅通过简单的属性就可以生成一个限制例如`PluginCdBlock` 等
- **..... 更多详细请通过[[传送门](https://hibikier.github.io/zhenxun_bot/)]查看文档!** - **更多详细请通过 [传送门](https://hibikier.github.io/zhenxun_bot/) 查看文档!**
## 简单部署 ## 🛠️ 简单部署
``` ```bash
# 获取代码 # 获取代码
git clone https://github.com/HibiKier/zhenxun_bot.git git clone https://github.com/HibiKier/zhenxun_bot.git
@ -134,636 +139,154 @@ poetry install # 安装依赖
# 开始运行 # 开始运行
poetry shell # 进入虚拟环境 poetry shell # 进入虚拟环境
python bot.py python bot.py # 运行机器人
# 首次后会在data目录下生成config.yaml文件
# config.yaml用户配置插件
``` ```
## 简单配置 ## 📝 简单配置
``` > [!TIP]
1.在.env.dev文件中 > config.yaml 需要启动一次 Bot 后生成
SUPERUSERS = [""] # 填写你的QQ 1.在 .env.dev 文件中填写你的机器人配置项
PLATFORM_SUPERUSERS = ' 2.在 configs/config.yaml 文件中修改你需要修改的插件配置项
{
"qq": [""], # 在此处填写你的qq
"dodo": [],
"kaiheila": [],
"discord": []
}
'
# 此处填写你的数据库地址
# 示例: "postgres://user:password@127.0.0.1:5432/database"
# 示例: "mysql://user:password@127.0.0.1:5432/database"
# 示例: "sqlite:data/db/zhenxun.db" 在data目录下建立db文件夹
DB_URL = "" # 数据库地址
<details>
<summary>数据库地址DB_URL配置说明</summary>
2.在configs/config.yaml文件中 # 该文件需要启动一次后生成 DB_URL 是基于 Tortoise ORM 的数据库连接字符串,用于指定项目所使用的数据库。以下是 DB_URL 的组成部分以及示例:
* 修改插件配置项
``` 格式为: ```<数据库类型>://<用户名>:<密码>@<主机>:<端口>/<数据库名>?<参数>```
## 功能列表 - 数据库类型:表示数据库类型,例如 postgres、mysql、sqlite 等。
- 用户名:数据库的用户名,例如 root。
- 密码:数据库的密码,例如 123456。
- 主机:数据库的主机地址,例如 127.0.0.1(本地)或远程服务器 IP。
- 端口数据库的端口号例如PostgreSQL5432, MySQL3306
- 数据库名:指定要使用的数据库名称,例如 zhenxun。
- 参数(可选):用于传递额外的配置,例如字符集设置。
</details>
## 📋 功能列表
> [!NOTE]
> 真寻原 `plugins` 插件文件夹已迁移至 [插件仓库](https://github.com/zhenxun-org/zhenxun_bot_plugins) ,现在本体仅保留核心功能
<details> <details>
<summary>内置功能</summary> <summary>内置功能</summary>
**真寻原 `plugins` 插件文件夹已迁移至其他仓库,当前内置仅保留必要的功能** ### 🔧 基础功能
### 基础功能 - 昵称系统(群与群与私聊分开)
- 签到/我的签到/好感度排行/好感度总排行(影响色图概率和开箱次数,支持配置)
- 商店/我的金币/购买道具/使用道具/金币排行(完整的商店添加/购买/使用流程)
- 查看当前群欢迎消息
- 个人信息查看(群组内权限,聊天频率等)
- 消息撤回
- 功能统计可视化
- 关于
- 三种样式的帮助菜单
- [x] 昵称系统(群与群与私聊分开) ### 🛠️ 管理员功能
- [x] 签到/我的签到/好感度排行/好感度总排行(影响色图概率和开箱次数,支持配置)
- [x] 商店/我的金币/购买道具/使用道具/金币排行(完整的商店添加/购买/使用流程)
- [x] 查看当前群欢迎消息
- [x] 个人信息查看(群组内权限,聊天频率等)
- [x] 消息撤回
- [x] 功能统计可视化
- [x] 关于
- [x] 三种样式的帮助菜单
### 管理员功能 - 管理员帮助
- 更新群组成员信息
- 95%的群功能开关
- 查看群内被动技能状态
- 自定义群欢迎消息(是真寻的不是管家的!)
- ban/unban支持设置 ban 时长)= 群组及用户的黑名单
- 休息吧/醒来(群组内真寻状态)
- [x] 管理员帮助 ### 🧑‍💼 超级用户功能
- [x] 更新群组成员信息
- [x] 95%的群功能开关
- [x] 查看群内被动技能状态
- [x] 自定义群欢迎消息(是真寻的不是管家的!)
- [x] ban/unban支持设置 ban 时长)= 群组及用户的黑名单
- [x] 休息吧/醒来(群组内真寻状态)
### 超级用户功能 - 超级用户帮助
- 添加/删除权限(是真寻的管理员权限,不是群管理员)
- 群组管理,退群指令等
- 广播
- 自检(检查系统状态)
- 所有群组/所有好友
- 退出指定群
- 更新好友信息/更新群信息
- 修改群权限
- 检查更新
- 重启
- 添加/删除/查看群白名单
- 功能开关(更多设置)
- 功能状态
- 执行 SQL
- 重载配置
- 清理临时数据
- 增删群认证
- 同意/拒绝好友/群聊请求
- 添加/移除/更新插件/插件商店plugins 库以及扩展库)
- WebUI API对真寻前端的支持
- [x] 超级用户帮助 #### 🛡️ 超级用户的被动技能
- [x] 添加/删除权限(是真寻的管理员权限,不是群管理员)
- [x] 群组管理,退群指令等
- [x] 广播
- [x] 自检(检查系统状态)
- [x] 所有群组/所有好友
- [x] 退出指定群
- [x] 更新好友信息/更新群信息
- [x] 修改群权限
- [x] 检查更新
- [x] 重启
- [x] 添加/删除/查看群白名单
- [x] 功能开关(更多设置)
- [x] 功能状态
- [x] 执行 SQL
- [x] 重载配置
- [x] 清理临时数据
- [x] 增删群认证
- [x] 同意/拒绝好友/群聊请求
- [x] 添加/移除/更新插件/插件商店plugins 库以及扩展库)
- [x] WebUI API对真寻前端的支持
#### 超级用户的被动技能 - 邀请入群提醒(别人邀请真寻入群,可配置自动同意)
- [x] 邀请入群提醒(别人邀请真寻入群,可配置自动同意) - 添加好友提醒(别人添加真寻好友,可配置自动同意)
- [x] 添加好友提醒(别人添加真寻好友,可配置自动同意) ### 🤖 被动技能
### 被动技能 - 群早晚安
- [x] 群早晚安 ### 👻 看不见的技能
### 看不见的技能 - 功能调用统计
- 聊天记录统计
- [x] 功能调用统计 - 检测恶意触发命令(将被最高权限 ban 掉 30 分钟,只有最高权限(9 级)可以进行 unban
- [x] 聊天记录统计 - 自动同意好友/群组请求,加群请求将会提醒管理员,退群提示,加群欢迎等等
- [x] 检测恶意触发命令(将被最高权限 ban 掉 30 分钟,只有最高权限(9 级)可以进行 unban - 群聊时间检测(当群聊最后一人发言时间大于当前 48 小时后将关闭该群所有通知(即被动技能))
- [x] 自动同意好友/群组请求,加群请求将会提醒管理员,退群提示,加群欢迎等等 - 群管理员监控,自动为新晋管理员增加权限,为失去群管理员的用户删除权限
- [x] 群聊时间检测(当群聊最后一人发言时间大于当前 48 小时后将关闭该群所有通知(即被动技能)) - 群权限系统
- [x] 群管理员监控,自动为新晋管理员增加权限,为失去群管理员的用户删除权限 - 定时更新权限
- [x] 群权限系统 - 自动配置重载
- [x] 定时更新权限 - 强制入群保护
- [x] 自动配置重载 - 自定备份(可配置)
- [x] 强制入群保护 - 笨蛋检测(当使用功能名称当指令时真寻会跳出来狠狠嘲笑并帮助)
- [x] 自定备份(可配置)
- [x] 笨蛋检测(当使用功能名称当指令时真寻会跳出来狠狠嘲笑并帮助)
### 更多插件
- [更多插件](https://github.com/zhenxun-org/zhenxun_bot_plugins)
- [第三方插件索引库](https://github.com/zhenxun-org/zhenxun_bot_plugins_index)
</details> </details>
## [爱发电](https://afdian.com/a/HibiKier) ## 💖 赞助
<details> <details>
<summary>爱发电 以及 感谢投喂 </summary> <summary>爱发电</summary>
<img width="365px" height="450px" src="https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs_image/afd.jpg"> <a href="https://afdian.com/a/HibiKier">
<img width="365px" height="450px" src=./docs_image/afd.jpg>
</a>
</details>
### 感谢名单 ### 赞助名单
(可以告诉我你的 **github** 地址,我偷偷换掉 0v|) (可以告诉我你的 **github** 地址,我偷偷换掉 0v|)
[shenqi](https://afdian.net/u/fa923a8cfe3d11eba61752540025c377) [shenqi](https://afdian.net/u/fa923a8cfe3d11eba61752540025c377) [A_Kyuu](https://afdian.net/u/b83954fc2c1211eba9eb52540025c377) [疯狂混沌](https://afdian.net/u/789a2f9200cd11edb38352540025c377) [投冥](https://afdian.net/a/144514mm) [茶喵](https://afdian.net/u/fd22382eac4d11ecbfc652540025c377) [AemokpaTNR](https://afdian.net/u/1169bb8c8a9611edb0c152540025c377) [爱发电用户\_wrxn](https://afdian.net/u/4aa03d20db4311ecb1e752540025c377) [qqw](https://afdian.net/u/b71db4e2cc3e11ebb76652540025c377) [溫一壺月光下酒](https://afdian.net/u/ad667a5c650c11ed89bf52540025c377) [伝木](https://afdian.net/u/246b80683f9511edba7552540025c377) [阿奎](https://afdian.net/u/da41f72845d511ed930d52540025c377) [醉梦尘逸](https://afdian.net/u/bc11d2683cd011ed99b552540025c377) [Abc](https://afdian.net/u/870dc10a3cd311ed828852540025c377) [本喵无敌哒](https://afdian.net/u/dffaa9005bc911ebb69b52540025c377) [椎名冬羽](https://afdian.net/u/ca1ebd64395e11ed81b452540025c377) [kaito](https://afdian.net/u/a055e20a498811eab1f052540025c377) [笑柒 XIAO_Q7](https://afdian.net/u/4696db5c529111ec84ea52540025c377) [请问一份爱多少钱](https://afdian.net/u/f57ef6602dbd11ed977f52540025c377) [咸鱼鱼鱼鱼](https://afdian.net/u/8e39b9a400e011ed9f4a52540025c377) [Kafka](https://afdian.net/u/41d66798ef6911ecbc5952540025c377) [墨然](https://afdian.net/u/8aa5874a644d11eb8a6752540025c377) [爱发电用户\_T9e4](https://afdian.net/u/2ad1bb82f3a711eca22852540025c377) [笑柒 XIAO_Q7](https://afdian.net/u/4696db5c529111ec84ea52540025c377) [noahzark](https://afdian.net/a/noahzark) [腊条](https://afdian.net/u/f739c4d69eca11eba94b52540025c377) [zeroller](https://afdian.net/u/0e599e96257211ed805152540025c377) [爱发电用户\_4jrf](https://afdian.net/u/6b2cdcc817c611ed949152540025c377) [爱发电用户\_TBsd](https://afdian.net/u/db638b60217911ed9efd52540025c377) [烟寒若雨](https://afdian.net/u/067bd2161eec11eda62b52540025c377) [ln](https://afdian.net/u/b51914ba1c6611ed8a4e52540025c377) [爱发电用户\_b9S4](https://afdian.net/u/3d8f30581a2911edba6d52540025c377) [爱发电用户\_c58s](https://afdian.net/u/a6ad8dda195e11ed9a4152540025c377) [爱发电用户\_eNr9](https://afdian.net/u/05fdb41c0c9a11ed814952540025c377) [MangataAkihi](https://github.com/Sakuracio) [](https://afdian.net/u/69b76e9ec77b11ec874f52540025c377) [爱发电用户\_Bc6j](https://afdian.net/u/8546be24f44111eca64052540025c377) [大魔王](https://github.com/xipesoy) [CopilotLaLaLa](https://github.com/CopilotLaLaLa) [嘿小欧](https://afdian.net/u/daa4bec4f24911ec82e552540025c377) [回忆的秋千](https://afdian.net/u/e315d9c6f14f11ecbeef52540025c377) [十年くん](https://github.com/shinianj) [](https://afdian.net/u/9b266244f23911eca19052540025c377) [yajiwa](https://github.com/yajiwa) [爆金币](https://afdian.net/u/0d78879ef23711ecb22452540025c377)...
[A_Kyuu](https://afdian.net/u/b83954fc2c1211eba9eb52540025c377)
[疯狂混沌](https://afdian.net/u/789a2f9200cd11edb38352540025c377)
[投冥](https://afdian.net/a/144514mm)
[茶喵](https://afdian.net/u/fd22382eac4d11ecbfc652540025c377)
[AemokpaTNR](https://afdian.net/u/1169bb8c8a9611edb0c152540025c377)
[爱发电用户\_wrxn](https://afdian.net/u/4aa03d20db4311ecb1e752540025c377)
[qqw](https://afdian.net/u/b71db4e2cc3e11ebb76652540025c377)
[溫一壺月光下酒](https://afdian.net/u/ad667a5c650c11ed89bf52540025c377)
[伝木](https://afdian.net/u/246b80683f9511edba7552540025c377)
[阿奎](https://afdian.net/u/da41f72845d511ed930d52540025c377)
[醉梦尘逸](https://afdian.net/u/bc11d2683cd011ed99b552540025c377)
[Abc](https://afdian.net/u/870dc10a3cd311ed828852540025c377)
[本喵无敌哒](https://afdian.net/u/dffaa9005bc911ebb69b52540025c377)
[椎名冬羽](https://afdian.net/u/ca1ebd64395e11ed81b452540025c377)
[kaito](https://afdian.net/u/a055e20a498811eab1f052540025c377)
[笑柒 XIAO_Q7](https://afdian.net/u/4696db5c529111ec84ea52540025c377)
[请问一份爱多少钱](https://afdian.net/u/f57ef6602dbd11ed977f52540025c377)
[咸鱼鱼鱼鱼](https://afdian.net/u/8e39b9a400e011ed9f4a52540025c377)
[Kafka](https://afdian.net/u/41d66798ef6911ecbc5952540025c377)
[墨然](https://afdian.net/u/8aa5874a644d11eb8a6752540025c377)
[爱发电用户\_T9e4](https://afdian.net/u/2ad1bb82f3a711eca22852540025c377)
[笑柒 XIAO_Q7](https://afdian.net/u/4696db5c529111ec84ea52540025c377)
[noahzark](https://afdian.net/a/noahzark)
[腊条](https://afdian.net/u/f739c4d69eca11eba94b52540025c377)
[zeroller](https://afdian.net/u/0e599e96257211ed805152540025c377)
[爱发电用户\_4jrf](https://afdian.net/u/6b2cdcc817c611ed949152540025c377)
[爱发电用户\_TBsd](https://afdian.net/u/db638b60217911ed9efd52540025c377)
[烟寒若雨](https://afdian.net/u/067bd2161eec11eda62b52540025c377)
[ln](https://afdian.net/u/b51914ba1c6611ed8a4e52540025c377)
[爱发电用户\_b9S4](https://afdian.net/u/3d8f30581a2911edba6d52540025c377)
[爱发电用户\_c58s](https://afdian.net/u/a6ad8dda195e11ed9a4152540025c377)
[爱发电用户\_eNr9](https://afdian.net/u/05fdb41c0c9a11ed814952540025c377)
[MangataAkihi](https://github.com/Sakuracio)
[](https://afdian.net/u/69b76e9ec77b11ec874f52540025c377)
[爱发电用户\_Bc6j](https://afdian.net/u/8546be24f44111eca64052540025c377)
[大魔王](https://github.com/xipesoy)
[CopilotLaLaLa](https://github.com/CopilotLaLaLa)
[嘿小欧](https://afdian.net/u/daa4bec4f24911ec82e552540025c377)
[回忆的秋千](https://afdian.net/u/e315d9c6f14f11ecbeef52540025c377)
[十年くん](https://github.com/shinianj)
[](https://afdian.net/u/9b266244f23911eca19052540025c377)
[yajiwa](https://github.com/yajiwa)
[爆金币](https://afdian.net/u/0d78879ef23711ecb22452540025c377)
...
</details> ## 📜 贡献指南
<!-- ## 更新 欢迎查看我们的 [贡献指南](CONTRIBUTING.md) 和 [行为守则](CODE_OF_CONDUCT.md) 以了解如何参与贡献。
### 2024/8/11 ## ❔ 需要帮助?
- 更新 dev --> > [!TIP]
> 发起 [issue](https://github.com/HibiKier/zhenxun_bot/issues/new/choose) 前,我们希望你能够阅读过或者了解 [提问的智慧](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md)
>
> - 善用[搜索引擎](https://www.google.com/)
> - 查阅 issue 中是否有类似问题,如果没有请按照模板发起 issue
<!-- ### 2024/1/25 欢迎前往 [issue](https://github.com/HibiKier/zhenxun_bot/issues/new/choose) 中提出你遇到的问题,或者加入我们的 [用户群](https://qm.qq.com/q/mRNtLSl6uc) 或 [技术群](https://qm.qq.com/q/YYYt5rkMYc)与我们联系
* 重构webui ## 🛠️ 进度追踪
### 2023/12/28 Project [zhenxun_bot](https://github.com/users/HibiKier/projects/2)
* 修复B站动态获取失败的时候会发送空消息 ## 🌟 特别感谢
### 2023/9/6
* 修正b站订阅
### 2023/8/28
* 重构`红包`功能, 允许一个群聊中有多个用户发起的红包,发送`开`等命令会开启群中所有条件允许的红包,新增`红包结算排行`,在红包退回或抢完时统计,在`塞红包`时at可以发送专属红包
* 开箱添加`更新武器箱图片`超级用户命令,用于导入数据表后更新图片
### 2023/8/20
* 修复词条回答包含at时使用模糊|正则等问时无法正确匹配问题
* 修复开箱时最后开箱日期数据未更新
### 2023/8/7
* 添加 本地图库插件 防吞图特性 [@pull/1468](https://github.com/HibiKier/zhenxun_bot/pull/1468)
### 2023/5/28
* 修复群聊数据无法初始化
### 2023/5/24
* 轮盘结算信息使用图片发送
### 2023/5/23
* 修复群聊数据无法初始化
* 修复修改图库配置重载后上传图片时提示的图库与配置不符
### 2023/5/22
* 群聊中B站订阅所有管理员共享增删操作
* 数据库中所有user_qq改名以及user_id和group_id改为字符串
* 修改查看词条图片等显示问题
### 2023/5/16
* 修复因明日方舟新增“中坚寻访”导致抽卡模拟不可用的问题 [@pull/1418](https://github.com/HibiKier/zhenxun_bot/pull/1418)
### 2023/4/16
* 修复开箱更新未登录时没有停止更新
* 修复更新色图问题
* fix bug [@pull/1368](https://github.com/HibiKier/zhenxun_bot/pull/1368)
* `BilibiliSub`的部分字段改为字符串
### 2023/4/5
* 词条正则回答中允许使用$1.$2..来获取()捕获组
### 2023/4/3
* 修复帮助命令`-super`无效
### 2023/4/1
* 修复开箱偶尔出现`未抽取到任何皮肤`
* 修改优化开箱显示图片
### 2023/3/28
* 补全注释`SCRIPT`中的sql语句
* 罕见物品更新时会收录所有包含该物品的箱子,可以通过`更新皮肤ALL1 -S`强制更新所有罕见物品所属箱子
### 2023/3/27
* 优化开箱更新
### 2023/3/25
* 删除BUFF_SKIN表约束新增`skin_id`字段
* 开箱新增更新指定刀具皮肤命令(某些箱子金色无法通过api获取)
* 修复词条At时bug与模糊查询时无法替换占位符问题
### 2023/3/20
* 修复BuildImage类text居中类型bug [@pull/1301](https://github.com/HibiKier/zhenxun_bot/pull/1317)
* 修复原神今日素材有时发不出图片的问题 [@pull/1301](https://github.com/HibiKier/zhenxun_bot/pull/1317)
* 修复首次签到时使用道具后签到报错
* 修复词条添加错误
### 2023/3/19
* 优化代码
* 查看武器箱及皮肤添加更新次数
* 修复添加群认证会检测群聊是否存在
* 修复色图r连发时未检测当前会话是否为群聊
### 2023/3/18
* 修复色图重复发送相同图片
* 修复签到好感度进度条错误
### 2023/3/12 \[v0.1.6.7]
* 新增`更新武器箱ALL`命令来更新所有武器箱
* 新增`查看武器箱`命令
* 色图bug修复、增加指令 [@pull/1301](https://github.com/HibiKier/zhenxun_bot/pull/1301)
### 2023/3/9
* 更正sql语句 [@pull/1302](https://github.com/HibiKier/zhenxun_bot/pull/1302)
* 修改签到卡片中签到增加好感度显示错误 [@pull/1299](https://github.com/HibiKier/zhenxun_bot/pull/1299)
### 2023/3/5
* 更新开箱会记录箱子数据以及开箱时箱子价格加入花费
* 修复开箱BUG
### 2023/3/4
* 重写翻译使用百度翻译API
* 新增开箱日志以及自动更新武器箱
### 2023/3/2
* 修复config.yaml中把False也当成None的问题 [@pull/1288](https://github.com/HibiKier/zhenxun_bot/pull/1288)
* 删除道具表无用字段(props) [@pull/1287](https://github.com/HibiKier/zhenxun_bot/pull/1287)
* 修复词云
* 修复我的签到签到图片
* 更正BuffSkin添加语句
* 修复词条单图片/表情/at无法添加
### 2022/3/1
* 重写开箱更新箱子,允许更新目前所有箱子的皮肤
* 修复消息统计
### 2023/2/28
* 把Config的type字段默认类型由str改为None [@pull/1283](https://github.com/HibiKier/zhenxun_bot/pull/1283)
* 修复同意群聊请求以及添加群认证 更新变成查询的问题 [@pull/1282](https://github.com/HibiKier/zhenxun_bot/pull/1282)
### 2023/2/26
* Config提供`type`字段确定配置项类型
* 重写开箱功能
### 2023/2/25
* 修复ys查询尘歌壶背景尺寸与内容不匹配的问题 [@pull/1270](https://github.com/HibiKier/zhenxun_bot/pull/1275)
* 更换cos url [@pull/1270](https://github.com/HibiKier/zhenxun_bot/pull/1274)
### 2023/2/20
* chat_history部分字段调整为可null [@pull/1270](https://github.com/HibiKier/zhenxun_bot/pull/1270)
### 2023/2/19
* 修正了`重载插件`的帮助提示
* 修改BUG
### 2023/2/18
* 数据库舍弃`gino`使用`tortoise`
* 昵称提供命令`全局昵称设置`
* `manager_group`群管理操作中`退群``修改群权限``添加/删除群白名单``添加/删除群认证`在群聊中使用命令时且未指定群聊时,默认指定当前群聊
* 修复插件帮助命令不生效的问题 [@pull/1263](https://github.com/HibiKier/zhenxun_bot/pull/1263)
* 解决开红包经常误触的问题,有红包和未领取的时候才会触发“开”命令 [@pull/1257](https://github.com/HibiKier/zhenxun_bot/pull/1257)
* 细节优化,原神今日素材重写 [@pull/1258](https://github.com/HibiKier/zhenxun_bot/pull/1258)
### 2023/1/31
* 修复B站转发卡片BUG [@pull/1249](https://github.com/HibiKier/zhenxun_bot/pull/1249)
### 2023/1/27
* 替换pixiv反向代理地址 [@pull/1244](https://github.com/HibiKier/zhenxun_bot/pull/1244)
### 2022/12/31
* 修复epic报错优化简介 [@pull/1226](https://github.com/HibiKier/zhenxun_bot/pull/1226)
* 修复词条在某些回答下出错
* 原神黄历改为PIL
* 允许真寻自身触发命令,提供配置项 `self_message:STATUS`
### 2022/12/27 \[v0.1.6.6]
* 添加权限检查依赖注入
### 2022/12/26
* 优化`gamedraw`插件
* 提供全局被动控制
* 群被动状态改为图片
* 修复epic获取到的简介不是中文的bug [@pull/1221](https://github.com/HibiKier/zhenxun_bot/pull/1221)
## 2022/12/24
* 修复群管理员权限检测会阻挡超级用户权限
### 2022/12/23
* 优化`管理员帮助``超级用户帮助`图片
* 重新移植`gamedraw`
* 修复pil帮助私聊时无法生成
### 2022/12/17
* 修复查看插件仓库当已安装插件版本不一致时出错
### 2022/12/15
* 修复自定义群欢迎消息无法使用
### 2022/12/13
* 修复.unban
### 2022/12/12
* 修改HTML帮助禁用提示文本错误
* 修复HTML帮助私聊无法生成
### 2022/12/11
* 词条问题支持真寻的昵称开头与at真寻开头并优化回复
* 帮助新增HTML生成新布局添加配置`TYPE`切换
* 更正私聊时功能管理回复错误
* 修复加入新群聊时初始化功能开关错误
* 添加单例注解
* 添加统计表
### 2022/12/10
* 重写帮助,删除 `详细帮助` 命令
### 2022/12/4
* 优化管理代码
### 2022/11/28
* 修复web_ui群组无法获取
* 修复web_ui修改插件数据时cmd格式错误
### 2022/11/28
* :bug: Fix a bug in open_cases to get vanilla knives' prices [@pull/1188](https://github.com/HibiKier/zhenxun_bot/pull/1188)
### 2022/11/24
* 修复管理员插件加载路径错误
### 2022/11/23
* 修复webui插件无法获取修改
### 2022/11/22
* fix switch_rule [@pull/1185](https://github.com/HibiKier/zhenxun_bot/pull/1185)
### 2022/11/21 \[v0.1.6.5]
* 优化manager, hook代码
* 修复pid搜图 [@pull/1180](https://github.com/HibiKier/zhenxun_bot/pull/1180)
### 2022/11/19
* 修改优化帮助图片生成逻辑
### 2022/11/18
* poetry添加适配器依赖更新支持py3.10 [@pull/1176](https://github.com/HibiKier/zhenxun_bot/pull/1176)
### 2022/11/13
* 更新天气api
* 使用道具可以附带额外信息供函数使用
* 限制帮助图片最小宽度
### 2022/11/12
* 更新yiqing插件数据显示 [@pull/1168](https://github.com/HibiKier/zhenxun_bot/pull/1168)
### 2022/11/11
* fix: B站直播订阅的相关问题 [@pull/1158](https://github.com/HibiKier/zhenxun_bot/pull/1158)
### 2022/10/30
* 商店简介动态行数,根据文字长度自动换行
### 2022/10/28
* 为exec指令进行了SELECT语句适配,添加了查看所有表指令 [@pull/1155](https://github.com/HibiKier/zhenxun_bot/pull/1155)
* 修复复读 [@pull/1154](https://github.com/HibiKier/zhenxun_bot/pull/1154)
### 2022/10/23
* 复读修改回图片下载
### 2022/10/22
* 更新依赖注入
### 2022/10/16 \[v0.1.6.4]
* 修改商店道具icon可以为空
### 2022/10/15
* nonebot2版本更新为rc1
* 我的道具改为图片形式
* 商品添加图标与是否为被动道具(被动道具无法被主动使用)
* 商品添加使用前方法和使用后方法类似hook使用方法具体查看文档或签到商品文件中注册的例子
* 新增用户使用道具,花费金币(包括插件)及用途记录
* 更细致的金币使用依赖注入
* 更多的依赖注入(包含图片获取等等..
* 修复我的道具仅有被动或主动道具时图片显示错误
* 色图插件p站反向代理失效 [@pull/1139](https://github.com/HibiKier/zhenxun_bot/pull/1139)
### 2022/10/9
* 修复碧蓝档案角色获取问题,换源 [@pull/1124](https://github.com/HibiKier/zhenxun_bot/pull/1124)
### 2022/10/7
* 修复 B 站请求返回 -401 错误 [@pull/1119](https://github.com/HibiKier/zhenxun_bot/pull/1119)
* 关闭功能与被动时不再区分大小写,同名时仅被动关闭操作生效
### 2022/9/30
* 修改重置开箱的使用权限 [@pull/1118](https://github.com/HibiKier/zhenxun_bot/pull/1118)
### 2022/9/27
* 更新b站转发解析 [@pull/1117](https://github.com/HibiKier/zhenxun_bot/pull/1117)
### 2022/9/24
* 修复b站订阅 [@pull/1112](https://github.com/HibiKier/zhenxun_bot/pull/1112)
* fix: 重载赛马娘卡池失败 [@pull/1114](https://github.com/HibiKier/zhenxun_bot/pull/1114)
### 2022/9/19
* 更换bilibili_sub获取用户昵称用的API&尝试修了一下get_video() [@pull/1097](https://github.com/HibiKier/zhenxun_bot/pull/1097)
* 修复csgo每日开箱可以多开一次
### 2022/9/18
* 修复 bilireq 版本过低导致 B 站视频解析错误 [@pull/1090](https://github.com/HibiKier/zhenxun_bot/pull/1096)
### 2022/9/16
* fix: bilibili_sub, azur_draw_card [@pull/1090](https://github.com/HibiKier/zhenxun_bot/pull/1090)
* 修复原神资源查询查询完毕后图片存储错误
* b站订阅发送 与 b站订阅 使用相同开关关闭b站订阅
### 2022/9/10
* 自定义群欢迎消息参数不完全时提示报错
* 修改bt插件的url地址 [@pull/1067](https://github.com/HibiKier/zhenxun_bot/pull/1067)
### 2022/9/8
* 添加插件数据初始化判断
### 2022/9/4
* 旧词条提供图片迁移需要重新获取old_model文件并将数据库中user_qq为0的数据删除
### 2022/9/3
* 原神玩家查询增加须弥地区 [@pull/1053](https://github.com/HibiKier/zhenxun_bot/pull/1053)
* av号覆盖全面且修复av号链接 [@pull/1033](https://github.com/HibiKier/zhenxun_bot/pull/1033)
* 修复词条含有CQ回答的模糊匹配无法被解析
* 禁言检测图片在内存中获取图片hash
* B站订阅在群里中任意群管理员可以统一管理原来为管理员1无法删除管理员2的订阅
* 修复原神资源查询地图api数据变更导致更新的地图不完全
### 2022/8/27
* 修复签到积分双倍后日志记录获得积分变4倍问题 [@pull/1044](https://github.com/HibiKier/zhenxun_bot/pull/1044)
### 2022/8/26
* 修复群管理员无法添加词条
* 修复词条关键词"问"前空格问题
### 2022/8/23
* 修了下模糊匹配 issue#1026 [@pull/1026](https://github.com/HibiKier/zhenxun_bot/pull/1026)
### 2022/8/22
* 修复首次安装时词条旧表出错(因为根本就没有这张表!)
* 取消配置替换定时任务,统一存储
* 对米游社cookie进行判断整合米游社签到信息 [@pull/1014](https://github.com/HibiKier/zhenxun_bot/pull/1014)
* 修正尘歌壶和质变仪图片获取地址 [@pull/1010](https://github.com/HibiKier/zhenxun_bot/pull/1010)
* 修复词库问答 **很多** 问题[@pull/1012](https://github.com/HibiKier/zhenxun_bot/pull/1012)
### 2022/8/21 \[v0.1.6.3]
* 重构群词条改为词库Plus增加 精准|模糊|正则 问题匹配问题与回答均支持atimageface超级用户额外提供 全局|私聊 词库设置,数据迁移目前只提供了问题和回答都是纯文本的词条
* 修复b站转发解析av号无法解析
* B站订阅直播订阅支持短号
* 开箱提供重置开箱命令,重置今日所有开箱数据(重置次数,并不会删除今日已开箱记录)
* 提供全局字典GDict通过from utils.utils import GDict导入
* 适配omega 13w张图的数据结构表建议删表重导
* 除首次启动外将配置替换加入单次定时任务,加快启动速度
* fix: WordBank.check() [@pull/1008](https://github.com/HibiKier/zhenxun_bot/pull/1008)
* 改进插件 `我有一个朋友`,避免触发过于频繁 [@pull/1001](https://github.com/HibiKier/zhenxun_bot/pull/1001)
* 原神便笺新增洞天宝钱和参量质变仪提示 [@pull/1005](https://github.com/HibiKier/zhenxun_bot/pull/1005)
* 新增米游社签到功能,自动领取(白嫖)米游币 [@pull/991](https://github.com/HibiKier/zhenxun_bot/pull/991)
### 2022/8/14
* 修复epic未获取到时间时出错
* 修复订阅主播时动态获取的id是直播间id
### 2022/8/8
* 修复赛马娘重载卡池失败的问题 [@pull/969](https://github.com/HibiKier/zhenxun_bot/pull/969)
### 2022/8/3
* 修复 bili动态链接在投稿视频时URL和分割线连在一起 [@pull/951](https://github.com/HibiKier/zhenxun_bot/pull/961)
* 更新 Epic 免费游戏商城链接拼接规则 [@pull/957](https://github.com/HibiKier/zhenxun_bot/pull/957)
### 2022/8/6
* 修复了原神自动签到返回invalid request的问题新增查看我的cookie命令 [@pull/971](https://github.com/HibiKier/zhenxun_bot/pull/971) -->
<br>
**..... 更多更新信息请查看文档**
## Todo
- [x] web 管理
## **特别感谢**
首席设计师:[酥酥/coldly-ss](https://github.com/coldly-ss) 首席设计师:[酥酥/coldly-ss](https://github.com/coldly-ss)
## 感谢 ## 🙏 感谢
[botuniverse / onebot](https://github.com/botuniverse/onebot) :超棒的机器人协议 [botuniverse / onebot](https://github.com/botuniverse/onebot) :超棒的机器人协议
[Mrs4s / go-cqhttp](https://github.com/Mrs4s/go-cqhttp) cqhttp 的 golang 实现,轻量、原生跨平台. [Mrs4s / go-cqhttp](https://github.com/Mrs4s/go-cqhttp) cqhttp 的 golang 实现,轻量、原生跨平台.
@ -778,3 +301,58 @@ python bot.py
[Kyomotoi / AnimeThesaurus](https://github.com/Kyomotoi/AnimeThesaurus) :一个~~特二刺螈~~(文爱)的适用于任何 bot 的词库 [Kyomotoi / AnimeThesaurus](https://github.com/Kyomotoi/AnimeThesaurus) :一个~~特二刺螈~~(文爱)的适用于任何 bot 的词库
[Ailitonia / omega-miya](https://github.com/Ailitonia/omega-miya) :基于 nonebot2 的 qq 机器人 [Ailitonia / omega-miya](https://github.com/Ailitonia/omega-miya) :基于 nonebot2 的 qq 机器人
[KimigaiiWuyi / GenshinUID](https://github.com/KimigaiiWuyi/GenshinUID) :一个基于 HoshinoBot/NoneBot2 的原神 UID 查询插件 [KimigaiiWuyi / GenshinUID](https://github.com/KimigaiiWuyi/GenshinUID) :一个基于 HoshinoBot/NoneBot2 的原神 UID 查询插件
## 📊 统计与活跃贡献者
<a href="https://next.ossinsight.io/widgets/official/compose-last-28-days-stats?repo_id=368008334" target="_blank" style="display: block" align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://next.ossinsight.io/widgets/official/compose-last-28-days-stats/thumbnail.png?repo_id=368008334&image_size=auto&color_scheme=dark" width="800" height="auto">
<img alt="Performance Stats of HibiKier/zhenxun_bot - Last 28 days" src="https://next.ossinsight.io/widgets/official/compose-last-28-days-stats/thumbnail.png?repo_id=368008334&image_size=auto&color_scheme=light" width="800" height="auto">
</picture>
</a>
<a href="https://next.ossinsight.io/widgets/official/compose-recent-active-contributors?repo_id=368008334&limit=30" target="_blank" style="display: block" align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://next.ossinsight.io/widgets/official/compose-recent-active-contributors/thumbnail.png?repo_id=368008334&limit=30&image_size=auto&color_scheme=dark" width="800" height="auto">
<img alt="Active Contributors of HibiKier/zhenxun_bot - Last 28 days" src="https://next.ossinsight.io/widgets/official/compose-recent-active-contributors/thumbnail.png?repo_id=368008334&limit=30&image_size=auto&color_scheme=light" width="800" height="auto">
</picture>
</a>
## 👨‍💻 开发者
感谢以下开发者对 绪山真寻 Bot 作出的贡献:
<a href="https://github.com/HibiKier/zhenxun_bot/graphs/contributors" style="display: block" align="center">
<img src="https://contrib.rocks/image?repo=HibiKier/zhenxun_bot&max=1000" alt="contributors"/>
</a>
## 📸 WebUI界面展示
<div style="display: flex; flex-wrap: wrap; justify-content: space-between;">
<div style="width: 48%; margin-bottom: 10px;">
<img src="./docs_image/webui00.png" alt="webui00" style="width: 100%; height: auto;">
</div>
<div style="width: 48%; margin-bottom: 10px;">
<img src="./docs_image/webui01.png" alt="webui01" style="width: 100%; height: auto;">
</div>
<div style="width: 48%; margin-bottom: 10px;">
<img src="./docs_image/webui02.png" alt="webui02" style="width: 100%; height: auto;">
</div>
<div style="width: 48%; margin-bottom: 10px;">
<img src="./docs_image/webui03.png" alt="webui03" style="width: 100%; height: auto;">
</div>
<div style="width: 48%; margin-bottom: 10px;">
<img src="./docs_image/webui04.png" alt="webui04" style="width: 100%; height: auto;">
</div>
<div style="width: 48%; margin-bottom: 10px;">
<img src="./docs_image/webui05.png" alt="webui05" style="width: 100%; height: auto;">
</div>
<div style="width: 48%; margin-bottom: 10px;">
<img src="./docs_image/webui06.png" alt="webui06" style="width: 100%; height: auto;">
</div>
<div style="width: 48%; margin-bottom: 10px;">
<img src="./docs_image/webui07.png" alt="webui07" style="width: 100%; height: auto;">
</div>
</div>

View File

@ -44,7 +44,7 @@ body {
} }
.main { .main {
height: 448px; height: 444px;
width: 335px; width: 335px;
padding: 0 30px; padding: 0 30px;
position: relative; position: relative;

View File

@ -2,20 +2,12 @@ from datetime import datetime
import uuid import uuid
import nonebot import nonebot
from nonebot import require
from nonebot.adapters import Bot from nonebot.adapters import Bot
from nonebot.drivers import Driver from nonebot.drivers import Driver
from tortoise import Tortoise from tortoise import Tortoise
from tortoise.exceptions import OperationalError from tortoise.exceptions import OperationalError
import ujson as json import ujson as json
require("nonebot_plugin_apscheduler")
require("nonebot_plugin_alconna")
require("nonebot_plugin_session")
require("nonebot_plugin_userinfo")
require("nonebot_plugin_htmlrender")
# require("nonebot_plugin_uninfo")
from zhenxun.models.bot_connect_log import BotConnectLog from zhenxun.models.bot_connect_log import BotConnectLog
from zhenxun.models.bot_console import BotConsole from zhenxun.models.bot_console import BotConsole
from zhenxun.models.goods_info import GoodsInfo from zhenxun.models.goods_info import GoodsInfo

View File

@ -387,7 +387,7 @@ class AuthChecker:
session=session, session=session,
) )
raise IgnoredException("该群未开启此功能...") raise IgnoredException("该群未开启此功能...")
if not plugin.status and plugin.block_type == BlockType.GROUP: if plugin.block_type == BlockType.GROUP:
"""全局群组禁用""" """全局群组禁用"""
try: try:
if self.is_send_limit_message(plugin, sid) and not is_poke: if self.is_send_limit_message(plugin, sid) and not is_poke:
@ -410,7 +410,7 @@ class AuthChecker:
raise IgnoredException("该插件在群组中已被禁用...") raise IgnoredException("该插件在群组中已被禁用...")
else: else:
sid = user_id sid = user_id
if not plugin.status and plugin.block_type == BlockType.PRIVATE: if plugin.block_type == BlockType.PRIVATE:
"""全局私聊禁用""" """全局私聊禁用"""
try: try:
if self.is_send_limit_message(plugin, sid) and not is_poke: if self.is_send_limit_message(plugin, sid) and not is_poke:

View File

@ -120,7 +120,7 @@ async def _():
if module_list := await PluginInfo.all().values("id", "module_path"): if module_list := await PluginInfo.all().values("id", "module_path"):
module2id = {m["module_path"]: m["id"] for m in module_list} module2id = {m["module_path"]: m["id"] for m in module_list}
for plugin in get_loaded_plugins(): for plugin in get_loaded_plugins():
load_plugin.append(plugin.name) load_plugin.append(plugin.module_name)
await _handle_setting(plugin, plugin_list, limit_list, task_list) await _handle_setting(plugin, plugin_list, limit_list, task_list)
create_list = [] create_list = []
update_list = [] update_list = []
@ -198,8 +198,8 @@ async def _():
10, 10,
) )
await data_migration() await data_migration()
await PluginInfo.filter(module__in=load_plugin).update(load_status=True) await PluginInfo.filter(module_path__in=load_plugin).update(load_status=True)
await PluginInfo.filter(module__not_in=load_plugin).update(load_status=False) await PluginInfo.filter(module_path__not_in=load_plugin).update(load_status=False)
manager.init() manager.init()
if limit_list: if limit_list:
for limit in limit_list: for limit in limit_list:

View File

@ -89,7 +89,7 @@ async def _(bot: Bot):
async with aiofiles.open(RESTART_MARK, encoding="utf8") as f: async with aiofiles.open(RESTART_MARK, encoding="utf8") as f:
bot_id, user_id = (await f.read()).split() bot_id, user_id = (await f.read()).split()
if bot := nonebot.get_bot(bot_id): if bot := nonebot.get_bot(bot_id):
if target := PlatformUtils.get_target(bot, user_id): if target := PlatformUtils.get_target(user_id=user_id):
await MessageUtils.build_message( await MessageUtils.build_message(
f"{BotConfig.self_nickname}已成功重启!" f"{BotConfig.self_nickname}已成功重启!"
).send(target, bot=bot) ).send(target, bot=bot)

View File

@ -37,8 +37,7 @@ async def _():
update_list = [] update_list = []
if modules := await TaskInfo.annotate().values_list("module", flat=True): if modules := await TaskInfo.annotate().values_list("module", flat=True):
for bot in nonebot.get_bots().values(): for bot in nonebot.get_bots().values():
group_list, _ = await PlatformUtils.get_group_list(bot) group_list, _ = await PlatformUtils.get_group_list(bot, True)
group_list = [g for g in group_list if g.channel_id is None]
for group in group_list: for group in group_list:
try: try:
last_message = ( last_message = (

View File

@ -126,7 +126,9 @@ async def _(session: Uninfo, arparma: Arparma):
async def _(session: Uninfo, arparma: Arparma, nickname: str = UserName()): async def _(session: Uninfo, arparma: Arparma, nickname: str = UserName()):
logger.info("查看道具", arparma.header_result, session=session) logger.info("查看道具", arparma.header_result, session=session)
if image := await ShopManage.my_props( if image := await ShopManage.my_props(
session.user.id, nickname, PlatformUtils.get_platform(session) session.user.id,
nickname,
PlatformUtils.get_platform(session),
): ):
await MessageUtils.build_message(image.pic2bytes()).finish(reply_to=True) await MessageUtils.build_message(image.pic2bytes()).finish(reply_to=True)
return await MessageUtils.build_message("你的道具为空捏...").send(reply_to=True) return await MessageUtils.build_message("你的道具为空捏...").send(reply_to=True)

View File

@ -47,7 +47,7 @@ class BroadcastManage:
group.group_id, group.group_id,
): ):
target = PlatformUtils.get_target( target = PlatformUtils.get_target(
bot, None, group.channel_id or group.group_id group_id=group.group_id, channel_id=group.channel_id
) )
if target: if target:
await MessageUtils.build_message(message_list).send( await MessageUtils.build_message(message_list).send(

View File

@ -13,6 +13,7 @@ from zhenxun.utils.enum import PluginType
from .api.logs import router as ws_log_routes from .api.logs import router as ws_log_routes
from .api.logs.log_manager import LOG_STORAGE from .api.logs.log_manager import LOG_STORAGE
from .api.menu import router as menu_router
from .api.tabs.dashboard import router as dashboard_router from .api.tabs.dashboard import router as dashboard_router
from .api.tabs.database import router as database_router from .api.tabs.database import router as database_router
from .api.tabs.main import router as main_router from .api.tabs.main import router as main_router
@ -80,6 +81,7 @@ BaseApiRouter.include_router(manage_router)
BaseApiRouter.include_router(database_router) BaseApiRouter.include_router(database_router)
BaseApiRouter.include_router(plugin_router) BaseApiRouter.include_router(plugin_router)
BaseApiRouter.include_router(system_router) BaseApiRouter.include_router(system_router)
BaseApiRouter.include_router(menu_router)
WsApiRouter = APIRouter(prefix="/zhenxun/socket") WsApiRouter = APIRouter(prefix="/zhenxun/socket")
@ -112,6 +114,6 @@ async def _():
app.include_router(BaseApiRouter) app.include_router(BaseApiRouter)
app.include_router(WsApiRouter) app.include_router(WsApiRouter)
await init_public(app) await init_public(app)
logger.info("<g>API启动成功</g>", "Web UI") logger.info("<g>API启动成功</g>", "WebUi")
except Exception as e: except Exception as e:
logger.error("<g>API启动失败</g>", "Web UI", e=e) logger.error("<g>API启动失败</g>", "WebUi", e=e)

View File

@ -1 +1,2 @@
from .tabs import * # noqa: F403 from .menu import * # noqa: F403f
from .tabs import * # noqa: F403f

View File

@ -0,0 +1,26 @@
from fastapi import APIRouter
from fastapi.responses import JSONResponse
from zhenxun.services.log import logger
from ...base_model import Result
from ...utils import authentication
from .data_source import menu_manage
from .model import MenuData
router = APIRouter(prefix="/menu")
@router.get(
"/get_menus",
dependencies=[authentication()],
response_model=Result[list[MenuData]],
response_class=JSONResponse,
deprecated="获取菜单列表", # type: ignore
)
async def _() -> Result[list[MenuData]]:
try:
return Result.ok(menu_manage.get_menus(), "拿到菜单了哦!")
except Exception as e:
logger.error(f"{router.prefix}/get_menus 调用错误", "WebUi", e=e)
return Result.fail(f"发生了一点错误捏 {type(e)}: {e}")

View File

@ -0,0 +1,64 @@
import ujson as json
from zhenxun.configs.path_config import DATA_PATH
from zhenxun.services.log import logger
from .model import MenuData, MenuItem
class MenuManage:
def __init__(self) -> None:
self.file = DATA_PATH / "web_ui" / "menu.json"
self.menu = []
if self.file.exists():
try:
self.menu = json.load(self.file.open(encoding="utf8"))
except Exception as e:
logger.warning("菜单文件损坏,已重新生成...", "WebUi", e=e)
if not self.menu:
self.menu = [
MenuItem(
name="仪表盘",
module="dashboard",
router="/dashboard",
icon="dashboard",
default=True,
),
MenuItem(
name="真寻控制台",
module="command",
router="/command",
icon="command",
),
MenuItem(
name="插件列表", module="plugin", router="/plugin", icon="plugin"
),
MenuItem(
name="插件商店", module="store", router="/store", icon="store"
),
MenuItem(
name="好友/群组", module="manage", router="/manage", icon="user"
),
MenuItem(
name="数据库管理",
module="database",
router="/database",
icon="database",
),
MenuItem(
name="系统信息", module="system", router="/system", icon="system"
),
]
self.save()
def get_menus(self):
return MenuData(menus=self.menu)
def save(self):
self.file.parent.mkdir(parents=True, exist_ok=True)
temp = [menu.dict() for menu in self.menu]
with self.file.open("w", encoding="utf8") as f:
json.dump(temp, f, ensure_ascii=False, indent=4)
menu_manage = MenuManage()

View File

@ -0,0 +1,21 @@
from pydantic import BaseModel
class MenuItem(BaseModel):
module: str
"""模块名称"""
name: str
"""菜单名称"""
router: str
"""路由"""
icon: str
"""图标"""
default: bool = False
"""默认选中"""
class MenuData(BaseModel):
bot_type: str = "zhenxun"
"""bot类型"""
menus: list[MenuItem]
"""菜单列表"""

View File

@ -1,20 +1,14 @@
from datetime import datetime, timedelta
from fastapi import APIRouter from fastapi import APIRouter
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
import nonebot import nonebot
from nonebot import require from nonebot import require
from nonebot.config import Config from nonebot.config import Config
from tortoise.expressions import RawSQL
from tortoise.functions import Count
from zhenxun.models.bot_connect_log import BotConnectLog from zhenxun.services.log import logger
from zhenxun.models.chat_history import ChatHistory
from zhenxun.models.statistics import Statistics
from ....base_model import BaseResultModel, QueryModel, Result from ....base_model import BaseResultModel, QueryModel, Result
from ....utils import authentication from ....utils import authentication
from .data_source import BotManage from .data_source import ApiDataSource
from .model import AllChatAndCallCount, BotInfo, ChatCallMonthCount, QueryChatCallCount from .model import AllChatAndCallCount, BotInfo, ChatCallMonthCount, QueryChatCallCount
require("plugin_store") require("plugin_store")
@ -33,8 +27,9 @@ driver = nonebot.get_driver()
) )
async def _() -> Result[list[BotInfo]]: async def _() -> Result[list[BotInfo]]:
try: try:
return Result.ok(await BotManage.get_bot_list(), "拿到信息啦!") return Result.ok(await ApiDataSource.get_bot_list(), "拿到信息啦!")
except Exception as e: except Exception as e:
logger.error(f"{router.prefix}/get_bot_list 调用错误", "WebUi", e=e)
return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") return Result.fail(f"发生了一点错误捏 {type(e)}: {e}")
@ -46,29 +41,13 @@ async def _() -> Result[list[BotInfo]]:
description="获取聊天/调用记录的全部和今日数量", description="获取聊天/调用记录的全部和今日数量",
) )
async def _(bot_id: str | None = None) -> Result[QueryChatCallCount]: async def _(bot_id: str | None = None) -> Result[QueryChatCallCount]:
now = datetime.now() try:
query = ChatHistory
if bot_id:
query = query.filter(bot_id=bot_id)
chat_all_count = await query.annotate().count()
chat_day_count = await query.filter(
create_time__gte=now - timedelta(hours=now.hour, minutes=now.minute)
).count()
query = Statistics
if bot_id:
query = query.filter(bot_id=bot_id)
call_all_count = await query.annotate().count()
call_day_count = await query.filter(
create_time__gte=now - timedelta(hours=now.hour, minutes=now.minute)
).count()
return Result.ok( return Result.ok(
QueryChatCallCount( await ApiDataSource.get_chat_and_call_count(bot_id), "拿到信息啦!"
chat_num=chat_all_count,
chat_day=chat_day_count,
call_num=call_all_count,
call_day=call_day_count,
)
) )
except Exception as e:
logger.error(f"{router.prefix}/get_chat_and_call_count 调用错误", "WebUi", e=e)
return Result.fail(f"发生了一点错误捏 {type(e)}: {e}")
@router.get( @router.get(
@ -79,41 +58,15 @@ async def _(bot_id: str | None = None) -> Result[QueryChatCallCount]:
description="获取聊天/调用记录的全部数据次数", description="获取聊天/调用记录的全部数据次数",
) )
async def _(bot_id: str | None = None) -> Result[AllChatAndCallCount]: async def _(bot_id: str | None = None) -> Result[AllChatAndCallCount]:
now = datetime.now() try:
query = ChatHistory
if bot_id:
query = query.filter(bot_id=bot_id)
chat_week_count = await query.filter(
create_time__gte=now - timedelta(days=7, hours=now.hour, minutes=now.minute)
).count()
chat_month_count = await query.filter(
create_time__gte=now - timedelta(days=30, hours=now.hour, minutes=now.minute)
).count()
chat_year_count = await query.filter(
create_time__gte=now - timedelta(days=365, hours=now.hour, minutes=now.minute)
).count()
query = Statistics
if bot_id:
query = query.filter(bot_id=bot_id)
call_week_count = await query.filter(
create_time__gte=now - timedelta(days=7, hours=now.hour, minutes=now.minute)
).count()
call_month_count = await query.filter(
create_time__gte=now - timedelta(days=30, hours=now.hour, minutes=now.minute)
).count()
call_year_count = await query.filter(
create_time__gte=now - timedelta(days=365, hours=now.hour, minutes=now.minute)
).count()
return Result.ok( return Result.ok(
AllChatAndCallCount( await ApiDataSource.get_all_chat_and_call_count(bot_id), "拿到信息啦!"
chat_week=chat_week_count,
chat_month=chat_month_count,
chat_year=chat_year_count,
call_week=call_week_count,
call_month=call_month_count,
call_year=call_year_count,
) )
except Exception as e:
logger.error(
f"{router.prefix}/get_all_chat_and_call_count 调用错误", "WebUi", e=e
) )
return Result.fail(f"发生了一点错误捏 {type(e)}: {e}")
@router.get( @router.get(
@ -124,48 +77,13 @@ async def _(bot_id: str | None = None) -> Result[AllChatAndCallCount]:
deprecated="获取聊天/调用记录的一个月数量", # type: ignore deprecated="获取聊天/调用记录的一个月数量", # type: ignore
) )
async def _(bot_id: str | None = None) -> Result[ChatCallMonthCount]: async def _(bot_id: str | None = None) -> Result[ChatCallMonthCount]:
now = datetime.now() try:
filter_date = now - timedelta(days=30, hours=now.hour, minutes=now.minute)
chat_query = ChatHistory
call_query = Statistics
if bot_id:
chat_query = chat_query.filter(bot_id=bot_id)
call_query = call_query.filter(bot_id=bot_id)
chat_date_list = (
await chat_query.filter(create_time__gte=filter_date)
.annotate(date=RawSQL("DATE(create_time)"), count=Count("id"))
.group_by("date")
.values("date", "count")
)
call_date_list = (
await call_query.filter(create_time__gte=filter_date)
.annotate(date=RawSQL("DATE(create_time)"), count=Count("id"))
.group_by("date")
.values("date", "count")
)
date_list = []
chat_count_list = []
call_count_list = []
chat_date2cnt = {str(date["date"]): date["count"] for date in chat_date_list}
call_date2cnt = {str(date["date"]): date["count"] for date in call_date_list}
date = now.date()
for _ in range(30):
if str(date) in chat_date2cnt:
chat_count_list.append(chat_date2cnt[str(date)])
else:
chat_count_list.append(0)
if str(date) in call_date2cnt:
call_count_list.append(call_date2cnt[str(date)])
else:
call_count_list.append(0)
date_list.append(str(date)[5:])
date -= timedelta(days=1)
chat_count_list.reverse()
call_count_list.reverse()
date_list.reverse()
return Result.ok( return Result.ok(
ChatCallMonthCount(chat=chat_count_list, call=call_count_list, date=date_list) await ApiDataSource.get_chat_and_call_month(bot_id), "拿到信息啦!"
) )
except Exception as e:
logger.error(f"{router.prefix}/get_chat_and_call_month 调用错误", "WebUi", e=e)
return Result.fail(f"发生了一点错误捏 {type(e)}: {e}")
@router.post( @router.post(
@ -176,18 +94,11 @@ async def _(bot_id: str | None = None) -> Result[ChatCallMonthCount]:
deprecated="获取Bot连接记录", # type: ignore deprecated="获取Bot连接记录", # type: ignore
) )
async def _(query: QueryModel) -> Result[BaseResultModel]: async def _(query: QueryModel) -> Result[BaseResultModel]:
total = await BotConnectLog.all().count() try:
if total % query.size: return Result.ok(await ApiDataSource.get_connect_log(query), "拿到信息啦!")
total += 1 except Exception as e:
data = ( logger.error(f"{router.prefix}/get_connect_log 调用错误", "WebUi", e=e)
await BotConnectLog.all() return Result.fail(f"发生了一点错误捏 {type(e)}: {e}")
.order_by("-id")
.offset((query.index - 1) * query.size)
.limit(query.size)
)
for v in data:
v.connect_time = v.connect_time.replace(tzinfo=None).replace(microsecond=0)
return Result.ok(BaseResultModel(total=total, data=data))
@router.get( @router.get(

View File

@ -4,13 +4,17 @@ import time
import nonebot import nonebot
from nonebot.adapters import Bot from nonebot.adapters import Bot
from nonebot.drivers import Driver from nonebot.drivers import Driver
from tortoise.expressions import RawSQL
from tortoise.functions import Count
from zhenxun.models.bot_connect_log import BotConnectLog
from zhenxun.models.chat_history import ChatHistory from zhenxun.models.chat_history import ChatHistory
from zhenxun.models.statistics import Statistics from zhenxun.models.statistics import Statistics
from zhenxun.utils.platform import PlatformUtils from zhenxun.utils.platform import PlatformUtils
from ....base_model import BaseResultModel, QueryModel
from ..main.data_source import bot_live from ..main.data_source import bot_live
from .model import BotInfo from .model import AllChatAndCallCount, BotInfo, ChatCallMonthCount, QueryChatCallCount
driver: Driver = nonebot.get_driver() driver: Driver = nonebot.get_driver()
@ -24,7 +28,7 @@ async def _():
CONNECT_TIME = int(time.time()) CONNECT_TIME = int(time.time())
class BotManage: class ApiDataSource:
@classmethod @classmethod
async def __build_bot_info(cls, bot: Bot) -> BotInfo: async def __build_bot_info(cls, bot: Bot) -> BotInfo:
"""构建Bot信息 """构建Bot信息
@ -47,8 +51,7 @@ class BotManage:
bot_info = BotInfo( bot_info = BotInfo(
self_id=bot.self_id, nickname=nickname, ava_url=ava_url, platform=platform self_id=bot.self_id, nickname=nickname, ava_url=ava_url, platform=platform
) )
group_list, _ = await PlatformUtils.get_group_list(bot) group_list, _ = await PlatformUtils.get_group_list(bot, True)
group_list = [g for g in group_list if g.channel_id is None]
friend_list, _ = await PlatformUtils.get_friend_list(bot) friend_list, _ = await PlatformUtils.get_friend_list(bot)
bot_info.group_count = len(group_list) bot_info.group_count = len(group_list)
bot_info.friend_count = len(friend_list) bot_info.friend_count = len(friend_list)
@ -77,3 +80,161 @@ class BotManage:
for _, bot in nonebot.get_bots().items(): for _, bot in nonebot.get_bots().items():
bot_list.append(await cls.__build_bot_info(bot)) bot_list.append(await cls.__build_bot_info(bot))
return bot_list return bot_list
@classmethod
async def get_chat_and_call_count(cls, bot_id: str | None) -> QueryChatCallCount:
"""获取今日聊天和调用次数
参数:
bot_id: bot id
返回:
QueryChatCallCount: 数据内容
"""
now = datetime.now()
query = ChatHistory
if bot_id:
query = query.filter(bot_id=bot_id)
chat_all_count = await query.annotate().count()
chat_day_count = await query.filter(
create_time__gte=now - timedelta(hours=now.hour, minutes=now.minute)
).count()
query = Statistics
if bot_id:
query = query.filter(bot_id=bot_id)
call_all_count = await query.annotate().count()
call_day_count = await query.filter(
create_time__gte=now - timedelta(hours=now.hour, minutes=now.minute)
).count()
return QueryChatCallCount(
chat_num=chat_all_count,
chat_day=chat_day_count,
call_num=call_all_count,
call_day=call_day_count,
)
@classmethod
async def get_all_chat_and_call_count(
cls, bot_id: str | None
) -> AllChatAndCallCount:
"""获取全部聊天和调用记录
参数:
bot_id: bot id
返回:
AllChatAndCallCount: 数据内容
"""
now = datetime.now()
query = ChatHistory
if bot_id:
query = query.filter(bot_id=bot_id)
chat_week_count = await query.filter(
create_time__gte=now - timedelta(days=7, hours=now.hour, minutes=now.minute)
).count()
chat_month_count = await query.filter(
create_time__gte=now
- timedelta(days=30, hours=now.hour, minutes=now.minute)
).count()
chat_year_count = await query.filter(
create_time__gte=now
- timedelta(days=365, hours=now.hour, minutes=now.minute)
).count()
query = Statistics
if bot_id:
query = query.filter(bot_id=bot_id)
call_week_count = await query.filter(
create_time__gte=now - timedelta(days=7, hours=now.hour, minutes=now.minute)
).count()
call_month_count = await query.filter(
create_time__gte=now
- timedelta(days=30, hours=now.hour, minutes=now.minute)
).count()
call_year_count = await query.filter(
create_time__gte=now
- timedelta(days=365, hours=now.hour, minutes=now.minute)
).count()
return AllChatAndCallCount(
chat_week=chat_week_count,
chat_month=chat_month_count,
chat_year=chat_year_count,
call_week=call_week_count,
call_month=call_month_count,
call_year=call_year_count,
)
@classmethod
async def get_chat_and_call_month(cls, bot_id: str | None) -> ChatCallMonthCount:
"""获取一个月内的调用/消息记录次数并根据日期对数据填充0
参数:
bot_id: bot id
返回:
ChatCallMonthCount: 数据内容
"""
now = datetime.now()
filter_date = now - timedelta(days=30, hours=now.hour, minutes=now.minute)
chat_query = ChatHistory
call_query = Statistics
if bot_id:
chat_query = chat_query.filter(bot_id=bot_id)
call_query = call_query.filter(bot_id=bot_id)
chat_date_list = (
await chat_query.filter(create_time__gte=filter_date)
.annotate(date=RawSQL("DATE(create_time)"), count=Count("id"))
.group_by("date")
.values("date", "count")
)
call_date_list = (
await call_query.filter(create_time__gte=filter_date)
.annotate(date=RawSQL("DATE(create_time)"), count=Count("id"))
.group_by("date")
.values("date", "count")
)
date_list = []
chat_count_list = []
call_count_list = []
chat_date2cnt = {str(date["date"]): date["count"] for date in chat_date_list}
call_date2cnt = {str(date["date"]): date["count"] for date in call_date_list}
date = now.date()
for _ in range(30):
if str(date) in chat_date2cnt:
chat_count_list.append(chat_date2cnt[str(date)])
else:
chat_count_list.append(0)
if str(date) in call_date2cnt:
call_count_list.append(call_date2cnt[str(date)])
else:
call_count_list.append(0)
date_list.append(str(date)[5:])
date -= timedelta(days=1)
chat_count_list.reverse()
call_count_list.reverse()
date_list.reverse()
return ChatCallMonthCount(
chat=chat_count_list, call=call_count_list, date=date_list
)
@classmethod
async def get_connect_log(cls, query: QueryModel) -> BaseResultModel:
"""获取bot连接日志
参数:
query: 查询模型
返回:
BaseResultModel: 数据内容
"""
total = await BotConnectLog.all().count()
if total % query.size:
total += 1
data = (
await BotConnectLog.all()
.order_by("-id")
.offset((query.index - 1) * query.size)
.limit(query.size)
)
for v in data:
v.connect_time = v.connect_time.replace(tzinfo=None).replace(microsecond=0)
return BaseResultModel(total=total, data=data)

View File

@ -3,14 +3,15 @@ from fastapi.responses import JSONResponse
import nonebot import nonebot
from nonebot.drivers import Driver from nonebot.drivers import Driver
from tortoise import Tortoise from tortoise import Tortoise
from tortoise.exceptions import OperationalError
from zhenxun.configs.config import BotConfig from zhenxun.configs.config import BotConfig
from zhenxun.models.plugin_info import PluginInfo from zhenxun.models.plugin_info import PluginInfo
from zhenxun.models.task_info import TaskInfo from zhenxun.models.task_info import TaskInfo
from zhenxun.services.log import logger
from ....base_model import BaseResultModel, QueryModel, Result from ....base_model import BaseResultModel, QueryModel, Result
from ....utils import authentication from ....utils import authentication
from .data_source import ApiDataSource, type2sql
from .models.model import Column, SqlModel, SqlText from .models.model import Column, SqlModel, SqlText
from .models.sql_log import SqlLog from .models.sql_log import SqlLog
@ -20,52 +21,6 @@ router = APIRouter(prefix="/database")
driver: Driver = nonebot.get_driver() driver: Driver = nonebot.get_driver()
SQL_DICT = {}
SELECT_TABLE_MYSQL_SQL = """
SELECT table_name AS name, table_comment AS `desc`
FROM information_schema.tables
WHERE table_schema = DATABASE();
"""
SELECT_TABLE_SQLITE_SQL = """
SELECT name FROM sqlite_master WHERE type='table';
"""
SELECT_TABLE_PSQL_SQL = """
select a.tablename as name,d.description as desc from pg_tables a
left join pg_class c on relname=tablename
left join pg_description d on oid=objoid and objsubid=0 where a.schemaname='public'
"""
SELECT_TABLE_COLUMN_PSQL_SQL = """
SELECT column_name, data_type, character_maximum_length as max_length, is_nullable
FROM information_schema.columns
WHERE table_name = '{}';
"""
SELECT_TABLE_COLUMN_MYSQL_SQL = """
SHOW COLUMNS FROM {};
"""
SELECT_TABLE_COLUMN_SQLITE_SQL = """
PRAGMA table_info({});
"""
type2sql = {
"mysql": SELECT_TABLE_MYSQL_SQL,
"sqlite": SELECT_TABLE_SQLITE_SQL,
"postgres": SELECT_TABLE_PSQL_SQL,
}
type2sql_column = {
"mysql": SELECT_TABLE_COLUMN_MYSQL_SQL,
"sqlite": SELECT_TABLE_COLUMN_SQLITE_SQL,
"postgres": SELECT_TABLE_COLUMN_PSQL_SQL,
}
@driver.on_startup @driver.on_startup
async def _(): async def _():
for plugin in nonebot.get_loaded_plugins(): for plugin in nonebot.get_loaded_plugins():
@ -73,7 +28,7 @@ async def _():
sql_list = [] sql_list = []
if plugin.metadata and plugin.metadata.extra: if plugin.metadata and plugin.metadata.extra:
sql_list = plugin.metadata.extra.get("sql_list") sql_list = plugin.metadata.extra.get("sql_list")
if module in SQL_DICT: if module in ApiDataSource.SQL_DICT:
raise ValueError(f"{module} 常用SQL module 重复") raise ValueError(f"{module} 常用SQL module 重复")
if sql_list: if sql_list:
SqlModel( SqlModel(
@ -81,15 +36,15 @@ async def _():
module=module, module=module,
sql_list=sql_list, sql_list=sql_list,
) )
SQL_DICT[module] = SqlModel ApiDataSource.SQL_DICT[module] = SqlModel
if SQL_DICT: if ApiDataSource.SQL_DICT:
result = await PluginInfo.filter(module__in=SQL_DICT.keys()).values_list( result = await PluginInfo.filter(
"module", "name" module__in=ApiDataSource.SQL_DICT.keys()
) ).values_list("module", "name")
module2name = {r[0]: r[1] for r in result} module2name = {r[0]: r[1] for r in result}
for s in SQL_DICT: for s in ApiDataSource.SQL_DICT:
module = SQL_DICT[s].module module = ApiDataSource.SQL_DICT[s].module
SQL_DICT[s].name = module2name.get(module, module) ApiDataSource.SQL_DICT[s].name = module2name.get(module, module)
@router.get( @router.get(
@ -100,10 +55,14 @@ async def _():
description="获取数据库表", description="获取数据库表",
) )
async def _() -> Result[list[dict]]: async def _() -> Result[list[dict]]:
try:
db = Tortoise.get_connection("default") db = Tortoise.get_connection("default")
sql_type = BotConfig.get_sql_type() sql_type = BotConfig.get_sql_type()
query = await db.execute_query_dict(type2sql[sql_type]) query = await db.execute_query_dict(type2sql[sql_type])
return Result.ok(query) return Result.ok(query)
except Exception as e:
logger.error(f"{router.prefix}/get_table_list 调用错误", "WebUi", e=e)
return Result.fail(f"发生了一点错误捏 {type(e)}: {e}")
@router.get( @router.get(
@ -114,34 +73,13 @@ async def _() -> Result[list[dict]]:
description="获取表字段", description="获取表字段",
) )
async def _(table_name: str) -> Result[list[Column]]: async def _(table_name: str) -> Result[list[Column]]:
db = Tortoise.get_connection("default") try:
sql_type = BotConfig.get_sql_type() return Result.ok(
sql = type2sql_column[sql_type] await ApiDataSource.get_table_column(table_name), "拿到信息啦!"
query = await db.execute_query_dict(sql.format(table_name))
result_list = []
if sql_type == "sqlite":
result_list.extend(
Column(
column_name=result["name"],
data_type=result["type"],
max_length=-1,
is_nullable="YES" if result["notnull"] == 1 else "NO",
) )
for result in query except Exception as e:
) logger.error(f"{router.prefix}/get_table_column 调用错误", "WebUi", e=e)
elif sql_type == "mysql": return Result.fail(f"发生了一点错误捏 {type(e)}: {e}")
result_list.extend(
Column(
column_name=result["Field"],
data_type=result["Type"],
max_length=-1,
is_nullable=result["Null"],
)
for result in query
)
else:
result_list.extend(Column(**result) for result in query)
return Result.ok(result_list)
@router.post( @router.post(
@ -163,7 +101,8 @@ async def _(sql: SqlText, request: Request) -> Result[list[dict]]:
result = await TaskInfo.raw(sql.sql) result = await TaskInfo.raw(sql.sql)
await SqlLog.add(ip or "0.0.0.0", sql.sql, str(result)) await SqlLog.add(ip or "0.0.0.0", sql.sql, str(result))
return Result.ok(info="执行成功啦!") return Result.ok(info="执行成功啦!")
except OperationalError as e: except Exception as e:
logger.error(f"{router.prefix}/exec_sql 调用错误", "WebUi", e=e)
await SqlLog.add(ip or "0.0.0.0", sql.sql, str(e), False) await SqlLog.add(ip or "0.0.0.0", sql.sql, str(e), False)
return Result.warning_(f"sql执行错误: {e}") return Result.warning_(f"sql执行错误: {e}")
@ -176,6 +115,7 @@ async def _(sql: SqlText, request: Request) -> Result[list[dict]]:
description="sql日志列表", description="sql日志列表",
) )
async def _(query: QueryModel) -> Result[BaseResultModel]: async def _(query: QueryModel) -> Result[BaseResultModel]:
try:
total = await SqlLog.all().count() total = await SqlLog.all().count()
if total % query.size: if total % query.size:
total += 1 total += 1
@ -186,6 +126,9 @@ async def _(query: QueryModel) -> Result[BaseResultModel]:
.limit(query.size) .limit(query.size)
) )
return Result.ok(BaseResultModel(total=total, data=data)) return Result.ok(BaseResultModel(total=total, data=data))
except Exception as e:
logger.error(f"{router.prefix}/get_sql_log 调用错误", "WebUi", e=e)
return Result.fail(f"发生了一点错误捏 {type(e)}: {e}")
@router.get( @router.get(
@ -197,5 +140,5 @@ async def _(query: QueryModel) -> Result[BaseResultModel]:
) )
async def _(plugin_name: str | None = None) -> Result[dict]: async def _(plugin_name: str | None = None) -> Result[dict]:
if plugin_name: if plugin_name:
return Result.ok(SQL_DICT.get(plugin_name)) return Result.ok(ApiDataSource.SQL_DICT.get(plugin_name))
return Result.ok(str(SQL_DICT)) return Result.ok(str(ApiDataSource.SQL_DICT))

View File

@ -0,0 +1,90 @@
from tortoise import Tortoise
from zhenxun.configs.config import BotConfig
from .models.model import Column
SELECT_TABLE_MYSQL_SQL = """
SELECT table_name AS name, table_comment AS `desc`
FROM information_schema.tables
WHERE table_schema = DATABASE();
"""
SELECT_TABLE_SQLITE_SQL = """
SELECT name FROM sqlite_master WHERE type='table';
"""
SELECT_TABLE_PSQL_SQL = """
select a.tablename as name,d.description as desc from pg_tables a
left join pg_class c on relname=tablename
left join pg_description d on oid=objoid and objsubid=0 where a.schemaname='public'
"""
SELECT_TABLE_COLUMN_PSQL_SQL = """
SELECT column_name, data_type, character_maximum_length as max_length, is_nullable
FROM information_schema.columns
WHERE table_name = '{}';
"""
SELECT_TABLE_COLUMN_MYSQL_SQL = """
SHOW COLUMNS FROM {};
"""
SELECT_TABLE_COLUMN_SQLITE_SQL = """
PRAGMA table_info({});
"""
type2sql = {
"mysql": SELECT_TABLE_MYSQL_SQL,
"sqlite": SELECT_TABLE_SQLITE_SQL,
"postgres": SELECT_TABLE_PSQL_SQL,
}
type2sql_column = {
"mysql": SELECT_TABLE_COLUMN_MYSQL_SQL,
"sqlite": SELECT_TABLE_COLUMN_SQLITE_SQL,
"postgres": SELECT_TABLE_COLUMN_PSQL_SQL,
}
class ApiDataSource:
SQL_DICT = {} # noqa: RUF012
@classmethod
async def get_table_column(cls, table_name: str) -> list[Column]:
"""获取表字段信息
参数:
table_name: 表名
返回:
list[Column]: 字段数据
"""
db = Tortoise.get_connection("default")
sql_type = BotConfig.get_sql_type()
sql = type2sql_column[sql_type]
query = await db.execute_query_dict(sql.format(table_name))
result_list = []
if sql_type == "sqlite":
result_list.extend(
Column(
column_name=result["name"],
data_type=result["type"],
max_length=-1,
is_nullable="YES" if result["notnull"] == 1 else "NO",
)
for result in query
)
elif sql_type == "mysql":
result_list.extend(
Column(
column_name=result["Field"],
data_type=result["Type"],
max_length=-1,
is_nullable=result["Null"],
)
for result in query
)
else:
result_list.extend(Column(**result) for result in query)
return result_list

View File

@ -1,7 +1,5 @@
import asyncio import asyncio
import contextlib import contextlib
from datetime import datetime, timedelta
from pathlib import Path
import time import time
from fastapi import APIRouter from fastapi import APIRouter
@ -9,28 +7,26 @@ from fastapi.responses import JSONResponse
import nonebot import nonebot
from nonebot.config import Config from nonebot.config import Config
from starlette.websockets import WebSocket, WebSocketDisconnect, WebSocketState from starlette.websockets import WebSocket, WebSocketDisconnect, WebSocketState
from tortoise.functions import Count
from websockets.exceptions import ConnectionClosedError, ConnectionClosedOK from websockets.exceptions import ConnectionClosedError, ConnectionClosedOK
from zhenxun.models.bot_connect_log import BotConnectLog from zhenxun.models.bot_console import BotConsole
from zhenxun.models.chat_history import ChatHistory
from zhenxun.models.group_console import GroupConsole
from zhenxun.models.plugin_info import PluginInfo
from zhenxun.models.statistics import Statistics
from zhenxun.services.log import logger from zhenxun.services.log import logger
from zhenxun.utils.common_utils import CommonUtils
from zhenxun.utils.platform import PlatformUtils from zhenxun.utils.platform import PlatformUtils
from ....base_model import Result from ....base_model import Result
from ....config import AVA_URL, GROUP_AVA_URL, QueryDateType from ....config import QueryDateType
from ....utils import authentication, get_system_status from ....utils import authentication, get_system_status
from .data_source import bot_live from .data_source import ApiDataSource
from .model import ( from .model import (
ActiveGroup, ActiveGroup,
BaseInfo, BaseInfo,
BotBlockModule,
BotManageUpdateParam,
BotStatusParam,
HotPlugin, HotPlugin,
NonebotData, NonebotData,
QueryCount, QueryCount,
TemplateBaseInfo,
) )
driver = nonebot.get_driver() driver = nonebot.get_driver()
@ -56,64 +52,14 @@ async def _(bot_id: str | None = None) -> Result[list[BaseInfo]]:
返回: 返回:
Result: 获取指定bot信息与bot列表 Result: 获取指定bot信息与bot列表
""" """
global run_time try:
bot_list: list[TemplateBaseInfo] = [] result = await ApiDataSource.get_base_info(bot_id)
if bots := nonebot.get_bots(): if not result:
select_bot: BaseInfo Result.warning_("无Bot连接...")
for _, bot in bots.items(): return Result.ok(result, "拿到信息啦!")
login_info = await bot.get_login_info() except Exception as e:
bot_list.append( logger.error(f"{router.prefix}/get_base_info 调用错误", "WebUi", e=e)
TemplateBaseInfo( return Result.fail(f"发生了一点错误捏 {type(e)}: {e}")
bot=bot, # type: ignore
self_id=bot.self_id,
nickname=login_info["nickname"],
ava_url=AVA_URL.format(bot.self_id),
)
)
# 获取指定qq号的bot信息若无指定 则获取第一个
if _bl := [b for b in bot_list if b.self_id == bot_id]:
select_bot = _bl[0]
else:
select_bot = bot_list[0]
select_bot.is_select = True
now = datetime.now()
# 今日累计接收消息
select_bot.received_messages = await ChatHistory.filter(
bot_id=select_bot.self_id,
create_time__gte=now - timedelta(hours=now.hour),
).count()
# 群聊数量
select_bot.group_count = len(await select_bot.bot.get_group_list())
# 好友数量
select_bot.friend_count = len(await select_bot.bot.get_friend_list())
for bot in bot_list:
bot.bot = None # type: ignore
# 插件加载数量
select_bot.plugin_count = await PluginInfo.all().count()
fail_count = await PluginInfo.filter(load_status=False).count()
select_bot.fail_plugin_count = fail_count
select_bot.success_plugin_count = (
select_bot.plugin_count - select_bot.fail_plugin_count
)
# 连接时间
select_bot.connect_time = bot_live.get(select_bot.self_id) or 0
if select_bot.connect_time:
connect_date = datetime.fromtimestamp(select_bot.connect_time)
select_bot.connect_date = connect_date.strftime("%Y-%m-%d %H:%M:%S")
version_file = Path() / "__version__"
if version_file.exists():
if text := version_file.open().read():
if ver := text.replace("__version__: ", "").strip():
select_bot.version = ver
day_call = await Statistics.filter(
create_time__gte=now - timedelta(hours=now.hour)
).count()
select_bot.day_call = day_call
select_bot.connect_count = await BotConnectLog.filter(
bot_id=select_bot.self_id
).count()
return Result.ok([BaseInfo(**e.dict()) for e in bot_list], "拿到信息啦!")
return Result.warning_("无Bot连接...")
@router.get( @router.get(
@ -124,32 +70,11 @@ async def _(bot_id: str | None = None) -> Result[list[BaseInfo]]:
description="获取接收消息数量", description="获取接收消息数量",
) )
async def _(bot_id: str | None = None) -> Result[QueryCount]: async def _(bot_id: str | None = None) -> Result[QueryCount]:
now = datetime.now() try:
query = ChatHistory return Result.ok(await ApiDataSource.get_all_chat_count(bot_id), "拿到信息啦!")
if bot_id: except Exception as e:
query = query.filter(bot_id=bot_id) logger.error(f"{router.prefix}/get_all_chat_count 调用错误", "WebUi", e=e)
all_count = await query.annotate().count() return Result.fail(f"发生了一点错误捏 {type(e)}: {e}")
day_count = await query.filter(
create_time__gte=now - timedelta(hours=now.hour, minutes=now.minute)
).count()
week_count = await query.filter(
create_time__gte=now - timedelta(days=7, hours=now.hour, minutes=now.minute)
).count()
month_count = await query.filter(
create_time__gte=now - timedelta(days=30, hours=now.hour, minutes=now.minute)
).count()
year_count = await query.filter(
create_time__gte=now - timedelta(days=365, hours=now.hour, minutes=now.minute)
).count()
return Result.ok(
QueryCount(
num=all_count,
day=day_count,
week=week_count,
month=month_count,
year=year_count,
)
)
@router.get( @router.get(
@ -160,32 +85,11 @@ async def _(bot_id: str | None = None) -> Result[QueryCount]:
description="获取调用次数", description="获取调用次数",
) )
async def _(bot_id: str | None = None) -> Result[QueryCount]: async def _(bot_id: str | None = None) -> Result[QueryCount]:
now = datetime.now() try:
query = Statistics return Result.ok(await ApiDataSource.get_all_call_count(bot_id), "拿到信息啦!")
if bot_id: except Exception as e:
query = query.filter(bot_id=bot_id) logger.error(f"{router.prefix}/get_all_call_count 调用错误", "WebUi", e=e)
all_count = await query.annotate().count() return Result.fail(f"发生了一点错误捏 {type(e)}: {e}")
day_count = await query.filter(
create_time__gte=now - timedelta(hours=now.hour, minutes=now.minute)
).count()
week_count = await query.filter(
create_time__gte=now - timedelta(days=7, hours=now.hour, minutes=now.minute)
).count()
month_count = await query.filter(
create_time__gte=now - timedelta(days=30, hours=now.hour, minutes=now.minute)
).count()
year_count = await query.filter(
create_time__gte=now - timedelta(days=365, hours=now.hour, minutes=now.minute)
).count()
return Result.ok(
QueryCount(
num=all_count,
day=day_count,
week=week_count,
month=month_count,
year=year_count,
)
)
@router.get( @router.get(
@ -196,19 +100,18 @@ async def _(bot_id: str | None = None) -> Result[QueryCount]:
description="好友/群组数量", description="好友/群组数量",
) )
async def _(bot_id: str) -> Result[dict[str, int]]: async def _(bot_id: str) -> Result[dict[str, int]]:
if bots := nonebot.get_bots(): try:
if bot_id not in bots: bot = nonebot.get_bot(bot_id)
return Result.warning_("指定Bot未连接...")
bot = bots[bot_id]
platform = PlatformUtils.get_platform(bot)
if platform == "qq":
data = { data = {
"friend_count": len(await bot.get_friend_list()), "friend_count": len(await PlatformUtils.get_friend_list(bot)),
"group_count": len(await bot.get_group_list()), "group_count": len(await PlatformUtils.get_group_list(bot)),
} }
return Result.ok(data) return Result.ok(data, "拿到信息啦!")
return Result.warning_("暂不支持该平台...") except (ValueError, KeyError):
return Result.warning_("无Bot连接...") return Result.warning_("指定Bot未连接...")
except Exception as e:
logger.error(f"{router.prefix}/get_fg_count 调用错误", "WebUi", e=e)
return Result.fail(f"发生了一点错误捏 {type(e)}: {e}")
@router.get( @router.get(
@ -219,6 +122,7 @@ async def _(bot_id: str) -> Result[dict[str, int]]:
description="获取nb数据", description="获取nb数据",
) )
async def _() -> Result[NonebotData]: async def _() -> Result[NonebotData]:
global run_time
return Result.ok(NonebotData(config=driver.config, run_time=int(run_time))) return Result.ok(NonebotData(config=driver.config, run_time=int(run_time)))
@ -241,6 +145,7 @@ async def _() -> Result[Config]:
description="获取nb运行时间", description="获取nb运行时间",
) )
async def _() -> Result[int]: async def _() -> Result[int]:
global run_time
return Result.ok(int(run_time)) return Result.ok(int(run_time))
@ -254,48 +159,13 @@ async def _() -> Result[int]:
async def _( async def _(
date_type: QueryDateType | None = None, bot_id: str | None = None date_type: QueryDateType | None = None, bot_id: str | None = None
) -> Result[list[ActiveGroup]]: ) -> Result[list[ActiveGroup]]:
query = ChatHistory try:
now = datetime.now() return Result.ok(
if bot_id: await ApiDataSource.get_active_group(date_type, bot_id), "拿到信息啦!"
query = query.filter(bot_id=bot_id)
if date_type == QueryDateType.DAY:
query = query.filter(create_time__gte=now - timedelta(hours=now.hour))
if date_type == QueryDateType.WEEK:
query = query.filter(create_time__gte=now - timedelta(days=7))
if date_type == QueryDateType.MONTH:
query = query.filter(create_time__gte=now - timedelta(days=30))
if date_type == QueryDateType.YEAR:
query = query.filter(create_time__gte=now - timedelta(days=365))
data_list = (
await query.annotate(count=Count("id"))
.filter(group_id__not_isnull=True)
.group_by("group_id")
.order_by("-count")
.limit(5)
.values_list("group_id", "count")
) )
id2name = {} except Exception as e:
if data_list: logger.error(f"{router.prefix}/get_active_group 调用错误", "WebUi", e=e)
if info_list := await GroupConsole.filter( return Result.fail(f"发生了一点错误捏 {type(e)}: {e}")
group_id__in=[x[0] for x in data_list]
).all():
for group_info in info_list:
id2name[group_info.group_id] = group_info.group_name
active_group_list = [
ActiveGroup(
group_id=data[0],
name=id2name.get(data[0]) or data[0],
chat_num=data[1],
ava_img=GROUP_AVA_URL.format(data[0], data[0]),
)
for data in data_list
]
active_group_list = sorted(
active_group_list, key=lambda x: x.chat_num, reverse=True
)
if len(active_group_list) > 5:
active_group_list = active_group_list[:5]
return Result.ok(active_group_list)
@router.get( @router.get(
@ -308,37 +178,66 @@ async def _(
async def _( async def _(
date_type: QueryDateType | None = None, bot_id: str | None = None date_type: QueryDateType | None = None, bot_id: str | None = None
) -> Result[list[HotPlugin]]: ) -> Result[list[HotPlugin]]:
query = Statistics try:
now = datetime.now() return Result.ok(
if bot_id: await ApiDataSource.get_hot_plugin(date_type, bot_id), "拿到信息啦!"
query = query.filter(bot_id=bot_id)
if date_type == QueryDateType.DAY:
query = query.filter(create_time__gte=now - timedelta(hours=now.hour))
if date_type == QueryDateType.WEEK:
query = query.filter(create_time__gte=now - timedelta(days=7))
if date_type == QueryDateType.MONTH:
query = query.filter(create_time__gte=now - timedelta(days=30))
if date_type == QueryDateType.YEAR:
query = query.filter(create_time__gte=now - timedelta(days=365))
data_list = (
await query.annotate(count=Count("id"))
.group_by("plugin_name")
.order_by("-count")
.limit(5)
.values_list("plugin_name", "count")
) )
hot_plugin_list = [] except Exception as e:
module_list = [x[0] for x in data_list] logger.error(f"{router.prefix}/get_hot_plugin 调用错误", "WebUi", e=e)
plugins = await PluginInfo.filter(module__in=module_list).all() return Result.fail(f"发生了一点错误捏 {type(e)}: {e}")
module2name = {p.module: p.name for p in plugins}
for data in data_list:
module = data[0] @router.post(
name = module2name.get(module) or module "/change_bot_status",
hot_plugin_list.append(HotPlugin(module=module, name=name, count=data[1])) dependencies=[authentication()],
hot_plugin_list = sorted(hot_plugin_list, key=lambda x: x.count, reverse=True) response_model=Result,
if len(hot_plugin_list) > 5: response_class=JSONResponse,
hot_plugin_list = hot_plugin_list[:5] description="修改bot全局开关",
return Result.ok(hot_plugin_list) )
async def _(param: BotStatusParam):
try:
await BotConsole.set_bot_status(param.status, param.bot_id)
return Result.ok(info="修改bot全局开关成功")
except (ValueError, KeyError):
return Result.fail("Bot未初始化...")
@router.get(
"/get_bot_block_module",
dependencies=[authentication()],
response_model=Result[BotBlockModule],
response_class=JSONResponse,
description="获取bot层面的禁用模块",
)
async def _(bot_id: str) -> Result[BotBlockModule]:
try:
return Result.ok(
await ApiDataSource.get_bot_block_module(bot_id), "拿到信息啦!"
)
except Exception as e:
logger.error(f"{router.prefix}/get_bot_block_module 调用错误", "WebUi", e=e)
return Result.fail(f"发生了一点错误捏 {type(e)}: {e}")
@router.post(
"/update_bot_manage",
dependencies=[authentication()],
response_model=Result,
response_class=JSONResponse,
description="修改bot全局开关",
)
async def _(param: BotManageUpdateParam):
try:
bot_data = await BotConsole.get_or_none(bot_id=param.bot_id)
if not bot_data:
return Result.fail("Bot数据不存在...")
bot_data.block_plugins = CommonUtils.convert_module_format(param.block_plugins)
bot_data.block_tasks = CommonUtils.convert_module_format(param.block_tasks)
await bot_data.save(update_fields=["block_plugins", "block_tasks"])
return Result.ok()
except Exception as e:
logger.error(f"{router.prefix}/update_bot_manage 调用错误", "WebUi", e=e)
return Result.fail(f"发生了一点错误捏 {type(e)}: {e}")
@ws_router.websocket("/system_status") @ws_router.websocket("/system_status")

View File

@ -1,8 +1,33 @@
from datetime import datetime, timedelta
from pathlib import Path
import time import time
import nonebot import nonebot
from nonebot.adapters.onebot.v11 import Bot from nonebot.adapters import Bot
from nonebot.drivers import Driver from nonebot.drivers import Driver
from tortoise.functions import Count
from zhenxun.models.bot_connect_log import BotConnectLog
from zhenxun.models.bot_console import BotConsole
from zhenxun.models.chat_history import ChatHistory
from zhenxun.models.group_console import GroupConsole
from zhenxun.models.plugin_info import PluginInfo
from zhenxun.models.statistics import Statistics
from zhenxun.models.task_info import TaskInfo
from zhenxun.services.log import logger
from zhenxun.utils.common_utils import CommonUtils
from zhenxun.utils.enum import PluginType
from zhenxun.utils.platform import PlatformUtils
from ....config import AVA_URL, GROUP_AVA_URL, QueryDateType
from .model import (
ActiveGroup,
BaseInfo,
BotBlockModule,
HotPlugin,
QueryCount,
TemplateBaseInfo,
)
driver: Driver = nonebot.get_driver() driver: Driver = nonebot.get_driver()
@ -33,3 +58,313 @@ async def _(bot: Bot):
@driver.on_bot_disconnect @driver.on_bot_disconnect
async def _(bot: Bot): async def _(bot: Bot):
bot_live.remove(bot.self_id) bot_live.remove(bot.self_id)
class ApiDataSource:
@classmethod
async def __build_bot_info(cls, bot: Bot) -> TemplateBaseInfo:
"""构建bot信息
参数:
bot: bot实例
返回:
TemplateBaseInfo: bot信息
"""
login_info = None
try:
login_info = await bot.get_login_info()
except Exception as e:
logger.warning("调用接口get_login_info失败", "WebUi", e=e)
return TemplateBaseInfo(
bot=bot,
self_id=bot.self_id,
nickname=login_info["nickname"] if login_info else bot.self_id,
ava_url=AVA_URL.format(bot.self_id),
)
@classmethod
def __get_bot_version(cls) -> str:
"""获取bot版本
返回:
str | None: 版本
"""
version_file = Path() / "__version__"
if version_file.exists():
if text := version_file.open().read():
return text.replace("__version__: ", "").strip()
return "unknown"
@classmethod
async def __init_bot_base_data(cls, select_bot: TemplateBaseInfo):
"""初始化bot的基础数据
参数:
select_bot: bot
"""
now = datetime.now()
# 今日累计接收消息
select_bot.received_messages = await ChatHistory.filter(
bot_id=select_bot.self_id,
create_time__gte=now - timedelta(hours=now.hour),
).count()
# 群聊数量
select_bot.group_count = len(await PlatformUtils.get_group_list(select_bot.bot))
# 好友数量
select_bot.friend_count = len(
await PlatformUtils.get_friend_list(select_bot.bot)
)
select_bot.status = await BotConsole.get_bot_status(select_bot.self_id)
# 连接时间
select_bot.connect_time = bot_live.get(select_bot.self_id) or 0
if select_bot.connect_time:
connect_date = datetime.fromtimestamp(select_bot.connect_time)
select_bot.connect_date = connect_date.strftime("%Y-%m-%d %H:%M:%S")
select_bot.version = cls.__get_bot_version()
day_call = await Statistics.filter(
create_time__gte=now - timedelta(hours=now.hour)
).count()
select_bot.day_call = day_call
select_bot.connect_count = await BotConnectLog.filter(
bot_id=select_bot.self_id
).count()
@classmethod
async def get_base_info(cls, bot_id: str | None) -> list[BaseInfo] | None:
"""获取bot信息
参数:
bot_id: bot id
返回:
list[BaseInfo] | None: bot列表
"""
bots = nonebot.get_bots()
if not bots:
return None
select_bot: BaseInfo
bot_list = [await cls.__build_bot_info(bot) for _, bot in bots.items()]
# 获取指定qq号的bot信息若无指定 则获取第一个
if _bl := [b for b in bot_list if b.self_id == bot_id]:
select_bot = _bl[0]
else:
select_bot = bot_list[0]
await cls.__init_bot_base_data(select_bot)
for bot in bot_list:
bot.bot = None # type: ignore
select_bot.is_select = True
return [BaseInfo(**e.dict()) for e in bot_list]
@classmethod
async def get_all_chat_count(cls, bot_id: str | None) -> QueryCount:
"""获取年/月/周/日聊天次数
参数:
bot_id: bot id
返回:
QueryCount: 数据内容
"""
now = datetime.now()
query = ChatHistory
if bot_id:
query = query.filter(bot_id=bot_id)
all_count = await query.annotate().count()
day_count = await query.filter(
create_time__gte=now - timedelta(hours=now.hour, minutes=now.minute)
).count()
week_count = await query.filter(
create_time__gte=now - timedelta(days=7, hours=now.hour, minutes=now.minute)
).count()
month_count = await query.filter(
create_time__gte=now
- timedelta(days=30, hours=now.hour, minutes=now.minute)
).count()
year_count = await query.filter(
create_time__gte=now
- timedelta(days=365, hours=now.hour, minutes=now.minute)
).count()
return QueryCount(
num=all_count,
day=day_count,
week=week_count,
month=month_count,
year=year_count,
)
@classmethod
async def get_all_call_count(cls, bot_id: str | None) -> QueryCount:
"""获取年/月/周/日调用次数
参数:
bot_id: bot id
返回:
QueryCount: 数据内容
"""
now = datetime.now()
query = Statistics
if bot_id:
query = query.filter(bot_id=bot_id)
all_count = await query.annotate().count()
day_count = await query.filter(
create_time__gte=now - timedelta(hours=now.hour, minutes=now.minute)
).count()
week_count = await query.filter(
create_time__gte=now - timedelta(days=7, hours=now.hour, minutes=now.minute)
).count()
month_count = await query.filter(
create_time__gte=now
- timedelta(days=30, hours=now.hour, minutes=now.minute)
).count()
year_count = await query.filter(
create_time__gte=now
- timedelta(days=365, hours=now.hour, minutes=now.minute)
).count()
return QueryCount(
num=all_count,
day=day_count,
week=week_count,
month=month_count,
year=year_count,
)
@classmethod
def __get_query(
cls,
base_query: type[ChatHistory | Statistics],
date_type: QueryDateType | None = None,
bot_id: str | None = None,
):
"""构建日期查询条件
参数:
date_type: 日期类型.
bot_id: bot id.
"""
query = base_query
now = datetime.now()
if bot_id:
query = query.filter(bot_id=bot_id)
if date_type == QueryDateType.DAY:
query = query.filter(create_time__gte=now - timedelta(hours=now.hour))
if date_type == QueryDateType.WEEK:
query = query.filter(create_time__gte=now - timedelta(days=7))
if date_type == QueryDateType.MONTH:
query = query.filter(create_time__gte=now - timedelta(days=30))
if date_type == QueryDateType.YEAR:
query = query.filter(create_time__gte=now - timedelta(days=365))
return query
@classmethod
async def get_active_group(
cls, date_type: QueryDateType | None = None, bot_id: str | None = None
) -> list[ActiveGroup]:
"""获取活跃群组
参数:
date_type: 日期类型.
bot_id: bot id.
返回:
list[ActiveGroup]: 活跃群组列表
"""
query = cls.__get_query(ChatHistory, date_type, bot_id)
data_list = (
await query.annotate(count=Count("id"))
.filter(group_id__not_isnull=True)
.group_by("group_id")
.order_by("-count")
.limit(5)
.values_list("group_id", "count")
)
id2name = {}
if data_list:
if info_list := await GroupConsole.filter(
group_id__in=[x[0] for x in data_list]
).all():
for group_info in info_list:
id2name[group_info.group_id] = group_info.group_name
active_group_list = [
ActiveGroup(
group_id=data[0],
name=id2name.get(data[0]) or data[0],
chat_num=data[1],
ava_img=GROUP_AVA_URL.format(data[0], data[0]),
)
for data in data_list
]
active_group_list = sorted(
active_group_list, key=lambda x: x.chat_num, reverse=True
)
if len(active_group_list) > 5:
active_group_list = active_group_list[:5]
return active_group_list
@classmethod
async def get_hot_plugin(
cls, date_type: QueryDateType | None = None, bot_id: str | None = None
) -> list[HotPlugin]:
"""获取热门插件
参数:
date_type: 日期类型.
bot_id: bot id.
返回:
list[HotPlugin]: 热门插件列表
"""
query = cls.__get_query(Statistics, date_type, bot_id)
data_list = (
await query.annotate(count=Count("id"))
.group_by("plugin_name")
.order_by("-count")
.limit(5)
.values_list("plugin_name", "count")
)
hot_plugin_list = []
module_list = [x[0] for x in data_list]
plugins = await PluginInfo.filter(module__in=module_list).all()
module2name = {p.module: p.name for p in plugins}
for data in data_list:
module = data[0]
name = module2name.get(module) or module
hot_plugin_list.append(HotPlugin(module=module, name=name, count=data[1]))
hot_plugin_list = sorted(hot_plugin_list, key=lambda x: x.count, reverse=True)
if len(hot_plugin_list) > 5:
hot_plugin_list = hot_plugin_list[:5]
return hot_plugin_list
@classmethod
async def get_bot_block_module(cls, bot_id: str) -> BotBlockModule | None:
"""获取bot层面的禁用模块
参数:
bot_id: bot id
返回:
BotBlockModule | None: 数据内容
"""
bot_data = await BotConsole.get_or_none(bot_id=bot_id)
if not bot_data:
return None
block_tasks = []
block_plugins = []
all_plugins = await PluginInfo.filter(
load_status=True, plugin_type=PluginType.NORMAL
).values("module", "name")
all_task = await TaskInfo.annotate().values("module", "name")
if bot_data.block_tasks:
tasks = CommonUtils.convert_module_format(bot_data.block_tasks)
block_tasks = [t["module"] for t in all_task if t["module"] in tasks]
if bot_data.block_plugins:
plugins = CommonUtils.convert_module_format(bot_data.block_plugins)
block_plugins = [t["module"] for t in all_plugins if t["module"] in plugins]
return BotBlockModule(
bot_id=bot_id,
block_tasks=block_tasks,
block_plugins=block_plugins,
all_plugins=all_plugins,
all_tasks=all_task,
)

View File

@ -1,8 +1,45 @@
from typing import Any
from nonebot.adapters import Bot from nonebot.adapters import Bot
from nonebot.config import Config from nonebot.config import Config
from pydantic import BaseModel from pydantic import BaseModel
class BotManageUpdateParam(BaseModel):
"""bot更新参数"""
bot_id: str
"""bot id"""
block_plugins: list[str]
"""禁用插件"""
block_tasks: list[str]
"""禁用被动"""
class BotStatusParam(BaseModel):
"""bot状态参数"""
bot_id: str
"""bot id"""
status: bool
"""状态"""
class BotBlockModule(BaseModel):
"""bot禁用模块参数"""
bot_id: str
"""bot id"""
block_plugins: list[str]
"""禁用插件"""
block_tasks: list[str]
"""禁用被动"""
all_plugins: list[dict[str, Any]]
"""所有插件"""
all_tasks: list[dict[str, Any]]
"""所有被动"""
class SystemStatus(BaseModel): class SystemStatus(BaseModel):
""" """
系统状态 系统状态
@ -36,13 +73,8 @@ class BaseInfo(BaseModel):
"""连接日期""" """连接日期"""
connect_count: int = 0 connect_count: int = 0
"""连接次数""" """连接次数"""
status: bool = False
plugin_count: int = 0 """全局状态"""
"""加载插件数量"""
success_plugin_count: int = 0
"""加载成功插件数量"""
fail_plugin_count: int = 0
"""加载失败插件数量"""
is_select: bool = False is_select: bool = False
"""当前选择""" """当前选择"""

View File

@ -2,16 +2,9 @@ from fastapi import APIRouter
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
import nonebot import nonebot
from nonebot.adapters.onebot.v11 import ActionFailed from nonebot.adapters.onebot.v11 import ActionFailed
from tortoise.functions import Count
from zhenxun.configs.config import BotConfig
from zhenxun.models.ban_console import BanConsole
from zhenxun.models.chat_history import ChatHistory
from zhenxun.models.fg_request import FgRequest from zhenxun.models.fg_request import FgRequest
from zhenxun.models.group_console import GroupConsole from zhenxun.models.group_console import GroupConsole
from zhenxun.models.plugin_info import PluginInfo
from zhenxun.models.statistics import Statistics
from zhenxun.models.task_info import TaskInfo
from zhenxun.services.log import logger from zhenxun.services.log import logger
from zhenxun.utils.enum import RequestHandleType, RequestType from zhenxun.utils.enum import RequestHandleType, RequestType
from zhenxun.utils.exception import NotFoundError from zhenxun.utils.exception import NotFoundError
@ -20,20 +13,17 @@ from zhenxun.utils.platform import PlatformUtils
from ....base_model import Result from ....base_model import Result
from ....config import AVA_URL, GROUP_AVA_URL from ....config import AVA_URL, GROUP_AVA_URL
from ....utils import authentication from ....utils import authentication
from .data_source import ApiDataSource
from .model import ( from .model import (
ClearRequest, ClearRequest,
DeleteFriend, DeleteFriend,
Friend, Friend,
FriendRequestResult,
GroupDetail, GroupDetail,
GroupRequestResult,
GroupResult, GroupResult,
HandleRequest, HandleRequest,
LeaveGroup, LeaveGroup,
Plugin,
ReqResult, ReqResult,
SendMessage, SendMessageParam,
Task,
UpdateGroup, UpdateGroup,
UserDetail, UserDetail,
) )
@ -52,19 +42,21 @@ async def _(bot_id: str) -> Result:
""" """
获取群信息 获取群信息
""" """
if not (bots := nonebot.get_bots()):
return Result.warning_("无Bot连接...")
if bot_id not in bots:
return Result.warning_("指定Bot未连接...")
group_list_result = [] group_list_result = []
try: try:
group_list = await bots[bot_id].get_group_list() bot = nonebot.get_bot(bot_id)
group_list, _ = await PlatformUtils.get_group_list(bot)
for g in group_list: for g in group_list:
gid = g["group_id"] ava_url = GROUP_AVA_URL.format(g.group_id, g.group_id)
g["ava_url"] = GROUP_AVA_URL.format(gid, gid) group_list_result.append(
group_list_result.append(GroupResult(**g)) GroupResult(
group_id=g.group_id, group_name=g.group_name, ava_url=ava_url
)
)
except (ValueError, KeyError):
return Result.warning_("指定Bot未连接...")
except Exception as e: except Exception as e:
logger.error("调用API错误", "/get_group_list", e=e) logger.error(f"{router.prefix}/get_group_list 调用错误", "WebUi", e=e)
return Result.fail(f"{type(e)}: {e}") return Result.fail(f"{type(e)}: {e}")
return Result.ok(group_list_result, "拿到了新鲜出炉的数据!") return Result.ok(group_list_result, "拿到了新鲜出炉的数据!")
@ -78,25 +70,11 @@ async def _(bot_id: str) -> Result:
) )
async def _(group: UpdateGroup) -> Result[str]: async def _(group: UpdateGroup) -> Result[str]:
try: try:
group_id = group.group_id await ApiDataSource.update_group(group)
if db_group := await GroupConsole.get_group(group_id):
task_list = await TaskInfo.all().values_list("module", flat=True)
db_group.level = group.level
db_group.status = group.status
if group.close_plugins:
group.close_plugins = [f"<{module}" for module in group.close_plugins]
db_group.block_plugin = ",".join(group.close_plugins) + ","
if group.task:
if block_task := [t for t in task_list if t not in group.task]:
block_task = [f"<{module}" for module in block_task]
db_group.block_task = ",".join(block_task) + "," # type: ignore
await db_group.save(
update_fields=["level", "status", "block_plugin", "block_task"]
)
except Exception as e:
logger.error("调用API错误", "/get_group", e=e)
return Result.fail(f"{type(e)}: {e}")
return Result.ok(info="已完成记录!") return Result.ok(info="已完成记录!")
except Exception as e:
logger.error(f"{router.prefix}/update_group 调用错误", "WebUi", e=e)
return Result.fail(f"发生了一点错误捏 {type(e)}: {e}")
@router.get( @router.get(
@ -110,24 +88,24 @@ async def _(bot_id: str) -> Result[list[Friend]]:
""" """
获取群信息 获取群信息
""" """
if bots := nonebot.get_bots():
if bot_id not in bots:
return Result.warning_("指定Bot未连接...")
try: try:
platform = PlatformUtils.get_platform(bots[bot_id]) bot = nonebot.get_bot(bot_id)
if platform != "qq": friend_list, _ = await PlatformUtils.get_friend_list(bot)
return Result.warning_("该平台暂不支持该功能...") result_list = []
friend_list = await bots[bot_id].get_friend_list()
for f in friend_list: for f in friend_list:
f["ava_url"] = AVA_URL.format(f["user_id"]) ava_url = AVA_URL.format(f.user_id)
result_list.append(
Friend(user_id=f.user_id, nickname=f.nickname, ava_url=ava_url)
)
return Result.ok( return Result.ok(
[Friend(**f) for f in friend_list if str(f["user_id"]) != bot_id], result_list,
"拿到了新鲜出炉的数据!", "拿到了新鲜出炉的数据!",
) )
except (ValueError, KeyError):
return Result.warning_("指定Bot未连接...")
except Exception as e: except Exception as e:
logger.error("调用API错误", "/get_group_list", e=e) logger.error("调用API错误", "/get_group_list", e=e)
return Result.fail(f"{type(e)}: {e}") return Result.fail(f"{type(e)}: {e}")
return Result.warning_("无Bot连接...")
@router.get( @router.get(
@ -138,6 +116,7 @@ async def _(bot_id: str) -> Result[list[Friend]]:
description="获取请求数量", description="获取请求数量",
) )
async def _() -> Result[dict[str, int]]: async def _() -> Result[dict[str, int]]:
try:
f_count = await FgRequest.filter( f_count = await FgRequest.filter(
request_type=RequestType.FRIEND, handle_type__isnull=True request_type=RequestType.FRIEND, handle_type__isnull=True
).count() ).count()
@ -148,7 +127,10 @@ async def _() -> Result[dict[str, int]]:
"friend_count": f_count, "friend_count": f_count,
"group_count": g_count, "group_count": g_count,
} }
return Result.ok(data, f"{BotConfig.self_nickname}带来了最新的数据!") return Result.ok(data, "拿到了新鲜出炉的数据!")
except Exception as e:
logger.error("调用API错误", "/get_request_count", e=e)
return Result.fail(f"{type(e)}: {e}")
@router.get( @router.get(
@ -160,43 +142,10 @@ async def _() -> Result[dict[str, int]]:
) )
async def _() -> Result[ReqResult]: async def _() -> Result[ReqResult]:
try: try:
req_result = ReqResult() return Result.ok(await ApiDataSource.get_request_list(), "拿到信息啦!")
data_list = await FgRequest.filter(handle_type__isnull=True).all()
for req in data_list:
if req.request_type == RequestType.FRIEND:
req_result.friend.append(
FriendRequestResult(
oid=req.id,
bot_id=req.bot_id,
id=req.user_id,
flag=req.flag,
nickname=req.nickname,
comment=req.comment,
ava_url=AVA_URL.format(req.user_id),
type=str(req.request_type).lower(),
)
)
else:
req_result.group.append(
GroupRequestResult(
oid=req.id,
bot_id=req.bot_id,
id=req.user_id,
flag=req.flag,
nickname=req.nickname,
comment=req.comment,
ava_url=GROUP_AVA_URL.format(req.group_id, req.group_id),
type=str(req.request_type).lower(),
invite_group=req.group_id,
group_name=None,
)
)
req_result.friend.reverse()
req_result.group.reverse()
except Exception as e: except Exception as e:
logger.error("调用API错误", "/get_request", e=e) logger.error(f"{router.prefix}/get_request_list 调用错误", "WebUi", e=e)
return Result.fail(f"{type(e)}: {e}") return Result.fail(f"发生了一点错误捏 {type(e)}: {e}")
return Result.ok(req_result, f"{BotConfig.self_nickname}带来了最新的数据!")
@router.post( @router.post(
@ -220,23 +169,21 @@ async def _(cr: ClearRequest) -> Result:
response_class=JSONResponse, response_class=JSONResponse,
description="拒绝请求", description="拒绝请求",
) )
async def _(parma: HandleRequest) -> Result: async def _(param: HandleRequest) -> Result:
try: try:
if bots := nonebot.get_bots(): bot = nonebot.get_bot(param.bot_id)
bot_id = parma.bot_id
if bot_id not in nonebot.get_bots():
return Result.warning_("指定Bot未连接...")
try: try:
await FgRequest.refused(bots[bot_id], parma.id) await FgRequest.refused(bot, param.id)
except ActionFailed: except ActionFailed:
await FgRequest.expire(parma.id) await FgRequest.expire(param.id)
return Result.warning_("请求失败,可能该请求已失效或请求数据错误...") return Result.warning_("请求失败,可能该请求已失效或请求数据错误...")
except NotFoundError: except NotFoundError:
return Result.warning_("未找到此Id请求...") return Result.warning_("未找到此Id请求...")
return Result.ok(info="成功处理了请求!") return Result.ok(info="成功处理了请求!")
return Result.warning_("无Bot连接...") except (ValueError, KeyError):
return Result.warning_("指定Bot未连接...")
except Exception as e: except Exception as e:
logger.error("调用API错误", "/refuse_request", e=e) logger.error(f"{router.prefix}/refuse_request 调用错误", "WebUi", e=e)
return Result.fail(f"{type(e)}: {e}") return Result.fail(f"{type(e)}: {e}")
@ -247,8 +194,8 @@ async def _(parma: HandleRequest) -> Result:
response_class=JSONResponse, response_class=JSONResponse,
description="忽略请求", description="忽略请求",
) )
async def _(parma: HandleRequest) -> Result: async def _(param: HandleRequest) -> Result:
await FgRequest.ignore(parma.id) await FgRequest.ignore(param.id)
return Result.ok(info="成功处理了请求!") return Result.ok(info="成功处理了请求!")
@ -259,13 +206,10 @@ async def _(parma: HandleRequest) -> Result:
response_class=JSONResponse, response_class=JSONResponse,
description="同意请求", description="同意请求",
) )
async def _(parma: HandleRequest) -> Result: async def _(param: HandleRequest) -> Result:
try: try:
if bots := nonebot.get_bots(): bot = nonebot.get_bot(param.bot_id)
bot_id = parma.bot_id if not (req := await FgRequest.get_or_none(id=param.id)):
if bot_id not in nonebot.get_bots():
return Result.warning_("指定Bot未连接...")
if not (req := await FgRequest.get_or_none(id=parma.id)):
return Result.warning_("未找到此Id请求...") return Result.warning_("未找到此Id请求...")
if req.request_type == RequestType.GROUP: if req.request_type == RequestType.GROUP:
if group := await GroupConsole.get_group(group_id=req.group_id): if group := await GroupConsole.get_group(group_id=req.group_id):
@ -277,14 +221,15 @@ async def _(parma: HandleRequest) -> Result:
defaults={"group_flag": 1}, defaults={"group_flag": 1},
) )
try: try:
await FgRequest.approve(bots[bot_id], parma.id) await FgRequest.approve(bot, param.id)
return Result.ok(info="成功处理了请求!") return Result.ok(info="成功处理了请求!")
except ActionFailed: except ActionFailed:
await FgRequest.expire(parma.id) await FgRequest.expire(param.id)
return Result.warning_("请求失败,可能该请求已失效或请求数据错误...") return Result.warning_("请求失败,可能该请求已失效或请求数据错误...")
return Result.warning_("无Bot连接...") except (ValueError, KeyError):
return Result.warning_("指定Bot未连接...")
except Exception as e: except Exception as e:
logger.error("调用API错误", "/approve_request", e=e) logger.error(f"{router.prefix}/approve_request 调用错误", "WebUi", e=e)
return Result.fail(f"{type(e)}: {e}") return Result.fail(f"{type(e)}: {e}")
@ -297,19 +242,19 @@ async def _(parma: HandleRequest) -> Result:
) )
async def _(param: LeaveGroup) -> Result: async def _(param: LeaveGroup) -> Result:
try: try:
if bots := nonebot.get_bots(): bot = nonebot.get_bot(param.bot_id)
bot_id = param.bot_id platform = PlatformUtils.get_platform(bot)
platform = PlatformUtils.get_platform(bots[bot_id])
if platform != "qq": if platform != "qq":
return Result.warning_("该平台不支持退群操作...") return Result.warning_("该平台不支持退群操作...")
group_list = await bots[bot_id].get_group_list() group_list, _ = await PlatformUtils.get_group_list(bot)
if param.group_id not in [str(g["group_id"]) for g in group_list]: if param.group_id not in [g.group_id for g in group_list]:
return Result.warning_("Bot未在该群聊中...") return Result.warning_("Bot未在该群聊中...")
await bots[bot_id].set_group_leave(group_id=param.group_id) await bot.set_group_leave(group_id=param.group_id)
return Result.ok(info="成功处理了请求!") return Result.ok(info="成功处理了请求!")
return Result.warning_("无Bot连接...") except (ValueError, KeyError):
return Result.warning_("指定Bot未连接...")
except Exception as e: except Exception as e:
logger.error("调用API错误", "/leave_group", e=e) logger.error(f"{router.prefix}/leave_group 调用错误", "WebUi", e=e)
return Result.fail(f"{type(e)}: {e}") return Result.fail(f"{type(e)}: {e}")
@ -322,19 +267,19 @@ async def _(param: LeaveGroup) -> Result:
) )
async def _(param: DeleteFriend) -> Result: async def _(param: DeleteFriend) -> Result:
try: try:
if bots := nonebot.get_bots(): bot = nonebot.get_bot(param.bot_id)
bot_id = param.bot_id platform = PlatformUtils.get_platform(bot)
platform = PlatformUtils.get_platform(bots[bot_id])
if platform != "qq": if platform != "qq":
return Result.warning_("该平台不支持删除好友操作...") return Result.warning_("该平台不支持删除好友操作...")
friend_list = await bots[bot_id].get_friend_list() friend_list, _ = await PlatformUtils.get_friend_list(bot)
if param.user_id not in [str(g["user_id"]) for g in friend_list]: if param.user_id not in [f.user_id for f in friend_list]:
return Result.warning_("Bot未有其好友...") return Result.warning_("Bot未有其好友...")
await bots[bot_id].delete_friend(user_id=param.user_id) await bot.delete_friend(user_id=param.user_id)
return Result.ok(info="成功处理了请求!") return Result.ok(info="成功处理了请求!")
return Result.warning_("Bot未连接...") except (ValueError, KeyError):
return Result.warning_("指定Bot未连接...")
except Exception as e: except Exception as e:
logger.error("调用API错误", "/delete_friend", e=e) logger.error(f"{router.prefix}/delete_friend 调用错误", "WebUi", e=e)
return Result.fail(f"{type(e)}: {e}") return Result.fail(f"{type(e)}: {e}")
@ -346,43 +291,18 @@ async def _(param: DeleteFriend) -> Result:
description="获取好友详情", description="获取好友详情",
) )
async def _(bot_id: str, user_id: str) -> Result[UserDetail]: async def _(bot_id: str, user_id: str) -> Result[UserDetail]:
if bots := nonebot.get_bots(): try:
if bot_id in bots: result = await ApiDataSource.get_friend_detail(bot_id, user_id)
if fd := [ return (
x Result.ok(result, "拿到信息啦!")
for x in await bots[bot_id].get_friend_list() if result
if str(x["user_id"]) == user_id else Result.warning_("未找到该好友...")
]:
like_plugin_list = (
await Statistics.filter(user_id=user_id)
.annotate(count=Count("id"))
.group_by("plugin_name")
.order_by("-count")
.limit(5)
.values_list("plugin_name", "count")
) )
like_plugin = {} except (ValueError, KeyError):
module_list = [x[0] for x in like_plugin_list] return Result.warning_("指定Bot未连接...")
plugins = await PluginInfo.filter(module__in=module_list).all() except Exception as e:
module2name = {p.module: p.name for p in plugins} logger.error(f"{router.prefix}/get_friend_detail 调用错误", "WebUi", e=e)
for data in like_plugin_list: return Result.fail(f"发生了一点错误捏 {type(e)}: {e}")
name = module2name.get(data[0]) or data[0]
like_plugin[name] = data[1]
user = fd[0]
user_detail = UserDetail(
user_id=user_id,
ava_url=AVA_URL.format(user_id),
nickname=user["nickname"],
remark=user["remark"],
is_ban=await BanConsole.is_ban(user_id),
chat_count=await ChatHistory.filter(user_id=user_id).count(),
call_count=await Statistics.filter(user_id=user_id).count(),
like_plugin=like_plugin,
)
return Result.ok(user_detail)
else:
return Result.warning_("未添加指定好友...")
return Result.warning_("无Bot连接...")
@router.get( @router.get(
@ -392,90 +312,12 @@ async def _(bot_id: str, user_id: str) -> Result[UserDetail]:
response_class=JSONResponse, response_class=JSONResponse,
description="获取群组详情", description="获取群组详情",
) )
async def _(bot_id: str, group_id: str) -> Result[GroupDetail]: async def _(group_id: str) -> Result[GroupDetail]:
if not (bots := nonebot.get_bots()): try:
return Result.warning_("无Bot连接...") return Result.ok(await ApiDataSource.get_group_detail(group_id), "拿到信息啦!")
if bot_id not in bots: except Exception as e:
return Result.warning_("未添加指定群组...") logger.error(f"{router.prefix}/get_group_detail 调用错误", "WebUi", e=e)
group = await GroupConsole.get_or_none(group_id=group_id) return Result.fail(f"发生了一点错误捏 {type(e)}: {e}")
if not group:
return Result.warning_("指定群组未被收录...")
like_plugin_list = (
await Statistics.filter(group_id=group_id)
.annotate(count=Count("id"))
.group_by("plugin_name")
.order_by("-count")
.limit(5)
.values_list("plugin_name", "count")
)
like_plugin = {}
plugins = await PluginInfo.get_plugins()
module2name = {p.module: p.name for p in plugins}
for data in like_plugin_list:
name = module2name.get(data[0]) or data[0]
like_plugin[name] = data[1]
close_plugins: list[Plugin] = []
if group.block_plugin:
for module in group.block_plugin.replace("<", "").split(","):
if module:
plugin = Plugin(
module=module,
plugin_name=module,
is_super_block=False,
)
plugin.plugin_name = module2name.get(module) or module
close_plugins.append(plugin)
exists_modules = [p.module for p in close_plugins]
if group.superuser_block_plugin:
for module in group.superuser_block_plugin.replace("<", "").split(","):
if module and module not in exists_modules:
plugin = Plugin(
module=module,
plugin_name=module,
is_super_block=True,
)
plugin.plugin_name = module2name.get(module) or module
close_plugins.append(plugin)
all_task = await TaskInfo.annotate().values_list("module", "name")
task_module2name = {x[0]: x[1] for x in all_task}
task_list = []
if group.block_task or group.superuser_block_plugin:
sbp = group.superuser_block_plugin.replace("<", "").split(",")
split_task = group.block_task.replace("<", "").split(",")
for task in all_task:
task_list.append(
Task(
name=task[0],
zh_name=task_module2name.get(task[0]) or task[0],
status=task[0] not in split_task and task[0] not in sbp,
is_super_block=task[0] in sbp,
)
)
else:
for task in all_task:
task_list.append(
Task(
name=task[0],
zh_name=task_module2name.get(task[0]) or task[0],
status=True,
is_super_block=False,
)
)
group_detail = GroupDetail(
group_id=group_id,
ava_url=GROUP_AVA_URL.format(group_id, group_id),
name=group.group_name,
member_count=group.member_count,
max_member_count=group.max_member_count,
chat_count=await ChatHistory.filter(group_id=group_id).count(),
call_count=await Statistics.filter(group_id=group_id).count(),
like_plugin=like_plugin,
level=group.level,
status=group.status,
close_plugins=close_plugins,
task=task_list,
)
return Result.ok(group_detail)
@router.post( @router.post(
@ -483,25 +325,17 @@ async def _(bot_id: str, group_id: str) -> Result[GroupDetail]:
dependencies=[authentication()], dependencies=[authentication()],
response_model=Result, response_model=Result,
response_class=JSONResponse, response_class=JSONResponse,
description="获取群组详情", description="发送消息",
) )
async def _(param: SendMessage) -> Result: async def _(param: SendMessageParam) -> Result:
if not (bots := nonebot.get_bots()):
return Result.warning_("无Bot连接...")
if param.bot_id in bots:
platform = PlatformUtils.get_platform(bots[param.bot_id])
if platform != "qq":
return Result.warning_("暂不支持该平台...")
try: try:
if param.user_id: bot = nonebot.get_bot(param.bot_id)
await bots[param.bot_id].send_private_msg( await PlatformUtils.send_message(
user_id=str(param.user_id), message=param.message bot, param.user_id, param.group_id, param.message
) )
else:
await bots[param.bot_id].send_group_msg(
group_id=str(param.group_id), message=param.message
)
except Exception as e:
return Result.fail(str(e))
return Result.ok("发送成功!") return Result.ok("发送成功!")
except (ValueError, KeyError):
return Result.warning_("指定Bot未连接...") return Result.warning_("指定Bot未连接...")
except Exception as e:
logger.error(f"{router.prefix}/send_message 调用错误", "WebUi", e=e)
return Result.fail(f"发生了一点错误捏 {type(e)}: {e}")

View File

@ -3,7 +3,7 @@ import nonebot
from nonebot import on_message from nonebot import on_message
from nonebot.adapters.onebot.v11 import MessageEvent from nonebot.adapters.onebot.v11 import MessageEvent
from nonebot_plugin_alconna import At, Hyper, Image, Text, UniMsg from nonebot_plugin_alconna import At, Hyper, Image, Text, UniMsg
from nonebot_plugin_session import EventSession from nonebot_plugin_uninfo import Uninfo
from starlette.websockets import WebSocket, WebSocketDisconnect, WebSocketState from starlette.websockets import WebSocket, WebSocketDisconnect, WebSocketState
from zhenxun.models.group_member_info import GroupInfoUser from zhenxun.models.group_member_info import GroupInfoUser
@ -28,7 +28,7 @@ matcher = on_message(block=False, priority=1, rule=lambda: bool(ws_conn))
@driver.on_shutdown @driver.on_shutdown
async def _(): async def _():
if ws_conn: if ws_conn and ws_conn.client_state == WebSocketState.CONNECTED:
await ws_conn.close() await ws_conn.close()
@ -36,7 +36,7 @@ async def _():
async def _(websocket: WebSocket): async def _(websocket: WebSocket):
global ws_conn global ws_conn
await websocket.accept() await websocket.accept()
if not ws_conn: if not ws_conn or ws_conn.client_state != WebSocketState.CONNECTED:
ws_conn = websocket ws_conn = websocket
try: try:
while websocket.client_state == WebSocketState.CONNECTED: while websocket.client_state == WebSocketState.CONNECTED:
@ -80,25 +80,24 @@ async def message_handle(
@matcher.handle() @matcher.handle()
async def _( async def _(
message: UniMsg, event: MessageEvent, session: EventSession, uname: str = UserName() message: UniMsg, event: MessageEvent, session: Uninfo, uname: str = UserName()
): ):
global ws_conn, ID2NAME, ID_LIST global ws_conn, ID2NAME, ID_LIST
uid = session.id1 if ws_conn and ws_conn.client_state == WebSocketState.CONNECTED:
if ws_conn and ws_conn.client_state == WebSocketState.CONNECTED and uid:
msg_id = event.message_id msg_id = event.message_id
if msg_id in ID_LIST: if msg_id in ID_LIST:
return return
ID_LIST.append(msg_id) ID_LIST.append(msg_id)
if len(ID_LIST) > 50: if len(ID_LIST) > 50:
ID_LIST = ID_LIST[40:] ID_LIST = ID_LIST[40:]
gid = session.id3 or session.id2 gid = session.group.id if session.group else None
messages = await message_handle(message, gid) messages = await message_handle(message, gid)
data = Message( data = Message(
object_id=gid or uid, object_id=gid or session.user.id,
user_id=uid, user_id=session.user.id,
group_id=gid, group_id=gid,
message=messages, message=messages,
name=uname, name=uname,
ava_url=AVA_URL.format(uid), ava_url=AVA_URL.format(session.user.id),
) )
await ws_conn.send_json(data.dict()) await ws_conn.send_json(data.dict())

View File

@ -0,0 +1,274 @@
import nonebot
from tortoise.functions import Count
from zhenxun.models.ban_console import BanConsole
from zhenxun.models.chat_history import ChatHistory
from zhenxun.models.fg_request import FgRequest
from zhenxun.models.group_console import GroupConsole
from zhenxun.models.plugin_info import PluginInfo
from zhenxun.models.statistics import Statistics
from zhenxun.models.task_info import TaskInfo
from zhenxun.utils.common_utils import CommonUtils
from zhenxun.utils.enum import RequestType
from zhenxun.utils.platform import PlatformUtils
from ....config import AVA_URL, GROUP_AVA_URL
from .model import (
FriendRequestResult,
GroupDetail,
GroupRequestResult,
Plugin,
ReqResult,
Task,
UpdateGroup,
UserDetail,
)
class ApiDataSource:
@classmethod
async def update_group(cls, group: UpdateGroup):
"""更新群组数据
参数:
group: UpdateGroup
"""
db_group = await GroupConsole.get_group(group.group_id) or GroupConsole(
group_id=group.group_id
)
task_list = await TaskInfo.all().values_list("module", flat=True)
db_group.level = group.level
db_group.status = group.status
if group.close_plugins:
db_group.block_plugin = CommonUtils.convert_module_format(
group.close_plugins
)
else:
db_group.block_plugin = ""
if group.task:
if block_task := [t for t in task_list if t not in group.task]:
db_group.block_task = CommonUtils.convert_module_format(block_task) # type: ignore
else:
db_group.block_task = CommonUtils.convert_module_format(task_list) # type: ignore
await db_group.save()
@classmethod
async def get_request_list(cls) -> ReqResult:
"""获取好友与群组请求列表
返回:
ReqResult: 数据内容
"""
req_result = ReqResult()
data_list = await FgRequest.filter(handle_type__isnull=True).all()
for req in data_list:
if req.request_type == RequestType.FRIEND:
req_result.friend.append(
FriendRequestResult(
oid=req.id,
bot_id=req.bot_id,
id=req.user_id,
flag=req.flag,
nickname=req.nickname,
comment=req.comment,
ava_url=AVA_URL.format(req.user_id),
type=str(req.request_type).lower(),
)
)
else:
req_result.group.append(
GroupRequestResult(
oid=req.id,
bot_id=req.bot_id,
id=req.user_id,
flag=req.flag,
nickname=req.nickname,
comment=req.comment,
ava_url=GROUP_AVA_URL.format(req.group_id, req.group_id),
type=str(req.request_type).lower(),
invite_group=req.group_id,
group_name=None,
)
)
req_result.friend.reverse()
req_result.group.reverse()
return req_result
@classmethod
async def get_friend_detail(cls, bot_id: str, user_id: str) -> UserDetail | None:
"""获取好友详情
参数:
bot_id: bot id
user_id: 用户id
返回:
UserDetail | None: 详情数据
"""
bot = nonebot.get_bot(bot_id)
friend_list, _ = await PlatformUtils.get_friend_list(bot)
fd = [x for x in friend_list if x == user_id]
if not fd:
return None
like_plugin_list = (
await Statistics.filter(user_id=user_id)
.annotate(count=Count("id"))
.group_by("plugin_name")
.order_by("-count")
.limit(5)
.values_list("plugin_name", "count")
)
like_plugin = {}
module_list = [x[0] for x in like_plugin_list]
plugins = await PluginInfo.filter(module__in=module_list).all()
module2name = {p.module: p.name for p in plugins}
for data in like_plugin_list:
name = module2name.get(data[0]) or data[0]
like_plugin[name] = data[1]
user = fd[0]
return UserDetail(
user_id=user_id,
ava_url=AVA_URL.format(user_id),
nickname=user.user_name,
remark="",
is_ban=await BanConsole.is_ban(user_id),
chat_count=await ChatHistory.filter(user_id=user_id).count(),
call_count=await Statistics.filter(user_id=user_id).count(),
like_plugin=like_plugin,
)
@classmethod
async def __get_group_detail_like_plugin(cls, group_id: str) -> dict[str, int]:
"""获取群组喜爱的插件
参数:
group_id: 群组id
返回:
dict[str, int]: 插件与调用次数
"""
like_plugin_list = (
await Statistics.filter(group_id=group_id)
.annotate(count=Count("id"))
.group_by("plugin_name")
.order_by("-count")
.limit(5)
.values_list("plugin_name", "count")
)
like_plugin = {}
plugins = await PluginInfo.get_plugins()
module2name = {p.module: p.name for p in plugins}
for data in like_plugin_list:
name = module2name.get(data[0]) or data[0]
like_plugin[name] = data[1]
return like_plugin
@classmethod
async def __get_group_detail_disable_plugin(
cls, group: GroupConsole
) -> list[Plugin]:
"""获取群组禁用插件
参数:
group: GroupConsole
返回:
list[Plugin]: 禁用插件数据列表
"""
disable_plugins: list[Plugin] = []
plugins = await PluginInfo.get_plugins()
module2name = {p.module: p.name for p in plugins}
if group.block_plugin:
for module in CommonUtils.convert_module_format(group.block_plugin):
if module:
plugin = Plugin(
module=module,
plugin_name=module,
is_super_block=False,
)
plugin.plugin_name = module2name.get(module) or module
disable_plugins.append(plugin)
exists_modules = [p.module for p in disable_plugins]
if group.superuser_block_plugin:
for module in CommonUtils.convert_module_format(
group.superuser_block_plugin
):
if module and module not in exists_modules:
plugin = Plugin(
module=module,
plugin_name=module,
is_super_block=True,
)
plugin.plugin_name = module2name.get(module) or module
disable_plugins.append(plugin)
return disable_plugins
@classmethod
async def __get_group_detail_task(cls, group: GroupConsole) -> list[Task]:
"""获取群组被动技能状态
参数:
group: GroupConsole
返回:
list[Task]: 群组被动列表
"""
all_task = await TaskInfo.annotate().values_list("module", "name")
task_module2name = {x[0]: x[1] for x in all_task}
task_list = []
if group.block_task or group.superuser_block_plugin:
sbp = CommonUtils.convert_module_format(group.superuser_block_task)
tasks = CommonUtils.convert_module_format(group.block_task)
task_list.extend(
Task(
name=task[0],
zh_name=task_module2name.get(task[0]) or task[0],
status=task[0] not in tasks and task[0] not in sbp,
is_super_block=task[0] in sbp,
)
for task in all_task
)
else:
task_list.extend(
Task(
name=task[0],
zh_name=task_module2name.get(task[0]) or task[0],
status=True,
is_super_block=False,
)
for task in all_task
)
return task_list
@classmethod
async def get_group_detail(cls, group_id: str) -> GroupDetail | None:
"""获取群组详情
参数:
group_id: 群组id
返回:
GroupDetail | None: 群组详情数据
"""
group = await GroupConsole.get_or_none(group_id=group_id)
if not group:
return None
like_plugin = await cls.__get_group_detail_like_plugin(group_id)
disable_plugins: list[Plugin] = await cls.__get_group_detail_disable_plugin(
group
)
task_list = await cls.__get_group_detail_task(group)
return GroupDetail(
group_id=group_id,
ava_url=GROUP_AVA_URL.format(group_id, group_id),
name=group.group_name,
member_count=group.member_count,
max_member_count=group.max_member_count,
chat_count=await ChatHistory.filter(group_id=group_id).count(),
call_count=await Statistics.filter(group_id=group_id).count(),
like_plugin=like_plugin,
level=group.level,
status=group.status,
close_plugins=disable_plugins,
task=task_list,
)

View File

@ -257,7 +257,7 @@ class Message(BaseModel):
"""用户头像""" """用户头像"""
class SendMessage(BaseModel): class SendMessageParam(BaseModel):
""" """
发送消息 发送消息
""" """

View File

@ -1,18 +1,14 @@
import re
import cattrs
from fastapi import APIRouter, Query from fastapi import APIRouter, Query
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from zhenxun.configs.config import Config
from zhenxun.models.plugin_info import PluginInfo as DbPluginInfo from zhenxun.models.plugin_info import PluginInfo as DbPluginInfo
from zhenxun.services.log import logger from zhenxun.services.log import logger
from zhenxun.utils.enum import BlockType, PluginType from zhenxun.utils.enum import BlockType, PluginType
from ....base_model import Result from ....base_model import Result
from ....utils import authentication from ....utils import authentication
from .data_source import ApiDataSource
from .model import ( from .model import (
PluginConfig,
PluginCount, PluginCount,
PluginDetail, PluginDetail,
PluginInfo, PluginInfo,
@ -34,31 +30,12 @@ async def _(
plugin_type: list[PluginType] = Query(None), menu_type: str | None = None plugin_type: list[PluginType] = Query(None), menu_type: str | None = None
) -> Result[list[PluginInfo]]: ) -> Result[list[PluginInfo]]:
try: try:
plugin_list: list[PluginInfo] = [] return Result.ok(
query = DbPluginInfo await ApiDataSource.get_plugin_list(plugin_type, menu_type), "拿到信息啦!"
if plugin_type:
query = query.filter(plugin_type__in=plugin_type, load_status=True)
if menu_type:
query = query.filter(menu_type=menu_type)
plugins = await query.all()
for plugin in plugins:
plugin_info = PluginInfo(
module=plugin.module,
plugin_name=plugin.name,
default_status=plugin.default_status,
limit_superuser=plugin.limit_superuser,
cost_gold=plugin.cost_gold,
menu_type=plugin.menu_type,
version=plugin.version or "0",
level=plugin.level,
status=plugin.status,
author=plugin.author,
) )
plugin_list.append(plugin_info)
except Exception as e: except Exception as e:
logger.error("调用API错误", "/get_plugins", e=e) logger.error(f"{router.prefix}/get_plugin_list 调用错误", "WebUi", e=e)
return Result.fail(f"{type(e)}: {e}") return Result.fail(f"发生了一点错误捏 {type(e)}: {e}")
return Result.ok(plugin_list, "拿到了新鲜出炉的数据!")
@router.get( @router.get(
@ -69,12 +46,14 @@ async def _(
deprecated="获取插件数量", # type: ignore deprecated="获取插件数量", # type: ignore
) )
async def _() -> Result[int]: async def _() -> Result[int]:
try:
plugin_count = PluginCount() plugin_count = PluginCount()
plugin_count.normal = await DbPluginInfo.filter( plugin_count.normal = await DbPluginInfo.filter(
plugin_type=PluginType.NORMAL, load_status=True plugin_type=PluginType.NORMAL, load_status=True
).count() ).count()
plugin_count.admin = await DbPluginInfo.filter( plugin_count.admin = await DbPluginInfo.filter(
plugin_type__in=[PluginType.ADMIN, PluginType.SUPER_AND_ADMIN], load_status=True plugin_type__in=[PluginType.ADMIN, PluginType.SUPER_AND_ADMIN],
load_status=True,
).count() ).count()
plugin_count.superuser = await DbPluginInfo.filter( plugin_count.superuser = await DbPluginInfo.filter(
plugin_type__in=[PluginType.SUPERUSER, PluginType.SUPER_AND_ADMIN], plugin_type__in=[PluginType.SUPERUSER, PluginType.SUPER_AND_ADMIN],
@ -83,7 +62,10 @@ async def _() -> Result[int]:
plugin_count.other = await DbPluginInfo.filter( plugin_count.other = await DbPluginInfo.filter(
plugin_type__in=[PluginType.HIDDEN, PluginType.DEPENDANT], load_status=True plugin_type__in=[PluginType.HIDDEN, PluginType.DEPENDANT], load_status=True
).count() ).count()
return Result.ok(plugin_count) return Result.ok(plugin_count, "拿到信息啦!")
except Exception as e:
logger.error(f"{router.prefix}/get_plugin_count 调用错误", "WebUi", e=e)
return Result.fail(f"发生了一点错误捏 {type(e)}: {e}")
@router.post( @router.post(
@ -93,33 +75,15 @@ async def _() -> Result[int]:
response_class=JSONResponse, response_class=JSONResponse,
description="更新插件参数", description="更新插件参数",
) )
async def _(plugin: UpdatePlugin) -> Result: async def _(param: UpdatePlugin) -> Result:
try: try:
db_plugin = await DbPluginInfo.get_or_none( await ApiDataSource.update_plugin(param)
module=plugin.module, load_status=True
)
if not db_plugin:
return Result.fail("插件不存在...")
db_plugin.default_status = plugin.default_status
db_plugin.limit_superuser = plugin.limit_superuser
db_plugin.cost_gold = plugin.cost_gold
db_plugin.level = plugin.level
db_plugin.menu_type = plugin.menu_type
db_plugin.block_type = plugin.block_type
db_plugin.status = plugin.block_type != BlockType.ALL
await db_plugin.save()
# 配置项
if plugin.configs and (configs := Config.get(plugin.module)):
for key in plugin.configs:
if c := configs.configs.get(key):
value = plugin.configs[key]
if c.type and value is not None:
value = cattrs.structure(value, c.type)
Config.set_config(plugin.module, key, value)
except Exception as e:
logger.error("调用API错误", "/update_plugins", e=e)
return Result.fail(f"{type(e)}: {e}")
return Result.ok(info="已经帮你写好啦!") return Result.ok(info="已经帮你写好啦!")
except (ValueError, KeyError):
return Result.fail("插件数据不存在...")
except Exception as e:
logger.error(f"{router.prefix}/update_plugin 调用错误", "WebUi", e=e)
return Result.fail(f"{type(e)}: {e}")
@router.post( @router.post(
@ -130,7 +94,8 @@ async def _(plugin: UpdatePlugin) -> Result:
description="开关插件", description="开关插件",
) )
async def _(param: PluginSwitch) -> Result: async def _(param: PluginSwitch) -> Result:
db_plugin = await DbPluginInfo.get_or_none(module=param.module, load_status=True) try:
db_plugin = await DbPluginInfo.get_plugin(module=param.module)
if not db_plugin: if not db_plugin:
return Result.fail("插件不存在...") return Result.fail("插件不存在...")
if not param.status: if not param.status:
@ -141,6 +106,9 @@ async def _(param: PluginSwitch) -> Result:
db_plugin.status = True db_plugin.status = True
await db_plugin.save() await db_plugin.save()
return Result.ok(info="成功改变了开关状态!") return Result.ok(info="成功改变了开关状态!")
except Exception as e:
logger.error(f"{router.prefix}/change_switch 调用错误", "WebUi", e=e)
return Result.fail(f"{type(e)}: {e}")
@router.get( @router.get(
@ -151,12 +119,20 @@ async def _(param: PluginSwitch) -> Result:
description="获取插件类型", description="获取插件类型",
) )
async def _() -> Result[list[str]]: async def _() -> Result[list[str]]:
try:
menu_type_list = [] menu_type_list = []
result = await DbPluginInfo.annotate().values_list("menu_type", flat=True) result = (
await DbPluginInfo.filter(load_status=True)
.annotate()
.values_list("menu_type", flat=True)
)
for r in result: for r in result:
if r not in menu_type_list and r: if r not in menu_type_list and r:
menu_type_list.append(r) menu_type_list.append(r)
return Result.ok(menu_type_list) return Result.ok(menu_type_list)
except Exception as e:
logger.error(f"{router.prefix}/get_plugin_menu_type 调用错误", "WebUi", e=e)
return Result.fail(f"{type(e)}: {e}")
@router.get( @router.get(
@ -167,46 +143,12 @@ async def _() -> Result[list[str]]:
description="获取插件详情", description="获取插件详情",
) )
async def _(module: str) -> Result[PluginDetail]: async def _(module: str) -> Result[PluginDetail]:
db_plugin = await DbPluginInfo.get_or_none(module=module, load_status=True) try:
if not db_plugin: return Result.ok(
return Result.fail("插件不存在...") await ApiDataSource.get_plugin_detail(module), "已经帮你写好啦!"
config_list = []
if config := Config.get(module):
for cfg in config.configs:
type_str = ""
type_inner = None
if r := re.search(r"<class '(.*)'>", str(config.configs[cfg].type)):
type_str = r[1]
elif r := re.search(r"typing\.(.*)\[(.*)\]", str(config.configs[cfg].type)):
type_str = r[1]
if type_str:
type_str = type_str.lower()
type_inner = r[2]
if type_inner:
type_inner = [x.strip() for x in type_inner.split(",")]
config_list.append(
PluginConfig(
module=module,
key=cfg,
value=config.configs[cfg].value,
help=config.configs[cfg].help,
default_value=config.configs[cfg].default_value,
type=type_str,
type_inner=type_inner, # type: ignore
) )
) except (ValueError, KeyError):
plugin_info = PluginDetail( return Result.fail("插件数据不存在...")
module=module, except Exception as e:
plugin_name=db_plugin.name, logger.error(f"{router.prefix}/get_plugin 调用错误", "WebUi", e=e)
default_status=db_plugin.default_status, return Result.fail(f"{type(e)}: {e}")
limit_superuser=db_plugin.limit_superuser,
cost_gold=db_plugin.cost_gold,
menu_type=db_plugin.menu_type,
version=db_plugin.version or "0",
level=db_plugin.level,
status=db_plugin.status,
author=db_plugin.author,
config_list=config_list,
block_type=db_plugin.block_type,
)
return Result.ok(plugin_info)

View File

@ -0,0 +1,152 @@
import re
import cattrs
from fastapi import Query
from zhenxun.configs.config import Config
from zhenxun.configs.utils import ConfigGroup
from zhenxun.models.plugin_info import PluginInfo as DbPluginInfo
from zhenxun.utils.enum import BlockType, PluginType
from .model import PluginConfig, PluginDetail, PluginInfo, UpdatePlugin
class ApiDataSource:
@classmethod
async def get_plugin_list(
cls, plugin_type: list[PluginType] = Query(None), menu_type: str | None = None
) -> list[PluginInfo]:
"""获取插件列表
参数:
plugin_type: 插件类型.
menu_type: 菜单类型.
返回:
list[PluginInfo]: 插件数据列表
"""
plugin_list: list[PluginInfo] = []
query = DbPluginInfo
if plugin_type:
query = query.filter(plugin_type__in=plugin_type, load_status=True)
if menu_type:
query = query.filter(menu_type=menu_type, load_status=True)
plugins = await query.all()
for plugin in plugins:
plugin_info = PluginInfo(
module=plugin.module,
plugin_name=plugin.name,
default_status=plugin.default_status,
limit_superuser=plugin.limit_superuser,
cost_gold=plugin.cost_gold,
menu_type=plugin.menu_type,
version=plugin.version or "0",
level=plugin.level,
status=plugin.status,
author=plugin.author,
)
plugin_list.append(plugin_info)
return plugin_list
@classmethod
async def update_plugin(cls, param: UpdatePlugin) -> DbPluginInfo:
"""更新插件数据
参数:
param: UpdatePlugin
返回:
DbPluginInfo | None: 插件数据
"""
db_plugin = await DbPluginInfo.get_plugin(module=param.module)
if not db_plugin:
raise ValueError("插件不存在")
db_plugin.default_status = param.default_status
db_plugin.limit_superuser = param.limit_superuser
db_plugin.cost_gold = param.cost_gold
db_plugin.level = param.level
db_plugin.menu_type = param.menu_type
db_plugin.block_type = param.block_type
db_plugin.status = param.block_type != BlockType.ALL
await db_plugin.save()
# 配置项
if param.configs and (configs := Config.get(param.module)):
for key in param.configs:
if c := configs.configs.get(key):
value = param.configs[key]
if c.type and value is not None:
value = cattrs.structure(value, c.type)
Config.set_config(param.module, key, value)
Config.save(save_simple_data=True)
return db_plugin
@classmethod
def __build_plugin_config(
cls, module: str, cfg: str, config: ConfigGroup
) -> PluginConfig:
"""获取插件配置项
参数:
module: 模块名
cfg: cfg
config: ConfigGroup
返回:
lPluginConfig: 配置数据
"""
type_str = ""
type_inner = None
if r := re.search(r"<class '(.*)'>", str(config.configs[cfg].type)):
type_str = r[1]
elif r := re.search(r"typing\.(.*)\[(.*)\]", str(config.configs[cfg].type)):
type_str = r[1]
if type_str:
type_str = type_str.lower()
type_inner = r[2]
if type_inner:
type_inner = [x.strip() for x in type_inner.split(",")]
return PluginConfig(
module=module,
key=cfg,
value=config.configs[cfg].value,
help=config.configs[cfg].help,
default_value=config.configs[cfg].default_value,
type=type_str,
type_inner=type_inner, # type: ignore
)
@classmethod
async def get_plugin_detail(cls, module: str) -> PluginDetail:
"""获取插件详情
参数:
module: 模块名
异常:
ValueError: 插件不存在
返回:
PluginDetail: 插件详情数据
"""
db_plugin = await DbPluginInfo.get_plugin(module=module)
if not db_plugin:
raise ValueError("插件不存在")
config_list = []
if config := Config.get(module):
config_list.extend(
cls.__build_plugin_config(module, cfg, config) for cfg in config.configs
)
return PluginDetail(
module=module,
plugin_name=db_plugin.name,
default_status=db_plugin.default_status,
limit_superuser=db_plugin.limit_superuser,
cost_gold=db_plugin.cost_gold,
menu_type=db_plugin.menu_type,
version=db_plugin.version or "0",
level=db_plugin.level,
status=db_plugin.status,
author=db_plugin.author,
config_list=config_list,
block_type=db_plugin.block_type,
)

View File

@ -31,7 +31,7 @@ class Result(Generic[RT], BaseModel):
"""info""" """info"""
warning: str | None = None warning: str | None = None
"""警告信息""" """警告信息"""
data: RT = None data: RT | None = None
"""返回数据""" """返回数据"""
@classmethod @classmethod

View File

@ -0,0 +1,8 @@
from nonebot import require
require("nonebot_plugin_apscheduler")
require("nonebot_plugin_alconna")
require("nonebot_plugin_session")
require("nonebot_plugin_userinfo")
require("nonebot_plugin_htmlrender")
require("nonebot_plugin_uninfo")

View File

@ -1,3 +1,5 @@
from typing import overload
from nonebot.adapters import Bot from nonebot.adapters import Bot
from nonebot_plugin_uninfo import Session, SupportScope, Uninfo, get_interface from nonebot_plugin_uninfo import Session, SupportScope, Uninfo, get_interface
@ -62,6 +64,34 @@ class CommonUtils:
return True return True
return False return False
@staticmethod
def format(name: str) -> str:
return f"<{name},"
@overload
@classmethod
def convert_module_format(cls, data: str) -> list[str]: ...
@overload
@classmethod
def convert_module_format(cls, data: list[str]) -> str: ...
@classmethod
def convert_module_format(cls, data: str | list[str]) -> str | list[str]:
"""
`<aaa,<bbb,<ccc,` `["aaa", "bbb", "ccc"]` 之间进行相互转换
参数:
data (str | list[str]): 输入数据可能是格式化字符串或字符串列表
返回:
str | list[str]: 根据输入类型返回转换后的数据
"""
if isinstance(data, str):
return [item.strip(",") for item in data.split("<") if item]
elif isinstance(data, list):
return "".join(cls.format(item) for item in data)
class SqlUtils: class SqlUtils:
@classmethod @classmethod

View File

@ -5,14 +5,11 @@ from typing import Literal
import httpx import httpx
import nonebot import nonebot
from nonebot.adapters import Bot from nonebot.adapters import Bot
from nonebot.adapters.dodo import Bot as DodoBot
from nonebot.adapters.kaiheila import Bot as KaiheilaBot
from nonebot.adapters.onebot.v11 import Bot as v11Bot
from nonebot.adapters.onebot.v12 import Bot as v12Bot
from nonebot.utils import is_coroutine_callable from nonebot.utils import is_coroutine_callable
from nonebot_plugin_alconna import SupportScope from nonebot_plugin_alconna import SupportScope
from nonebot_plugin_alconna.uniseg import Receipt, Target, UniMessage from nonebot_plugin_alconna.uniseg import Receipt, Target, UniMessage
from nonebot_plugin_uninfo import Uninfo, get_interface from nonebot_plugin_uninfo import SceneType, Uninfo, get_interface
from nonebot_plugin_uninfo.model import Member
from pydantic import BaseModel from pydantic import BaseModel
from zhenxun.configs.config import BotConfig from zhenxun.configs.config import BotConfig
@ -35,6 +32,8 @@ class UserData(BaseModel):
"""用户id""" """用户id"""
group_id: str | None = None group_id: str | None = None
"""群组id""" """群组id"""
channel_id: str | None = None
"""频道id"""
role: str | None = None role: str | None = None
"""角色""" """角色"""
avatar_url: str | None = None avatar_url: str | None = None
@ -68,7 +67,7 @@ class PlatformUtils:
group_id: 群组id group_id: 群组id
duration: 禁言时长(分钟) duration: 禁言时长(分钟)
""" """
if isinstance(bot, v11Bot): if cls.get_platform(bot) == "qq":
await bot.set_group_ban( await bot.set_group_ban(
group_id=int(group_id), group_id=int(group_id),
user_id=int(user_id), user_id=int(user_id),
@ -116,148 +115,80 @@ class PlatformUtils:
返回: 返回:
list[UserData]: 用户数据列表 list[UserData]: 用户数据列表
""" """
if isinstance(bot, v11Bot): if interface := get_interface(bot):
if member_list := await bot.get_group_member_list(group_id=int(group_id)): members: list[Member] = await interface.get_members(
return [ SceneType.GROUP, group_id
UserData(
name=user["nickname"],
card=user["card"],
user_id=user["user_id"],
group_id=user["group_id"],
role=user["role"],
join_time=user["join_time"],
) )
for user in member_list
]
if isinstance(bot, v12Bot):
if member_list := await bot.get_group_member_list(group_id=group_id):
return [ return [
UserData( UserData(
name=user["user_name"], name=member.user.name or "",
card=user["user_displayname"], card=member.nick,
user_id=user["user_id"], user_id=member.user.id,
group_id=group_id, group_id=group_id,
role=member.role.id if member.role else "",
avatar_url=member.user.avatar,
join_time=int(member.joined_at.timestamp())
if member.joined_at
else None,
) )
for user in member_list for member in members
] ]
if isinstance(bot, DodoBot):
if result_data := await bot.get_member_list(
island_source_id=group_id, page_size=100, max_id=0
):
max_id = result_data.max_id
result_list = result_data.list
while max_id == 100:
result_data = await bot.get_member_list(
island_source_id=group_id, page_size=100, max_id=0
)
result_list += result_data.list
max_id = result_data.max_id
return [
UserData(
name=user.nick_name,
card=user.personal_nick_name,
avatar_url=user.avatar_url,
user_id=user.dodo_source_id,
group_id=user.island_source_id,
join_time=int(user.join_time.timestamp()),
)
for user in result_list
]
if isinstance(bot, KaiheilaBot):
if result_data := await bot.guild_userList(guild_id=group_id):
if result_data.users:
data_list = []
for user in result_data.users:
second = None
if user.joined_at:
second = int(user.joined_at / 1000)
data_list.append(
UserData(
name=user.nickname or "",
avatar_url=user.avatar,
user_id=user.id_, # type: ignore
group_id=group_id,
join_time=second,
)
)
return data_list
return [] return []
@classmethod @classmethod
async def get_user( async def get_user(
cls, bot: Bot, user_id: str, group_id: str | None = None cls,
bot: Bot,
user_id: str,
group_id: str | None = None,
channel_id: str | None = None,
) -> UserData | None: ) -> UserData | None:
"""获取用户信息 """获取用户信息
参数: 参数:
bot: Bot bot: Bot
user_id: 用户id user_id: 用户id
group_id: 群组/频道id. group_id: 群组id.
channel_id: 频道id.
返回: 返回:
UserData | None: 用户数据 UserData | None: 用户数据
""" """
if isinstance(bot, v11Bot): if interface := get_interface(bot):
if group_id: member = None
if user := await bot.get_group_member_info( user = None
group_id=int(group_id), user_id=int(user_id) if channel_id:
): member = await interface.get_member(
return UserData( SceneType.CHANNEL_TEXT, channel_id, user_id
name=user["nickname"],
card=user["card"],
user_id=user["user_id"],
group_id=user["group_id"],
role=user["role"],
join_time=user["join_time"],
) )
elif friend_list := await bot.get_friend_list(): if member:
for f in friend_list: user = member.user
if f["user_id"] == int(user_id): elif group_id:
member = await interface.get_member(SceneType.GROUP, group_id, user_id)
if member:
user = member.user
else:
user = await interface.get_user(user_id)
if not user:
return None
if member:
return UserData( return UserData(
name=f["nickname"], name=user.name or "",
card=f["remark"], card=member.nick,
user_id=f["user_id"], user_id=user.id,
)
if isinstance(bot, v12Bot):
if group_id:
if user := await bot.get_group_member_info(
group_id=group_id, user_id=user_id
):
return UserData(
name=user["user_name"],
card=user["user_displayname"],
user_id=user["user_id"],
group_id=group_id, group_id=group_id,
channel_id=channel_id,
role=member.role.id if member.role else None,
join_time=int(member.joined_at.timestamp())
if member.joined_at
else None,
) )
elif friend_list := await bot.get_friend_list(): else:
for f in friend_list:
if f["user_id"] == int(user_id):
return UserData( return UserData(
name=f["user_name"], name=user.name or "",
card=f["user_remark"], user_id=user.id,
user_id=f["user_id"],
)
if isinstance(bot, DodoBot) and group_id:
if user := await bot.get_member_info(
island_source_id=group_id, dodo_source_id=user_id
):
return UserData(
name=user.nick_name,
card=user.personal_nick_name,
avatar_url=user.avatar_url,
user_id=user.dodo_source_id,
group_id=user.island_source_id,
join_time=int(user.join_time.timestamp()),
)
if isinstance(bot, KaiheilaBot) and group_id:
if user := await bot.user_view(guild_id=group_id, user_id=user_id):
second = int(user.joined_at / 1000) if user.joined_at else None
return UserData(
name=user.nickname or "",
avatar_url=user.avatar,
user_id=user_id,
group_id=group_id, group_id=group_id,
join_time=second, channel_id=channel_id,
) )
return None return None
@ -337,7 +268,7 @@ class PlatformUtils:
返回: 返回:
Receipt | None: 是否发送成功 Receipt | None: 是否发送成功
""" """
if target := cls.get_target(bot, user_id, group_id): if target := cls.get_target(user_id=user_id, group_id=group_id):
send_message = ( send_message = (
MessageUtils.build_message(message) MessageUtils.build_message(message)
if isinstance(message, str) if isinstance(message, str)
@ -361,7 +292,9 @@ class PlatformUtils:
group_list, platform = await cls.get_group_list(bot) group_list, platform = await cls.get_group_list(bot)
if group_list: if group_list:
db_group = await GroupConsole.all() db_group = await GroupConsole.all()
db_group_id = [(group.group_id, group.channel_id) for group in db_group] db_group_id: list[tuple[str, str]] = [
(group.group_id, group.channel_id) for group in db_group
]
for group in group_list: for group in group_list:
group.platform = platform group.platform = platform
if (group.group_id, group.channel_id) not in db_group_id: if (group.group_id, group.channel_id) not in db_group_id:
@ -411,69 +344,43 @@ class PlatformUtils:
return "unknown" return "unknown"
@classmethod @classmethod
async def get_group_list(cls, bot: Bot) -> tuple[list[GroupConsole], str]: async def get_group_list(
cls, bot: Bot, only_group: bool = False
) -> tuple[list[GroupConsole], str]:
"""获取群组列表 """获取群组列表
参数: 参数:
bot: Bot bot: Bot
only_group: 是否只获取群组不获取channel
返回: 返回:
tuple[list[GroupConsole], str]: 群组列表, 平台 tuple[list[GroupConsole], str]: 群组列表, 平台
""" """
if isinstance(bot, v11Bot): if interface := get_interface(bot):
group_list = await bot.get_group_list() platform = cls.get_platform(bot)
return [ result_list = []
scenes = await interface.get_scenes(SceneType.GROUP)
for scene in scenes:
group_id = scene.id
result_list.append(
GroupConsole( GroupConsole(
group_id=str(g["group_id"]), group_id=scene.id,
group_name=g["group_name"], group_name=scene.name,
max_member_count=g["max_member_count"],
member_count=g["member_count"],
) )
for g in group_list )
], "qq" if not only_group and platform != "qq":
if isinstance(bot, v12Bot): if channel_list := await interface.get_scenes(
group_list = await bot.get_group_list() parent_scene_id=group_id
return [ ):
for channel in channel_list:
result_list.append(
GroupConsole( GroupConsole(
group_id=g.group_id, # type: ignore group_id=scene.id,
user_name=g.group_name, # type: ignore group_name=channel.name,
channel_id=channel.id,
) )
for g in group_list
], "qq"
if isinstance(bot, DodoBot):
island_list = await bot.get_island_list()
source_id_list = [
(g.island_source_id, g.island_name)
for g in island_list
if g.island_source_id
]
group_list = []
for id, name in source_id_list:
channel_list = await bot.get_channel_list(island_source_id=id)
group_list.append(GroupConsole(group_id=id, group_name=name))
group_list += [
GroupConsole(
group_id=id, group_name=c.channel_name, channel_id=c.channel_id
) )
for c in channel_list return result_list, platform
]
return group_list, "dodo"
if isinstance(bot, KaiheilaBot):
group_list = []
guilds = await bot.guild_list()
if guilds.guilds:
for guild_id, name in [(g.id_, g.name) for g in guilds.guilds if g.id_]:
view = await bot.guild_view(guild_id=guild_id)
group_list.append(GroupConsole(group_id=guild_id, group_name=name))
if view.channels:
group_list += [
GroupConsole(
group_id=guild_id, group_name=c.name, channel_id=c.id_
)
for c in view.channels
if c.type != 0
]
return group_list, "kaiheila"
return [], "" return [], ""
@classmethod @classmethod
@ -508,36 +415,17 @@ class PlatformUtils:
返回: 返回:
list[FriendUser]: 好友列表 list[FriendUser]: 好友列表
""" """
if isinstance(bot, v11Bot): if interface := get_interface(bot):
friend_list = await bot.get_friend_list() user_list = await interface.get_users()
return [ return [
FriendUser(user_id=str(f["user_id"]), user_name=f["nickname"]) FriendUser(user_id=u.id, user_name=u.name) for u in user_list
for f in friend_list ], cls.get_platform(bot)
], "qq"
if isinstance(bot, v12Bot):
friend_list = await bot.get_friend_list()
return [
FriendUser(
user_id=f.user_id, # type: ignore
user_name=f.user_displayname or f.user_remark or f.user_name, # type: ignore
)
for f in friend_list
], "qq"
# if isinstance(bot, DodoBot):
# # TODO: dodo好友列表
# pass
# if isinstance(bot, KaiheilaBot):
# # TODO: kaiheila好友列表
# pass
# if isinstance(bot, DiscordBot):
# # TODO: discord好友列表
# pass
return [], "" return [], ""
@classmethod @classmethod
def get_target( def get_target(
cls, cls,
bot: Bot, *,
user_id: str | None = None, user_id: str | None = None,
group_id: str | None = None, group_id: str | None = None,
channel_id: str | None = None, channel_id: str | None = None,
@ -554,14 +442,10 @@ class PlatformUtils:
target: 对应平台Target target: 对应平台Target
""" """
target = None target = None
if isinstance(bot, v11Bot | v12Bot):
if group_id:
target = Target(group_id)
elif user_id:
target = Target(user_id, private=True)
elif isinstance(bot, DodoBot | KaiheilaBot):
if group_id and channel_id: if group_id and channel_id:
target = Target(channel_id, parent_id=group_id, channel=True) target = Target(channel_id, parent_id=group_id, channel=True)
elif group_id:
target = Target(group_id)
elif user_id: elif user_id:
target = Target(user_id, private=True) target = Target(user_id, private=True)
return target return target
@ -646,7 +530,9 @@ async def broadcast_group(
) )
continue continue
target = PlatformUtils.get_target( target = PlatformUtils.get_target(
_bot, None, group.group_id, group.channel_id user_id=None,
group_id=group.group_id,
channel_id=group.channel_id,
) )
if target: if target:
_used_group.append(key) _used_group.append(key)