├── .gitignore ├── LICENSE ├── README.md ├── docs_image ├── help.png ├── tt.jpg ├── tt1.png ├── tt2.png ├── tt3.png └── tt4.png ├── nonebot_plugin_zxpm ├── __init__.py ├── commands │ ├── __init__.py │ ├── zxpm_add_group │ │ ├── __init__.py │ │ └── data_source.py │ ├── zxpm_admin_watch │ │ └── __init__.py │ ├── zxpm_ban │ │ ├── __init__.py │ │ └── _data_source.py │ ├── zxpm_bot_manage │ │ ├── __init__.py │ │ ├── bot_switch.py │ │ ├── command.py │ │ ├── full_function.py │ │ └── plugin.py │ ├── zxpm_help │ │ ├── __init__.py │ │ └── _data_source.py │ ├── zxpm_hooks │ │ ├── __init__.py │ │ ├── _auth_checker.py │ │ ├── zxpm_auth_hook.py │ │ └── zxpm_ban_hook.py │ ├── zxpm_init │ │ ├── __init__.py │ │ └── manager.py │ ├── zxpm_plugin_switch │ │ ├── __init__.py │ │ ├── _data_source.py │ │ └── command.py │ ├── zxpm_set_admin │ │ └── __init__.py │ └── zxpm_super_group │ │ └── __init__.py ├── config.py ├── db_connect │ └── __init__.py ├── enum.py ├── exception.py ├── extra │ ├── __init__.py │ └── limit.py ├── log.py ├── models │ ├── ban_console.py │ ├── bot_console.py │ ├── group_console.py │ ├── level_user.py │ ├── plugin_info.py │ └── plugin_limit.py ├── rules.py └── utils │ ├── build_image.py │ ├── image_template.py │ └── utils.py ├── poetry.lock └── pyproject.toml /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ 163 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | 7 |
8 | 9 |

10 | NoneBotPluginText 11 |

12 | 13 | # nonebot-plugin-zxpm 14 | 15 | _✨ 基于 [NoneBot2](https://github.com/nonebot/nonebot2) 的一个 插件管理插件 ✨_ 16 | 17 | ![python](https://img.shields.io/badge/python-v3.9%2B-blue) 18 | ![nonebot](https://img.shields.io/badge/nonebot-v2.1.3-yellow) 19 | ![onebot](https://img.shields.io/badge/onebot-v11-black) 20 | [![license](https://img.shields.io/badge/license-AGPL3.0-FE7D37)](https://github.com/HibiKier/zhenxun_bot/blob/main/LICENSE) 21 | 22 |
23 | 24 | ## 📖 介绍 25 | 26 | [小真寻](https://github.com/HibiKier/zhenxun_bot) 的插件权限管理系统,提供了 27 | 28 | - **细致的插件开关(全局关闭可以使用群白名单忽略)** 29 | - **Ban 群组/用户(消息屏蔽)** 30 | - **插件 Cd,Count,Block 限制(配置文件)** 31 | - 群管监测(权限自动设置) 32 | - 用户权限设置(超级用户设置) 33 | - 一个简单的帮助查看 34 | 35 | 继承了真寻的优良传统,高贵的**超级用户**不受权限控制,除非插件额外限制 36 | 37 | > [!NOTE] 38 | > 39 | >
小真寻也很可爱呀,也会很喜欢你!
40 | > 41 | >
42 | > 43 | > 44 | > 45 | >
46 | 47 | ## 💿 安装 48 | 49 | ```python 50 | pip install nonebot-plugin-zxpm 51 | ``` 52 | 53 | ```python 54 | nb plugin install nonebot-plugin-zxpm 55 | ``` 56 | 57 | ## 🎁 使用 58 | 59 | > [!IMPORTANT] 60 | > ZXPM 对插件进行了分类 61 | > `NORMAL`: 普通插件,没有特定标记的情况下都为这个类型 62 | > `ADMIN`: 群组管理员插件 63 | > `SUPERUSER`: 超级用户插件 64 | > `SUPER_AND_ADMIN`: 超级用户用于与管理员插件 65 | > `DEPENDANT`: 依赖插件,一般为没有主动触发命令的插件,受权限控制 66 | > `HIDDEN`: 隐藏插件,一般为没有主动触发命令的插件,不受权限控制,如消息统计 67 | > `PARENT`: 父插件,仅仅标记 68 | > 69 | > ZXPM 权限管理严格 70 | > 普通用户无法查看`ADMIN`,`SUPERUSER`,`SUPER_AND_ADMIN`插件的帮助 71 | > 权限管理员用户无法查看`SUPERUSER`插件的帮助 72 | 73 | ## ⚙️ 配置 74 | 75 | | 配置 | 类型 | 默认值 | 说明 | 76 | | :---------------------- | :--: | :---------------------------: | ---------------------------------------------------------------- | 77 | | zxpm_data_path | str | data/zxpm | 数据存储路径 | 78 | | zxpm_db_url | str | (`zxpm_data_path`)/db/zxpm.db | 数据库地址 URL,默认为 sqlite,存储路径在`zxpm_data_path`下 | 79 | | zxpm_notice_info_cd | int | 300 | 群/用户权限检测等各种检测提示信息 cd,为 0 时或永久 ban 时不提醒 | 80 | | zxpm_ban_reply | str | 才不会给你发消息. | 用户被 ban 时回复消息,为空时不回复 | 81 | | zxpm_ban_level | int | 5 | 使用 ban 功能的对应权限 | 82 | | zxpm_switch_level | int | 1 | 使用开关功能的对应权限 | 83 | | zxpm_admin_default_auth | int | 5 | 群组管理员默认权限 | 84 | | zxpm_font | str | msyh.ttc | 作图时字体 85 | | zxpm_limit_superuser | bool | False | 是否限制超级用户 | 86 | 87 | ## 🎉 帮助 88 | 89 | | 指令 | 格式 | 权限等级 | 参数 | 作用 | 示例 | 90 | | :--: | :---------: | :------: | :--: | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | 91 | | zxpm | zxpm [名称] | 0 | -s | 提供一个简单详情帮助图片 | `zxpm Ban`: 查看 Ban 的用户帮助
`zxpm Ban -s`: 查看 Ban 的超级用户帮助
`zxpm 40`: 查看 id 为 40 的插件帮助(id 通过给 bot 发送`插件列表`获取) | 92 | 93 | > [!NOTE] 94 | > 这里的指令只是写了个大概,功能开关指令比较复杂 95 | > 建议对 bot 发送 `zxpm ZXPM插件管理` 和 `zxpm ZXPM插件管理 -s` 来查看详细帮助 96 | > ZXPM 内部维护了一个插件数据库,收集所有含有`PluginMetaData`的插件 97 | > 对于其他插件,同样也可以使用`zxpm`来获取帮助信息 98 | 99 | ## 权限设置 100 | 101 | | 指令 | 格式 | 权限等级 | 参数 | 作用 | 示例 | 102 | | :------: | :-------------------: | :-------: | :--: | ------------------------ | ------------------ | 103 | | 添加权限 | 添加权限 [level] [at] | SUPERUSER | | 为用户提供插件管理等权限 | `添加权限 5 @user` | 104 | | 删除权限 | 删除权限 [at] | SUPERUSER | | 为用户提供插件管理等权限 | `删除权限 @user` | 105 | 106 | ## 白名单设置 107 | 108 | | 指令 | 格式 | 权限等级 | 参数 | 作用 | 示例 | 109 | | :------: | :-------------------: | :-------: | :--: | ------------------------ | ------------------ | 110 | | 添加群白名单 | 添加群白名单 ?[gid] | SUPERUSER | | 标记群为白名单中 | `添加群白名单`: 在群组中可以不加群号,默认当前群组
`添加群白名单 123123`: 指定群号 | 111 | | 删除群白名单 | 删除群白名单 ?[gid] | SUPERUSER | | 删除群为白名单中 | 同上 | 112 | 113 | ## Ban/unBan 114 | 115 | **群组管理员** 116 | 117 | | 指令 | 格式 | 权限等级 | 参数 | 作用 | 示例 | 118 | | :---: | :------------------------------: | :------: | :---: | --------------------------------------------------- | ------------------------------------------------------ | 119 | | ban | ban [At 用户] ?[-t [时长(分钟)]] | 5 | -t -g | 屏蔽用户或群组消息,权限等级低无法 Ban 等级高的用户 | `ban @用户`: 永久 ban
`ban @用户 -t 10`: ban 十分钟 | 120 | | unban | unban [At 用户] | 5 | | 解除蔽用户或群组消息 @user | `unban @用户`: 放出来 | 121 | 122 | **超级用户** 123 | 124 | ``` 125 | ban [At用户/用户Id] ?[-t [时长]] 126 | unban --id [idx] : 通过id来进行unban操作 127 | ban列表: 获取所有Ban数据 128 | 129 | 群组ban列表: 获取群组Ban数据 130 | 用户ban列表: 获取用户Ban数据 131 | 132 | ban列表 -u [用户Id]: 查找指定用户ban数据 133 | ban列表 -g [群组Id]: 查找指定群组ban数据 134 | 示例: 135 | ban列表 -u 123456789 : 查找用户123456789的ban数据 136 | ban列表 -g 123456789 : 查找群组123456789的ban数据 137 | 138 | 私聊下: 139 | 示例: 140 | ban 123456789 : 永久拉黑用户123456789 141 | ban 123456789 -t 100 : 拉黑用户123456789 100分钟 142 | 143 | ban -g 999999 : 拉黑群组为999999的群组 144 | ban -g 999999 -t 100 : 拉黑群组为999999的群组 100分钟 145 | 146 | unban 123456789 : 从小黑屋中拉出来 147 | unban -g 999999 : 将群组9999999从小黑屋中拉出来 148 | ``` 149 | 150 | ## 插件控制 151 | 152 | **群组管理员** 153 | 154 | ``` 155 | 格式: 156 | 开启/关闭[功能名称] : 开关功能 157 | 开启/关闭所有插件 : 开启/关闭当前群组所有插件状态 158 | 醒来 : 结束休眠 159 | 休息吧 : 群组休眠, 不会再响应命令 160 | 161 | 示例: 162 | 开启签到 : 开启签到 163 | 关闭签到 : 关闭签到 164 | ``` 165 | 166 | **超级用户** 167 | 168 | ``` 169 | 插件列表 170 | 开启/关闭[功能名称] ?[-t ["private", "p", "group", "g"](关闭类型)] ?[-g 群组Id] 171 | 172 | 开启/关闭插件df[功能名称]: 开启/关闭指定插件进群默认状态 173 | = 开启插件echo -df 174 | = 关闭插件echo -df 175 | 开启/关闭所有插件df: 开启/关闭所有插件进群默认状态 176 | 开启/关闭所有插件: 177 | 私聊中: 开启/关闭所有插件全局状态 178 | 群组中: 开启/关闭当前群组所有插件状态 179 | 180 | 私聊下: 181 | 示例: 182 | 开启签到 : 全局开启签到 183 | 关闭签到 : 全局关闭签到 184 | 关闭签到 -t p : 全局私聊关闭签到 185 | 关闭签到 -g 12345678 : 关闭群组12345678的签到功能(普通管理员无法开启) 186 | 187 | bot插件列表: bot插件列表状态 : bot插件列表 188 | bot开启/关闭所有插件 : bot所有插件开关 189 | bot开启/关闭插件[插件名称] : bot插件开关 190 | bot休眠 : bot休眠,屏蔽所有消息 191 | bot醒来 : bot醒来 192 | ``` 193 | 194 | **可修改配置文件** 195 | 196 | 在`(zxpm_data_path)/configs`路径下有以 下 3 个配置文件,且文件中已提供参数解释 197 | 默认路径: `data/zxpm/configs` 198 | 199 | - `plugin2block.yaml`: 插件阻塞配置 200 | 例如: 201 | 202 | ``` 203 | sign_in: 204 | status: true 205 | check_type: ALL 206 | watch_type: USER 207 | result: "你签那么快干什么啦" 208 | ``` 209 | 210 | - `plugin2cd.yaml`: 插件 CD 配置 211 | 例如: 212 | 213 | ``` 214 | sign_in: 215 | status: true 216 | check_type: ALL 217 | watch_type: USER 218 | result: 告辞 219 | cd: 12 220 | ``` 221 | 222 | - `plugin2count.yaml`: 插件每日次数配置 223 | 例如: 224 | 225 | ``` 226 | help: 227 | status: false 228 | watch_type: GROUP 229 | result: 再看就揍死你 230 | max_count: 2 231 | ``` 232 | 233 | ## ❤ 感谢 234 | 235 | - 可爱的小真寻 Bot [`zhenxun_bot`](https://github.com/HibiKier/zhenxun_bot): 我谢我自己,桀桀桀 236 | -------------------------------------------------------------------------------- /docs_image/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HibiKier/nonebot-plugin-zxpm/85bdba47cb30edc826876b6c6a3a864b7db54ccc/docs_image/help.png -------------------------------------------------------------------------------- /docs_image/tt.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HibiKier/nonebot-plugin-zxpm/85bdba47cb30edc826876b6c6a3a864b7db54ccc/docs_image/tt.jpg -------------------------------------------------------------------------------- /docs_image/tt1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HibiKier/nonebot-plugin-zxpm/85bdba47cb30edc826876b6c6a3a864b7db54ccc/docs_image/tt1.png -------------------------------------------------------------------------------- /docs_image/tt2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HibiKier/nonebot-plugin-zxpm/85bdba47cb30edc826876b6c6a3a864b7db54ccc/docs_image/tt2.png -------------------------------------------------------------------------------- /docs_image/tt3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HibiKier/nonebot-plugin-zxpm/85bdba47cb30edc826876b6c6a3a864b7db54ccc/docs_image/tt3.png -------------------------------------------------------------------------------- /docs_image/tt4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HibiKier/nonebot-plugin-zxpm/85bdba47cb30edc826876b6c6a3a864b7db54ccc/docs_image/tt4.png -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/__init__.py: -------------------------------------------------------------------------------- 1 | from nonebot import require 2 | from nonebot.plugin import PluginMetadata, inherit_supported_adapters 3 | 4 | from .config import Config 5 | from .enum import PluginType 6 | from .extra import PluginExtraData 7 | 8 | require("nonebot_plugin_alconna") 9 | require("nonebot_plugin_session") 10 | require("nonebot_plugin_uninfo") 11 | 12 | from .commands import * # noqa: F403 13 | 14 | __plugin_meta__ = PluginMetadata( 15 | name="ZXPM插件管理", 16 | description="真寻的插件管理系统", 17 | usage=""" 18 | 包含了插件功能开关,群组/用户ban,群管监测,设置权限功能 19 | 并提供一个简单的帮助接口,可以通过 zxpm [名称] 来获取帮助 20 | 可以通过 -s 参数来获取该功能超级用户帮助 21 | 例如: 22 | zxpm ban 23 | zxpm ban -s 24 | """, 25 | type="application", 26 | homepage="https://github.com/HibiKier/nonebot-plugin-zxpm", 27 | config=Config, 28 | supported_adapters=inherit_supported_adapters( 29 | "nonebot_plugin_alconna", 30 | "nonebot_plugin_session", 31 | "nonebot_plugin_uninfo", 32 | ), 33 | extra=PluginExtraData(plugin_type=PluginType.PARENT).to_dict(), 34 | ) 35 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/commands/__init__.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | 3 | import nonebot 4 | from nonebot.adapters import Bot 5 | from nonebot_plugin_uninfo import get_interface 6 | 7 | from ..log import logger 8 | from ..models.bot_console import BotConsole 9 | from ..models.group_console import GroupConsole 10 | from ..models.plugin_info import PluginInfo 11 | from .zxpm_ban import * # noqa: F403 12 | from .zxpm_help import * # noqa: F403 13 | from .zxpm_hooks import * # noqa: F403 14 | from .zxpm_init import * # noqa: F403 15 | from .zxpm_plugin_switch import * # noqa: F403 16 | from .zxpm_set_admin import * # noqa: F403 17 | from .zxpm_super_group import * # noqa: F403 18 | 19 | driver = nonebot.get_driver() 20 | 21 | with contextlib.suppress(ImportError): 22 | from nonebot.adapters.onebot.v11 import GroupIncreaseNoticeEvent # noqa: F401 23 | 24 | from .zxpm_add_group import * # noqa: F403 25 | from .zxpm_admin_watch import * # noqa: F403 26 | 27 | 28 | @driver.on_bot_connect 29 | async def _(bot: Bot): 30 | """更新bot群组信息 31 | 32 | 参数: 33 | bot: Bot 34 | """ 35 | try: 36 | if interface := get_interface(bot): 37 | scens = await interface.get_scenes() 38 | group_list = [(s.id, s.name) for s in scens if s.is_group] 39 | db_group_list = await GroupConsole.all().values_list("group_id", flat=True) 40 | block_modules = await PluginInfo.filter( 41 | load_status=True, default_status=False 42 | ).values_list("module", flat=True) 43 | block_modules = [f"<{module}" for module in block_modules] 44 | create_list = [] 45 | for gid, name in group_list: 46 | if gid not in db_group_list: 47 | group = GroupConsole(group_id=gid, group_name=name) 48 | if block_modules: 49 | group.block_plugin = ",".join(block_modules) + "," 50 | logger.debug(f"Bot: {bot.self_id} 添加创建群组Id: {group.group_id}") 51 | create_list.append(group) 52 | if create_list: 53 | await GroupConsole.bulk_create(create_list, 10) 54 | logger.debug( 55 | f"更新Bot: {bot.self_id} 共创建 {len(create_list)} 条群组数据..." 56 | ) 57 | except Exception as e: 58 | logger.error(f"获取Bot: {bot.self_id} 群组发生错误...", e=e) 59 | logger.debug(f"Bot: {bot.self_id} 建立连接...") 60 | if not await BotConsole.exists(bot_id=bot.self_id): 61 | await BotConsole.create(bot_id=bot.self_id) 62 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/commands/zxpm_add_group/__init__.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_notice 2 | from nonebot.adapters import Bot 3 | from nonebot.adapters.onebot.v11 import GroupIncreaseNoticeEvent 4 | from nonebot.adapters.onebot.v12 import GroupMemberIncreaseEvent 5 | from nonebot.plugin import PluginMetadata 6 | 7 | from ...enum import PluginType 8 | from ...extra import PluginExtraData 9 | from ...models.group_console import GroupConsole 10 | from ...rules import notice_rule 11 | from .data_source import GroupManager 12 | 13 | __plugin_meta__ = PluginMetadata( 14 | name="QQ群事件处理", 15 | description="群事件处理", 16 | usage="", 17 | extra=PluginExtraData( 18 | author="HibiKier", 19 | version="0.1", 20 | plugin_type=PluginType.HIDDEN, 21 | ).to_dict(), 22 | ) 23 | 24 | group_increase_handle = on_notice( 25 | priority=1, 26 | block=False, 27 | rule=notice_rule([GroupIncreaseNoticeEvent, GroupMemberIncreaseEvent]), 28 | ) 29 | """群员增加处理""" 30 | 31 | 32 | @group_increase_handle.handle() 33 | async def _(bot: Bot, event: GroupIncreaseNoticeEvent | GroupMemberIncreaseEvent): 34 | user_id = str(event.user_id) 35 | group_id = str(event.group_id) 36 | if user_id == bot.self_id: 37 | """新成员为bot本身""" 38 | group, _ = await GroupConsole.get_or_create( 39 | group_id=group_id, channel_id__isnull=True 40 | ) 41 | await GroupManager.add_bot(bot, group_id, group) 42 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/commands/zxpm_add_group/data_source.py: -------------------------------------------------------------------------------- 1 | from nonebot.adapters import Bot 2 | 3 | from ...config import ZxpmConfig 4 | from ...log import logger 5 | from ...models.group_console import GroupConsole 6 | from ...models.level_user import LevelUser 7 | from ...models.plugin_info import PluginInfo 8 | 9 | 10 | class GroupManager: 11 | @classmethod 12 | async def __handle_add_group( 13 | cls, bot: Bot, group_id: str, group: GroupConsole | None 14 | ): 15 | """允许群组并设置群认证,默认群功能开关 16 | 17 | 参数: 18 | bot: Bot 19 | group_id: 群组id 20 | group: GroupConsole 21 | """ 22 | block_plugin = "" 23 | if plugin_list := await PluginInfo.filter(default_status=False).all(): 24 | for plugin in plugin_list: 25 | block_plugin += f"<{plugin.module}," 26 | group_info = await bot.get_group_info(group_id=group_id) 27 | if group: 28 | group.block_plugin = block_plugin 29 | await group.save(update_fields=["block_plugin"]) 30 | else: 31 | await GroupConsole.create( 32 | group_id=group_info["group_id"], 33 | group_name=group_info["group_name"], 34 | max_member_count=group_info["max_member_count"], 35 | member_count=group_info["member_count"], 36 | block_plugin=block_plugin, 37 | ) 38 | 39 | @classmethod 40 | async def __refresh_level(cls, bot: Bot, group_id: str): 41 | """刷新权限 42 | 43 | 参数: 44 | bot: Bot 45 | group_id: 群组id 46 | """ 47 | admin_default_auth = ZxpmConfig.zxpm_admin_default_auth 48 | member_list = await bot.get_group_member_list(group_id=group_id) 49 | member_id_list = [str(user_info["user_id"]) for user_info in member_list] 50 | flag2u = await LevelUser.filter( 51 | user_id__in=member_id_list, group_id=group_id 52 | ).values_list("user_id", flat=True) 53 | # 即刻刷新权限 54 | for user_info in member_list: 55 | user_id = user_info["user_id"] 56 | role = user_info["role"] 57 | if user_id in bot.config.superusers: 58 | await LevelUser.set_level(user_id, user_info["group_id"], 9) 59 | logger.debug( 60 | "添加超级用户权限: 9", 61 | "入群检测", 62 | session=user_id, 63 | group_id=user_info["group_id"], 64 | ) 65 | elif ( 66 | admin_default_auth is not None 67 | and role in ["owner", "admin"] 68 | and user_id not in flag2u 69 | ): 70 | await LevelUser.set_level( 71 | user_id, 72 | user_info["group_id"], 73 | admin_default_auth, 74 | ) 75 | logger.debug( 76 | f"添加默认群管理员权限: {admin_default_auth}", 77 | "入群检测", 78 | session=user_id, 79 | group_id=user_info["group_id"], 80 | ) 81 | 82 | @classmethod 83 | async def add_bot(cls, bot: Bot, group_id: str, group: GroupConsole | None): 84 | """拉入bot 85 | 86 | 参数: 87 | bot: Bot 88 | operator_id: 操作者id 89 | group_id: 群组id 90 | group: GroupConsole 91 | """ 92 | await cls.__handle_add_group(bot, group_id, group) 93 | """刷新群管理员权限""" 94 | await cls.__refresh_level(bot, group_id) 95 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/commands/zxpm_admin_watch/__init__.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_notice 2 | from nonebot.adapters.onebot.v11 import GroupAdminNoticeEvent 3 | from nonebot.plugin import PluginMetadata 4 | 5 | from ...config import ZxpmConfig 6 | from ...enum import PluginType 7 | from ...extra import PluginExtraData 8 | from ...log import logger 9 | from ...models.level_user import LevelUser 10 | from ...rules import notice_rule 11 | 12 | __plugin_meta__ = PluginMetadata( 13 | name="群管理员变动监测", 14 | description="""检测群管理员变动, 添加与删除管理员默认权限, 15 | 当配置项 ADMIN_DEFAULT_AUTH 为空时, 不会添加管理员权限""", 16 | usage="", 17 | extra=PluginExtraData( 18 | author="HibiKier", 19 | version="0.1", 20 | plugin_type=PluginType.HIDDEN, 21 | ).to_dict(), 22 | ) 23 | 24 | 25 | admin_notice = on_notice(priority=5, rule=notice_rule(GroupAdminNoticeEvent)) 26 | 27 | 28 | @admin_notice.handle() 29 | async def _(event: GroupAdminNoticeEvent): 30 | if event.sub_type == "set": 31 | admin_default_auth = ZxpmConfig.zxpm_admin_default_auth 32 | if admin_default_auth is not None: 33 | await LevelUser.set_level( 34 | str(event.user_id), 35 | str(event.group_id), 36 | admin_default_auth, 37 | ) 38 | logger.info( 39 | f"成为管理员,添加权限: {admin_default_auth}", 40 | "群管理员变动监测", 41 | session=event.user_id, 42 | group_id=event.group_id, 43 | ) 44 | else: 45 | logger.warning( 46 | "配置项 MODULE: [admin_bot_manage] |" 47 | " KEY: [ADMIN_DEFAULT_AUTH] 为空" 48 | ) 49 | elif event.sub_type == "unset": 50 | await LevelUser.delete_level(str(event.user_id), str(event.group_id)) 51 | logger.info( 52 | "撤销群管理员, 取消权限等级", 53 | "群管理员变动监测", 54 | session=event.user_id, 55 | group_id=event.group_id, 56 | ) 57 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/commands/zxpm_ban/__init__.py: -------------------------------------------------------------------------------- 1 | from arclet.alconna import Args 2 | from nonebot.adapters import Bot 3 | from nonebot.permission import SUPERUSER 4 | from nonebot.plugin import PluginMetadata 5 | from nonebot_plugin_alconna import ( 6 | Alconna, 7 | Arparma, 8 | At, 9 | Match, 10 | Option, 11 | on_alconna, 12 | store_true, 13 | ) 14 | from nonebot_plugin_session import EventSession 15 | 16 | from ...config import ZxpmConfig 17 | from ...enum import PluginType 18 | from ...extra import PluginExtraData 19 | from ...log import logger 20 | from ...rules import admin_check 21 | from ...utils.utils import MessageUtils 22 | from ._data_source import BanManage 23 | 24 | __plugin_meta__ = PluginMetadata( 25 | name="Ban", 26 | description="你被逮捕了!丢进小黑屋!封禁用户以及群组,屏蔽消息", 27 | usage=""" 28 | 普通管理员 29 | 格式: 30 | ban [At用户] ?[-t [时长(分钟)]] 31 | 32 | 示例: 33 | ban @用户 : 永久拉黑用户 34 | ban @用户 -t 100 : 拉黑用户100分钟 35 | unban @用户 : 从小黑屋中拉出来 36 | """.strip(), 37 | extra=PluginExtraData( 38 | author="HibiKier", 39 | version="0.1", 40 | plugin_type=PluginType.SUPER_AND_ADMIN, 41 | admin_level=ZxpmConfig.zxpm_ban_level, 42 | superuser_help=""" 43 | 超级管理员额外命令 44 | 格式: 45 | ban [At用户/用户Id] ?[-t [时长]] 46 | unban --id [idx] : 通过id来进行unban操作 47 | ban列表: 获取所有Ban数据 48 | 49 | 群组ban列表: 获取群组Ban数据 50 | 用户ban列表: 获取用户Ban数据 51 | 52 | ban列表 -u [用户Id]: 查找指定用户ban数据 53 | ban列表 -g [群组Id]: 查找指定群组ban数据 54 | 示例: 55 | ban列表 -u 123456789 : 查找用户123456789的ban数据 56 | ban列表 -g 123456789 : 查找群组123456789的ban数据 57 | 58 | 私聊下: 59 | 示例: 60 | ban 123456789 : 永久拉黑用户123456789 61 | ban 123456789 -t 100 : 拉黑用户123456789 100分钟 62 | 63 | ban -g 999999 : 拉黑群组为999999的群组 64 | ban -g 999999 -t 100 : 拉黑群组为999999的群组 100分钟 65 | 66 | unban 123456789 : 从小黑屋中拉出来 67 | unban -g 999999 : 将群组9999999从小黑屋中拉出来 68 | """, 69 | ).to_dict(), 70 | ) 71 | 72 | 73 | _ban_matcher = on_alconna( 74 | Alconna( 75 | "ban", 76 | Args["user?", [str, At]], 77 | Option("-g|--group", Args["group_id", str]), 78 | Option("-t|--time", Args["duration", int]), 79 | ), 80 | rule=admin_check(ZxpmConfig.zxpm_ban_level), 81 | priority=5, 82 | block=True, 83 | ) 84 | 85 | _unban_matcher = on_alconna( 86 | Alconna( 87 | "unban", 88 | Args["user?", [str, At]], 89 | Option("-g|--group", Args["group_id", str]), 90 | Option("--id", Args["idx", int]), 91 | ), 92 | rule=admin_check(ZxpmConfig.zxpm_ban_level), 93 | priority=5, 94 | block=True, 95 | ) 96 | 97 | _status_matcher = on_alconna( 98 | Alconna( 99 | "ban列表", 100 | Option("-u", Args["user_id", str], help_text="查找用户"), 101 | Option("-g", Args["group_id", str], help_text="查找群组"), 102 | Option("--user", action=store_true, help_text="过滤用户"), 103 | Option("--group", action=store_true, help_text="过滤群组"), 104 | ), 105 | permission=SUPERUSER, 106 | priority=1, 107 | block=True, 108 | ) 109 | 110 | _status_matcher.shortcut( 111 | "用户ban列表", 112 | command="ban列表", 113 | arguments=["--user"], 114 | prefix=True, 115 | ) 116 | 117 | _status_matcher.shortcut( 118 | "群组ban列表", 119 | command="ban列表", 120 | arguments=["--group"], 121 | prefix=True, 122 | ) 123 | 124 | 125 | @_status_matcher.handle() 126 | async def _( 127 | arparma: Arparma, 128 | user_id: Match[str], 129 | group_id: Match[str], 130 | ): 131 | filter_type = None 132 | if arparma.find("user"): 133 | filter_type = "user" 134 | if arparma.find("group"): 135 | filter_type = "group" 136 | _user_id = user_id.result if user_id.available else None 137 | _group_id = group_id.result if group_id.available else None 138 | if image := await BanManage.build_ban_image(filter_type, _user_id, _group_id): 139 | await MessageUtils.build_message(image).finish(reply_to=True) 140 | else: 141 | await MessageUtils.build_message("数据为空捏...").finish(reply_to=True) 142 | 143 | 144 | @_ban_matcher.handle() 145 | async def _( 146 | bot: Bot, 147 | session: EventSession, 148 | arparma: Arparma, 149 | user: Match[str | At], 150 | duration: Match[int], 151 | group_id: Match[str], 152 | ): 153 | user_id = "" 154 | if not session.id1: 155 | await MessageUtils.build_message("用户id为空...").finish(reply_to=True) 156 | if user.available: 157 | if isinstance(user.result, At): 158 | user_id = user.result.target 159 | else: 160 | if session.id1 not in bot.config.superusers: 161 | await MessageUtils.build_message("权限不足捏...").finish(reply_to=True) 162 | user_id = user.result 163 | _duration = duration.result * 60 if duration.available else -1 164 | _duration_text = f"{duration.result} 分钟" if duration.available else " 到世界湮灭" 165 | if (gid := session.id3 or session.id2) and not group_id.available: 166 | if ( 167 | not user_id 168 | or user_id == bot.self_id 169 | and session.id1 not in bot.config.superusers 170 | ): 171 | _duration = 0.5 172 | await MessageUtils.build_message("倒反天罡,小小管理速速退下!").send() 173 | await BanManage.ban(session.id1, gid, 30, session, True) 174 | _duration_text = "半 分钟" 175 | logger.info("尝试ban Bot 反被拿下", arparma.header_result, session=session) 176 | await MessageUtils.build_message( 177 | [ 178 | "对 ", 179 | At(flag="user", target=session.id1), 180 | " 狠狠惩戒了一番,一脚踢进了小黑屋!" 181 | f" 在里面乖乖呆 {_duration_text}吧!", 182 | ] 183 | ).finish(reply_to=True) 184 | await BanManage.ban( 185 | user_id, gid, _duration, session, session.id1 in bot.config.superusers 186 | ) 187 | logger.info( 188 | "管理员Ban", 189 | arparma.header_result, 190 | session=session, 191 | target=f"{gid}:{user_id}", 192 | ) 193 | await MessageUtils.build_message( 194 | [ 195 | "对 ", 196 | ( 197 | At(flag="user", target=user_id) 198 | if isinstance(user.result, At) 199 | else user_id 200 | ), # type: ignore 201 | " 狠狠惩戒了一番,一脚踢进了小黑屋!", 202 | f" 在里面乖乖呆 {_duration_text} 吧!", 203 | ] 204 | ).finish(reply_to=True) 205 | elif session.id1 in bot.config.superusers: 206 | _group_id = group_id.result if group_id.available else None 207 | await BanManage.ban(user_id, _group_id, _duration, session, True) 208 | logger.info( 209 | "超级用户Ban", 210 | arparma.header_result, 211 | session=session, 212 | target=f"{_group_id}:{user_id}", 213 | ) 214 | at_msg = user_id or f"群组:{_group_id}" 215 | await MessageUtils.build_message( 216 | f"对 {at_msg} 狠狠惩戒了一番,一脚踢进了小黑屋!" 217 | ).finish(reply_to=True) 218 | 219 | 220 | @_unban_matcher.handle() 221 | async def _( 222 | bot: Bot, 223 | session: EventSession, 224 | arparma: Arparma, 225 | user: Match[str | At], 226 | group_id: Match[str], 227 | idx: Match[int], 228 | ): 229 | user_id = "" 230 | _idx = idx.result if idx.available else None 231 | if user.available: 232 | if isinstance(user.result, At): 233 | user_id = user.result.target 234 | else: 235 | if session.id1 not in bot.config.superusers: 236 | await MessageUtils.build_message("权限不足捏...").finish(reply_to=True) 237 | user_id = user.result 238 | if gid := session.id3 or session.id2: 239 | if group_id.available: 240 | gid = group_id.result 241 | is_unban, result = await BanManage.unban( 242 | user_id, gid, session, _idx, session.id1 in bot.config.superusers 243 | ) 244 | if not is_unban: 245 | await MessageUtils.build_message(result).finish(reply_to=True) 246 | logger.info( 247 | "管理员UnBan", 248 | arparma.header_result, 249 | session=session, 250 | target=f"{gid}:{result}", 251 | ) 252 | await MessageUtils.build_message( 253 | [ 254 | "将 ", 255 | ( 256 | At(flag="user", target=user_id) 257 | if isinstance(user.result, At) 258 | else result 259 | ), # type: ignore 260 | " 从黑屋中拉了出来并急救了一下!", 261 | ] 262 | ).finish(reply_to=True) 263 | elif session.id1 in bot.config.superusers: 264 | _group_id = group_id.result if group_id.available else None 265 | is_unban, result = await BanManage.unban( 266 | user_id, _group_id, session, _idx, True 267 | ) 268 | if not is_unban: 269 | await MessageUtils.build_message(result).finish(reply_to=True) 270 | logger.info( 271 | "超级用户UnBan", 272 | arparma.header_result, 273 | session=session, 274 | target=f"{_group_id}:{user_id}", 275 | ) 276 | at_msg = user_id or f"群组:{result}" 277 | await MessageUtils.build_message( 278 | f"对 {at_msg} 从黑屋中拉了出来并急救了一下!" 279 | ).finish(reply_to=True) 280 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/commands/zxpm_ban/_data_source.py: -------------------------------------------------------------------------------- 1 | import time 2 | from typing import Literal 3 | 4 | from nonebot_plugin_session import EventSession 5 | 6 | from ...models.ban_console import BanConsole 7 | from ...models.level_user import LevelUser 8 | from ...utils.image_template import ImageTemplate 9 | 10 | 11 | class BanManage: 12 | @classmethod 13 | async def build_ban_image( 14 | cls, 15 | filter_type: Literal["group", "user"] | None, 16 | user_id: str | None = None, 17 | group_id: str | None = None, 18 | ) -> bytes: 19 | """构造Ban列表图片 20 | 21 | 参数: 22 | filter_type: 过滤类型 23 | user_id: 用户id 24 | group_id: 群组id 25 | 26 | 返回: 27 | bytes: Ban列表图片 28 | """ 29 | data_list = None 30 | query = BanConsole 31 | if user_id: 32 | query = query.filter(user_id=user_id) 33 | elif group_id: 34 | query = query.filter(group_id=group_id) 35 | elif filter_type == "user": 36 | query = query.filter(group_id__isnull=True) 37 | elif filter_type == "group": 38 | query = query.filter(user_id__isnull=True) 39 | data_list = await query.all() 40 | if not data_list: 41 | return None 42 | column_name = [ 43 | "ID", 44 | "用户ID", 45 | "群组ID", 46 | "BAN LEVEL", 47 | "剩余时长(分钟)", 48 | "操作员ID", 49 | ] 50 | row_data = [] 51 | for data in data_list: 52 | duration = int((data.ban_time + data.duration - time.time()) / 60) 53 | if data.duration < 0: 54 | duration = "∞" 55 | row_data.append( 56 | [ 57 | data.id, 58 | data.user_id, 59 | data.group_id, 60 | data.ban_level, 61 | duration, 62 | data.operator, 63 | ] 64 | ) 65 | return ( 66 | await ImageTemplate.table_page( 67 | "Ban / UnBan 列表", "在黑屋中狠狠调教!", column_name, row_data 68 | ) 69 | ).pic2bytes() 70 | 71 | @classmethod 72 | async def is_ban(cls, user_id: str, group_id: str | None): 73 | """判断用户是否被ban 74 | 75 | 参数: 76 | user_id: 用户id 77 | 78 | 返回: 79 | bool: 是否被ban 80 | """ 81 | return await BanConsole.is_ban(user_id, group_id) 82 | 83 | @classmethod 84 | async def unban( 85 | cls, 86 | user_id: str | None, 87 | group_id: str | None, 88 | session: EventSession, 89 | idx: int | None = None, 90 | is_superuser: bool = False, 91 | ) -> tuple[bool, str]: 92 | """unban目标用户 93 | 94 | 参数: 95 | user_id: 用户id 96 | group_id: 群组id 97 | session: Session 98 | idx: 指定id 99 | is_superuser: 是否为超级用户操作 100 | 101 | 返回: 102 | tuple[bool, str]: 是否unban成功, 群组/用户id或提示 103 | """ 104 | user_level = 9999 105 | if not is_superuser and user_id and session.id1: 106 | user_level = await LevelUser.get_user_level(session.id1, group_id) 107 | if idx: 108 | ban_data = await BanConsole.get_or_none(id=idx) 109 | if not ban_data: 110 | return False, "该用户/群组不在黑名单中不足捏..." 111 | if ban_data.ban_level > user_level: 112 | return False, "unBan权限等级不足捏..." 113 | await ban_data.delete() 114 | return True, str(ban_data.user_id or ban_data.group_id) 115 | elif await BanConsole.check_ban_level(user_id, group_id, user_level): 116 | await BanConsole.unban(user_id, group_id) 117 | return True, str(group_id) 118 | return False, "该用户/群组不在黑名单中不足捏..." 119 | 120 | @classmethod 121 | async def ban( 122 | cls, 123 | user_id: str | None, 124 | group_id: str | None, 125 | duration: int, 126 | session: EventSession, 127 | is_superuser: bool, 128 | ): 129 | """ban掉目标用户 130 | 131 | 参数: 132 | user_id: 用户id 133 | group_id: 群组id 134 | duration: 时长,秒 135 | session: Session 136 | is_superuser: 是否为超级用户操作 137 | """ 138 | level = 9999 139 | if not is_superuser and user_id and session.id1: 140 | level = await LevelUser.get_user_level(session.id1, group_id) 141 | await BanConsole.ban(user_id, group_id, level, duration, session.id1) 142 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/commands/zxpm_bot_manage/__init__.py: -------------------------------------------------------------------------------- 1 | import nonebot 2 | from nonebot.adapters import Bot 3 | from nonebot.plugin import PluginMetadata 4 | from zhenxun_utils.common_utils import CommonUtils 5 | from zhenxun_utils.enum import PluginType 6 | from zhenxun_utils.platform import PlatformUtils 7 | 8 | from ...models.bot_console import BotConsole 9 | from ...models.plugin_info import PluginInfo 10 | from .bot_switch import * # noqa: F403 11 | from .plugin import * # noqa: F403 12 | 13 | driver = nonebot.get_driver() 14 | 15 | __plugin_meta__ = PluginMetadata( 16 | name="Bot管理", 17 | description="指定bot对象的功能/被动开关和状态", 18 | usage=""" 19 | 指令: 20 | bot被动状态 : bot的被动技能状态 21 | bot开启/关闭被动[被动名称] : 被动技能开关 22 | bot开启/关闭所有被动 : 所有被动技能开关 23 | bot插件列表: bot插件列表状态 : bot插件列表 24 | bot开启/关闭所有插件 : 所有插件开关 25 | bot开启/关闭插件[插件名称] : 插件开关 26 | bot休眠 : bot休眠,屏蔽所有消息 27 | bot醒来 : bot醒来 28 | """.strip(), 29 | ) 30 | 31 | from zhenxun_utils.log import logger 32 | 33 | 34 | @driver.on_bot_connect 35 | async def init_bot_console(bot: Bot): 36 | """初始化Bot管理 37 | 38 | 参数: 39 | bot: Bot 40 | """ 41 | 42 | async def _filter_blocked_items( 43 | items_list: list[str], block_list: list[str] 44 | ) -> list[str]: 45 | """过滤被block的项目 46 | 47 | 参数: 48 | items_list: 需要过滤的项目列表 49 | block_list: block列表 50 | 51 | 返回: 52 | list: 过滤后且经过格式化的项目列表 53 | """ 54 | return [item for item in items_list if item not in block_list] 55 | 56 | plugin_list = [ 57 | plugin.module 58 | for plugin in await PluginInfo.get_plugins( 59 | plugin_type__in=[PluginType.NORMAL, PluginType.DEPENDANT, PluginType.ADMIN] 60 | ) 61 | ] 62 | platform = PlatformUtils.get_platform(bot) 63 | bot_data, created = await BotConsole.get_or_create( 64 | bot_id=bot.self_id, platform=platform 65 | ) 66 | 67 | if not created: 68 | plugin_list = await _filter_blocked_items( 69 | plugin_list, await bot_data.get_plugins(bot.self_id, False) 70 | ) 71 | 72 | bot_data.available_plugins = CommonUtils.convert_module_format(plugin_list) 73 | await bot_data.save(update_fields=["available_plugins"]) 74 | logger.info("初始化Bot管理完成...") 75 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/commands/zxpm_bot_manage/bot_switch.py: -------------------------------------------------------------------------------- 1 | from nonebot_plugin_alconna import AlconnaMatch, Match 2 | from nonebot_plugin_uninfo import Uninfo 3 | from zhenxun_utils.log import logger 4 | from zhenxun_utils.message import MessageUtils 5 | 6 | from ...models.bot_console import BotConsole 7 | from .command import bot_manage 8 | 9 | 10 | @bot_manage.assign("bot_switch.enable") 11 | async def enable_bot_switch( 12 | session: Uninfo, 13 | bot_id: Match[str] = AlconnaMatch("bot_id"), 14 | ): 15 | if not bot_id.available: 16 | await MessageUtils.build_message("bot_id 不能为空").finish() 17 | 18 | else: 19 | logger.info( 20 | f"开启 {bot_id.result} ", 21 | "bot_manage.bot_switch.enable", 22 | session=session, 23 | ) 24 | try: 25 | await BotConsole.set_bot_status(True, bot_id.result) 26 | except ValueError: 27 | await MessageUtils.build_message(f"bot_id {bot_id.result} 不存在").finish() 28 | 29 | await MessageUtils.build_message(f"已开启 {bot_id.result} ").finish() 30 | 31 | 32 | @bot_manage.assign("bot_switch.disable") 33 | async def diasble_bot_switch( 34 | session: Uninfo, 35 | bot_id: Match[str] = AlconnaMatch("bot_id"), 36 | ): 37 | if not bot_id.available: 38 | await MessageUtils.build_message("bot_id 不能为空").finish() 39 | 40 | else: 41 | logger.info( 42 | f"禁用 {bot_id.result} ", 43 | "bot_manage.bot_switch.disable", 44 | session=session, 45 | ) 46 | try: 47 | await BotConsole.set_bot_status(False, bot_id.result) 48 | except ValueError: 49 | await MessageUtils.build_message(f"bot_id {bot_id.result} 不存在").finish() 50 | 51 | await MessageUtils.build_message(f"已禁用 {bot_id.result} ").finish() 52 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/commands/zxpm_bot_manage/command.py: -------------------------------------------------------------------------------- 1 | from arclet.alconna import Alconna, Args, Option, Subcommand 2 | from arclet.alconna.action import store_false 3 | from nonebot.permission import SUPERUSER 4 | from nonebot_plugin_alconna import on_alconna 5 | 6 | bot_manage = on_alconna( 7 | Alconna( 8 | "bot_manage", 9 | Subcommand( 10 | "task", 11 | Option( 12 | "list", 13 | action=store_false, 14 | help_text="查看 bot_id 下的所有可用被动", 15 | ), 16 | Option("-b|--bot", Args["bot_id", str], help_text="指定 bot_id"), 17 | Subcommand( 18 | "enable", 19 | Args["feature_name?", str], 20 | ), 21 | Subcommand( 22 | "disable", 23 | Args["feature_name?", str], 24 | ), 25 | ), 26 | Subcommand( 27 | "plugin", 28 | Option( 29 | "list", 30 | action=store_false, 31 | help_text="查看 bot_id 下的所有可用插件", 32 | ), 33 | Option("-b|--bot", Args["bot_id", str], help_text="指定 bot_id"), 34 | Subcommand( 35 | "enable", 36 | Args["plugin_name?", str], 37 | ), 38 | Subcommand( 39 | "disable", 40 | Args["plugin_name?", str], 41 | ), 42 | ), 43 | Subcommand( 44 | "full_function", 45 | Subcommand( 46 | "enable", 47 | Args["bot_id?", str], 48 | ), 49 | Subcommand( 50 | "disable", 51 | Args["bot_id?", str], 52 | ), 53 | ), 54 | Subcommand( 55 | "bot_switch", 56 | Subcommand( 57 | "enable", 58 | Args["bot_id?", str], 59 | ), 60 | Subcommand( 61 | "disable", 62 | Args["bot_id?", str], 63 | ), 64 | ), 65 | ), 66 | permission=SUPERUSER, 67 | priority=5, 68 | block=True, 69 | ) 70 | 71 | bot_manage.shortcut( 72 | r"bot被动状态", 73 | command="bot_manage", 74 | arguments=["task", "list"], 75 | prefix=True, 76 | ) 77 | 78 | bot_manage.shortcut( 79 | r"bot开启被动\s*(?P.+)", 80 | command="bot_manage", 81 | arguments=["task", "enable", "{name}"], 82 | prefix=True, 83 | ) 84 | 85 | bot_manage.shortcut( 86 | r"bot关闭被动\s*(?P.+)", 87 | command="bot_manage", 88 | arguments=["task", "disable", "{name}"], 89 | prefix=True, 90 | ) 91 | 92 | bot_manage.shortcut( 93 | r"bot开启(全部|所有)被动", 94 | command="bot_manage", 95 | arguments=["task", "enable"], 96 | prefix=True, 97 | ) 98 | 99 | bot_manage.shortcut( 100 | r"bot关闭(全部|所有)被动", 101 | command="bot_manage", 102 | arguments=["task", "disable"], 103 | prefix=True, 104 | ) 105 | 106 | bot_manage.shortcut( 107 | r"bot插件列表", 108 | command="bot_manage", 109 | arguments=["plugin", "list"], 110 | prefix=True, 111 | ) 112 | 113 | bot_manage.shortcut( 114 | r"bot开启(全部|所有)插件", 115 | command="bot_manage", 116 | arguments=["plugin", "enable"], 117 | prefix=True, 118 | ) 119 | 120 | bot_manage.shortcut( 121 | r"bot关闭(全部|所有)插件", 122 | command="bot_manage", 123 | arguments=["plugin", "disable"], 124 | prefix=True, 125 | ) 126 | 127 | bot_manage.shortcut( 128 | r"bot开启\s*(?P.+)", 129 | command="bot_manage", 130 | arguments=["plugin", "enable", "{name}"], 131 | prefix=True, 132 | ) 133 | 134 | bot_manage.shortcut( 135 | r"bot关闭\s*(?P.+)", 136 | command="bot_manage", 137 | arguments=["plugin", "disable", "{name}"], 138 | prefix=True, 139 | ) 140 | 141 | bot_manage.shortcut( 142 | r"bot休眠\s*(?P.+)?", 143 | command="bot_manage", 144 | arguments=["bot_switch", "disable", "{bot_id}"], 145 | prefix=True, 146 | ) 147 | 148 | bot_manage.shortcut( 149 | r"bot醒来\s*(?P.+)?", 150 | command="bot_manage", 151 | arguments=["bot_switch", "enable", "{bot_id}"], 152 | prefix=True, 153 | ) 154 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/commands/zxpm_bot_manage/full_function.py: -------------------------------------------------------------------------------- 1 | from nonebot_plugin_alconna import AlconnaMatch, Match 2 | from nonebot_plugin_uninfo import Uninfo 3 | from zhenxun_utils.log import logger 4 | from zhenxun_utils.message import MessageUtils 5 | 6 | from ...models.bot_console import BotConsole 7 | from .command import bot_manage 8 | 9 | 10 | @bot_manage.assign("full_function.enable") 11 | async def enable_full_function( 12 | session: Uninfo, 13 | bot_id: Match[str] = AlconnaMatch("bot_id"), 14 | ): 15 | if not bot_id.available: 16 | await MessageUtils.build_message("bot_id 不能为空").finish() 17 | 18 | else: 19 | logger.info( 20 | f"开启 {bot_id.result} 的所有可用插件及被动", 21 | "bot_manage.full_function.enable", 22 | session=session, 23 | ) 24 | await BotConsole.enable_all(bot_id.result, "tasks") 25 | await BotConsole.enable_all(bot_id.result, "plugins") 26 | 27 | await MessageUtils.build_message( 28 | f"已开启 {bot_id.result} 的所有插件及被动" 29 | ).finish() 30 | 31 | 32 | @bot_manage.assign("full_function.disable") 33 | async def diasble_full_function( 34 | session: Uninfo, 35 | bot_id: Match[str] = AlconnaMatch("bot_id"), 36 | ): 37 | if not bot_id.available: 38 | await MessageUtils.build_message("bot_id 不能为空").finish() 39 | 40 | else: 41 | logger.info( 42 | f"禁用 {bot_id.result} 的所有可用插件及被动", 43 | "bot_manage.full_function.disable", 44 | session=session, 45 | ) 46 | await BotConsole.disable_all(bot_id.result, "tasks") 47 | await BotConsole.disable_all(bot_id.result, "plugins") 48 | 49 | await MessageUtils.build_message( 50 | f"已禁用 {bot_id.result} 的所有插件及被动" 51 | ).finish() 52 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/commands/zxpm_bot_manage/plugin.py: -------------------------------------------------------------------------------- 1 | from nonebot_plugin_alconna import AlconnaMatch, Match 2 | from nonebot_plugin_uninfo import Uninfo 3 | from zhenxun_utils._build_image import BuildImage 4 | from zhenxun_utils._image_template import ImageTemplate, RowStyle 5 | from zhenxun_utils.enum import PluginType 6 | from zhenxun_utils.log import logger 7 | from zhenxun_utils.message import MessageUtils 8 | 9 | from ...models.bot_console import BotConsole 10 | from ...models.plugin_info import PluginInfo 11 | from .command import bot_manage 12 | 13 | 14 | def task_row_style(column: str, text: str) -> RowStyle: 15 | """被动技能文本风格 16 | 17 | 参数: 18 | column: 表头 19 | text: 文本内容 20 | 21 | 返回: 22 | RowStyle: RowStyle 23 | """ 24 | style = RowStyle() 25 | if column in {"全局状态"}: 26 | style.font_color = "#67C23A" if text == "开启" else "#F56C6C" 27 | return style 28 | 29 | 30 | @bot_manage.assign("plugin.list") 31 | async def bot_plugin(session: Uninfo, bot_id: Match[str] = AlconnaMatch("bot_id")): 32 | logger.info("获取全部 bot 的所有可用插件", "bot_manage.plugin", session=session) 33 | column_name = [ 34 | "ID", 35 | "模块", 36 | "名称", 37 | "全局状态", 38 | "禁用类型", 39 | "加载状态", 40 | "菜单分类", 41 | "作者", 42 | "版本", 43 | "金币花费", 44 | ] 45 | if bot_id.available: 46 | data_dict = { 47 | bot_id.result: await BotConsole.get_plugins( 48 | bot_id=bot_id.result, status=False 49 | ) 50 | } 51 | else: 52 | data_dict = await BotConsole.get_plugins(status=False) 53 | db_plugin_list = await PluginInfo.filter( 54 | load_status=True, plugin_type__not=PluginType.HIDDEN 55 | ).all() 56 | img_list = [] 57 | for __bot_id, tk in data_dict.items(): 58 | column_data = [ 59 | [ 60 | plugin.id, 61 | plugin.module, 62 | plugin.name, 63 | "开启" if plugin.module not in tk else "关闭", 64 | plugin.block_type, 65 | "SUCCESS" if plugin.load_status else "ERROR", 66 | plugin.menu_type, 67 | plugin.author, 68 | plugin.version, 69 | plugin.cost_gold, 70 | ] 71 | for plugin in db_plugin_list 72 | ] 73 | img = await ImageTemplate.table_page( 74 | f"{__bot_id}插件列表", 75 | None, 76 | column_name, 77 | column_data, 78 | text_style=task_row_style, 79 | ) 80 | img_list.append(img) 81 | result = await BuildImage.auto_paste(img_list, 3) 82 | await MessageUtils.build_message(result).finish() 83 | 84 | 85 | @bot_manage.assign("plugin.enable") 86 | async def enable_plugin( 87 | session: Uninfo, 88 | plugin_name: Match[str] = AlconnaMatch("plugin_name"), 89 | bot_id: Match[str] = AlconnaMatch("bot_id"), 90 | ): 91 | if plugin_name.available: 92 | plugin: PluginInfo | None = await PluginInfo.get_plugin(name=plugin_name.result) 93 | if not plugin: 94 | await MessageUtils.build_message("未找到该插件...").finish() 95 | if bot_id.available: 96 | logger.info( 97 | f"开启 {bot_id.result} 的插件 {plugin_name.result}", 98 | "bot_manage.plugin.disable", 99 | session=session, 100 | ) 101 | await BotConsole.enable_plugin(bot_id.result, plugin.module) 102 | await MessageUtils.build_message( 103 | f"已开启 {bot_id.result} 的插件 {plugin_name.result}" 104 | ).finish() 105 | else: 106 | logger.info( 107 | f"开启全部 bot 的插件: {plugin_name.result}", 108 | "bot_manage.plugin.disable", 109 | session=session, 110 | ) 111 | await BotConsole.enable_plugin(None, plugin.module) 112 | await MessageUtils.build_message( 113 | f"已禁用全部 bot 的插件: {plugin_name.result}" 114 | ).finish() 115 | elif bot_id.available: 116 | logger.info( 117 | f"开启 {bot_id.result} 全部插件", 118 | "bot_manage.plugin.disable", 119 | session=session, 120 | ) 121 | await BotConsole.enable_all(bot_id.result, "plugins") 122 | await MessageUtils.build_message(f"已开启 {bot_id.result} 全部插件").finish() 123 | else: 124 | bot_id_list = await BotConsole.annotate().values_list("bot_id", flat=True) 125 | for __bot_id in bot_id_list: 126 | await BotConsole.enable_all(__bot_id, "plugins") # type: ignore 127 | logger.info( 128 | "开启全部 bot 全部插件", 129 | "bot_manage.plugin.disable", 130 | session=session, 131 | ) 132 | await MessageUtils.build_message("开启全部 bot 全部插件").finish() 133 | 134 | 135 | @bot_manage.assign("plugin.disable") 136 | async def disable_plugin( 137 | session: Uninfo, 138 | plugin_name: Match[str] = AlconnaMatch("plugin_name"), 139 | bot_id: Match[str] = AlconnaMatch("bot_id"), 140 | ): 141 | if plugin_name.available: 142 | plugin = await PluginInfo.get_plugin(name=plugin_name.result) 143 | if not plugin: 144 | await MessageUtils.build_message("未找到该插件...").finish() 145 | if bot_id.available: 146 | logger.info( 147 | f"禁用 {bot_id.result} 的插件 {plugin_name.result}", 148 | "bot_manage.plugin.disable", 149 | session=session, 150 | ) 151 | await BotConsole.disable_plugin(bot_id.result, plugin.module) 152 | await MessageUtils.build_message( 153 | f"已禁用 {bot_id.result} 的插件 {plugin_name.result}" 154 | ).finish() 155 | else: 156 | logger.info( 157 | f"禁用全部 bot 的插件: {plugin_name.result}", 158 | "bot_manage.plugin.disable", 159 | session=session, 160 | ) 161 | await BotConsole.disable_plugin(None, plugin.module) 162 | await MessageUtils.build_message( 163 | f"已禁用全部 bot 的插件: {plugin_name.result}" 164 | ).finish() 165 | elif bot_id.available: 166 | logger.info( 167 | f"禁用 {bot_id.result} 全部插件", 168 | "bot_manage.plugin.disable", 169 | session=session, 170 | ) 171 | await BotConsole.disable_all(bot_id.result, "plugins") 172 | await MessageUtils.build_message(f"已禁用 {bot_id.result} 全部插件").finish() 173 | else: 174 | bot_id_list = await BotConsole.annotate().values_list("bot_id", flat=True) 175 | for __bot_id in bot_id_list: 176 | await BotConsole.disable_all(__bot_id, "plugins") # type: ignore 177 | logger.info( 178 | "禁用全部 bot 全部插件", 179 | "bot_manage.plugin.disable", 180 | session=session, 181 | ) 182 | await MessageUtils.build_message("禁用全部 bot 全部插件").finish() 183 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/commands/zxpm_help/__init__.py: -------------------------------------------------------------------------------- 1 | from nonebot.adapters import Bot 2 | from nonebot.plugin import PluginMetadata 3 | from nonebot_plugin_alconna import ( 4 | Alconna, 5 | AlconnaQuery, 6 | Args, 7 | Option, 8 | Query, 9 | on_alconna, 10 | store_true, 11 | ) 12 | from nonebot_plugin_session import EventSession 13 | 14 | from ...enum import PluginType 15 | from ...extra import PluginExtraData 16 | from ...log import logger 17 | from ...utils.utils import MessageUtils 18 | from ._data_source import get_plugin_help 19 | 20 | __plugin_meta__ = PluginMetadata( 21 | name="ZXPM帮助", 22 | description="ZXPM帮助,通过 ZXPM [名称]来获取帮助指令", 23 | usage="", 24 | extra=PluginExtraData( 25 | author="HibiKier", 26 | version="0.1", 27 | plugin_type=PluginType.DEPENDANT, 28 | ).to_dict(), 29 | ) 30 | 31 | 32 | _matcher = on_alconna( 33 | Alconna( 34 | "zxpm", 35 | Args["name", str], 36 | Option("-s|--superuser", action=store_true, help_text="超级用户帮助"), 37 | ), 38 | aliases={"ZXPM"}, 39 | priority=1, 40 | block=True, 41 | ) 42 | 43 | 44 | @_matcher.handle() 45 | async def _( 46 | bot: Bot, 47 | name: str, 48 | session: EventSession, 49 | is_superuser: Query[bool] = AlconnaQuery("superuser.value", False), 50 | ): 51 | if not session.id1: 52 | await MessageUtils.build_message("用户id为空...").finish() 53 | _is_superuser = is_superuser.result if is_superuser.available else False 54 | if _is_superuser and session.id1 not in bot.config.superusers: 55 | _is_superuser = False 56 | if result := await get_plugin_help(session.id1, name, _is_superuser): 57 | await MessageUtils.build_message(result).send(reply_to=True) 58 | else: 59 | await MessageUtils.build_message("没有此功能的帮助信息...").send(reply_to=True) 60 | logger.info(f"查看帮助详情: {name}", "帮助", session=session) 61 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/commands/zxpm_help/_data_source.py: -------------------------------------------------------------------------------- 1 | import nonebot 2 | 3 | from ...enum import PluginType 4 | from ...models.level_user import LevelUser 5 | from ...models.plugin_info import PluginInfo 6 | from ...utils.image_template import ImageTemplate 7 | 8 | driver = nonebot.get_driver() 9 | 10 | 11 | async def get_user_allow_help(user_id: str) -> list[PluginType]: 12 | """获取用户可访问插件类型列表 13 | 14 | 参数: 15 | user_id: 用户id 16 | 17 | 返回: 18 | list[PluginType]: 插件类型列表 19 | """ 20 | type_list = [PluginType.NORMAL, PluginType.DEPENDANT] 21 | for level in await LevelUser.filter(user_id=user_id).values_list( 22 | "user_level", flat=True 23 | ): 24 | if level > 0: # type: ignore 25 | type_list.extend((PluginType.ADMIN, PluginType.SUPER_AND_ADMIN)) 26 | break 27 | if user_id in driver.config.superusers: 28 | type_list.append(PluginType.SUPERUSER) 29 | return type_list 30 | 31 | 32 | async def get_plugin_help(user_id: str, name: str, is_superuser: bool) -> str | bytes: 33 | """获取功能的帮助信息 34 | 35 | 参数: 36 | user_id: 用户id 37 | name: 插件名称或id 38 | is_superuser: 是否为超级用户 39 | """ 40 | type_list = await get_user_allow_help(user_id) 41 | if name.isdigit(): 42 | plugin = await PluginInfo.get_or_none(id=int(name), plugin_type__in=type_list) 43 | else: 44 | plugin = await PluginInfo.get_or_none( 45 | name__iexact=name, load_status=True, plugin_type__in=type_list 46 | ) 47 | if plugin: 48 | _plugin = nonebot.get_plugin_by_module_name(plugin.module_path) 49 | if _plugin and _plugin.metadata: 50 | items = None 51 | if is_superuser: 52 | extra = _plugin.metadata.extra 53 | if usage := extra.get("superuser_help"): 54 | items = { 55 | "简介": _plugin.metadata.description, 56 | "用法": usage, 57 | } 58 | else: 59 | items = { 60 | "简介": _plugin.metadata.description, 61 | "用法": _plugin.metadata.usage, 62 | } 63 | if items: 64 | return (await ImageTemplate.hl_page(plugin.name, items)).pic2bytes() 65 | return "糟糕! 该功能没有帮助喔..." 66 | return "没有查找到这个功能噢..." 67 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/commands/zxpm_hooks/__init__.py: -------------------------------------------------------------------------------- 1 | from .zxpm_auth_hook import * # noqa: F403 2 | from .zxpm_ban_hook import * # noqa: F403 3 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/commands/zxpm_hooks/_auth_checker.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | 3 | from nonebot.adapters import Bot, Event 4 | from nonebot.exception import IgnoredException 5 | from nonebot.matcher import Matcher 6 | from nonebot_plugin_alconna import At, UniMsg 7 | from nonebot_plugin_session import EventSession 8 | from pydantic import BaseModel 9 | 10 | from ...config import ZxpmConfig 11 | from ...enum import BlockType, LimitWatchType, PluginLimitType, PluginType 12 | from ...extra.limit import CountLimiter, FreqLimiter, UserBlockLimiter 13 | from ...log import logger 14 | from ...models.bot_console import BotConsole 15 | from ...models.group_console import GroupConsole 16 | from ...models.level_user import LevelUser 17 | from ...models.plugin_info import PluginInfo 18 | from ...models.plugin_limit import PluginLimit 19 | from ...utils.utils import MessageUtils 20 | 21 | 22 | class Limit(BaseModel): 23 | limit: PluginLimit 24 | limiter: FreqLimiter | UserBlockLimiter | CountLimiter 25 | 26 | class Config: 27 | arbitrary_types_allowed = True 28 | 29 | 30 | class LimitManage: 31 | add_module = [] # noqa: RUF012 32 | 33 | cd_limit: dict[str, Limit] = {} # noqa: RUF012 34 | block_limit: dict[str, Limit] = {} # noqa: RUF012 35 | count_limit: dict[str, Limit] = {} # noqa: RUF012 36 | 37 | @classmethod 38 | def add_limit(cls, limit: PluginLimit): 39 | """添加限制 40 | 41 | 参数: 42 | limit: PluginLimit 43 | """ 44 | if limit.module not in cls.add_module: 45 | cls.add_module.append(limit.module) 46 | if limit.limit_type == PluginLimitType.BLOCK: 47 | cls.block_limit[limit.module] = Limit( 48 | limit=limit, limiter=UserBlockLimiter() 49 | ) 50 | elif limit.limit_type == PluginLimitType.CD: 51 | cls.cd_limit[limit.module] = Limit( 52 | limit=limit, limiter=FreqLimiter(limit.cd) 53 | ) 54 | elif limit.limit_type == PluginLimitType.COUNT: 55 | cls.count_limit[limit.module] = Limit( 56 | limit=limit, limiter=CountLimiter(limit.max_count) 57 | ) 58 | 59 | @classmethod 60 | def unblock( 61 | cls, module: str, user_id: str, group_id: str | None, channel_id: str | None 62 | ): 63 | """解除插件block 64 | 65 | 参数: 66 | module: 模块名 67 | user_id: 用户id 68 | group_id: 群组id 69 | channel_id: 频道id 70 | """ 71 | if limit_model := cls.block_limit.get(module): 72 | limit = limit_model.limit 73 | limiter: UserBlockLimiter = limit_model.limiter # type: ignore 74 | key_type = user_id 75 | if group_id and limit.watch_type == LimitWatchType.GROUP: 76 | key_type = channel_id or group_id 77 | limiter.set_false(key_type) 78 | 79 | @classmethod 80 | async def check( 81 | cls, 82 | module: str, 83 | user_id: str, 84 | group_id: str | None, 85 | channel_id: str | None, 86 | session: EventSession, 87 | ): 88 | """检测限制 89 | 90 | 参数: 91 | module: 模块名 92 | user_id: 用户id 93 | group_id: 群组id 94 | channel_id: 频道id 95 | session: Session 96 | 97 | 异常: 98 | IgnoredException: IgnoredException 99 | """ 100 | if limit_model := cls.cd_limit.get(module): 101 | await cls.__check(limit_model, user_id, group_id, channel_id, session) 102 | if limit_model := cls.block_limit.get(module): 103 | await cls.__check(limit_model, user_id, group_id, channel_id, session) 104 | if limit_model := cls.count_limit.get(module): 105 | await cls.__check(limit_model, user_id, group_id, channel_id, session) 106 | 107 | @classmethod 108 | async def __check( 109 | cls, 110 | limit_model: Limit | None, 111 | user_id: str, 112 | group_id: str | None, 113 | channel_id: str | None, 114 | session: EventSession, 115 | ): 116 | """检测限制 117 | 118 | 参数: 119 | limit_model: Limit 120 | user_id: 用户id 121 | group_id: 群组id 122 | channel_id: 频道id 123 | session: Session 124 | 125 | 异常: 126 | IgnoredException: IgnoredException 127 | """ 128 | if not limit_model: 129 | return 130 | limit = limit_model.limit 131 | limiter = limit_model.limiter 132 | is_limit = ( 133 | LimitWatchType.ALL 134 | or (group_id and limit.watch_type == LimitWatchType.GROUP) 135 | or (not group_id and limit.watch_type == LimitWatchType.USER) 136 | ) 137 | key_type = user_id 138 | if group_id and limit.watch_type == LimitWatchType.GROUP: 139 | key_type = channel_id or group_id 140 | if is_limit and not limiter.check(key_type): 141 | if limit.result: 142 | await MessageUtils.build_message(limit.result).send() 143 | logger.debug( 144 | f"{limit.module}({limit.limit_type}) 正在限制中...", 145 | "HOOK", 146 | session=session, 147 | ) 148 | raise IgnoredException(f"{limit.module} 正在限制中...") 149 | else: 150 | if isinstance(limiter, FreqLimiter): 151 | limiter.start_cd(key_type) 152 | if isinstance(limiter, UserBlockLimiter): 153 | limiter.set_true(key_type) 154 | if isinstance(limiter, CountLimiter): 155 | limiter.increase(key_type) 156 | 157 | 158 | class IsSuperuserException(Exception): 159 | pass 160 | 161 | 162 | class AuthChecker: 163 | """ 164 | 权限检查 165 | """ 166 | 167 | def __init__(self): 168 | check_notice_info_cd = ZxpmConfig.zxpm_notice_info_cd 169 | self._flmt = FreqLimiter(check_notice_info_cd) 170 | self._flmt_g = FreqLimiter(check_notice_info_cd) 171 | self._flmt_s = FreqLimiter(check_notice_info_cd) 172 | self._flmt_c = FreqLimiter(check_notice_info_cd) 173 | 174 | def is_send_limit_message(self, plugin: PluginInfo, sid: str) -> bool: 175 | """是否发送提示消息 176 | 177 | 参数: 178 | plugin: PluginInfo 179 | 180 | 返回: 181 | bool: 是否发送提示消息 182 | """ 183 | if ZxpmConfig.zxpm_notice_info_cd == 0: 184 | return False 185 | if plugin.plugin_type == PluginType.DEPENDANT: 186 | return False 187 | return plugin.module != "ai" if self._flmt_s.check(sid) else False 188 | 189 | async def auth( 190 | self, 191 | matcher: Matcher, 192 | event: Event, 193 | bot: Bot, 194 | session: EventSession, 195 | message: UniMsg, 196 | ): 197 | """权限检查 198 | 199 | 参数: 200 | matcher: matcher 201 | bot: bot 202 | session: EventSession 203 | message: UniMsg 204 | """ 205 | is_ignore = False 206 | user_id = session.id1 207 | group_id = session.id3 208 | channel_id = session.id2 209 | if not group_id: 210 | group_id = channel_id 211 | channel_id = None 212 | try: 213 | from nonebot.adapters.onebot.v11 import PokeNotifyEvent 214 | 215 | if matcher.type == "notice" and not isinstance(event, PokeNotifyEvent): 216 | """过滤除poke外的notice""" 217 | return 218 | except ImportError: 219 | if matcher.type == "notice": 220 | return 221 | if user_id and matcher.plugin and (module_path := matcher.plugin.module_name): 222 | if plugin := await PluginInfo.get_or_none(module_path=module_path): 223 | if plugin.plugin_type == PluginType.HIDDEN: 224 | logger.debug("插件为HIDDEN,已跳过...") 225 | return 226 | try: 227 | if session.id1 not in bot.config.superusers: 228 | await self.auth_bot(plugin, bot.self_id) 229 | await self.auth_group(plugin, session, message) 230 | await self.auth_admin(plugin, session) 231 | await self.auth_plugin(plugin, session, event) 232 | await self.auth_limit(plugin, session) 233 | except IsSuperuserException: 234 | logger.debug( 235 | "超级用户或被ban跳过权限检测...", "HOOK", session=session 236 | ) 237 | except IgnoredException: 238 | is_ignore = True 239 | LimitManage.unblock( 240 | matcher.plugin.name, user_id, group_id, channel_id 241 | ) 242 | except AssertionError as e: 243 | is_ignore = True 244 | logger.debug("消息无法发送", session=session, e=e) 245 | if is_ignore: 246 | raise IgnoredException("权限检测 ignore") 247 | 248 | async def auth_limit(self, plugin: PluginInfo, session: EventSession): 249 | """插件限制 250 | 251 | 参数: 252 | plugin: PluginInfo 253 | session: EventSession 254 | """ 255 | user_id = session.id1 256 | group_id = session.id3 257 | channel_id = session.id2 258 | if not group_id: 259 | group_id = channel_id 260 | channel_id = None 261 | limit_list: list[PluginLimit] = await plugin.plugin_limit.filter( 262 | status=True 263 | ).all() # type: ignore 264 | for limit in limit_list: 265 | LimitManage.add_limit(limit) 266 | if user_id: 267 | await LimitManage.check( 268 | plugin.module, user_id, group_id, channel_id, session 269 | ) 270 | 271 | async def auth_plugin( 272 | self, plugin: PluginInfo, session: EventSession, event: Event 273 | ): 274 | """插件状态 275 | 276 | 参数: 277 | plugin: PluginInfo 278 | session: EventSession 279 | """ 280 | group_id = session.id3 281 | channel_id = session.id2 282 | if not group_id: 283 | group_id = channel_id 284 | channel_id = None 285 | if user_id := session.id1: 286 | is_poke = False 287 | with contextlib.suppress(ImportError): 288 | from nonebot.adapters.onebot.v11 import PokeNotifyEvent 289 | 290 | is_poke = isinstance(event, PokeNotifyEvent) 291 | if group_id: 292 | sid = group_id or user_id 293 | if await GroupConsole.is_superuser_block_plugin( 294 | group_id, plugin.module 295 | ): 296 | """超级用户群组插件状态""" 297 | if self.is_send_limit_message(plugin, sid) and not is_poke: 298 | self._flmt_s.start_cd(group_id or user_id) 299 | await MessageUtils.build_message( 300 | "超级管理员禁用了该群此功能..." 301 | ).send(reply_to=True) 302 | logger.debug( 303 | f"{plugin.name}({plugin.module}) 超级管理员禁用了该群此功能...", 304 | "HOOK", 305 | session=session, 306 | ) 307 | raise IgnoredException("超级管理员禁用了该群此功能...") 308 | if await GroupConsole.is_normal_block_plugin(group_id, plugin.module): 309 | """群组插件状态""" 310 | if self.is_send_limit_message(plugin, sid) and not is_poke: 311 | self._flmt_s.start_cd(group_id or user_id) 312 | await MessageUtils.build_message("该群未开启此功能...").send( 313 | reply_to=True 314 | ) 315 | logger.debug( 316 | f"{plugin.name}({plugin.module}) 未开启此功能...", 317 | "HOOK", 318 | session=session, 319 | ) 320 | raise IgnoredException("该群未开启此功能...") 321 | if not plugin.status and plugin.block_type == BlockType.GROUP: 322 | """全局群组禁用""" 323 | try: 324 | if self.is_send_limit_message(plugin, sid) and not is_poke: 325 | self._flmt_c.start_cd(group_id) 326 | await MessageUtils.build_message( 327 | "该功能在群组中已被禁用..." 328 | ).send(reply_to=True) 329 | except Exception as e: 330 | logger.error( 331 | "auth_plugin 发送消息失败", "HOOK", session=session, e=e 332 | ) 333 | logger.debug( 334 | f"{plugin.name}({plugin.module}) 该插件在群组中已被禁用...", 335 | "HOOK", 336 | session=session, 337 | ) 338 | raise IgnoredException("该插件在群组中已被禁用...") 339 | else: 340 | sid = user_id 341 | if not plugin.status and plugin.block_type == BlockType.PRIVATE: 342 | """全局私聊禁用""" 343 | try: 344 | if self.is_send_limit_message(plugin, sid) and not is_poke: 345 | self._flmt_c.start_cd(user_id) 346 | await MessageUtils.build_message( 347 | "该功能在私聊中已被禁用..." 348 | ).send() 349 | except Exception as e: 350 | logger.error( 351 | "auth_admin 发送消息失败", "HOOK", session=session, e=e 352 | ) 353 | logger.debug( 354 | f"{plugin.name}({plugin.module}) 该插件在私聊中已被禁用...", 355 | "HOOK", 356 | session=session, 357 | ) 358 | raise IgnoredException("该插件在私聊中已被禁用...") 359 | if not plugin.status and plugin.block_type == BlockType.ALL: 360 | """全局状态""" 361 | if group_id and await GroupConsole.is_super_group(group_id): 362 | raise IsSuperuserException() 363 | logger.debug( 364 | f"{plugin.name}({plugin.module}) 全局未开启此功能...", 365 | "HOOK", 366 | session=session, 367 | ) 368 | if self.is_send_limit_message(plugin, sid) and not is_poke: 369 | self._flmt_s.start_cd(group_id or user_id) 370 | await MessageUtils.build_message("全局未开启此功能...").send() 371 | raise IgnoredException("全局未开启此功能...") 372 | 373 | async def auth_bot(self, plugin: PluginInfo, bot_id: str): 374 | """机器人权限 375 | 376 | 参数: 377 | plugin: PluginInfo 378 | bot_id: bot_id 379 | """ 380 | if not await BotConsole.get_bot_status(bot_id): 381 | logger.debug("Bot休眠中阻断权限检测...", "AuthChecker") 382 | raise IgnoredException("BotConsole休眠权限检测 ignore") 383 | if await BotConsole.is_block_plugin(bot_id, plugin.module): 384 | logger.debug( 385 | f"Bot插件 {plugin.name}({plugin.module}) 权限检查结果为关闭...", 386 | "AuthChecker", 387 | ) 388 | raise IgnoredException("BotConsole插件权限检测 ignore") 389 | 390 | async def auth_admin(self, plugin: PluginInfo, session: EventSession): 391 | """管理员命令 个人权限 392 | 393 | 参数: 394 | plugin: PluginInfo 395 | session: EventSession 396 | """ 397 | user_id = session.id1 398 | if user_id and plugin.admin_level: 399 | if group_id := session.id3 or session.id2: 400 | if not await LevelUser.check_level( 401 | user_id, group_id, plugin.admin_level 402 | ): 403 | try: 404 | if self._flmt.check(user_id): 405 | self._flmt.start_cd(user_id) 406 | await MessageUtils.build_message( 407 | [ 408 | At(flag="user", target=user_id), 409 | f"你的权限不足喔," 410 | f"该功能需要的权限等级: {plugin.admin_level}", 411 | ] 412 | ).send(reply_to=True) 413 | except Exception as e: 414 | logger.error( 415 | "auth_admin 发送消息失败", "HOOK", session=session, e=e 416 | ) 417 | logger.debug( 418 | f"{plugin.name}({plugin.module}) 管理员权限不足...", 419 | "HOOK", 420 | session=session, 421 | ) 422 | raise IgnoredException("管理员权限不足...") 423 | elif not await LevelUser.check_level(user_id, None, plugin.admin_level): 424 | try: 425 | await MessageUtils.build_message( 426 | f"你的权限不足喔,该功能需要的权限等级: {plugin.admin_level}" 427 | ).send() 428 | except Exception as e: 429 | logger.error( 430 | "auth_admin 发送消息失败", "HOOK", session=session, e=e 431 | ) 432 | logger.debug( 433 | f"{plugin.name}({plugin.module}) 管理员权限不足...", 434 | "HOOK", 435 | session=session, 436 | ) 437 | raise IgnoredException("权限不足") 438 | 439 | async def auth_group( 440 | self, plugin: PluginInfo, session: EventSession, message: UniMsg 441 | ): 442 | """群黑名单检测 群总开关检测 443 | 444 | 参数: 445 | plugin: PluginInfo 446 | session: EventSession 447 | message: UniMsg 448 | """ 449 | if not (group_id := session.id3 or session.id2): 450 | return 451 | text = message.extract_plain_text() 452 | group = await GroupConsole.get_group(group_id) 453 | if not group: 454 | """群不存在""" 455 | raise IgnoredException("群不存在") 456 | if group.level < 0: 457 | """群权限小于0""" 458 | logger.debug( 459 | "群黑名单, 群权限-1...", 460 | "HOOK", 461 | session=session, 462 | ) 463 | raise IgnoredException("群黑名单") 464 | if not group.status: 465 | """群休眠""" 466 | if text.strip() != "醒来": 467 | logger.debug("群休眠状态...", "HOOK", session=session) 468 | raise IgnoredException("群休眠状态") 469 | if plugin.level > group.level: 470 | """插件等级大于群等级""" 471 | logger.debug( 472 | f"{plugin.name}({plugin.module}) 群等级限制.." 473 | f"该功能需要的群等级: {plugin.level}..", 474 | "HOOK", 475 | session=session, 476 | ) 477 | raise IgnoredException(f"{plugin.name}({plugin.module}) 群等级限制...") 478 | 479 | 480 | checker = AuthChecker() 481 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/commands/zxpm_hooks/zxpm_auth_hook.py: -------------------------------------------------------------------------------- 1 | from nonebot.adapters import Bot, Event 2 | from nonebot.matcher import Matcher 3 | from nonebot.message import run_postprocessor, run_preprocessor 4 | from nonebot_plugin_alconna import UniMsg 5 | from nonebot_plugin_session import EventSession 6 | 7 | from ._auth_checker import LimitManage, checker 8 | 9 | 10 | # # 权限检测 11 | @run_preprocessor 12 | async def _( 13 | matcher: Matcher, event: Event, bot: Bot, session: EventSession, message: UniMsg 14 | ): 15 | await checker.auth( 16 | matcher, 17 | event, 18 | bot, 19 | session, 20 | message, 21 | ) 22 | 23 | 24 | # 解除命令block阻塞 25 | @run_postprocessor 26 | async def _( 27 | matcher: Matcher, 28 | exception: Exception | None, 29 | bot: Bot, 30 | event: Event, 31 | session: EventSession, 32 | ): 33 | user_id = session.id1 34 | group_id = session.id3 35 | channel_id = session.id2 36 | if not group_id: 37 | group_id = channel_id 38 | channel_id = None 39 | if user_id and matcher.plugin: 40 | module = matcher.plugin.name 41 | LimitManage.unblock(module, user_id, group_id, channel_id) 42 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/commands/zxpm_hooks/zxpm_ban_hook.py: -------------------------------------------------------------------------------- 1 | from nonebot.adapters import Bot, Event 2 | from nonebot.exception import IgnoredException 3 | from nonebot.matcher import Matcher 4 | from nonebot.message import run_preprocessor 5 | from nonebot.typing import T_State 6 | from nonebot_plugin_alconna import At 7 | from nonebot_plugin_session import EventSession 8 | 9 | from ...config import ZxpmConfig 10 | from ...extra import PluginType 11 | from ...extra.limit import FreqLimiter 12 | from ...log import logger 13 | from ...models.ban_console import BanConsole 14 | from ...models.group_console import GroupConsole 15 | from ...utils.utils import MessageUtils 16 | 17 | _flmt = FreqLimiter(300) 18 | 19 | 20 | # 检查是否被ban 21 | @run_preprocessor 22 | async def _( 23 | matcher: Matcher, bot: Bot, event: Event, state: T_State, session: EventSession 24 | ): 25 | if plugin := matcher.plugin: 26 | if metadata := plugin.metadata: 27 | extra = metadata.extra 28 | if extra.get("plugin_type") in [PluginType.HIDDEN, PluginType.DEPENDANT]: 29 | return 30 | user_id = session.id1 31 | group_id = session.id3 or session.id2 32 | if group_id: 33 | if user_id in bot.config.superusers: 34 | return 35 | if await BanConsole.is_ban(None, group_id): 36 | logger.debug("群组处于黑名单中...", "ban_hook") 37 | raise IgnoredException("群组处于黑名单中...") 38 | if g := await GroupConsole.get_group(group_id): 39 | if g.level < 0: 40 | logger.debug("群黑名单, 群权限-1...", "ban_hook") 41 | raise IgnoredException("群黑名单, 群权限-1..") 42 | if user_id: 43 | ban_result = ZxpmConfig.zxpm_ban_reply 44 | if user_id in bot.config.superusers: 45 | return 46 | if await BanConsole.is_ban(user_id, group_id): 47 | time = await BanConsole.check_ban_time(user_id, group_id) 48 | if time == -1: 49 | time_str = "∞" 50 | else: 51 | time = abs(int(time)) 52 | if time < 60: 53 | time_str = f"{time!s} 秒" 54 | else: 55 | minute = int(time / 60) 56 | if minute > 60: 57 | hours = minute // 60 58 | minute %= 60 59 | time_str = f"{hours} 小时 {minute}分钟" 60 | else: 61 | time_str = f"{minute} 分钟" 62 | if ( 63 | time != -1 64 | and ban_result 65 | and _flmt.check(user_id) 66 | and ZxpmConfig.zxpm_ban_reply 67 | ): 68 | _flmt.start_cd(user_id) 69 | await MessageUtils.build_message( 70 | [ 71 | At(flag="user", target=user_id), 72 | f"{ban_result}\n在..在 {time_str} 后才会理你喔", 73 | ] 74 | ).send() 75 | logger.debug("用户处于黑名单中...", "ban_hook") 76 | raise IgnoredException("用户处于黑名单中...") 77 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/commands/zxpm_init/__init__.py: -------------------------------------------------------------------------------- 1 | import nonebot 2 | from nonebot import get_loaded_plugins 3 | from nonebot.drivers import Driver 4 | from nonebot.plugin import Plugin, PluginMetadata 5 | from ruamel.yaml import YAML 6 | 7 | from ...enum import PluginType 8 | from ...extra import PluginExtraData, PluginSetting 9 | from ...log import logger 10 | from ...models.plugin_info import PluginInfo 11 | from ...models.plugin_limit import PluginLimit 12 | from .manager import manager 13 | 14 | _yaml = YAML(pure=True) 15 | _yaml.allow_unicode = True 16 | _yaml.indent = 2 17 | 18 | driver: Driver = nonebot.get_driver() 19 | 20 | 21 | async def _handle_setting( 22 | plugin: Plugin, 23 | plugin_list: list[PluginInfo], 24 | limit_list: list[PluginLimit], 25 | ): 26 | """处理插件设置 27 | 28 | 参数: 29 | plugin: Plugin 30 | plugin_list: 插件列表 31 | limit_list: 插件限制列表 32 | """ 33 | metadata = plugin.metadata 34 | if not metadata: 35 | if not plugin.sub_plugins: 36 | return 37 | """父插件""" 38 | metadata = PluginMetadata(name=plugin.name, description="", usage="") 39 | extra = metadata.extra 40 | extra_data = PluginExtraData(**extra) 41 | logger.debug(f"{metadata.name}:{plugin.name} -> {extra}", "初始化插件数据") 42 | setting = extra_data.setting or PluginSetting() 43 | if metadata.type == "library": 44 | extra_data.plugin_type = PluginType.HIDDEN 45 | if extra_data.plugin_type == PluginType.HIDDEN: 46 | extra_data.menu_type = "" 47 | if plugin.sub_plugins: 48 | extra_data.plugin_type = PluginType.PARENT 49 | plugin_list.append( 50 | PluginInfo( 51 | module=plugin.name, 52 | module_path=plugin.module_name, 53 | name=metadata.name, 54 | author=extra_data.author, 55 | version=extra_data.version, 56 | level=setting.level, 57 | default_status=setting.default_status, 58 | limit_superuser=setting.limit_superuser, 59 | menu_type=extra_data.menu_type, 60 | cost_gold=setting.cost_gold, 61 | plugin_type=extra_data.plugin_type, 62 | admin_level=extra_data.admin_level, 63 | parent=(plugin.parent_plugin.module_name if plugin.parent_plugin else None), 64 | ) 65 | ) 66 | if extra_data.limits: 67 | limit_list.extend( 68 | PluginLimit( 69 | module=plugin.name, 70 | module_path=plugin.module_name, 71 | limit_type=limit._type, 72 | watch_type=limit.watch_type, 73 | status=limit.status, 74 | check_type=limit.check_type, 75 | result=limit.result, 76 | cd=getattr(limit, "cd", None), 77 | max_count=getattr(limit, "max_count", None), 78 | ) 79 | for limit in extra_data.limits 80 | ) 81 | 82 | 83 | @driver.on_startup 84 | async def _(): 85 | """ 86 | 初始化插件数据配置 87 | """ 88 | plugin_list: list[PluginInfo] = [] 89 | limit_list: list[PluginLimit] = [] 90 | module2id = {} 91 | load_plugin = [] 92 | if module_list := await PluginInfo.all().values("id", "module_path"): 93 | module2id = {m["module_path"]: m["id"] for m in module_list} 94 | for plugin in get_loaded_plugins(): 95 | load_plugin.append(plugin.module_name) 96 | await _handle_setting(plugin, plugin_list, limit_list) 97 | create_list = [] 98 | update_list = [] 99 | for plugin in plugin_list: 100 | if plugin.module_path not in module2id: 101 | create_list.append(plugin) 102 | else: 103 | plugin.id = module2id[plugin.module_path] 104 | await plugin.save( 105 | update_fields=[ 106 | "name", 107 | "author", 108 | "version", 109 | "admin_level", 110 | "plugin_type", 111 | ] 112 | ) 113 | update_list.append(plugin) 114 | if create_list: 115 | await PluginInfo.bulk_create(create_list, 10) 116 | await PluginInfo.filter(module_path__in=load_plugin).update(load_status=True) 117 | await PluginInfo.filter(module_path__not_in=load_plugin).update(load_status=False) 118 | manager.init() 119 | if limit_list: 120 | for limit in limit_list: 121 | if not manager.exist(limit.module_path, limit.limit_type): 122 | """不存在,添加""" 123 | manager.add(limit.module_path, limit) 124 | manager.save_file() 125 | await manager.load_to_db() 126 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/commands/zxpm_init/manager.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | from pathlib import Path 3 | 4 | from ruamel.yaml import YAML 5 | 6 | from ...config import ZxpmConfig 7 | from ...enum import BlockType, LimitCheckType, PluginLimitType 8 | from ...extra.limit import BaseBlock, PluginCdBlock, PluginCountBlock 9 | from ...log import logger 10 | from ...models.plugin_info import PluginInfo 11 | from ...models.plugin_limit import PluginLimit 12 | 13 | _yaml = YAML(pure=True) 14 | _yaml.indent = 2 15 | _yaml.allow_unicode = True 16 | 17 | 18 | CD_TEST = """需要cd的功能 19 | 自定义的功能需要cd也可以在此配置 20 | key:模块名称 21 | cd:cd 时长(秒) 22 | status:此限制的开关状态 23 | check_type:'PRIVATE'/'GROUP'/'ALL',限制私聊/群聊/全部 24 | watch_type:监听对象,以user_id或group_id作为键来限制,'USER':用户id,'GROUP':群id 25 | 示例:'USER':用户N秒内触发1次,'GROUP':群N秒内触发1次 26 | result 为 "" 或 None 时则不回复 27 | result示例:"你冲的太快了,先生,请稍后再冲" 28 | result回复:"老色批你冲的太快了,欧尼酱先生,请稍后再冲@老色批" 29 | """ 30 | 31 | 32 | BLOCK_TEST = """用户调用阻塞 33 | 即 当用户调用此功能还未结束时 34 | 用发送消息阻止用户重复调用此命令直到该命令结束 35 | key:模块名称 36 | status:此限制的开关状态 37 | check_type:'PRIVATE'/'GROUP'/'ALL',限制私聊/群聊/全部 38 | watch_type:监听对象,以user_id或group_id作为键来限制,'USER':用户id,'GROUP':群id 39 | 示例:'USER':阻塞用户,'group':阻塞群聊 40 | result 为 "" 或 None 时则不回复 41 | result示例:"你冲的太快了,先生,请稍后再冲" 42 | result回复:"老色批你冲的太快了,欧尼酱先生,请稍后再冲@老色批" 43 | """ 44 | 45 | COUNT_TEST = """命令每日次数限制 46 | 即 用户/群聊 每日可调用命令的次数 [数据内存存储,重启将会重置] 47 | 每日调用直到 00:00 刷新 48 | key:模块名称 49 | max_count: 每日调用上限 50 | status:此限制的开关状态 51 | watch_type:监听对象,以user_id或group_id作为键来限制,'USER':用户id,'GROUP':群id 52 | 示例:'USER':用户上限,'group':群聊上限 53 | result 为 "" 或 None 时则不回复 54 | result示例:"你冲的太快了,先生,请稍后再冲" 55 | result回复:"老色批你冲的太快了,欧尼酱先生,请稍后再冲@老色批" 56 | """ 57 | 58 | 59 | class Manager: 60 | """ 61 | 插件命令 cd 管理器 62 | """ 63 | 64 | def __init__(self): 65 | if isinstance(ZxpmConfig.zxpm_data_path, str): 66 | ZxpmConfig.zxpm_data_path = Path(ZxpmConfig.zxpm_data_path) 67 | BASE_PATH = ZxpmConfig.zxpm_data_path / "configs" 68 | BASE_PATH.mkdir(parents=True, exist_ok=True) 69 | self.cd_file = BASE_PATH / "plugins2cd.yaml" 70 | self.block_file = BASE_PATH / "plugins2block.yaml" 71 | self.count_file = BASE_PATH / "plugins2count.yaml" 72 | self.cd_data = {} 73 | self.block_data = {} 74 | self.count_data = {} 75 | 76 | def add( 77 | self, 78 | module: str, 79 | data: BaseBlock | PluginCdBlock | PluginCountBlock | PluginLimit, 80 | ): 81 | """添加限制""" 82 | if isinstance(data, PluginLimit): 83 | check_type = BlockType.ALL 84 | if LimitCheckType.GROUP == data.check_type: 85 | check_type = BlockType.GROUP 86 | elif LimitCheckType.PRIVATE == data.check_type: 87 | check_type = BlockType.PRIVATE 88 | if data.limit_type == PluginLimitType.CD: 89 | data = PluginCdBlock( 90 | status=data.status, 91 | check_type=check_type, 92 | watch_type=data.watch_type, 93 | result=data.result, 94 | cd=data.cd, 95 | ) 96 | elif data.limit_type == PluginLimitType.BLOCK: 97 | data = BaseBlock( 98 | status=data.status, 99 | check_type=check_type, 100 | watch_type=data.watch_type, 101 | result=data.result, 102 | ) 103 | elif data.limit_type == PluginLimitType.COUNT: 104 | data = PluginCountBlock( 105 | status=data.status, 106 | watch_type=data.watch_type, 107 | result=data.result, 108 | max_count=data.max_count, 109 | ) 110 | if isinstance(data, PluginCdBlock): 111 | self.cd_data[module] = data 112 | elif isinstance(data, PluginCountBlock): 113 | self.count_data[module] = data 114 | elif isinstance(data, BaseBlock): 115 | self.block_data[module] = data 116 | 117 | def exist(self, module: str, type: PluginLimitType): 118 | """是否存在""" 119 | if type == PluginLimitType.CD: 120 | return module in self.cd_data 121 | elif type == PluginLimitType.BLOCK: 122 | return module in self.block_data 123 | elif type == PluginLimitType.COUNT: 124 | return module in self.count_data 125 | 126 | def init(self): 127 | if not self.cd_file.exists(): 128 | self.save_cd_file() 129 | if not self.block_file.exists(): 130 | self.save_block_file() 131 | if not self.count_file.exists(): 132 | self.save_count_file() 133 | self.__load_file() 134 | 135 | def __load_file(self): 136 | self.__load_block_file() 137 | self.__load_cd_file() 138 | self.__load_count_file() 139 | 140 | def save_file(self): 141 | """保存文件""" 142 | self.save_cd_file() 143 | self.save_block_file() 144 | self.save_count_file() 145 | 146 | def save_cd_file(self): 147 | """保存文件""" 148 | self._extracted_from_save_file_3("PluginCdLimit", CD_TEST, self.cd_data) 149 | 150 | def save_block_file(self): 151 | """保存文件""" 152 | self._extracted_from_save_file_3( 153 | "PluginBlockLimit", BLOCK_TEST, self.block_data 154 | ) 155 | 156 | def save_count_file(self): 157 | """保存文件""" 158 | self._extracted_from_save_file_3( 159 | "PluginCountLimit", COUNT_TEST, self.count_data 160 | ) 161 | 162 | def _extracted_from_save_file_3(self, type_: str, after: str, data: dict): 163 | """保存文件 164 | 165 | 参数: 166 | type_: 类型参数 167 | after: 备注 168 | """ 169 | temp_data = deepcopy(data) 170 | if not temp_data: 171 | temp_data = { 172 | "test": { 173 | "status": False, 174 | "check_type": "ALL", 175 | "limit_type": "USER", 176 | "result": "你冲的太快了,请稍后再冲", 177 | } 178 | } 179 | if type_ == "PluginCdLimit": 180 | temp_data["test"]["cd"] = 5 181 | elif type_ == "PluginCountLimit": 182 | temp_data["test"]["max_count"] = 5 183 | del temp_data["test"]["check_type"] 184 | else: 185 | for v in temp_data: 186 | temp_data[v] = temp_data[v].dict() 187 | if check_type := temp_data[v].get("check_type"): 188 | temp_data[v]["check_type"] = str(check_type) 189 | if watch_type := temp_data[v].get("watch_type"): 190 | temp_data[v]["watch_type"] = str(watch_type) 191 | if type_ == "PluginCountLimit": 192 | del temp_data[v]["check_type"] 193 | file = self.block_file 194 | if type_ == "PluginCdLimit": 195 | file = self.cd_file 196 | elif type_ == "PluginCountLimit": 197 | file = self.count_file 198 | with open(file, "w", encoding="utf8") as f: 199 | _yaml.dump({type_: temp_data}, f) 200 | with open(file, encoding="utf8") as rf: 201 | _data = _yaml.load(rf) 202 | _data.yaml_set_comment_before_after_key(after=after, key=type_) 203 | with open(file, "w", encoding="utf8") as wf: 204 | _yaml.dump(_data, wf) 205 | 206 | def __load_cd_file(self): 207 | self.cd_data: dict[str, PluginCdBlock] = {} 208 | if self.cd_file.exists(): 209 | with open(self.cd_file, encoding="utf8") as f: 210 | temp = _yaml.load(f) 211 | if "PluginCdLimit" in temp.keys(): 212 | for k, v in temp["PluginCdLimit"].items(): 213 | self.cd_data[k] = PluginCdBlock.parse_obj(v) 214 | 215 | def __load_block_file(self): 216 | self.block_data: dict[str, BaseBlock] = {} 217 | if self.block_file.exists(): 218 | with open(self.block_file, encoding="utf8") as f: 219 | temp = _yaml.load(f) 220 | if "PluginBlockLimit" in temp.keys(): 221 | for k, v in temp["PluginBlockLimit"].items(): 222 | self.block_data[k] = BaseBlock.parse_obj(v) 223 | 224 | def __load_count_file(self): 225 | self.count_data: dict[str, PluginCountBlock] = {} 226 | if self.count_file.exists(): 227 | with open(self.count_file, encoding="utf8") as f: 228 | temp = _yaml.load(f) 229 | if "PluginCountLimit" in temp.keys(): 230 | for k, v in temp["PluginCountLimit"].items(): 231 | self.count_data[k] = PluginCountBlock.parse_obj(v) 232 | 233 | def __replace_data( 234 | self, 235 | db_data: PluginLimit | None, 236 | limit: PluginCdBlock | BaseBlock | PluginCountBlock, 237 | ) -> PluginLimit: 238 | """替换数据""" 239 | if not db_data: 240 | db_data = PluginLimit() 241 | db_data.status = limit.status 242 | check_type = LimitCheckType.ALL 243 | if BlockType.GROUP == limit.check_type: 244 | check_type = LimitCheckType.GROUP 245 | elif BlockType.PRIVATE == limit.check_type: 246 | check_type = LimitCheckType.PRIVATE 247 | db_data.check_type = check_type 248 | db_data.watch_type = limit.watch_type 249 | db_data.result = limit.result or "" 250 | return db_data 251 | 252 | def __set_data( 253 | self, 254 | k: str, 255 | db_data: PluginLimit | None, 256 | limit: PluginCdBlock | BaseBlock | PluginCountBlock, 257 | limit_type: PluginLimitType, 258 | module2plugin: dict[str, PluginInfo], 259 | ) -> tuple[PluginLimit, bool]: 260 | """设置数据 261 | 262 | 参数: 263 | k: 模块名 264 | db_data: 数据库数据 265 | limit: 文件数据 266 | limit_type: 限制类型 267 | module2plugin: 模块:插件信息 268 | 269 | 返回: 270 | tuple[PluginLimit, bool]: PluginLimit,是否创建 271 | """ 272 | if not db_data: 273 | return ( 274 | PluginLimit( 275 | module=k, 276 | module_path=module2plugin[k].module_path, 277 | limit_type=limit_type, 278 | plugin=module2plugin[k], 279 | cd=getattr(limit, "cd", None), 280 | max_count=getattr(limit, "max_count", None), 281 | status=limit.status, 282 | check_type=limit.check_type, 283 | watch_type=limit.watch_type, 284 | result=limit.result, 285 | ), 286 | True, 287 | ) 288 | db_data = self.__replace_data(db_data, limit) 289 | if limit_type == PluginLimitType.CD: 290 | db_data.cd = limit.cd # type: ignore 291 | if limit_type == PluginLimitType.COUNT: 292 | db_data.max_count = limit.max_count # type: ignore 293 | return db_data, False 294 | 295 | def __get_file_data(self, limit_type: PluginLimitType) -> dict: 296 | """获取文件数据 297 | 298 | 参数: 299 | limit_type: 限制类型 300 | 301 | 返回: 302 | dict: 文件数据 303 | """ 304 | if limit_type == PluginLimitType.CD: 305 | return self.cd_data 306 | elif limit_type == PluginLimitType.COUNT: 307 | return self.count_data 308 | else: 309 | return self.block_data 310 | 311 | def __set_db_limits( 312 | self, 313 | db_limits: list[PluginLimit], 314 | module2plugin: dict[str, PluginInfo], 315 | limit_type: PluginLimitType, 316 | ) -> tuple[list[PluginLimit], list[PluginLimit], list[int]]: 317 | """更新限制数据 318 | 319 | 参数: 320 | db_limits: 数据库limits 321 | module2plugin: 模块:插件信息 322 | 323 | 返回: 324 | tuple[list[PluginLimit], list[PluginLimit]]: 创建列表,更新列表 325 | """ 326 | update_list = [] 327 | create_list = [] 328 | delete_list = [] 329 | db_type_limits = [ 330 | limit for limit in db_limits if limit.limit_type == limit_type 331 | ] 332 | if data := self.__get_file_data(limit_type): 333 | db_type_limit_modules = [ 334 | (limit.module, limit.id) for limit in db_type_limits 335 | ] 336 | delete_list.extend( 337 | id for module, id in db_type_limit_modules if module not in data.keys() 338 | ) 339 | for k, v in data.items(): 340 | if not module2plugin.get(k): 341 | if k != "test": 342 | logger.warning( 343 | f"插件模块 {k} 未加载,已过滤当前 {v._type} 限制..." 344 | ) 345 | continue 346 | db_data = [limit for limit in db_type_limits if limit.module == k] 347 | db_data, is_create = self.__set_data( 348 | k, db_data[0] if db_data else None, v, limit_type, module2plugin 349 | ) 350 | if is_create: 351 | create_list.append(db_data) 352 | else: 353 | update_list.append(db_data) 354 | else: 355 | delete_list = [limit.id for limit in db_type_limits] 356 | return create_list, update_list, delete_list 357 | 358 | async def __set_all_limit( 359 | self, 360 | ) -> tuple[list[PluginLimit], list[PluginLimit], list[int]]: 361 | """获取所有插件限制数据 362 | 363 | 返回: 364 | tuple[list[PluginLimit], list[PluginLimit]]: 创建列表,更新列表 365 | """ 366 | db_limits = await PluginLimit.all() 367 | modules = set( 368 | list(self.cd_data.keys()) 369 | + list(self.block_data.keys()) 370 | + list(self.count_data.keys()) 371 | ) 372 | plugins = await PluginInfo.get_plugins(module__in=modules) 373 | module2plugin = {p.module: p for p in plugins} 374 | create_list, update_list, delete_list = self.__set_db_limits( 375 | db_limits, module2plugin, PluginLimitType.CD 376 | ) 377 | create_list1, update_list1, delete_list1 = self.__set_db_limits( 378 | db_limits, module2plugin, PluginLimitType.COUNT 379 | ) 380 | create_list2, update_list2, delete_list2 = self.__set_db_limits( 381 | db_limits, module2plugin, PluginLimitType.BLOCK 382 | ) 383 | all_create = create_list + create_list1 + create_list2 384 | all_update = update_list + update_list1 + update_list2 385 | all_delete = delete_list + delete_list1 + delete_list2 386 | return all_create, all_update, all_delete 387 | 388 | async def load_to_db(self): 389 | """读取配置文件""" 390 | 391 | create_list, update_list, delete_list = await self.__set_all_limit() 392 | if create_list: 393 | await PluginLimit.bulk_create(create_list) 394 | if update_list: 395 | for limit in update_list: 396 | await limit.save( 397 | update_fields=[ 398 | "status", 399 | "check_type", 400 | "watch_type", 401 | "result", 402 | "cd", 403 | "max_count", 404 | ] 405 | ) 406 | # TODO: tortoise.exceptions.OperationalError:syntax error at or near "GROUP" 407 | # await PluginLimit.bulk_update( 408 | # update_list, 409 | # ["status", "check_type", "watch_type", "result", "cd", "max_count"], 410 | # ) 411 | if delete_list: 412 | await PluginLimit.filter(id__in=delete_list).delete() 413 | cnt = await PluginLimit.filter(status=True).count() 414 | logger.info(f"已经加载 {cnt} 个插件限制.") 415 | 416 | 417 | manager = Manager() 418 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/commands/zxpm_plugin_switch/__init__.py: -------------------------------------------------------------------------------- 1 | from nonebot.adapters import Bot 2 | from nonebot.plugin import PluginMetadata 3 | from nonebot_plugin_alconna import AlconnaQuery, Arparma, Match, Query 4 | from nonebot_plugin_session import EventSession 5 | 6 | from ...config import ZxpmConfig 7 | from ...enum import BlockType, PluginType 8 | from ...extra import PluginExtraData 9 | from ...log import logger 10 | from ...utils.utils import MessageUtils 11 | from ._data_source import PluginManage, build_plugin 12 | from .command import _group_status_matcher, _status_matcher 13 | 14 | __plugin_meta__ = PluginMetadata( 15 | name="ZXPM功能开关", 16 | description="对群组内的功能限制,超级用户可以对群组以及全局的功能被动开关限制", 17 | usage=""" 18 | 普通管理员 19 | 格式: 20 | 开启/关闭[功能名称] : 开关功能 21 | 开启/关闭所有插件 : 开启/关闭当前群组所有插件状态 22 | 醒来 : 结束休眠 23 | 休息吧 : 群组休眠, 不会再响应命令 24 | 25 | 示例: 26 | 开启签到 : 开启签到 27 | 关闭签到 : 关闭签到 28 | 29 | """.strip(), 30 | extra=PluginExtraData( 31 | author="HibiKier", 32 | version="0.1", 33 | plugin_type=PluginType.SUPER_AND_ADMIN, 34 | admin_level=ZxpmConfig.zxpm_switch_level, 35 | superuser_help=""" 36 | 格式: 37 | 插件列表 38 | 开启/关闭[功能名称] ?[-t ["private", "p", "group", "g"](关闭类型)] ?[-g 群组Id] 39 | 40 | 开启/关闭插件df[功能名称]: 开启/关闭指定插件进群默认状态 41 | = 开启插件echo -df 42 | = 关闭插件echo -df 43 | 开启/关闭所有插件df: 开启/关闭所有插件进群默认状态 44 | 开启/关闭所有插件: 45 | 私聊中: 开启/关闭所有插件全局状态 46 | 群组中: 开启/关闭当前群组所有插件状态 47 | 48 | 49 | 私聊下: 50 | 示例: 51 | 开启签到 : 全局开启签到 52 | 关闭签到 : 全局关闭签到 53 | 关闭签到 -t p : 全局私聊关闭签到 54 | 关闭签到 -g 12345678 : 关闭群组12345678的签到功能(普通管理员无法开启) 55 | """, 56 | ).to_dict(), 57 | ) 58 | 59 | 60 | @_status_matcher.assign("$main") 61 | async def _( 62 | bot: Bot, 63 | session: EventSession, 64 | arparma: Arparma, 65 | ): 66 | if session.id1 in bot.config.superusers: 67 | image = await build_plugin() 68 | logger.info( 69 | "查看功能列表", 70 | arparma.header_result, 71 | session=session, 72 | ) 73 | await MessageUtils.build_message(image.pic2bytes()).finish(reply_to=True) 74 | else: 75 | await MessageUtils.build_message("权限不足捏...").finish(reply_to=True) 76 | 77 | 78 | @_status_matcher.assign("open") 79 | async def _( 80 | bot: Bot, 81 | session: EventSession, 82 | arparma: Arparma, 83 | plugin_name: Match[str], 84 | group: Match[str], 85 | task: Query[bool] = AlconnaQuery("task.value", False), 86 | default_status: Query[bool] = AlconnaQuery("default.value", False), 87 | all: Query[bool] = AlconnaQuery("all.value", False), 88 | ): 89 | if not all.result and not plugin_name.available: 90 | await MessageUtils.build_message("请输入功能名称").finish(reply_to=True) 91 | name = plugin_name.result 92 | if gid := session.id3 or session.id2: 93 | """修改当前群组的数据""" 94 | if session.id1 in bot.config.superusers and default_status.result: 95 | """单个插件的进群默认修改""" 96 | result = await PluginManage.set_default_status(name, True) 97 | logger.info( 98 | f"超级用户开启 {name} 功能进群默认开关", 99 | arparma.header_result, 100 | session=session, 101 | ) 102 | elif all.result: 103 | """所有插件""" 104 | result = await PluginManage.set_all_plugin_status( 105 | True, default_status.result, gid 106 | ) 107 | logger.info( 108 | "开启群组中全部功能", 109 | arparma.header_result, 110 | session=session, 111 | ) 112 | else: 113 | result = await PluginManage.unblock_group_plugin(name, gid) 114 | logger.info(f"开启功能 {name}", arparma.header_result, session=session) 115 | await MessageUtils.build_message(result).finish(reply_to=True) 116 | elif session.id1 in bot.config.superusers: 117 | """私聊""" 118 | group_id = group.result if group.available else None 119 | if all.result: 120 | result = await PluginManage.set_all_plugin_status( 121 | True, default_status.result, group_id 122 | ) 123 | logger.info( 124 | "超级用户开启全部功能全局开关" 125 | f" {f'指定群组: {group_id}' if group_id else ''}", 126 | arparma.header_result, 127 | session=session, 128 | ) 129 | await MessageUtils.build_message(result).finish(reply_to=True) 130 | if default_status.result: 131 | result = await PluginManage.set_default_status(name, True) 132 | logger.info( 133 | f"超级用户开启 {name} 功能进群默认开关", 134 | arparma.header_result, 135 | session=session, 136 | target=group_id, 137 | ) 138 | await MessageUtils.build_message(result).finish(reply_to=True) 139 | result = await PluginManage.superuser_unblock(name, None, group_id) 140 | logger.info( 141 | f"超级用户开启功能 {name}", 142 | arparma.header_result, 143 | session=session, 144 | target=group_id, 145 | ) 146 | await MessageUtils.build_message(result).finish(reply_to=True) 147 | 148 | 149 | @_status_matcher.assign("close") 150 | async def _( 151 | bot: Bot, 152 | session: EventSession, 153 | arparma: Arparma, 154 | plugin_name: Match[str], 155 | block_type: Match[str], 156 | group: Match[str], 157 | task: Query[bool] = AlconnaQuery("task.value", False), 158 | default_status: Query[bool] = AlconnaQuery("default.value", False), 159 | all: Query[bool] = AlconnaQuery("all.value", False), 160 | ): 161 | if not all.result and not plugin_name.available: 162 | await MessageUtils.build_message("请输入功能名称").finish(reply_to=True) 163 | name = plugin_name.result 164 | if gid := session.id3 or session.id2: 165 | """修改当前群组的数据""" 166 | if session.id1 in bot.config.superusers and default_status.result: 167 | """单个插件的进群默认修改""" 168 | result = await PluginManage.set_default_status(name, False) 169 | logger.info( 170 | f"超级用户开启 {name} 功能进群默认开关", 171 | arparma.header_result, 172 | session=session, 173 | ) 174 | elif all.result: 175 | """所有插件""" 176 | result = await PluginManage.set_all_plugin_status( 177 | False, default_status.result, gid 178 | ) 179 | logger.info("关闭群组中全部功能", arparma.header_result, session=session) 180 | else: 181 | result = await PluginManage.block_group_plugin(name, gid) 182 | logger.info(f"关闭功能 {name}", arparma.header_result, session=session) 183 | await MessageUtils.build_message(result).finish(reply_to=True) 184 | elif session.id1 in bot.config.superusers: 185 | group_id = group.result if group.available else None 186 | if all.result: 187 | result = await PluginManage.set_all_plugin_status( 188 | False, default_status.result, group_id 189 | ) 190 | logger.info( 191 | "超级用户关闭全部功能全局开关" 192 | f" {f'指定群组: {group_id}' if group_id else ''}", 193 | arparma.header_result, 194 | session=session, 195 | ) 196 | await MessageUtils.build_message(result).finish(reply_to=True) 197 | if default_status.result: 198 | result = await PluginManage.set_default_status(name, False) 199 | logger.info( 200 | f"超级用户关闭 {name} 功能进群默认开关", 201 | arparma.header_result, 202 | session=session, 203 | target=group_id, 204 | ) 205 | await MessageUtils.build_message(result).finish(reply_to=True) 206 | _type = BlockType.ALL 207 | if block_type.result in ["p", "private"]: 208 | if block_type.available: 209 | _type = BlockType.PRIVATE 210 | elif block_type.result in ["g", "group"]: 211 | if block_type.available: 212 | _type = BlockType.GROUP 213 | result = await PluginManage.superuser_block(name, _type, group_id) 214 | logger.info( 215 | f"超级用户关闭功能 {name}, 禁用类型: {_type}", 216 | arparma.header_result, 217 | session=session, 218 | target=group_id, 219 | ) 220 | await MessageUtils.build_message(result).finish(reply_to=True) 221 | 222 | 223 | @_group_status_matcher.handle() 224 | async def _( 225 | session: EventSession, 226 | arparma: Arparma, 227 | status: str, 228 | ): 229 | if gid := session.id3 or session.id2: 230 | if status == "sleep": 231 | await PluginManage.sleep(gid) 232 | logger.info("进行休眠", arparma.header_result, session=session) 233 | await MessageUtils.build_message("那我先睡觉了...").finish() 234 | else: 235 | if await PluginManage.is_wake(gid): 236 | await MessageUtils.build_message("我还醒着呢!").finish() 237 | await PluginManage.wake(gid) 238 | logger.info("醒来", arparma.header_result, session=session) 239 | await MessageUtils.build_message("呜..醒来了...").finish() 240 | return MessageUtils.build_message("群组id为空...").send() 241 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/commands/zxpm_plugin_switch/_data_source.py: -------------------------------------------------------------------------------- 1 | from ...enum import BlockType, PluginType 2 | from ...models.group_console import GroupConsole 3 | from ...models.plugin_info import PluginInfo 4 | from ...utils.image_template import BuildImage, ImageTemplate, RowStyle 5 | 6 | 7 | class GroupInfoNotFound(Exception): 8 | """ 9 | 群组未找到 10 | """ 11 | 12 | pass 13 | 14 | 15 | def plugin_row_style(column: str, text: str) -> RowStyle: 16 | """被动技能文本风格 17 | 18 | 参数: 19 | column: 表头 20 | text: 文本内容 21 | 22 | 返回: 23 | RowStyle: RowStyle 24 | """ 25 | style = RowStyle() 26 | if ( 27 | column == "全局状态" 28 | and text == "开启" 29 | or column != "全局状态" 30 | and column == "加载状态" 31 | and text == "SUCCESS" 32 | ): 33 | style.font_color = "#67C23A" 34 | elif column in {"全局状态", "加载状态"}: 35 | style.font_color = "#F56C6C" 36 | return style 37 | 38 | 39 | async def build_plugin() -> BuildImage: 40 | column_name = [ 41 | "ID", 42 | "模块", 43 | "名称", 44 | "全局状态", 45 | "禁用类型", 46 | "加载状态", 47 | "菜单分类", 48 | "作者", 49 | "版本", 50 | "金币花费", 51 | ] 52 | plugin_list = await PluginInfo.filter(plugin_type__not=PluginType.HIDDEN).all() 53 | column_data = [ 54 | [ 55 | plugin.id, 56 | plugin.module, 57 | plugin.name, 58 | "开启" if plugin.status else "关闭", 59 | plugin.block_type, 60 | "SUCCESS" if plugin.load_status else "ERROR", 61 | plugin.menu_type, 62 | plugin.author, 63 | plugin.version, 64 | plugin.cost_gold, 65 | ] 66 | for plugin in plugin_list 67 | ] 68 | return await ImageTemplate.table_page( 69 | "Plugin", 70 | "插件状态", 71 | column_name, 72 | column_data, 73 | text_style=plugin_row_style, 74 | ) 75 | 76 | 77 | def task_row_style(column: str, text: str) -> RowStyle: 78 | """被动技能文本风格 79 | 80 | 参数: 81 | column: 表头 82 | text: 文本内容 83 | 84 | 返回: 85 | RowStyle: RowStyle 86 | """ 87 | style = RowStyle() 88 | if column in {"群组状态", "全局状态"}: 89 | style.font_color = "#67C23A" if text == "开启" else "#F56C6C" 90 | return style 91 | 92 | 93 | class PluginManage: 94 | @classmethod 95 | async def set_default_status(cls, plugin_name: str, status: bool) -> str: 96 | """设置插件进群默认状态 97 | 98 | 参数: 99 | plugin_name: 插件名称 100 | status: 状态 101 | 102 | 返回: 103 | str: 返回信息 104 | """ 105 | if plugin_name.isdigit(): 106 | plugin = await PluginInfo.get_or_none(id=int(plugin_name)) 107 | else: 108 | plugin = await PluginInfo.get_or_none( 109 | name=plugin_name, load_status=True, plugin_type__not=PluginType.PARENT 110 | ) 111 | if plugin: 112 | plugin.default_status = status 113 | await plugin.save(update_fields=["default_status"]) 114 | status_text = "开启" if status else "关闭" 115 | return f"成功将 {plugin.name} 进群默认状态修改为: {status_text}" 116 | return "没有找到这个功能喔..." 117 | 118 | @classmethod 119 | async def set_all_plugin_status( 120 | cls, status: bool, is_default: bool = False, group_id: str | None = None 121 | ) -> str: 122 | """修改所有插件状态 123 | 124 | 参数: 125 | status: 状态 126 | is_default: 是否进群默认. 127 | group_id: 指定群组id. 128 | 129 | 返回: 130 | str: 返回信息 131 | """ 132 | if is_default: 133 | await PluginInfo.filter(plugin_type=PluginType.NORMAL).update( 134 | default_status=status 135 | ) 136 | return f'成功将所有功能进群默认状态修改为: {"开启" if status else "关闭"}' 137 | if group_id: 138 | if group := await GroupConsole.get_or_none( 139 | group_id=group_id, channel_id__isnull=True 140 | ): 141 | module_list = await PluginInfo.filter( 142 | plugin_type=PluginType.NORMAL 143 | ).values_list("module", flat=True) 144 | if status: 145 | for module in module_list: 146 | group.block_plugin = group.block_plugin.replace( 147 | f"<{module},", "" 148 | ) 149 | else: 150 | module_list = [f"<{module}" for module in module_list] 151 | group.block_plugin = ",".join(module_list) + "," # type: ignore 152 | await group.save(update_fields=["block_plugin"]) 153 | return f'成功将此群组所有功能状态修改为: {"开启" if status else "关闭"}' 154 | return "获取群组失败..." 155 | await PluginInfo.filter(plugin_type=PluginType.NORMAL).update( 156 | status=status, block_type=None if status else BlockType.ALL 157 | ) 158 | return f'成功将所有功能全局状态修改为: {"开启" if status else "关闭"}' 159 | 160 | @classmethod 161 | async def is_wake(cls, group_id: str) -> bool: 162 | """是否醒来 163 | 164 | 参数: 165 | group_id: 群组id 166 | 167 | 返回: 168 | bool: 是否醒来 169 | """ 170 | if c := await GroupConsole.get_or_none( 171 | group_id=group_id, channel_id__isnull=True 172 | ): 173 | return c.status 174 | return False 175 | 176 | @classmethod 177 | async def sleep(cls, group_id: str): 178 | """休眠 179 | 180 | 参数: 181 | group_id: 群组id 182 | """ 183 | await GroupConsole.filter(group_id=group_id, channel_id__isnull=True).update( 184 | status=False 185 | ) 186 | 187 | @classmethod 188 | async def wake(cls, group_id: str): 189 | """醒来 190 | 191 | 参数: 192 | group_id: 群组id 193 | """ 194 | await GroupConsole.filter(group_id=group_id, channel_id__isnull=True).update( 195 | status=True 196 | ) 197 | 198 | @classmethod 199 | async def block(cls, module: str): 200 | """禁用 201 | 202 | 参数: 203 | module: 模块名 204 | """ 205 | await PluginInfo.filter(module=module).update(status=False) 206 | 207 | @classmethod 208 | async def unblock(cls, module: str): 209 | """启用 210 | 211 | 参数: 212 | module: 模块名 213 | """ 214 | await PluginInfo.filter(module=module).update(status=True) 215 | 216 | @classmethod 217 | async def block_group_plugin(cls, plugin_name: str, group_id: str) -> str: 218 | """禁用群组插件 219 | 220 | 参数: 221 | plugin_name: 插件名称 222 | group_id: 群组id 223 | 224 | 返回: 225 | str: 返回信息 226 | """ 227 | return await cls._change_group_plugin(plugin_name, group_id, False) 228 | 229 | @classmethod 230 | async def unblock_group_plugin(cls, plugin_name: str, group_id: str) -> str: 231 | """启用群组插件 232 | 233 | 参数: 234 | plugin_name: 插件名称 235 | group_id: 群组id 236 | 237 | 返回: 238 | str: 返回信息 239 | """ 240 | return await cls._change_group_plugin(plugin_name, group_id, True) 241 | 242 | @classmethod 243 | async def _change_group_plugin( 244 | cls, plugin_name: str, group_id: str, status: bool 245 | ) -> str: 246 | """修改群组插件状态 247 | 248 | 参数: 249 | plugin_name: 插件名称 250 | group_id: 群组id 251 | status: 插件状态 252 | 253 | 返回: 254 | str: 返回信息 255 | """ 256 | 257 | if plugin_name.isdigit(): 258 | plugin = await PluginInfo.get_or_none(id=int(plugin_name)) 259 | else: 260 | plugin = await PluginInfo.get_or_none( 261 | name=plugin_name, load_status=True, plugin_type__not=PluginType.PARENT 262 | ) 263 | if plugin: 264 | status_str = "开启" if status else "关闭" 265 | if status: 266 | if await GroupConsole.is_normal_block_plugin(group_id, plugin.module): 267 | await GroupConsole.set_unblock_plugin(group_id, plugin.module) 268 | return f"已成功{status_str} {plugin.name} 功能!" 269 | elif not await GroupConsole.is_normal_block_plugin(group_id, plugin.module): 270 | await GroupConsole.set_block_plugin(group_id, plugin.module) 271 | return f"已成功{status_str} {plugin.name} 功能!" 272 | return f"该功能已经{status_str}了喔,不要重复{status_str}..." 273 | return "没有找到这个功能喔..." 274 | 275 | @classmethod 276 | async def superuser_block( 277 | cls, plugin_name: str, block_type: BlockType | None, group_id: str | None 278 | ) -> str: 279 | """超级用户禁用插件 280 | 281 | 参数: 282 | plugin_name: 插件名称 283 | block_type: 禁用类型 284 | group_id: 群组id 285 | 286 | 返回: 287 | str: 返回信息 288 | """ 289 | if plugin_name.isdigit(): 290 | plugin = await PluginInfo.get_or_none(id=int(plugin_name)) 291 | else: 292 | plugin = await PluginInfo.get_or_none( 293 | name=plugin_name, load_status=True, plugin_type__not=PluginType.PARENT 294 | ) 295 | if plugin: 296 | if group_id: 297 | if not await GroupConsole.is_superuser_block_plugin( 298 | group_id, plugin.module 299 | ): 300 | await GroupConsole.set_block_plugin(group_id, plugin.module, True) 301 | return f"已成功关闭群组 {group_id} 的 {plugin_name} 功能!" 302 | return "此群组该功能已被超级用户关闭,不要重复关闭..." 303 | plugin.block_type = block_type 304 | plugin.status = not bool(block_type) 305 | await plugin.save(update_fields=["status", "block_type"]) 306 | if not block_type: 307 | return f"已成功将 {plugin.name} 全局启用!" 308 | if block_type == BlockType.ALL: 309 | return f"已成功将 {plugin.name} 全局关闭!" 310 | if block_type == BlockType.GROUP: 311 | return f"已成功将 {plugin.name} 全局群组关闭!" 312 | if block_type == BlockType.PRIVATE: 313 | return f"已成功将 {plugin.name} 全局私聊关闭!" 314 | return "没有找到这个功能喔..." 315 | 316 | @classmethod 317 | async def superuser_unblock( 318 | cls, plugin_name: str, block_type: BlockType | None, group_id: str | None 319 | ) -> str: 320 | """超级用户开启插件 321 | 322 | 参数: 323 | plugin_name: 插件名称 324 | block_type: 禁用类型 325 | group_id: 群组id 326 | 327 | 返回: 328 | str: 返回信息 329 | """ 330 | if plugin_name.isdigit(): 331 | plugin = await PluginInfo.get_or_none(id=int(plugin_name)) 332 | else: 333 | plugin = await PluginInfo.get_or_none( 334 | name=plugin_name, load_status=True, plugin_type__not=PluginType.PARENT 335 | ) 336 | if plugin: 337 | if group_id: 338 | if await GroupConsole.is_superuser_block_plugin( 339 | group_id, plugin.module 340 | ): 341 | await GroupConsole.set_unblock_plugin(group_id, plugin.module, True) 342 | return f"已成功开启群组 {group_id} 的 {plugin_name} 功能!" 343 | return "此群组该功能已被超级用户开启,不要重复开启..." 344 | plugin.block_type = block_type 345 | plugin.status = not bool(block_type) 346 | await plugin.save(update_fields=["status", "block_type"]) 347 | if not block_type: 348 | return f"已成功将 {plugin.name} 全局启用!" 349 | if block_type == BlockType.ALL: 350 | return f"已成功将 {plugin.name} 全局开启!" 351 | if block_type == BlockType.GROUP: 352 | return f"已成功将 {plugin.name} 全局群组开启!" 353 | if block_type == BlockType.PRIVATE: 354 | return f"已成功将 {plugin.name} 全局私聊开启!" 355 | return "没有找到这个功能喔..." 356 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/commands/zxpm_plugin_switch/command.py: -------------------------------------------------------------------------------- 1 | from nonebot.rule import to_me 2 | from nonebot_plugin_alconna import ( 3 | Alconna, 4 | Args, 5 | Option, 6 | Subcommand, 7 | on_alconna, 8 | store_true, 9 | ) 10 | 11 | from ...config import ZxpmConfig 12 | from ...rules import admin_check, ensure_group 13 | 14 | _status_matcher = on_alconna( 15 | Alconna( 16 | "switch", 17 | Option("-df|--default", action=store_true, help_text="进群默认开关"), 18 | Option("--all", action=store_true, help_text="全部插件/被动"), 19 | Option("-g|--group", Args["group?", str], help_text="指定群组"), 20 | Subcommand( 21 | "open", 22 | Args["plugin_name?", [str, int]], 23 | ), 24 | Subcommand( 25 | "close", 26 | Args["plugin_name?", [str, int]], 27 | Option( 28 | "-t|--type", 29 | Args["block_type?", ["all", "a", "private", "p", "group", "g"]], 30 | ), 31 | ), 32 | ), 33 | rule=admin_check(ZxpmConfig.zxpm_switch_level), 34 | priority=5, 35 | block=True, 36 | ) 37 | 38 | _group_status_matcher = on_alconna( 39 | Alconna("group-status", Args["status", ["sleep", "wake"]]), 40 | rule=admin_check(ZxpmConfig.zxpm_switch_level) & ensure_group & to_me(), 41 | priority=5, 42 | block=True, 43 | ) 44 | 45 | _status_matcher.shortcut( 46 | r"插件列表", 47 | command="switch", 48 | arguments=[], 49 | prefix=True, 50 | ) 51 | 52 | _status_matcher.shortcut( 53 | r"开启(插件|功能)df(?P.+)", 54 | command="switch", 55 | arguments=["open", "{name}", "-df"], 56 | prefix=True, 57 | ) 58 | 59 | _status_matcher.shortcut( 60 | r"关闭(插件|功能)df(?P.+)", 61 | command="switch", 62 | arguments=["close", "{name}", "-df"], 63 | prefix=True, 64 | ) 65 | 66 | 67 | _status_matcher.shortcut( 68 | r"开启所有(插件|功能)", 69 | command="switch", 70 | arguments=["open", "s", "--all"], 71 | prefix=True, 72 | ) 73 | 74 | _status_matcher.shortcut( 75 | r"开启所有(插件|功能)df", 76 | command="switch", 77 | arguments=["open", "s", "-df", "--all"], 78 | prefix=True, 79 | ) 80 | 81 | _status_matcher.shortcut( 82 | r"开启(?P.+)", 83 | command="switch", 84 | arguments=["open", "{name}"], 85 | prefix=True, 86 | ) 87 | 88 | 89 | _status_matcher.shortcut( 90 | r"关闭所有(插件|功能)", 91 | command="switch", 92 | arguments=["close", "s", "--all"], 93 | prefix=True, 94 | ) 95 | 96 | _status_matcher.shortcut( 97 | r"关闭所有(插件|功能)df", 98 | command="switch", 99 | arguments=["close", "s", "-df", "--all"], 100 | prefix=True, 101 | ) 102 | 103 | _status_matcher.shortcut( 104 | r"关闭(?P.+)", 105 | command="switch", 106 | arguments=["close", "{name}"], 107 | prefix=True, 108 | ) 109 | 110 | 111 | _group_status_matcher.shortcut( 112 | r"醒来", 113 | command="group-status", 114 | arguments=["wake"], 115 | prefix=True, 116 | ) 117 | 118 | _group_status_matcher.shortcut( 119 | r"休息吧", 120 | command="group-status", 121 | arguments=["sleep"], 122 | prefix=True, 123 | ) 124 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/commands/zxpm_set_admin/__init__.py: -------------------------------------------------------------------------------- 1 | from nonebot.permission import SUPERUSER 2 | from nonebot.plugin import PluginMetadata 3 | from nonebot_plugin_alconna import ( 4 | Alconna, 5 | Args, 6 | Arparma, 7 | At, 8 | Match, 9 | Option, 10 | Subcommand, 11 | on_alconna, 12 | ) 13 | from nonebot_plugin_session import EventSession, SessionLevel 14 | 15 | from ...enum import PluginType 16 | from ...extra import PluginExtraData 17 | from ...log import logger 18 | from ...models.level_user import LevelUser 19 | from ...utils.utils import MessageUtils 20 | 21 | __plugin_meta__ = PluginMetadata( 22 | name="用户权限管理", 23 | description="设置用户权限", 24 | usage=""" 25 | 权限设置 add [level: 权限等级] [at: at对象或用户id] ?[-g gid: 群组] 26 | 权限设置 delete [at: at对象或用户id] ?[-g gid: 群组] 27 | 28 | 添加权限 5 @user 29 | 权限设置 add 5 422 -g 352352 30 | 31 | 删除权限 @user 32 | 删除权限 1234123 -g 123123 33 | 34 | """.strip(), 35 | extra=PluginExtraData( 36 | author="HibiKier", 37 | version="0.1", 38 | plugin_type=PluginType.SUPERUSER, 39 | ).dict(), 40 | ) 41 | 42 | 43 | _matcher = on_alconna( 44 | Alconna( 45 | "权限设置", 46 | Subcommand( 47 | "add", 48 | Args["level", int]["uid", [str, At]], 49 | help_text="添加权限", 50 | ), 51 | Subcommand("delete", Args["uid", [str, At]], help_text="删除权限"), 52 | Option("-g|--group", Args["gid", str], help_text="指定群组"), 53 | ), 54 | permission=SUPERUSER, 55 | priority=5, 56 | block=True, 57 | ) 58 | 59 | _matcher.shortcut( 60 | "添加权限", 61 | command="权限设置", 62 | arguments=["add", "{%0}"], 63 | prefix=True, 64 | ) 65 | 66 | _matcher.shortcut( 67 | "删除权限", 68 | command="权限设置", 69 | arguments=["delete", "{%0}"], 70 | prefix=True, 71 | ) 72 | 73 | 74 | @_matcher.assign("add") 75 | async def _( 76 | session: EventSession, 77 | arparma: Arparma, 78 | level: int, 79 | gid: Match[str], 80 | uid: str | At, 81 | ): 82 | group_id = gid.result if gid.available else session.id3 or session.id2 83 | if group_id: 84 | if isinstance(uid, At): 85 | uid = uid.target 86 | user = await LevelUser.get_or_none(user_id=uid, group_id=group_id) 87 | old_level = user.user_level if user else 0 88 | await LevelUser.set_level(uid, group_id, level, 1) 89 | logger.info( 90 | f"修改权限: {old_level} -> {level}", arparma.header_result, session=session 91 | ) 92 | if session.level in [SessionLevel.LEVEL2, SessionLevel.LEVEL3]: 93 | await MessageUtils.build_message( 94 | [ 95 | "成功为 ", 96 | At(flag="user", target=uid), 97 | f" 设置权限:{old_level} -> {level}", 98 | ] 99 | ).finish(reply_to=True) 100 | await MessageUtils.build_message( 101 | f"成功为 \n群组:{group_id}\n用户:{uid} \n" 102 | f"设置权限!\n权限:{old_level} -> {level}" 103 | ).finish() 104 | await MessageUtils.build_message("设置权限时群组不能为空...").finish() 105 | 106 | 107 | @_matcher.assign("delete") 108 | async def _( 109 | session: EventSession, 110 | arparma: Arparma, 111 | gid: Match[str], 112 | uid: str | At, 113 | ): 114 | group_id = gid.result if gid.available else session.id3 or session.id2 115 | if group_id: 116 | if isinstance(uid, At): 117 | uid = uid.target 118 | if user := await LevelUser.get_or_none(user_id=uid, group_id=group_id): 119 | await user.delete() 120 | if session.level in [SessionLevel.LEVEL2, SessionLevel.LEVEL3]: 121 | logger.info( 122 | f"删除权限: {user.user_level} -> 0", 123 | arparma.header_result, 124 | session=session, 125 | ) 126 | await MessageUtils.build_message( 127 | ["成功删除 ", At(flag="user", target=uid), " 的权限等级!"] 128 | ).finish(reply_to=True) 129 | logger.info( 130 | f"删除群组用户权限: {user.user_level} -> 0", 131 | arparma.header_result, 132 | session=session, 133 | ) 134 | await MessageUtils.build_message( 135 | f"成功删除 \n群组:{group_id}\n用户:{uid} \n" 136 | f"的权限等级!\n权限:{user.user_level} -> 0" 137 | ).finish() 138 | await MessageUtils.build_message("对方目前暂无权限喔...").finish() 139 | await MessageUtils.build_message("设置权限时群组不能为空...").finish() 140 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/commands/zxpm_super_group/__init__.py: -------------------------------------------------------------------------------- 1 | from nonebot import logger 2 | from nonebot.permission import SUPERUSER 3 | from nonebot.plugin import PluginMetadata 4 | from nonebot_plugin_alconna import ( 5 | Alconna, 6 | Args, 7 | Arparma, 8 | Match, 9 | Option, 10 | Subcommand, 11 | on_alconna, 12 | store_true, 13 | ) 14 | from nonebot_plugin_session import EventSession 15 | 16 | from ...enum import PluginType 17 | from ...extra import PluginExtraData 18 | from ...models.group_console import GroupConsole 19 | from ...utils.utils import MessageUtils 20 | 21 | __plugin_meta__ = PluginMetadata( 22 | name="群白名单", 23 | description="群白名单", 24 | usage=""" 25 | 群白名单 26 | 添加/删除群白名单,当在群组中这五个命令且没有指定群号时,默认指定当前群组 27 | 指令: 28 | 格式: 29 | group-manage super-handle [群组Id] [--del 删除操作] : 添加/删除群白名单 30 | 31 | 快捷: 32 | group-manage super-handle : 添加/删除群白名单 33 | 34 | 示例: 35 | 添加/删除群白名单 1234567 : 添加/删除 1234567 为群白名单 36 | """.strip(), 37 | extra=PluginExtraData( 38 | author="HibiKier", 39 | version="0.1", 40 | plugin_type=PluginType.SUPERUSER, 41 | ).dict(), 42 | ) 43 | 44 | 45 | _matcher = on_alconna( 46 | Alconna( 47 | "group-manage", 48 | Option("--delete", action=store_true, help_text="删除"), 49 | Subcommand( 50 | "super-handle", 51 | Args["group_id?", str], 52 | help_text="添加/删除群白名单", 53 | ), 54 | ), 55 | permission=SUPERUSER, 56 | priority=1, 57 | block=True, 58 | ) 59 | 60 | 61 | _matcher.shortcut( 62 | "添加群白名单(?P.*?)", 63 | command="group-manage", 64 | arguments=["super-handle", "{gid}"], 65 | prefix=True, 66 | ) 67 | 68 | _matcher.shortcut( 69 | "删除群白名单(?P.*?)", 70 | command="group-manage", 71 | arguments=["super-handle", "{gid}", "--delete"], 72 | prefix=True, 73 | ) 74 | 75 | 76 | @_matcher.assign("super-handle") 77 | async def _(session: EventSession, arparma: Arparma, group_id: Match[str]): 78 | if group_id.available: 79 | gid = group_id.result 80 | else: 81 | gid = session.id3 or session.id2 82 | if not gid: 83 | await MessageUtils.build_message("群组id不能为空!").finish(reply_to=True) 84 | group, _ = await GroupConsole.get_or_create(group_id=gid) 85 | s = "删除" if arparma.find("delete") else "添加" 86 | group.is_super = not arparma.find("delete") 87 | await group.save(update_fields=["is_super"]) 88 | await MessageUtils.build_message(f"{s}群白名单成功!").send(reply_to=True) 89 | logger.info(f"{s}群白名单: {gid}") 90 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/config.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import nonebot 4 | from pydantic import BaseModel 5 | 6 | DEFAULT_DATA_PATH = Path() / "data" / "zxpm" 7 | 8 | 9 | class Config(BaseModel): 10 | zxpm_data_path: str | Path = str(DEFAULT_DATA_PATH.absolute()) 11 | """数据存储路径""" 12 | zxpm_db_url: str | None = None 13 | """DB_URL""" 14 | zxpm_notice_info_cd: int = 300 15 | """群/用户权限检测等各种检测提示信息cd,为0时不提醒""" 16 | zxpm_ban_reply: str = "才不会给你发消息." 17 | """用户被ban时回复消息,为空时不回复""" 18 | zxpm_ban_level: int = 5 19 | """使用ban功能的对应权限""" 20 | zxpm_switch_level: int = 1 21 | """群组插件开关管理对应权限""" 22 | zxpm_admin_default_auth: int = 5 23 | """群组管理员默认权限""" 24 | zxpm_font: str = "msyh.ttc" 25 | """字体""" 26 | zxpm_limit_superuser: bool = False 27 | """是否限制超管权限""" 28 | 29 | 30 | ZxpmConfig = nonebot.get_plugin_config(Config) 31 | 32 | if isinstance(ZxpmConfig.zxpm_data_path, str): 33 | ZxpmConfig.zxpm_data_path = Path(ZxpmConfig.zxpm_data_path) 34 | ZxpmConfig.zxpm_data_path.mkdir(parents=True, exist_ok=True) 35 | if ZxpmConfig.zxpm_db_url: 36 | if ZxpmConfig.zxpm_db_url.startswith("sqlite"): 37 | db_path = ZxpmConfig.zxpm_db_url.split(":")[-1] 38 | Path(db_path).parent.mkdir(parents=True, exist_ok=True) 39 | else: 40 | db_path = ZxpmConfig.zxpm_data_path / "db" / "zxpm.db" 41 | db_path.parent.mkdir(parents=True, exist_ok=True) 42 | ZxpmConfig.zxpm_db_url = f"sqlite:{db_path.absolute()!s}" 43 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/db_connect/__init__.py: -------------------------------------------------------------------------------- 1 | import nonebot 2 | from tortoise import Tortoise 3 | from tortoise.connection import connections 4 | from tortoise.models import Model as Model_ 5 | 6 | from ..config import ZxpmConfig 7 | from ..exception import DbConnectError 8 | from ..log import logger 9 | 10 | driver = nonebot.get_driver() 11 | 12 | MODELS: list[str] = [] 13 | 14 | 15 | class Model(Model_): 16 | """ 17 | 自动添加模块 18 | 19 | Args: 20 | Model_: Model 21 | """ 22 | 23 | def __init_subclass__(cls, **kwargs): 24 | MODELS.append(cls.__module__) 25 | 26 | 27 | @driver.on_startup 28 | async def _(): 29 | try: 30 | await Tortoise.init( 31 | db_url=ZxpmConfig.zxpm_db_url, 32 | modules={"models": MODELS}, 33 | timezone="Asia/Shanghai", 34 | ) 35 | await Tortoise.generate_schemas() 36 | logger.info("ZXPM数据库加载完成!") 37 | except Exception as e: 38 | raise DbConnectError(f"ZXPM数据库连接错误... e:{e}") from e 39 | 40 | 41 | @driver.on_shutdown 42 | async def disconnect(): 43 | await connections.close_all() 44 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/enum.py: -------------------------------------------------------------------------------- 1 | from strenum import StrEnum 2 | 3 | 4 | class PluginType(StrEnum): 5 | """ 6 | 插件类型 7 | """ 8 | 9 | SUPERUSER = "SUPERUSER" 10 | """超级用户""" 11 | ADMIN = "ADMIN" 12 | """管理员""" 13 | SUPER_AND_ADMIN = "ADMIN_SUPER" 14 | """管理员以及超级用户""" 15 | NORMAL = "NORMAL" 16 | """普通插件""" 17 | DEPENDANT = "DEPENDANT" 18 | """依赖插件,一般为没有主动触发命令的插件,受权限控制""" 19 | HIDDEN = "HIDDEN" 20 | """隐藏插件,一般为没有主动触发命令的插件,不受权限控制,如消息统计""" 21 | PARENT = "PARENT" 22 | """父插件,仅仅标记""" 23 | 24 | 25 | class BlockType(StrEnum): 26 | """ 27 | 禁用状态 28 | """ 29 | 30 | PRIVATE = "PRIVATE" 31 | GROUP = "GROUP" 32 | ALL = "ALL" 33 | 34 | 35 | class PluginLimitType(StrEnum): 36 | """ 37 | 插件限制类型 38 | """ 39 | 40 | CD = "CD" 41 | COUNT = "COUNT" 42 | BLOCK = "BLOCK" 43 | 44 | 45 | class LimitCheckType(StrEnum): 46 | """ 47 | 插件限制类型 48 | """ 49 | 50 | PRIVATE = "PRIVATE" 51 | GROUP = "GROUP" 52 | ALL = "ALL" 53 | 54 | 55 | class LimitWatchType(StrEnum): 56 | """ 57 | 插件限制监听对象 58 | """ 59 | 60 | USER = "USER" 61 | GROUP = "GROUP" 62 | ALL = "ALL" 63 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/exception.py: -------------------------------------------------------------------------------- 1 | class UserAndGroupIsNone(Exception): 2 | """ 3 | 用户/群组Id都为空 4 | """ 5 | 6 | pass 7 | 8 | 9 | class DbConnectError(Exception): 10 | """ 11 | 数据库连接错误 12 | """ 13 | 14 | pass 15 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/extra/__init__.py: -------------------------------------------------------------------------------- 1 | from nonebot.compat import PYDANTIC_V2 2 | from pydantic import BaseModel 3 | 4 | from ..enum import PluginType 5 | from .limit import BaseBlock, PluginCdBlock, PluginCountBlock 6 | 7 | 8 | class PluginSetting(BaseModel): 9 | """ 10 | 插件基本配置 11 | """ 12 | 13 | level: int = 5 14 | """群权限等级""" 15 | default_status: bool = True 16 | """进群默认开关状态""" 17 | limit_superuser: bool = False 18 | """是否限制超级用户""" 19 | cost_gold: int = 0 20 | """调用插件花费金币""" 21 | 22 | 23 | class PluginExtraData(BaseModel): 24 | """ 25 | 插件扩展信息 26 | """ 27 | 28 | author: str | None = None 29 | """作者""" 30 | version: str | None = None 31 | """版本""" 32 | plugin_type: PluginType = PluginType.NORMAL 33 | """插件类型""" 34 | menu_type: str = "功能" 35 | """菜单类型""" 36 | admin_level: int | None = None 37 | """管理员插件所需权限等级""" 38 | setting: PluginSetting | None = None 39 | """插件基本配置""" 40 | limits: list[BaseBlock | PluginCdBlock | PluginCountBlock] | None = None 41 | """插件限制""" 42 | superuser_help: str | None = None 43 | """超级用户帮助""" 44 | 45 | def to_dict(self): 46 | return self.model_dump() if PYDANTIC_V2 else self.dict() 47 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/extra/limit.py: -------------------------------------------------------------------------------- 1 | import time 2 | from collections import defaultdict 3 | from datetime import datetime 4 | from typing import Any 5 | 6 | import pytz 7 | from pydantic import BaseModel 8 | 9 | from ..enum import BlockType, LimitWatchType, PluginLimitType 10 | 11 | 12 | class BaseBlock(BaseModel): 13 | """ 14 | 插件阻断基本类(插件阻断限制) 15 | """ 16 | 17 | status: bool = True 18 | """限制状态""" 19 | check_type: BlockType = BlockType.ALL 20 | """检查类型""" 21 | watch_type: LimitWatchType = LimitWatchType.USER 22 | """监听对象""" 23 | result: str | None = None 24 | """阻断时回复内容""" 25 | _type: PluginLimitType = PluginLimitType.BLOCK 26 | """类型""" 27 | 28 | 29 | class PluginCdBlock(BaseBlock): 30 | """ 31 | 插件cd限制 32 | """ 33 | 34 | cd: int = 5 35 | """cd""" 36 | _type: PluginLimitType = PluginLimitType.CD 37 | """类型""" 38 | 39 | 40 | class PluginCountBlock(BaseBlock): 41 | """ 42 | 插件次数限制 43 | """ 44 | 45 | max_count: int 46 | """最大调用次数""" 47 | _type: PluginLimitType = PluginLimitType.COUNT 48 | """类型""" 49 | 50 | 51 | class CountLimiter: 52 | """ 53 | 每日调用命令次数限制 54 | """ 55 | 56 | tz = pytz.timezone("Asia/Shanghai") 57 | 58 | def __init__(self, max_num): 59 | self.today = -1 60 | self.count = defaultdict(int) 61 | self.max = max_num 62 | 63 | def check(self, key) -> bool: 64 | day = datetime.now(self.tz).day 65 | if day != self.today: 66 | self.today = day 67 | self.count.clear() 68 | return self.count[key] < self.max 69 | 70 | def get_num(self, key): 71 | return self.count[key] 72 | 73 | def increase(self, key, num=1): 74 | self.count[key] += num 75 | 76 | def reset(self, key): 77 | self.count[key] = 0 78 | 79 | 80 | class UserBlockLimiter: 81 | """ 82 | 检测用户是否正在调用命令 83 | """ 84 | 85 | def __init__(self): 86 | self.flag_data = defaultdict(bool) 87 | self.time = time.time() 88 | 89 | def set_true(self, key: Any): 90 | self.time = time.time() 91 | self.flag_data[key] = True 92 | 93 | def set_false(self, key: Any): 94 | self.flag_data[key] = False 95 | 96 | def check(self, key: Any) -> bool: 97 | if time.time() - self.time > 30: 98 | self.set_false(key) 99 | return not self.flag_data[key] 100 | 101 | 102 | class FreqLimiter: 103 | """ 104 | 命令冷却,检测用户是否处于冷却状态 105 | """ 106 | 107 | def __init__(self, default_cd_seconds: int): 108 | self.next_time = defaultdict(float) 109 | self.default_cd = default_cd_seconds 110 | 111 | def check(self, key: Any) -> bool: 112 | return time.time() >= self.next_time[key] 113 | 114 | def start_cd(self, key: Any, cd_time: int = 0): 115 | self.next_time[key] = time.time() + ( 116 | cd_time if cd_time > 0 else self.default_cd 117 | ) 118 | 119 | def left_time(self, key: Any) -> float: 120 | return self.next_time[key] - time.time() 121 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/log.py: -------------------------------------------------------------------------------- 1 | from typing import Any, overload 2 | 3 | from loguru import logger as logger_ 4 | from nonebot_plugin_session import Session 5 | 6 | 7 | class logger: 8 | TEMPLATE_A = "Adapter[{}] {}" 9 | TEMPLATE_B = "Adapter[{}] [{}]: {}" 10 | TEMPLATE_C = "Adapter[{}] 用户[{}] 触发 [{}]: {}" 11 | TEMPLATE_D = "Adapter[{}] 群聊[{}] 用户[{}] 触发" 12 | " [{}]: {}" 13 | TEMPLATE_E = "Adapter[{}] 群聊[{}] 用户[{}] 触发" 14 | " [{}] [Target]({}): {}" 15 | 16 | TEMPLATE_ADAPTER = "Adapter[{}] " 17 | TEMPLATE_USER = "用户[{}] " 18 | TEMPLATE_GROUP = "群聊[{}] " 19 | TEMPLATE_COMMAND = "CMD[{}] " 20 | TEMPLATE_PLATFORM = "平台[{}] " 21 | TEMPLATE_TARGET = "[Target]([{}]) " 22 | 23 | SUCCESS_TEMPLATE = "[{}]: {} | 参数[{}] 返回: [{}]" 24 | 25 | WARNING_TEMPLATE = "[{}]: {}" 26 | 27 | ERROR_TEMPLATE = "[{}]: {}" 28 | 29 | @overload 30 | @classmethod 31 | def info( 32 | cls, 33 | info: str, 34 | command: str | None = None, 35 | *, 36 | session: int | str | None = None, 37 | group_id: int | str | None = None, 38 | adapter: str | None = None, 39 | target: Any = None, 40 | platform: str | None = None, 41 | ): ... 42 | 43 | @overload 44 | @classmethod 45 | def info( 46 | cls, 47 | info: str, 48 | command: str | None = None, 49 | *, 50 | session: Session | None = None, 51 | target: Any = None, 52 | platform: str | None = None, 53 | ): ... 54 | 55 | @classmethod 56 | def info( 57 | cls, 58 | info: str, 59 | command: str | None = None, 60 | *, 61 | session: int | str | Session | None = None, 62 | group_id: int | str | None = None, 63 | adapter: str | None = None, 64 | target: Any = None, 65 | platform: str | None = None, 66 | ): 67 | user_id: str | None = session # type: ignore 68 | if isinstance(session, Session): 69 | user_id = session.id1 70 | adapter = session.bot_type 71 | if session.id3: 72 | group_id = f"{session.id3}:{session.id2}" 73 | elif session.id2: 74 | group_id = f"{session.id2}" 75 | platform = platform or session.platform 76 | template = cls.__parser_template( 77 | info, command, user_id, group_id, adapter, target, platform 78 | ) 79 | try: 80 | logger_.opt(colors=True).info(template) 81 | except Exception: 82 | logger_.info(template) 83 | 84 | @classmethod 85 | def success( 86 | cls, 87 | info: str, 88 | command: str, 89 | param: dict[str, Any] | None = None, 90 | result: str = "", 91 | ): 92 | param_str = "" 93 | if param: 94 | param_str = ",".join([f"{k}:{v}" for k, v in param.items()]) 95 | logger_.opt(colors=True).success( 96 | cls.SUCCESS_TEMPLATE.format(command, info, param_str, result) 97 | ) 98 | 99 | @overload 100 | @classmethod 101 | def warning( 102 | cls, 103 | info: str, 104 | command: str | None = None, 105 | *, 106 | session: int | str | None = None, 107 | group_id: int | str | None = None, 108 | adapter: str | None = None, 109 | target: Any = None, 110 | platform: str | None = None, 111 | e: Exception | None = None, 112 | ): ... 113 | 114 | @overload 115 | @classmethod 116 | def warning( 117 | cls, 118 | info: str, 119 | command: str | None = None, 120 | *, 121 | session: Session | None = None, 122 | adapter: str | None = None, 123 | target: Any = None, 124 | platform: str | None = None, 125 | e: Exception | None = None, 126 | ): ... 127 | 128 | @classmethod 129 | def warning( 130 | cls, 131 | info: str, 132 | command: str | None = None, 133 | *, 134 | session: int | str | Session | None = None, 135 | group_id: int | str | None = None, 136 | adapter: str | None = None, 137 | target: Any = None, 138 | platform: str | None = None, 139 | e: Exception | None = None, 140 | ): 141 | user_id: str | None = session # type: ignore 142 | if isinstance(session, Session): 143 | user_id = session.id1 144 | adapter = session.bot_type 145 | if session.id3: 146 | group_id = f"{session.id3}:{session.id2}" 147 | elif session.id2: 148 | group_id = f"{session.id2}" 149 | platform = platform or session.platform 150 | template = cls.__parser_template( 151 | info, command, user_id, group_id, adapter, target, platform 152 | ) 153 | if e: 154 | template += f" || 错误{type(e)}: {e}" 155 | try: 156 | logger_.opt(colors=True).warning(template) 157 | except Exception as e: 158 | logger_.warning(template) 159 | 160 | @overload 161 | @classmethod 162 | def error( 163 | cls, 164 | info: str, 165 | command: str | None = None, 166 | *, 167 | session: int | str | None = None, 168 | group_id: int | str | None = None, 169 | adapter: str | None = None, 170 | target: Any = None, 171 | platform: str | None = None, 172 | e: Exception | None = None, 173 | ): ... 174 | 175 | @overload 176 | @classmethod 177 | def error( 178 | cls, 179 | info: str, 180 | command: str | None = None, 181 | *, 182 | session: Session | None = None, 183 | target: Any = None, 184 | platform: str | None = None, 185 | e: Exception | None = None, 186 | ): ... 187 | 188 | @classmethod 189 | def error( 190 | cls, 191 | info: str, 192 | command: str | None = None, 193 | *, 194 | session: int | str | Session | None = None, 195 | group_id: int | str | None = None, 196 | adapter: str | None = None, 197 | target: Any = None, 198 | platform: str | None = None, 199 | e: Exception | None = None, 200 | ): 201 | user_id: str | None = session # type: ignore 202 | if isinstance(session, Session): 203 | user_id = session.id1 204 | adapter = session.bot_type 205 | if session.id3: 206 | group_id = f"{session.id3}:{session.id2}" 207 | elif session.id2: 208 | group_id = f"{session.id2}" 209 | platform = platform or session.platform 210 | template = cls.__parser_template( 211 | info, command, user_id, group_id, adapter, target, platform 212 | ) 213 | if e: 214 | template += f" || 错误 {type(e)}: {e}" 215 | try: 216 | logger_.opt(colors=True).error(template) 217 | except Exception as e: 218 | logger_.error(template) 219 | 220 | @overload 221 | @classmethod 222 | def debug( 223 | cls, 224 | info: str, 225 | command: str | None = None, 226 | *, 227 | session: int | str | None = None, 228 | group_id: int | str | None = None, 229 | adapter: str | None = None, 230 | target: Any = None, 231 | platform: str | None = None, 232 | e: Exception | None = None, 233 | ): ... 234 | 235 | @overload 236 | @classmethod 237 | def debug( 238 | cls, 239 | info: str, 240 | command: str | None = None, 241 | *, 242 | session: Session | None = None, 243 | target: Any = None, 244 | platform: str | None = None, 245 | e: Exception | None = None, 246 | ): ... 247 | 248 | @classmethod 249 | def debug( 250 | cls, 251 | info: str, 252 | command: str | None = None, 253 | *, 254 | session: int | str | Session | None = None, 255 | group_id: int | str | None = None, 256 | adapter: str | None = None, 257 | target: Any = None, 258 | platform: str | None = None, 259 | e: Exception | None = None, 260 | ): 261 | user_id: str | None = session # type: ignore 262 | if isinstance(session, Session): 263 | user_id = session.id1 264 | adapter = session.bot_type 265 | if session.id3: 266 | group_id = f"{session.id3}:{session.id2}" 267 | elif session.id2: 268 | group_id = f"{session.id2}" 269 | platform = platform or session.platform 270 | template = cls.__parser_template( 271 | info, command, user_id, group_id, adapter, target, platform 272 | ) 273 | if e: 274 | template += f" || 错误 {type(e)}: {e}" 275 | try: 276 | logger_.opt(colors=True).debug(template) 277 | except Exception as e: 278 | logger_.debug(template) 279 | 280 | @classmethod 281 | def __parser_template( 282 | cls, 283 | info: str, 284 | command: str | None = None, 285 | user_id: int | str | None = None, 286 | group_id: int | str | None = None, 287 | adapter: str | None = None, 288 | target: Any = None, 289 | platform: str | None = None, 290 | ) -> str: 291 | arg_list = [] 292 | template = "" 293 | if adapter is not None: 294 | template += cls.TEMPLATE_ADAPTER 295 | arg_list.append(adapter) 296 | if platform is not None: 297 | template += cls.TEMPLATE_PLATFORM 298 | arg_list.append(platform) 299 | if group_id is not None: 300 | template += cls.TEMPLATE_GROUP 301 | arg_list.append(group_id) 302 | if user_id is not None: 303 | template += cls.TEMPLATE_USER 304 | arg_list.append(user_id) 305 | if command is not None: 306 | template += cls.TEMPLATE_COMMAND 307 | arg_list.append(command) 308 | if target is not None: 309 | template += cls.TEMPLATE_TARGET 310 | arg_list.append(target) 311 | arg_list.append(info) 312 | template += "{}" 313 | return template.format(*arg_list) 314 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/models/ban_console.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from tortoise import fields 4 | from typing_extensions import Self 5 | 6 | from ..db_connect import Model 7 | from ..exception import UserAndGroupIsNone 8 | from ..log import logger 9 | 10 | 11 | class BanConsole(Model): 12 | id = fields.IntField(pk=True, generated=True, auto_increment=True) 13 | """自增id""" 14 | user_id = fields.CharField(255, null=True) 15 | """用户id""" 16 | group_id = fields.CharField(255, null=True) 17 | """群组id""" 18 | ban_level = fields.IntField() 19 | """使用ban命令的用户等级""" 20 | ban_time = fields.BigIntField() 21 | """ban开始的时间""" 22 | duration = fields.BigIntField() 23 | """ban时长""" 24 | operator = fields.CharField(255) 25 | """使用Ban命令的用户""" 26 | 27 | class Meta: # type: ignore 28 | table = "zxpm_ban_console" 29 | table_description = "封禁人员/群组数据表" 30 | 31 | @classmethod 32 | async def _get_data(cls, user_id: str | None, group_id: str | None) -> Self | None: 33 | """获取数据 34 | 35 | 参数: 36 | user_id: 用户id 37 | group_id: 群组id 38 | 39 | 异常: 40 | UserAndGroupIsNone: 用户id和群组id都为空 41 | 42 | 返回: 43 | Self | None: Self 44 | """ 45 | if not user_id and not group_id: 46 | raise UserAndGroupIsNone() 47 | if user_id: 48 | return ( 49 | await cls.get_or_none(user_id=user_id, group_id=group_id) 50 | if group_id 51 | else await cls.get_or_none(user_id=user_id, group_id__isnull=True) 52 | ) 53 | else: 54 | return await cls.get_or_none(user_id="", group_id=group_id) 55 | 56 | @classmethod 57 | async def check_ban_level( 58 | cls, user_id: str | None, group_id: str | None, level: int 59 | ) -> bool: 60 | """检测ban掉目标的用户与unban用户的权限等级大小 61 | 62 | 参数: 63 | user_id: 用户id 64 | group_id: 群组id 65 | level: 权限等级 66 | 67 | 返回: 68 | bool: 权限判断,能否unban 69 | """ 70 | user = await cls._get_data(user_id, group_id) 71 | if user: 72 | logger.debug( 73 | f"检测用户被ban等级,user_level: {user.ban_level},level: {level}", 74 | target=f"{group_id}:{user_id}", 75 | ) 76 | return user.ban_level <= level 77 | return False 78 | 79 | @classmethod 80 | async def check_ban_time( 81 | cls, user_id: str | None, group_id: str | None = None 82 | ) -> int: 83 | """检测用户被ban时长 84 | 85 | 参数: 86 | user_id: 用户id 87 | 88 | 返回: 89 | int: ban剩余时长,-1时为永久ban,0表示未被ban 90 | """ 91 | logger.debug("获取用户ban时长", target=f"{group_id}:{user_id}") 92 | user = await cls._get_data(user_id, group_id) 93 | if not user and user_id: 94 | user = await cls._get_data(user_id, None) 95 | if user: 96 | if user.duration == -1: 97 | return -1 98 | _time = time.time() - (user.ban_time + user.duration) 99 | return 0 if _time > 0 else int(time.time() - user.ban_time - user.duration) 100 | return 0 101 | 102 | @classmethod 103 | async def is_ban(cls, user_id: str | None, group_id: str | None = None) -> bool: 104 | """判断用户是否被ban 105 | 106 | 参数: 107 | user_id: 用户id 108 | 109 | 返回: 110 | bool: 是否被ban 111 | """ 112 | logger.debug("检测是否被ban", target=f"{group_id}:{user_id}") 113 | if await cls.check_ban_time(user_id, group_id): 114 | return True 115 | else: 116 | await cls.unban(user_id, group_id) 117 | return False 118 | 119 | @classmethod 120 | async def ban( 121 | cls, 122 | user_id: str | None, 123 | group_id: str | None, 124 | ban_level: int, 125 | duration: int, 126 | operator: str | None = None, 127 | ): 128 | """ban掉目标用户 129 | 130 | 参数: 131 | user_id: 用户id 132 | group_id: 群组id 133 | ban_level: 使用命令者的权限等级 134 | duration: 时长,分钟,-1时为永久 135 | operator: 操作者id 136 | """ 137 | logger.debug( 138 | f"封禁用户/群组,等级:{ban_level},时长: {duration}", 139 | target=f"{group_id}:{user_id}", 140 | ) 141 | target = await cls._get_data(user_id, group_id) 142 | if target: 143 | await cls.unban(user_id, group_id) 144 | await cls.create( 145 | user_id=user_id, 146 | group_id=group_id, 147 | ban_level=ban_level, 148 | ban_time=int(time.time()), 149 | duration=duration, 150 | operator=operator or 0, 151 | ) 152 | 153 | @classmethod 154 | async def unban(cls, user_id: str | None, group_id: str | None = None) -> bool: 155 | """unban用户 156 | 157 | 参数: 158 | user_id: 用户id 159 | group_id: 群组id 160 | 161 | 返回: 162 | bool: 是否被ban 163 | """ 164 | user = await cls._get_data(user_id, group_id) 165 | if user: 166 | logger.debug("解除封禁", target=f"{group_id}:{user_id}") 167 | await user.delete() 168 | return True 169 | return False 170 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/models/bot_console.py: -------------------------------------------------------------------------------- 1 | from typing import Literal, overload 2 | 3 | from tortoise import fields 4 | 5 | from ..db_connect import Model 6 | 7 | 8 | class BotConsole(Model): 9 | id = fields.IntField(pk=True, generated=True, auto_increment=True) 10 | """自增id""" 11 | bot_id = fields.CharField(255, unique=True, description="bot_id") 12 | """bot_id""" 13 | status = fields.BooleanField(default=True, description="Bot状态") 14 | """Bot状态""" 15 | create_time = fields.DatetimeField(auto_now_add=True, description="创建时间") 16 | """创建时间""" 17 | platform = fields.CharField(255, null=True, description="平台") 18 | """平台""" 19 | block_plugins = fields.TextField(default="", description="禁用插件") 20 | """禁用插件""" 21 | block_tasks = fields.TextField(default="", description="禁用被动技能") 22 | """禁用被动技能""" 23 | available_plugins = fields.TextField(default="", description="可用插件") 24 | """可用插件""" 25 | available_tasks = fields.TextField(default="", description="可用被动技能") 26 | """可用被动技能""" 27 | 28 | class Meta: # type: ignore 29 | table = "bot_console" 30 | table_description = "Bot数据表" 31 | 32 | @staticmethod 33 | def format(name: str) -> str: 34 | return f"<{name}," 35 | 36 | @overload 37 | @classmethod 38 | async def get_bot_status(cls) -> list[tuple[str, bool]]: ... 39 | 40 | @overload 41 | @classmethod 42 | async def get_bot_status(cls, bot_id: str) -> bool: ... 43 | 44 | @classmethod 45 | async def get_bot_status( 46 | cls, bot_id: str | None = None 47 | ) -> list[tuple[str, bool]] | bool: 48 | """ 49 | 获取bot状态 50 | 51 | 参数: 52 | bot_id (str, optional): bot_id. Defaults to None. 53 | 54 | 返回: 55 | list[tuple[str, bool]] | bool: bot状态 56 | """ 57 | if not bot_id: 58 | return await cls.all().values_list("bot_id", "status") 59 | result = await cls.get_or_none(bot_id=bot_id) 60 | return result.status if result else False 61 | 62 | @overload 63 | @classmethod 64 | async def get_tasks(cls) -> list[tuple[str, list[str]]]: ... 65 | 66 | @overload 67 | @classmethod 68 | async def get_tasks(cls, bot_id: str) -> list[str]: ... 69 | 70 | @overload 71 | @classmethod 72 | async def get_tasks(cls, *, status: bool) -> dict[str, list[str]]: ... 73 | 74 | @overload 75 | @classmethod 76 | async def get_tasks(cls, bot_id: str, status: bool = True) -> list[str]: ... 77 | 78 | @classmethod 79 | async def get_tasks(cls, bot_id: str | None = None, status: bool | None = True): 80 | """ 81 | 获取bot被动技能 82 | 83 | 参数: 84 | bot_id (str | None, optional): bot_id. Defaults to None. 85 | status (bool | None, optional): 被动状态. Defaults to True. 86 | 87 | 返回: 88 | list[tuple[str, str]] | str: 被动技能 89 | """ 90 | if not bot_id: 91 | task_field: Literal["available_tasks", "block_tasks"] = ( 92 | "available_tasks" if status else "block_tasks" 93 | ) 94 | data_list = await cls.all().values_list("bot_id", task_field) 95 | return {k: cls.convert_module_format(v) for k, v in data_list} 96 | result = await cls.get_or_none(bot_id=bot_id) 97 | if result: 98 | tasks = result.available_tasks if status else result.block_tasks 99 | return cls.convert_module_format(tasks) 100 | return [] 101 | 102 | @overload 103 | @classmethod 104 | async def get_plugins(cls) -> dict[str, list[str]]: ... 105 | 106 | @overload 107 | @classmethod 108 | async def get_plugins(cls, bot_id: str) -> list[str]: ... 109 | 110 | @overload 111 | @classmethod 112 | async def get_plugins(cls, *, status: bool) -> dict[str, list[str]]: ... 113 | 114 | @overload 115 | @classmethod 116 | async def get_plugins(cls, bot_id: str, status: bool = True) -> list[str]: ... 117 | 118 | @classmethod 119 | async def get_plugins(cls, bot_id: str | None = None, status: bool = True): 120 | """ 121 | 获取bot插件 122 | 123 | 参数: 124 | bot_id (str | None, optional): bot_id. Defaults to None. 125 | status (bool, optional): 插件状态. Defaults to True. 126 | 127 | 返回: 128 | list[tuple[str, str]] | str: 插件 129 | """ 130 | if not bot_id: 131 | plugin_field = "available_plugins" if status else "block_plugins" 132 | data_list = await cls.all().values_list("bot_id", plugin_field) 133 | return {k: cls.convert_module_format(v) for k, v in data_list} 134 | 135 | result = await cls.get_or_none(bot_id=bot_id) 136 | if result: 137 | plugins = result.available_plugins if status else result.block_plugins 138 | return cls.convert_module_format(plugins) 139 | return [] 140 | 141 | @classmethod 142 | async def set_bot_status(cls, status: bool, bot_id: str | None = None) -> None: 143 | """ 144 | 设置bot状态 145 | 146 | 参数: 147 | status (bool): 状态 148 | bot_id (str, optional): bot_id. Defaults to None. 149 | 150 | Raises: 151 | ValueError: 未找到 bot_id 152 | """ 153 | if bot_id: 154 | affected_rows = await cls.filter(bot_id=bot_id).update(status=status) 155 | if not affected_rows: 156 | raise ValueError(f"未找到 bot_id: {bot_id}") 157 | else: 158 | await cls.all().update(status=status) 159 | 160 | @overload 161 | @classmethod 162 | def convert_module_format(cls, data: str) -> list[str]: ... 163 | 164 | @overload 165 | @classmethod 166 | def convert_module_format(cls, data: list[str]) -> str: ... 167 | 168 | @classmethod 169 | def convert_module_format(cls, data: str | list[str]) -> str | list[str]: 170 | """ 171 | 在 ` None: 192 | """ 193 | 在 from_field 和 to_field 之间移动指定的 data 194 | 195 | 参数: 196 | bot_id (str): 目标 bot 的 ID 197 | from_field (str): 源字段名称 198 | to_field (str): 目标字段名称 199 | data (str): 要插入的内容 200 | 201 | Raises: 202 | ValueError: 如果 data 不在 from_field 和 to_field 中 203 | """ 204 | bot_data, _ = await cls.get_or_create(bot_id=bot_id) 205 | formatted_data = cls.format(data) 206 | 207 | from_list: str = getattr(bot_data, from_field) 208 | to_list: str = getattr(bot_data, to_field) 209 | 210 | if formatted_data not in (from_list + to_list): 211 | raise ValueError(f"{data} 不在源字段和目标字段中") 212 | 213 | if formatted_data in from_list: 214 | from_list = from_list.replace(formatted_data, "", 1) 215 | if formatted_data not in to_list: 216 | to_list += formatted_data 217 | 218 | setattr(bot_data, from_field, from_list) 219 | setattr(bot_data, to_field, to_list) 220 | 221 | await bot_data.save(update_fields=[from_field, to_field]) 222 | 223 | @classmethod 224 | async def disable_plugin(cls, bot_id: str | None, plugin_name: str) -> None: 225 | """ 226 | 禁用插件 227 | 228 | 参数: 229 | bot_id (str | None): bot_id 230 | plugin_name (str): 插件名称 231 | """ 232 | if bot_id: 233 | await cls._toggle_field( 234 | bot_id, 235 | "available_plugins", 236 | "block_plugins", 237 | plugin_name, 238 | ) 239 | else: 240 | bot_list = await cls.all() 241 | for bot in bot_list: 242 | await cls._toggle_field( 243 | bot.bot_id, 244 | "available_plugins", 245 | "block_plugins", 246 | plugin_name, 247 | ) 248 | 249 | @classmethod 250 | async def enable_plugin(cls, bot_id: str | None, plugin_name: str) -> None: 251 | """ 252 | 启用插件 253 | 254 | 参数: 255 | bot_id (str | None): bot_id 256 | plugin_name (str): 插件名称 257 | """ 258 | if bot_id: 259 | await cls._toggle_field( 260 | bot_id, 261 | "block_plugins", 262 | "available_plugins", 263 | plugin_name, 264 | ) 265 | else: 266 | bot_list = await cls.all() 267 | for bot in bot_list: 268 | await cls._toggle_field( 269 | bot.bot_id, 270 | "block_plugins", 271 | "available_plugins", 272 | plugin_name, 273 | ) 274 | 275 | @classmethod 276 | async def disable_task(cls, bot_id: str | None, task_name: str) -> None: 277 | """ 278 | 禁用被动技能 279 | 280 | 参数: 281 | bot_id (str | None): bot_id 282 | task_name (str): 被动技能名称 283 | """ 284 | if bot_id: 285 | await cls._toggle_field( 286 | bot_id, 287 | "available_tasks", 288 | "block_tasks", 289 | task_name, 290 | ) 291 | else: 292 | bot_list = await cls.all() 293 | for bot in bot_list: 294 | await cls._toggle_field( 295 | bot.bot_id, 296 | "available_tasks", 297 | "block_tasks", 298 | task_name, 299 | ) 300 | 301 | @classmethod 302 | async def enable_task(cls, bot_id: str | None, task_name: str) -> None: 303 | """ 304 | 启用被动技能 305 | 306 | 参数: 307 | bot_id (str | None): bot_id 308 | task_name (str): 被动技能名称 309 | """ 310 | if bot_id: 311 | await cls._toggle_field( 312 | bot_id, 313 | "block_tasks", 314 | "available_tasks", 315 | task_name, 316 | ) 317 | else: 318 | bot_list = await cls.all() 319 | for bot in bot_list: 320 | await cls._toggle_field( 321 | bot.bot_id, 322 | "block_tasks", 323 | "available_tasks", 324 | task_name, 325 | ) 326 | 327 | @classmethod 328 | async def disable_all( 329 | cls, 330 | bot_id: str, 331 | feat: Literal["plugins", "tasks"], 332 | ) -> None: 333 | """ 334 | 禁用全部插件或被动技能 335 | 336 | 参数: 337 | bot_id (str): bot_id 338 | feat (Literal["plugins", "tasks"]): 插件或被动技能 339 | """ 340 | bot_data, _ = await cls.get_or_create(bot_id=bot_id) 341 | if feat == "plugins": 342 | available_plugins = cls.convert_module_format(bot_data.available_plugins) 343 | block_plugins = cls.convert_module_format(bot_data.block_plugins) 344 | bot_data.block_plugins = cls.convert_module_format( 345 | available_plugins + block_plugins 346 | ) 347 | bot_data.available_plugins = "" 348 | elif feat == "tasks": 349 | available_tasks = cls.convert_module_format(bot_data.available_tasks) 350 | block_tasks = cls.convert_module_format(bot_data.block_tasks) 351 | bot_data.block_tasks = cls.convert_module_format( 352 | available_tasks + block_tasks 353 | ) 354 | bot_data.available_tasks = "" 355 | await bot_data.save( 356 | update_fields=[ 357 | "available_tasks", 358 | "block_tasks", 359 | "available_plugins", 360 | "block_plugins", 361 | ] 362 | ) 363 | 364 | @classmethod 365 | async def enable_all( 366 | cls, 367 | bot_id: str, 368 | feat: Literal["plugins", "tasks"], 369 | ) -> None: 370 | """ 371 | 启用全部插件或被动技能 372 | 373 | 参数: 374 | bot_id (str): bot_id 375 | feat (Literal["plugins", "tasks"]): 插件或被动技能 376 | """ 377 | bot_data, _ = await cls.get_or_create(bot_id=bot_id) 378 | if feat == "plugins": 379 | available_plugins = cls.convert_module_format(bot_data.available_plugins) 380 | block_plugins = cls.convert_module_format(bot_data.block_plugins) 381 | bot_data.available_plugins = cls.convert_module_format( 382 | available_plugins + block_plugins 383 | ) 384 | bot_data.block_plugins = "" 385 | elif feat == "tasks": 386 | available_tasks = cls.convert_module_format(bot_data.available_tasks) 387 | block_tasks = cls.convert_module_format(bot_data.block_tasks) 388 | bot_data.available_tasks = cls.convert_module_format( 389 | available_tasks + block_tasks 390 | ) 391 | bot_data.block_tasks = "" 392 | await bot_data.save( 393 | update_fields=[ 394 | "available_tasks", 395 | "block_tasks", 396 | "available_plugins", 397 | "block_plugins", 398 | ] 399 | ) 400 | 401 | @classmethod 402 | async def is_block_plugin(cls, bot_id: str, plugin_name: str) -> bool: 403 | """ 404 | 检查插件是否被禁用 405 | 406 | 参数: 407 | bot_id (str): bot_id 408 | plugin_name (str): 插件某款 409 | 410 | 返回: 411 | bool: 是否被禁用 412 | """ 413 | bot_data, _ = await cls.get_or_create(bot_id=bot_id) 414 | return cls.format(plugin_name) in bot_data.block_plugins 415 | 416 | @classmethod 417 | async def is_block_task(cls, bot_id: str, task_name: str) -> bool: 418 | """ 419 | 检查被动技能是否被禁用 420 | 421 | 参数: 422 | bot_id (str): bot_id 423 | task_name (str): 被动技能名称 424 | 425 | 返回: 426 | bool: 是否被禁用 427 | """ 428 | bot_data, _ = await cls.get_or_create(bot_id=bot_id) 429 | return cls.format(task_name) in bot_data.block_tasks 430 | 431 | @classmethod 432 | async def _run_script(cls): 433 | return [] 434 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/models/group_console.py: -------------------------------------------------------------------------------- 1 | from tortoise import fields 2 | from typing_extensions import Self 3 | 4 | from ..db_connect import Model 5 | 6 | 7 | class GroupConsole(Model): 8 | id = fields.IntField(pk=True, generated=True, auto_increment=True) 9 | """自增id""" 10 | group_id = fields.CharField(255, description="群组id") 11 | """群聊id""" 12 | channel_id = fields.CharField(255, null=True, description="频道id") 13 | """频道id""" 14 | group_name = fields.TextField(default="", description="群组名称") 15 | """群聊名称""" 16 | max_member_count = fields.IntField(default=0, description="最大人数") 17 | """最大人数""" 18 | member_count = fields.IntField(default=0, description="当前人数") 19 | """当前人数""" 20 | status = fields.BooleanField(default=True, description="群状态") 21 | """群状态""" 22 | level = fields.IntField(default=5, description="群权限") 23 | """群权限""" 24 | is_super = fields.BooleanField( 25 | default=False, description="超级用户指定,可以使用全局关闭的功能" 26 | ) 27 | """超级用户指定群,可以使用全局关闭的功能""" 28 | block_plugin = fields.TextField(default="", description="禁用插件") 29 | """禁用插件""" 30 | superuser_block_plugin = fields.TextField( 31 | default="", description="超级用户禁用插件" 32 | ) 33 | 34 | class Meta: # type: ignore 35 | table = "zxpm_group_console" 36 | table_description = "群组信息表" 37 | unique_together = ("group_id", "channel_id") 38 | 39 | @classmethod 40 | async def get_group( 41 | cls, group_id: str, channel_id: str | None = None 42 | ) -> Self | None: 43 | """获取群组 44 | 45 | 参数: 46 | group_id: 群组id 47 | channel_id: 频道id. 48 | 49 | 返回: 50 | Self: GroupConsole 51 | """ 52 | if channel_id: 53 | return await cls.get_or_none(group_id=group_id, channel_id=channel_id) 54 | return await cls.get_or_none(group_id=group_id, channel_id__isnull=True) 55 | 56 | @classmethod 57 | async def is_super_group(cls, group_id: str) -> bool: 58 | """是否超级用户指定群 59 | 60 | 参数: 61 | group_id: 群组id 62 | 63 | 返回: 64 | bool: 是否超级用户指定群 65 | """ 66 | return group.is_super if (group := await cls.get_group(group_id)) else False 67 | 68 | @classmethod 69 | async def is_superuser_block_plugin(cls, group_id: str, module: str) -> bool: 70 | """查看群组是否超级用户禁用功能 71 | 72 | 参数: 73 | group_id: 群组id 74 | module: 模块名称 75 | 76 | 返回: 77 | bool: 是否禁用被动 78 | """ 79 | return await cls.exists( 80 | group_id=group_id, 81 | superuser_block_plugin__contains=f"<{module},", 82 | ) 83 | 84 | @classmethod 85 | async def is_block_plugin(cls, group_id: str, module: str) -> bool: 86 | """查看群组是否禁用插件 87 | 88 | 参数: 89 | group_id: 群组id 90 | plugin: 插件名称 91 | 92 | 返回: 93 | bool: 是否禁用插件 94 | """ 95 | return await cls.exists( 96 | group_id=group_id, block_plugin__contains=f"<{module}," 97 | ) or await cls.exists( 98 | group_id=group_id, superuser_block_plugin__contains=f"<{module}," 99 | ) 100 | 101 | @classmethod 102 | async def set_block_plugin( 103 | cls, 104 | group_id: str, 105 | module: str, 106 | is_superuser: bool = False, 107 | ): 108 | """禁用群组插件 109 | 110 | 参数: 111 | group_id: 群组id 112 | task: 任务模块 113 | is_superuser: 是否为超级用户 114 | """ 115 | group, _ = await cls.get_or_create(group_id=group_id) 116 | if is_superuser: 117 | if f"<{module}," not in group.superuser_block_plugin: 118 | group.superuser_block_plugin += f"<{module}," 119 | elif f"<{module}," not in group.block_plugin: 120 | group.block_plugin += f"<{module}," 121 | await group.save(update_fields=["block_plugin", "superuser_block_plugin"]) 122 | 123 | @classmethod 124 | async def set_unblock_plugin( 125 | cls, 126 | group_id: str, 127 | module: str, 128 | is_superuser: bool = False, 129 | ): 130 | """禁用群组插件 131 | 132 | 参数: 133 | group_id: 群组id 134 | task: 任务模块 135 | is_superuser: 是否为超级用户 136 | """ 137 | group, _ = await cls.get_or_create(group_id=group_id) 138 | if is_superuser: 139 | if f"<{module}," in group.superuser_block_plugin: 140 | group.superuser_block_plugin = group.superuser_block_plugin.replace( 141 | f"<{module},", "" 142 | ) 143 | elif f"<{module}," in group.block_plugin: 144 | group.block_plugin = group.block_plugin.replace(f"<{module},", "") 145 | await group.save(update_fields=["block_plugin", "superuser_block_plugin"]) 146 | 147 | @classmethod 148 | async def is_normal_block_plugin( 149 | cls, group_id: str, module: str, channel_id: str | None = None 150 | ) -> bool: 151 | """查看群组是否禁用功能 152 | 153 | 参数: 154 | group_id: 群组id 155 | module: 模块名称 156 | channel_id: 频道id 157 | 158 | 返回: 159 | bool: 是否禁用被动 160 | """ 161 | return await cls.exists( 162 | group_id=group_id, 163 | channel_id=channel_id, 164 | block_plugin__contains=f"<{module},", 165 | ) 166 | 167 | @classmethod 168 | async def is_superuser_block_task(cls, group_id: str, task: str) -> bool: 169 | """查看群组是否超级用户禁用被动 170 | 171 | 参数: 172 | group_id: 群组id 173 | task: 模块名称 174 | 175 | 返回: 176 | bool: 是否禁用被动 177 | """ 178 | return await cls.exists( 179 | group_id=group_id, 180 | superuser_block_task__contains=f"<{task},", 181 | ) 182 | 183 | @classmethod 184 | async def is_block_task( 185 | cls, group_id: str, task: str, channel_id: str | None = None 186 | ) -> bool: 187 | """查看群组是否禁用被动 188 | 189 | 参数: 190 | group_id: 群组id 191 | task: 任务模块 192 | channel_id: 频道id 193 | 194 | 返回: 195 | bool: 是否禁用被动 196 | """ 197 | if not channel_id: 198 | return await cls.exists( 199 | group_id=group_id, 200 | channel_id__isnull=True, 201 | block_task__contains=f"<{task},", 202 | ) or await cls.exists( 203 | group_id=group_id, 204 | channel_id__isnull=True, 205 | superuser_block_task__contains=f"<{task},", 206 | ) 207 | return await cls.exists( 208 | group_id=group_id, channel_id=channel_id, block_task__contains=f"<{task}," 209 | ) or await cls.exists( 210 | group_id=group_id, 211 | channel_id__isnull=True, 212 | superuser_block_task__contains=f"<{task},", 213 | ) 214 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/models/level_user.py: -------------------------------------------------------------------------------- 1 | from tortoise import fields 2 | 3 | from ..db_connect import Model 4 | 5 | 6 | class LevelUser(Model): 7 | id = fields.IntField(pk=True, generated=True, auto_increment=True) 8 | """自增id""" 9 | user_id = fields.CharField(255) 10 | """用户id""" 11 | group_id = fields.CharField(255) 12 | """群聊id""" 13 | user_level = fields.BigIntField() 14 | """用户权限等级""" 15 | group_flag = fields.IntField(default=0) 16 | """特殊标记,是否随群管理员变更而设置权限""" 17 | 18 | class Meta: # type: ignore 19 | table = "zxpm_level_users" 20 | table_description = "用户权限数据库" 21 | unique_together = ("user_id", "group_id") 22 | 23 | @classmethod 24 | async def get_user_level(cls, user_id: str, group_id: str | None) -> int: 25 | """获取用户在群内的等级 26 | 27 | 参数: 28 | user_id: 用户id 29 | group_id: 群组id 30 | 31 | 返回: 32 | int: 权限等级 33 | """ 34 | if not group_id: 35 | return 0 36 | if user := await cls.get_or_none(user_id=user_id, group_id=group_id): 37 | return user.user_level 38 | return 0 39 | 40 | @classmethod 41 | async def set_level( 42 | cls, 43 | user_id: str, 44 | group_id: str, 45 | level: int, 46 | group_flag: int = 0, 47 | ): 48 | """设置用户在群内的权限 49 | 50 | 参数: 51 | user_id: 用户id 52 | group_id: 群组id 53 | level: 权限等级 54 | group_flag: 是否被自动更新刷新权限 0:是, 1:否. 55 | """ 56 | await cls.update_or_create( 57 | user_id=user_id, 58 | group_id=group_id, 59 | defaults={ 60 | "user_level": level, 61 | "group_flag": group_flag, 62 | }, 63 | ) 64 | 65 | @classmethod 66 | async def delete_level(cls, user_id: str, group_id: str) -> bool: 67 | """删除用户权限 68 | 69 | 参数: 70 | user_id: 用户id 71 | group_id: 群组id 72 | 73 | 返回: 74 | bool: 是否含有用户权限 75 | """ 76 | if user := await cls.get_or_none(user_id=user_id, group_id=group_id): 77 | await user.delete() 78 | return True 79 | return False 80 | 81 | @classmethod 82 | async def check_level(cls, user_id: str, group_id: str | None, level: int) -> bool: 83 | """检查用户权限等级是否大于 level 84 | 85 | 参数: 86 | user_id: 用户id 87 | group_id: 群组id 88 | level: 权限等级 89 | 90 | 返回: 91 | bool: 是否大于level 92 | """ 93 | if group_id: 94 | if user := await cls.get_or_none(user_id=user_id, group_id=group_id): 95 | return user.user_level >= level 96 | else: 97 | if user_list := await cls.filter(user_id=user_id).all(): 98 | user = max(user_list, key=lambda x: x.user_level) 99 | return user.user_level >= level 100 | return False 101 | 102 | @classmethod 103 | async def is_group_flag(cls, user_id: str, group_id: str) -> bool: 104 | """检测是否会被自动更新刷新权限 105 | 106 | 参数: 107 | user_id: 用户id 108 | group_id: 群组id 109 | 110 | 返回: 111 | bool: 是否会被自动更新权限刷新 112 | """ 113 | if user := await cls.get_or_none(user_id=user_id, group_id=group_id): 114 | return user.group_flag == 1 115 | return False 116 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/models/plugin_info.py: -------------------------------------------------------------------------------- 1 | from tortoise import fields 2 | from typing_extensions import Self 3 | 4 | from ..db_connect import Model 5 | from ..enum import BlockType, PluginType 6 | from .plugin_limit import PluginLimit # noqa: F401 7 | 8 | 9 | class PluginInfo(Model): 10 | id = fields.IntField(pk=True, generated=True, auto_increment=True) 11 | """自增id""" 12 | module = fields.CharField(255, description="模块名") 13 | """模块名""" 14 | module_path = fields.CharField(255, description="模块路径", unique=True) 15 | """模块路径""" 16 | name = fields.CharField(255, description="插件名称") 17 | """插件名称""" 18 | status = fields.BooleanField(default=True, description="全局开关状态") 19 | """全局开关状态""" 20 | block_type: BlockType | None = fields.CharEnumField( 21 | BlockType, default=None, null=True, description="禁用类型" 22 | ) 23 | """禁用类型""" 24 | load_status = fields.BooleanField(default=True, description="加载状态") 25 | """加载状态""" 26 | author = fields.CharField(255, null=True, description="作者") 27 | """作者""" 28 | version = fields.CharField(max_length=255, null=True, description="版本") 29 | """版本""" 30 | level = fields.IntField(default=5, description="所需群权限") 31 | """所需群权限""" 32 | default_status = fields.BooleanField(default=True, description="进群默认开关状态") 33 | """进群默认开关状态""" 34 | limit_superuser = fields.BooleanField(default=False, description="是否限制超级用户") 35 | """是否限制超级用户""" 36 | menu_type = fields.CharField(max_length=255, default="", description="菜单类型") 37 | """菜单类型""" 38 | plugin_type = fields.CharEnumField(PluginType, null=True, description="插件类型") 39 | """插件类型""" 40 | cost_gold = fields.IntField(default=0, description="调用插件所需金币") 41 | """调用插件所需金币""" 42 | plugin_limit = fields.ReverseRelation["PluginLimit"] 43 | """插件限制""" 44 | admin_level = fields.IntField(default=0, null=True, description="调用所需权限等级") 45 | """调用所需权限等级""" 46 | is_delete = fields.BooleanField(default=False, description="是否删除") 47 | """是否删除""" 48 | parent = fields.CharField(max_length=255, null=True, description="父插件") 49 | """父插件""" 50 | 51 | class Meta: # type: ignore 52 | table = "zxpm_plugin_info" 53 | table_description = "插件基本信息" 54 | 55 | @classmethod 56 | async def get_plugin(cls, load_status: bool = True, **kwargs) -> Self | None: 57 | """获取插件列表 58 | 59 | 参数: 60 | load_status: 加载状态. 61 | 62 | 返回: 63 | Self | None: 插件 64 | """ 65 | return await cls.get_or_none(load_status=load_status, **kwargs) 66 | 67 | @classmethod 68 | async def get_plugins(cls, load_status: bool = True, **kwargs) -> list[Self]: 69 | """获取插件列表 70 | 71 | 参数: 72 | load_status: 加载状态. 73 | 74 | 返回: 75 | list[Self]: 插件列表 76 | """ 77 | return await cls.filter(load_status=load_status, **kwargs).all() 78 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/models/plugin_limit.py: -------------------------------------------------------------------------------- 1 | from tortoise import fields 2 | 3 | from ..db_connect import Model 4 | from ..enum import LimitCheckType, LimitWatchType, PluginLimitType 5 | 6 | 7 | class PluginLimit(Model): 8 | id = fields.IntField(pk=True, generated=True, auto_increment=True) 9 | """自增id""" 10 | module = fields.CharField(255, description="模块名") 11 | """模块名""" 12 | module_path = fields.CharField(255, description="模块路径") 13 | """模块路径""" 14 | plugin = fields.ForeignKeyField( 15 | "models.PluginInfo", 16 | related_name="plugin_limit", 17 | on_delete=fields.CASCADE, 18 | description="所属插件", 19 | ) 20 | """所属插件""" 21 | limit_type = fields.CharEnumField(PluginLimitType, description="限制类型") 22 | """限制类型""" 23 | watch_type = fields.CharEnumField(LimitWatchType, description="监听类型") 24 | """监听类型""" 25 | status = fields.BooleanField(default=True, description="限制的开关状态") 26 | """限制的开关状态""" 27 | check_type = fields.CharEnumField( 28 | LimitCheckType, default=LimitCheckType.ALL, description="检查类型" 29 | ) 30 | """检查类型""" 31 | result = fields.CharField(max_length=255, null=True, description="返回信息") 32 | """返回信息""" 33 | cd = fields.IntField(null=True, description="cd") 34 | """cd""" 35 | max_count = fields.IntField(null=True, description="最大调用次数") 36 | """最大调用次数""" 37 | 38 | class Meta: # type: ignore 39 | table = "zxpm_plugin_limit" 40 | table_description = "插件限制" 41 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/rules.py: -------------------------------------------------------------------------------- 1 | from nonebot.adapters import Bot, Event 2 | from nonebot.internal.rule import Rule 3 | from nonebot.permission import SUPERUSER 4 | from nonebot_plugin_session import EventSession, SessionLevel 5 | 6 | from .models.level_user import LevelUser 7 | 8 | 9 | def ensure_group(session: EventSession) -> bool: 10 | """ 11 | 是否在群聊中 12 | 13 | 参数: 14 | session: session 15 | 16 | 返回: 17 | bool: bool 18 | """ 19 | return session.level in [SessionLevel.LEVEL2, SessionLevel.LEVEL3] 20 | 21 | 22 | def admin_check(a: int | None = None) -> Rule: 23 | """ 24 | 管理员权限等级检查 25 | 26 | 参数: 27 | a: 权限等级或 配置项 module 28 | key: 配置项 key. 29 | 30 | 返回: 31 | Rule: Rule 32 | """ 33 | 34 | async def _rule(bot: Bot, event: Event, session: EventSession) -> bool: 35 | if await SUPERUSER(bot, event): 36 | return True 37 | if session.id1 and session.id2: 38 | level = a 39 | if level is not None: 40 | return bool( 41 | await LevelUser.check_level(session.id1, session.id2, int(level)) 42 | ) 43 | return False 44 | 45 | return Rule(_rule) 46 | 47 | 48 | def notice_rule(event_type: type | list[type]) -> Rule: 49 | """ 50 | Notice限制 51 | 52 | 参数: 53 | event_type: Event类型 54 | 55 | 返回: 56 | Rule: Rule 57 | """ 58 | 59 | async def _rule(event: Event) -> bool: 60 | if isinstance(event_type, list): 61 | for et in event_type: 62 | if isinstance(event, et): 63 | return True 64 | else: 65 | return isinstance(event, event_type) 66 | return False 67 | 68 | return Rule(_rule) 69 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/utils/build_image.py: -------------------------------------------------------------------------------- 1 | import math 2 | import uuid 3 | from io import BytesIO 4 | from pathlib import Path 5 | from typing import Literal, TypeAlias, overload 6 | 7 | from nonebot.utils import run_sync 8 | from PIL import Image, ImageDraw, ImageFont 9 | from PIL.Image import Image as tImage 10 | from PIL.ImageFont import FreeTypeFont 11 | from typing_extensions import Self 12 | 13 | from ..config import ZxpmConfig 14 | 15 | ModeType = Literal[ 16 | "1", "CMYK", "F", "HSV", "I", "L", "LAB", "P", "RGB", "RGBA", "RGBX", "YCbCr" 17 | ] 18 | """图片类型""" 19 | 20 | ColorAlias: TypeAlias = str | tuple[int, int, int] | tuple[int, int, int, int] | None 21 | 22 | CenterType = Literal["center", "height", "width"] 23 | """ 24 | 粘贴居中 25 | 26 | center: 水平垂直居中 27 | 28 | height: 垂直居中 29 | 30 | width: 水平居中 31 | """ 32 | 33 | DEFAULT_FONT = ImageFont.load_default() 34 | 35 | 36 | class BuildImage: 37 | """ 38 | 快捷生成图片与操作图片的工具类 39 | """ 40 | 41 | def __init__( 42 | self, 43 | width: int = 0, 44 | height: int = 0, 45 | color: ColorAlias = (255, 255, 255), 46 | mode: ModeType = "RGBA", 47 | font: str | Path | FreeTypeFont = "HYWenHei-85W.ttf", 48 | font_size: int = 20, 49 | background: str | BytesIO | Path | bytes | None = None, 50 | ) -> None: 51 | self.uid = uuid.uuid1() 52 | self.width = width 53 | self.height = height 54 | self.color = color 55 | self.font_size = font_size 56 | self.font = self.load_font(font_size) 57 | if background: 58 | if isinstance(background, bytes): 59 | self.markImg = Image.open(BytesIO(background)) 60 | else: 61 | self.markImg = Image.open(background) 62 | if width and height: 63 | self.markImg = self.markImg.resize((width, height), Image.LANCZOS) 64 | else: 65 | self.width = self.markImg.width 66 | self.height = self.markImg.height 67 | elif width and height: 68 | self.markImg = Image.new(mode, (width, height), color) # type: ignore 69 | else: 70 | raise ValueError("长度和宽度不能为空...") 71 | self.draw = ImageDraw.Draw(self.markImg) 72 | 73 | @property 74 | def size(self) -> tuple[int, int]: 75 | return self.markImg.size 76 | 77 | @classmethod 78 | def open(cls, path: str | Path | bytes) -> Self: 79 | """打开图片 80 | 81 | 参数: 82 | path: 图片路径 83 | 84 | 返回: 85 | Self: BuildImage 86 | """ 87 | return cls(background=path) 88 | 89 | @classmethod 90 | async def build_text_image( 91 | cls, 92 | text: str, 93 | font: str | FreeTypeFont | Path = "HYWenHei-85W.ttf", 94 | size: int = 10, 95 | font_color: str | tuple[int, int, int] = (0, 0, 0), 96 | color: ColorAlias = None, 97 | padding: int | tuple[int, int, int, int] | None = None, 98 | ) -> Self: 99 | """构建文本图片 100 | 101 | 参数: 102 | text: 文本 103 | font: 字体路径 104 | size: 字体大小 105 | font_color: 字体颜色. 106 | color: 背景颜色 107 | padding: 外边距 108 | 109 | 返回: 110 | Self: Self 111 | """ 112 | if not text.strip(): 113 | return cls(1, 1) 114 | _font = None 115 | if isinstance(font, FreeTypeFont): 116 | _font = font 117 | elif isinstance(font, str | Path): 118 | _font = cls.load_font(size) 119 | width, height = cls.get_text_size(text, _font) 120 | if isinstance(padding, int): 121 | width += padding * 2 122 | height += padding * 2 123 | elif isinstance(padding, tuple): 124 | width += padding[1] + padding[3] 125 | height += padding[0] + padding[2] 126 | markImg = cls(width, height, color, font=_font) 127 | await markImg.text( 128 | (0, 0), text, fill=font_color, font=_font, center_type="center" 129 | ) 130 | return markImg 131 | 132 | @classmethod 133 | async def auto_paste( 134 | cls, 135 | img_list: list[Self | tImage], 136 | row: int, 137 | space: int = 10, 138 | padding: int = 50, 139 | color: ColorAlias = (255, 255, 255), 140 | background: str | BytesIO | Path | None = None, 141 | ) -> Self: 142 | """自动贴图 143 | 144 | 参数: 145 | img_list: 图片列表 146 | row: 一行图片的数量 147 | space: 图片之间的间距. 148 | padding: 外边距. 149 | color: 图片背景颜色. 150 | background: 图片背景图片. 151 | 152 | 返回: 153 | Self: Self 154 | """ 155 | if not img_list: 156 | raise ValueError("贴图类别为空...") 157 | width, height = img_list[0].size 158 | background_width = width * row + space * (row - 1) + padding * 2 159 | row_count = math.ceil(len(img_list) / row) 160 | if row_count == 1: 161 | background_width = ( 162 | sum(img.width for img in img_list) + space * (row - 1) + padding * 2 163 | ) 164 | background_height = height * row_count + space * (row_count - 1) + padding * 2 165 | background_image = cls( 166 | background_width, background_height, color=color, background=background 167 | ) 168 | _cur_width, _cur_height = padding, padding 169 | for img in img_list: 170 | await background_image.paste(img, (_cur_width, _cur_height)) 171 | _cur_width += space + img.width 172 | if _cur_width + padding >= background_image.width: 173 | _cur_height += space + img.height 174 | _cur_width = padding 175 | return background_image 176 | 177 | @classmethod 178 | def load_font(cls, font_size: int = 10) -> FreeTypeFont: 179 | """加载字体 180 | 181 | 参数: 182 | font: 字体名称 183 | font_size: 字体大小 184 | 185 | 返回: 186 | FreeTypeFont: 字体 187 | """ 188 | return ImageFont.truetype(ZxpmConfig.zxpm_font, font_size) 189 | 190 | @overload 191 | @classmethod 192 | def get_text_size( 193 | cls, text: str, font: FreeTypeFont | None = None 194 | ) -> tuple[int, int]: ... 195 | 196 | @overload 197 | @classmethod 198 | def get_text_size( 199 | cls, text: str, font: str | None = None, font_size: int = 10 200 | ) -> tuple[int, int]: ... 201 | 202 | @classmethod 203 | def get_text_size( 204 | cls, 205 | text: str, 206 | font: str | FreeTypeFont | None = "HYWenHei-85W.ttf", 207 | font_size: int = 10, 208 | ) -> tuple[int, int]: # sourcery skip: remove-unnecessary-cast 209 | """获取该字体下文本需要的长宽 210 | 211 | 参数: 212 | text: 文本内容 213 | font: 字体名称或FreeTypeFont 214 | font_size: 字体大小 215 | 216 | 返回: 217 | tuple[int, int]: 长宽 218 | """ 219 | _font = font 220 | if font and isinstance(font, str): 221 | _font = cls.load_font(font_size) 222 | temp_image = Image.new("RGB", (1, 1), (255, 255, 255)) 223 | draw = ImageDraw.Draw(temp_image) 224 | text_box = draw.textbbox((0, 0), str(text), font=_font) # type: ignore 225 | text_width = text_box[2] - text_box[0] 226 | text_height = text_box[3] - text_box[1] 227 | return text_width, text_height + 10 228 | # return _font.getsize(str(text)) # type: ignore 229 | 230 | def getsize(self, msg: str) -> tuple[int, int]: 231 | # sourcery skip: remove-unnecessary-cast 232 | """ 233 | 获取文字在该图片 font_size 下所需要的空间 234 | 235 | 参数: 236 | msg: 文本 237 | 238 | 返回: 239 | tuple[int, int]: 长宽 240 | """ 241 | temp_image = Image.new("RGB", (1, 1), (255, 255, 255)) 242 | draw = ImageDraw.Draw(temp_image) 243 | text_box = draw.textbbox((0, 0), str(msg), font=self.font) 244 | text_width = text_box[2] - text_box[0] 245 | text_height = text_box[3] - text_box[1] 246 | return text_width, text_height + 10 247 | # return self.font.getsize(msg) # type: ignore 248 | 249 | def __center_xy( 250 | self, 251 | pos: tuple[int, int], 252 | width: int, 253 | height: int, 254 | center_type: CenterType | None, 255 | ) -> tuple[int, int]: 256 | """ 257 | 根据居中类型定位xy 258 | 259 | 参数: 260 | pos: 定位 261 | image: image 262 | center_type: 居中类型 263 | 264 | 返回: 265 | tuple[int, int]: 定位 266 | """ 267 | # _width, _height = pos 268 | if self.width and self.height: 269 | if center_type == "center": 270 | width = int((self.width - width) / 2) 271 | height = int((self.height - height) / 2) 272 | elif center_type == "width": 273 | width = int((self.width - width) / 2) 274 | height = pos[1] 275 | elif center_type == "height": 276 | width = pos[0] 277 | height = int((self.height - height) / 2) 278 | return width, height 279 | 280 | @run_sync 281 | def paste( 282 | self, 283 | image: Self | tImage, 284 | pos: tuple[int, int] = (0, 0), 285 | center_type: CenterType | None = None, 286 | ) -> Self: 287 | """贴图 288 | 289 | 参数: 290 | image: BuildImage 或 Image 291 | pos: 定位. 292 | center_type: 居中. 293 | 294 | 返回: 295 | BuildImage: Self 296 | 297 | 异常: 298 | ValueError: 居中类型错误 299 | """ 300 | if center_type and center_type not in ["center", "height", "width"]: 301 | raise ValueError("center_type must be 'center', 'width' or 'height'") 302 | _image = image 303 | if isinstance(image, BuildImage): 304 | _image = image.markImg 305 | if _image.width and _image.height and center_type: 306 | pos = self.__center_xy(pos, _image.width, _image.height, center_type) 307 | try: 308 | self.markImg.paste(_image, pos, _image) # type: ignore 309 | except ValueError: 310 | self.markImg.paste(_image, pos) # type: ignore 311 | return self 312 | 313 | @run_sync 314 | def text( 315 | self, 316 | pos: tuple[int, int], 317 | text: str, 318 | fill: str | tuple[int, int, int] = (0, 0, 0), 319 | center_type: CenterType | None = None, 320 | font: FreeTypeFont | str | Path | None = None, 321 | font_size: int = 10, 322 | ) -> Self: # sourcery skip: remove-unnecessary-cast 323 | """ 324 | 在图片上添加文字 325 | 326 | 参数: 327 | pos: 文字位置 328 | text: 文字内容 329 | fill: 文字颜色. 330 | center_type: 居中类型. 331 | font: 字体. 332 | font_size: 字体大小. 333 | 334 | 返回: 335 | BuildImage: Self 336 | 337 | 异常: 338 | ValueError: 居中类型错误 339 | """ 340 | if center_type and center_type not in ["center", "height", "width"]: 341 | raise ValueError("center_type must be 'center', 'width' or 'height'") 342 | max_length_text = "" 343 | sentence = str(text).split("\n") 344 | for x in sentence: 345 | max_length_text = x if len(x) > len(max_length_text) else max_length_text 346 | if font: 347 | if not isinstance(font, FreeTypeFont): 348 | font = self.load_font(font_size) 349 | else: 350 | font = self.font 351 | if center_type: 352 | ttf_w, ttf_h = self.getsize(max_length_text) # type: ignore 353 | # ttf_h = ttf_h * len(sentence) 354 | pos = self.__center_xy(pos, ttf_w, ttf_h, center_type) 355 | self.draw.text(pos, str(text), fill=fill, font=font) 356 | return self 357 | 358 | def show(self): 359 | """ 360 | 说明: 361 | 显示图片 362 | """ 363 | self.markImg.show() 364 | 365 | def pic2bytes(self) -> bytes: 366 | """获取bytes 367 | 368 | 返回: 369 | bytes: bytes 370 | """ 371 | buf = BytesIO() 372 | self.markImg.save(buf, format="PNG") 373 | return buf.getvalue() 374 | 375 | @run_sync 376 | def line( 377 | self, 378 | xy: tuple[int, int, int, int], 379 | fill: tuple[int, int, int] | str = "#D8DEE4", 380 | width: int = 1, 381 | ) -> Self: 382 | """ 383 | 画线 384 | 385 | 参数: 386 | xy: 坐标 387 | fill: 填充. 388 | width: 线宽. 389 | 390 | 返回: 391 | BuildImage: Self 392 | """ 393 | self.draw.line(xy, fill, width) 394 | return self 395 | 396 | @run_sync 397 | def circle_corner( 398 | self, 399 | radii: int = 30, 400 | point_list: list[Literal["lt", "rt", "lb", "rb"]] | None = None, 401 | ) -> Self: 402 | """ 403 | 矩形四角变圆 404 | 405 | 参数: 406 | radii: 半径. 407 | point_list: 需要变化的角. 408 | 409 | 返回: 410 | BuildImage: Self 411 | """ 412 | if point_list is None: 413 | point_list = ["lt", "rt", "lb", "rb"] 414 | # 画圆(用于分离4个角) 415 | img = self.markImg.convert("RGBA") 416 | alpha = img.split()[-1] 417 | circle = Image.new("L", (radii * 2, radii * 2), 0) 418 | draw = ImageDraw.Draw(circle) 419 | draw.ellipse((0, 0, radii * 2, radii * 2), fill=255) # 黑色方形内切白色圆形 420 | w, h = img.size 421 | if "lt" in point_list: 422 | alpha.paste(circle.crop((0, 0, radii, radii)), (0, 0)) 423 | if "rt" in point_list: 424 | alpha.paste(circle.crop((radii, 0, radii * 2, radii)), (w - radii, 0)) 425 | if "lb" in point_list: 426 | alpha.paste(circle.crop((0, radii, radii, radii * 2)), (0, h - radii)) 427 | if "rb" in point_list: 428 | alpha.paste( 429 | circle.crop((radii, radii, radii * 2, radii * 2)), 430 | (w - radii, h - radii), 431 | ) 432 | img.putalpha(alpha) 433 | self.markImg = img 434 | self.draw = ImageDraw.Draw(self.markImg) 435 | return self 436 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/utils/image_template.py: -------------------------------------------------------------------------------- 1 | import random 2 | from collections.abc import Callable 3 | from io import BytesIO 4 | from pathlib import Path 5 | 6 | from PIL.ImageFont import FreeTypeFont 7 | from pydantic import BaseModel 8 | 9 | from .build_image import BuildImage 10 | 11 | 12 | class RowStyle(BaseModel): 13 | font: FreeTypeFont | str | Path | None = "HYWenHei-85W.ttf" 14 | """字体""" 15 | font_size: int = 20 16 | """字体大小""" 17 | font_color: str | tuple[int, int, int] = (0, 0, 0) 18 | """字体颜色""" 19 | 20 | class Config: 21 | arbitrary_types_allowed = True 22 | 23 | 24 | class ImageTemplate: 25 | color_list = ["#C2CEFE", "#FFA94C", "#3FE6A0", "#D1D4F5"] # noqa: RUF012 26 | 27 | @classmethod 28 | async def hl_page( 29 | cls, 30 | head_text: str, 31 | items: dict[str, str], 32 | row_space: int = 10, 33 | padding: int = 30, 34 | ) -> BuildImage: 35 | """列文档 (如插件帮助) 36 | 37 | 参数: 38 | head_text: 头标签文本 39 | items: 列内容 40 | row_space: 列间距. 41 | padding: 间距. 42 | 43 | 返回: 44 | BuildImage: 图片 45 | """ 46 | font = BuildImage.load_font(20) 47 | width, height = BuildImage.get_text_size(head_text, font) 48 | for title, item in items.items(): 49 | title_width, title_height = await cls.__get_text_size(title, font) 50 | it_width, it_height = await cls.__get_text_size(item, font) 51 | width = max([width, title_width, it_width]) 52 | height += title_height + it_height 53 | width = max([width + padding * 2 + 100, 300]) 54 | height = max([height + padding * 2 + 150, 100]) 55 | A = BuildImage(width + padding * 2, height + padding * 2, color="#FAF9FE") 56 | top_head = BuildImage(width, 100, color="#FFFFFF", font_size=40) 57 | await top_head.line((0, 1, width, 1), "#C2CEFE", 2) 58 | await top_head.text((15, 20), head_text, "#9FA3B2", "center") 59 | await top_head.circle_corner() 60 | await A.paste(top_head, (0, 20), "width") 61 | _min_width = top_head.width - 60 62 | cur_h = top_head.height + 35 + row_space * len(items) 63 | for title, item in items.items(): 64 | title_width, title_height = BuildImage.get_text_size(title, font) 65 | title_background = BuildImage( 66 | title_width + 6, title_height + 10, font=font, color="#C1CDFF" 67 | ) 68 | await title_background.text((3, 5), title) 69 | await title_background.circle_corner(5) 70 | _text_width, _text_height = await cls.__get_text_size(item, font) 71 | _width = max([title_background.width, _text_width, _min_width]) 72 | text_image = await cls.__build_text_image( 73 | item, _width, _text_height, font, color="#FDFCFA" 74 | ) 75 | B = BuildImage(_width + 20, title_height + text_image.height + 40) 76 | await B.paste(title_background, (10, 10)) 77 | await B.paste(text_image, (10, 20 + title_background.height)) 78 | await B.line((0, 0, 0, B.height), random.choice(cls.color_list)) 79 | await A.paste(B, (0, cur_h), "width") 80 | cur_h += B.height + row_space 81 | return A 82 | 83 | @classmethod 84 | async def table_page( 85 | cls, 86 | head_text: str, 87 | tip_text: str | None, 88 | column_name: list[str], 89 | data_list: list[list[str | tuple[Path | BuildImage, int, int]]], 90 | row_space: int = 35, 91 | column_space: int = 30, 92 | padding: int = 5, 93 | text_style: Callable[[str, str], RowStyle] | None = None, 94 | ) -> BuildImage: 95 | """表格页 96 | 97 | 参数: 98 | head_text: 标题文本. 99 | tip_text: 标题注释. 100 | column_name: 表头列表. 101 | data_list: 数据列表. 102 | row_space: 行间距. 103 | column_space: 列间距. 104 | padding: 文本内间距. 105 | text_style: 文本样式. 106 | 107 | 返回: 108 | BuildImage: 表格图片 109 | """ 110 | font = BuildImage.load_font(font_size=50) 111 | min_width, _ = BuildImage.get_text_size(head_text, font) 112 | table = await cls.table( 113 | column_name, 114 | data_list, 115 | row_space, 116 | column_space, 117 | padding, 118 | text_style, 119 | ) 120 | await table.circle_corner() 121 | table_bk = BuildImage( 122 | max(table.width, min_width) + 100, table.height + 50, "#EAEDF2" 123 | ) 124 | await table_bk.paste(table, center_type="center") 125 | height = table_bk.height + 200 126 | background = BuildImage(table_bk.width, height, (255, 255, 255), font_size=50) 127 | await background.paste(table_bk, (0, 200)) 128 | await background.text((0, 50), head_text, "#334762", center_type="width") 129 | if tip_text: 130 | text_image = await BuildImage.build_text_image(tip_text, size=22) 131 | await background.paste(text_image, (0, 110), center_type="width") 132 | return background 133 | 134 | @classmethod 135 | async def table( 136 | cls, 137 | column_name: list[str], 138 | data_list: list[list[str | tuple[Path | BuildImage, int, int]]], 139 | row_space: int = 25, 140 | column_space: int = 10, 141 | padding: int = 5, 142 | text_style: Callable[[str, str], RowStyle] | None = None, 143 | ) -> BuildImage: 144 | """表格 145 | 146 | 参数: 147 | column_name: 表头列表 148 | data_list: 数据列表 149 | row_space: 行间距. 150 | column_space: 列间距. 151 | padding: 文本内间距. 152 | text_style: 文本样式. 153 | min_width: 最低宽度 154 | 155 | 返回: 156 | BuildImage: 表格图片 157 | """ 158 | font = BuildImage.load_font(20) 159 | column_data = [] 160 | for i in range(len(column_name)): 161 | c = [] 162 | for lst in data_list: 163 | if len(lst) > i: 164 | c.append(lst[i]) 165 | else: 166 | c.append("") 167 | column_data.append(c) 168 | build_data_list = [] 169 | _, base_h = BuildImage.get_text_size("A", font) 170 | for i, column_list in enumerate(column_data): 171 | name_width, _ = BuildImage.get_text_size(column_name[i], font) 172 | _temp = {"width": name_width, "data": column_list} 173 | for s in column_list: 174 | if isinstance(s, tuple): 175 | w = s[1] 176 | else: 177 | w, _ = BuildImage.get_text_size(s, font) 178 | if w > _temp["width"]: 179 | _temp["width"] = w 180 | build_data_list.append(_temp) 181 | column_image_list = [] 182 | column_name_image_list: list[BuildImage] = [] 183 | for i, data in enumerate(build_data_list): 184 | column_name_image = await BuildImage.build_text_image( 185 | column_name[i], font, 12, "#C8CCCF" 186 | ) 187 | column_name_image_list.append(column_name_image) 188 | max_h = max(c.height for c in column_name_image_list) 189 | for i, data in enumerate(build_data_list): 190 | width = data["width"] + padding * 2 191 | height = (base_h + row_space) * (len(data["data"]) + 1) + padding * 2 192 | background = BuildImage(width, height, (255, 255, 255)) 193 | column_name_image = column_name_image_list[i] 194 | await background.paste(column_name_image, (0, 20), center_type="width") 195 | cur_h = max_h + row_space + 20 196 | for item in data["data"]: 197 | style = RowStyle(font=font) 198 | if text_style: 199 | style = text_style(column_name[i], item) 200 | if isinstance(item, tuple): 201 | """图片""" 202 | data, width, height = item 203 | image_ = None 204 | if isinstance(data, Path): 205 | image_ = BuildImage(width, height, background=data) 206 | elif isinstance(data, bytes): 207 | image_ = BuildImage(width, height, background=BytesIO(data)) 208 | elif isinstance(data, BuildImage): 209 | image_ = data 210 | if image_: 211 | await background.paste(image_, (padding, cur_h)) 212 | else: 213 | await background.text( 214 | (padding, cur_h), 215 | item if item is not None else "", 216 | style.font_color, 217 | font=style.font, 218 | font_size=style.font_size, 219 | ) 220 | cur_h += base_h + row_space 221 | column_image_list.append(background) 222 | # height = max([bk.height for bk in column_image_list]) 223 | # width = sum([bk.width for bk in column_image_list]) 224 | return await BuildImage.auto_paste( 225 | column_image_list, len(column_image_list), column_space 226 | ) 227 | 228 | @classmethod 229 | async def __build_text_image( 230 | cls, 231 | text: str, 232 | width: int, 233 | height: int, 234 | font: FreeTypeFont, 235 | font_color: str | tuple[int, int, int] = (0, 0, 0), 236 | color: str | tuple[int, int, int] = (255, 255, 255), 237 | ) -> BuildImage: 238 | """文本转图片 239 | 240 | 参数: 241 | text: 文本 242 | width: 宽度 243 | height: 长度 244 | font: 字体 245 | font_color: 文本颜色 246 | color: 背景颜色 247 | 248 | 返回: 249 | BuildImage: 文本转图片 250 | """ 251 | _, h = BuildImage.get_text_size("A", font) 252 | A = BuildImage(width, height, color=color) 253 | cur_h = 0 254 | for s in text.split("\n"): 255 | text_image = await BuildImage.build_text_image( 256 | s, font, font_color=font_color 257 | ) 258 | await A.paste(text_image, (0, cur_h)) 259 | cur_h += h 260 | return A 261 | 262 | @classmethod 263 | async def __get_text_size( 264 | cls, 265 | text: str, 266 | font: FreeTypeFont, 267 | ) -> tuple[int, int]: 268 | """获取文本所占大小 269 | 270 | 参数: 271 | text: 文本 272 | font: 字体 273 | 274 | 返回: 275 | tuple[int, int]: 宽, 高 276 | """ 277 | width = 0 278 | height = 0 279 | _, h = BuildImage.get_text_size("A", font) 280 | for s in text.split("\n"): 281 | s = s.strip() or "A" 282 | w, _ = BuildImage.get_text_size(s, font) 283 | width = max(width, w) 284 | height += h 285 | return width, height 286 | -------------------------------------------------------------------------------- /nonebot_plugin_zxpm/utils/utils.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | from pathlib import Path 3 | 4 | import nonebot 5 | from nonebot_plugin_alconna import At, AtAll, Image, Text, UniMessage, Video, Voice 6 | from pydantic import BaseModel 7 | 8 | from ..log import logger 9 | 10 | driver = nonebot.get_driver() 11 | 12 | MESSAGE_TYPE = ( 13 | str 14 | | int 15 | | float 16 | | Path 17 | | bytes 18 | | BytesIO 19 | | At 20 | | AtAll 21 | | Image 22 | | Text 23 | | Voice 24 | | Video 25 | ) 26 | 27 | 28 | class Config(BaseModel): 29 | image_to_bytes: bool = False 30 | 31 | 32 | class MessageUtils: 33 | @classmethod 34 | def __build_message(cls, msg_list: list[MESSAGE_TYPE]) -> list[Text | Image]: 35 | """构造消息 36 | 37 | 参数: 38 | msg_list: 消息列表 39 | 40 | 返回: 41 | list[Text | Text]: 构造完成的消息列表 42 | """ 43 | message_list = [] 44 | for msg in msg_list: 45 | if isinstance(msg, Image | Text | At | AtAll | Video | Voice): 46 | message_list.append(msg) 47 | elif isinstance(msg, str | int | float): 48 | message_list.append(Text(str(msg))) 49 | elif isinstance(msg, Path): 50 | if msg.exists(): 51 | message_list.append(Image(path=msg)) 52 | else: 53 | logger.warning(f"图片路径不存在: {msg}") 54 | elif isinstance(msg, bytes): 55 | message_list.append(Image(raw=msg)) 56 | elif isinstance(msg, BytesIO): 57 | message_list.append(Image(raw=msg)) 58 | return message_list 59 | 60 | @classmethod 61 | def build_message( 62 | cls, msg_list: MESSAGE_TYPE | list[MESSAGE_TYPE | list[MESSAGE_TYPE]] 63 | ) -> UniMessage: 64 | """构造消息 65 | 66 | 参数: 67 | msg_list: 消息列表 68 | 69 | 返回: 70 | UniMessage: 构造完成的消息列表 71 | """ 72 | message_list = [] 73 | if not isinstance(msg_list, list): 74 | msg_list = [msg_list] 75 | for m in msg_list: 76 | _data = m if isinstance(m, list) else [m] 77 | message_list += cls.__build_message(_data) # type: ignore 78 | return UniMessage(message_list) 79 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "nonebot-plugin-zxpm" 3 | version = "0.2.7" 4 | description = "真寻的插件管理系统" 5 | authors = ["HibiKier <775757368@qq.com>"] 6 | license = "AGPL3.0" 7 | readme = "README.md" 8 | homepage = "https://github.com/HibiKier/nonebot-plugin-zxpm" 9 | repository = "https://github.com/HibiKier/nonebot-plugin-zxpm" 10 | 11 | [tool.poetry.dependencies] 12 | python = "^3.10" 13 | nonebot2 = "^2.3.3" 14 | nonebot-plugin-alconna = ">=0.46.3,<1.0.0" 15 | nonebot-plugin-session = ">=0.3.2" 16 | ruamel-yaml = "^0.18.6" 17 | strenum = "^0.4.15" 18 | nonebot-plugin-uninfo = ">=0.6.0" 19 | zhenxun-utils = "^0.2.2" 20 | pytz = "^2025.1" 21 | tortoise-orm = "^0.20" 22 | 23 | 24 | [tool.black] 25 | line-length = 88 26 | target-version = ["py39", "py310", "py311", "py312"] 27 | include = '\.pyi?$' 28 | extend-exclude = ''' 29 | ''' 30 | 31 | [tool.ruff] 32 | line-length = 88 33 | target-version = "py310" 34 | 35 | [tool.ruff.lint] 36 | select = [ 37 | "F", # Pyflakes 38 | "W", # pycodestyle warnings 39 | "E", # pycodestyle errors 40 | "UP", # pyupgrade 41 | "ASYNC", # flake8-async 42 | "C4", # flake8-comprehensions 43 | "T10", # flake8-debugger 44 | "T20", # flake8-print 45 | "PYI", # flake8-pyi 46 | "PT", # flake8-pytest-style 47 | "Q", # flake8-quotes 48 | "RUF", # Ruff-specific rules 49 | ] 50 | ignore = [ 51 | "E402", # module-import-not-at-top-of-file 52 | "UP037", # quoted-annotation 53 | "RUF001", # ambiguous-unicode-character-string 54 | "RUF002", # ambiguous-unicode-character-docstring 55 | "RUF003", # ambiguous-unicode-character-comment 56 | ] 57 | 58 | [tool.ruff.lint.flake8-pytest-style] 59 | fixture-parentheses = false 60 | mark-parentheses = false 61 | 62 | [tool.pyright] 63 | pythonVersion = "3.10" 64 | pythonPlatform = "All" 65 | defineConstant = { PYDANTIC_V2 = true } 66 | executionEnvironments = [ 67 | { root = "./tests", extraPaths = [ 68 | "./", 69 | ] }, 70 | { root = "./" }, 71 | ] 72 | 73 | typeCheckingMode = "standard" 74 | reportShadowedImports = false 75 | disableBytesTypePromotions = true 76 | 77 | [tool.pytest.ini_options] 78 | asyncio_mode = "auto" 79 | 80 | [build-system] 81 | requires = ["poetry-core>=1.0.0"] 82 | build-backend = "poetry.core.masonry.api" 83 | --------------------------------------------------------------------------------