├── .gitignore ├── LICENSE ├── README.md ├── docs_image ├── 0.png ├── 1.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── 6.png ├── 7.png ├── 8.png ├── tt.jpg ├── tt1.png ├── tt2.png └── tt3.png ├── nonebot_plugin_zxui ├── __init__.py ├── config.py ├── models │ ├── ban_console.py │ ├── bot_connect_log.py │ ├── bot_console.py │ ├── chat_history.py │ ├── fg_request.py │ ├── group_console.py │ ├── level_user.py │ ├── plugin_info.py │ ├── plugin_limit.py │ └── statistics.py ├── stat │ ├── __init__.py │ ├── chat_history │ │ ├── __init__.py │ │ └── chat_message.py │ ├── record_request.py │ └── statistics │ │ ├── __init__.py │ │ └── statistics_hook.py ├── web_ui │ ├── __init__.py │ ├── api │ │ ├── __init__.py │ │ ├── logs │ │ │ ├── __init__.py │ │ │ ├── log_manager.py │ │ │ └── logs.py │ │ ├── menu │ │ │ ├── __init__.py │ │ │ ├── data_source.py │ │ │ └── model.py │ │ └── tabs │ │ │ ├── __init__.py │ │ │ ├── dashboard │ │ │ ├── __init__.py │ │ │ ├── data_source.py │ │ │ └── model.py │ │ │ ├── database │ │ │ ├── __init__.py │ │ │ ├── data_source.py │ │ │ └── models │ │ │ │ ├── model.py │ │ │ │ └── sql_log.py │ │ │ ├── main │ │ │ ├── __init__.py │ │ │ ├── data_source.py │ │ │ └── model.py │ │ │ ├── manage │ │ │ ├── __init__.py │ │ │ ├── chat.py │ │ │ ├── data_source.py │ │ │ └── model.py │ │ │ ├── plugin_manage │ │ │ ├── __init__.py │ │ │ ├── data_source.py │ │ │ └── model.py │ │ │ └── system │ │ │ ├── __init__.py │ │ │ └── model.py │ ├── auth │ │ └── __init__.py │ ├── base_model.py │ ├── config.py │ ├── public │ │ ├── __init__.py │ │ └── data_source.py │ └── utils.py └── 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 │ ├── extra │ ├── __init__.py │ └── limit.py │ └── rules.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 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | ENV/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | # PyCharm 164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 166 | # and can be added to the global gitignore or merged into this file. For a more nuclear 167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 168 | #.idea/ 169 | 170 | # PyPI configuration file 171 | .pypirc 172 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | 7 |
8 | 9 |

10 | NoneBotPluginText 11 |

12 | 13 | # nonebot-plugin-zxui 14 | 15 | _✨ 基于 [NoneBot2](https://github.com/nonebot/nonebot2) 的 小真寻WebUi API实现 ✨_ 16 | 17 | ![python](https://img.shields.io/badge/python-v3.10%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 | 内置 [ZXPM插件管理](https://github.com/HibiKier/nonebot-plugin-zxpm)(帮助看这个readme) 29 | 30 | > [!NOTE] 31 | > 32 | >
小真寻也很可爱呀,也会很喜欢你!
33 | > 34 | >
35 | > 36 | > 37 | > 38 | >
39 | 40 | ## 💿 安装 41 | 42 | ```python 43 | pip install nonebot-plugin-zxui 44 | ``` 45 | 46 | ```python 47 | nb plugin install nonebot-plugin-zxui 48 | ``` 49 | 50 | ## ⚙️ 配置 51 | 52 | 在`.env`中添加`localstore`配置方便数据文件修改配置: 53 | 54 | ``` 55 | LOCALSTORE_PLUGIN_DATA_DIR='{ 56 | "nonebot_plugin_zxui": "data/zxui" 57 | } 58 | ' 59 | ``` 60 | 61 | ### ZXUI 62 | 63 | | 配置 | 类型 | 默认值 | 说明 | 64 | | :---------------------- | :--: | :---------------------------: | ---------------------------------------------------------------- | 65 | |zxui_db_url| str| | 数据库地址 URL,默认为 sqlite,存储路径在`zxpm_data_path`| 66 | | zxui_username | str | | 必填项,登录用户名 67 | | zxui_password | str | | 必填项,登录密码 68 | | zxui_enable_chat_history | bool | 开启消息存储 | 存储消息记录 69 | | zxui_enable_call_history | bool | 开启调用记录存储 | 存储功能调用记录 70 | 71 | 72 | ### ZXPM 73 | 74 | | 配置 | 类型 | 默认值 | 说明 | 75 | | :---------------------- | :--: | :---------------------------: | ---------------------------------------------------------------- | 76 | | zxpm_notice_info_cd | int | 300 | 群/用户权限检测等各种检测提示信息 cd,为 0 时或永久 ban 时不提醒 | 77 | | zxpm_ban_reply | str | 才不会给你发消息. | 用户被 ban 时回复消息,为空时不回复 | 78 | | zxpm_ban_level | int | 5 | 使用 ban 功能的对应权限 | 79 | | zxpm_switch_level | int | 1 | 使用开关功能的对应权限 | 80 | | zxpm_admin_default_auth | int | 5 | 群组管理员默认权限 | 81 | | zxpm_limit_superuser | bool | False | 是否限制超级用户 82 | 83 | 84 | ## 🎉 帮助 85 | 86 | ### 访问地址 87 | 88 | 默认地址为 `nb地址:nb端口` ,可以在nonebot配置文件.env一致。 89 | 例如 你的env中配置文件为 90 | ``` 91 | HOST=127.0.0.1 92 | PORT=8080 93 | ``` 94 | 那么访问地址为`http://127.0.0.1:8080` 95 | 96 | ### 菜单 97 | 98 | 菜单文件存储在`data/zxui/menu.json`,可以根据自身需求修改 99 | 格式如下: 100 | 101 | ```json 102 | [ 103 | { 104 | "module": "dashboard", 105 | "name": "仪表盘", 106 | "router": "\/dashboard", 107 | "icon": "dashboard", 108 | "default": true 109 | }, 110 | ] 111 | ``` 112 | 113 | ### 更新UI 114 | 115 | 删除`data/zxui/web_ui`文件夹,重新运行插件即可。 116 | 117 | ## 🎁 后台示例图 118 |
119 | 120 | ![x](https://raw.githubusercontent.com/HibiKier/nonebot-plugin-zxui/main/docs_image/8.png) 121 | ![x](https://raw.githubusercontent.com/HibiKier/nonebot-plugin-zxui/main/docs_image/0.png) 122 | ![x](https://raw.githubusercontent.com/HibiKier/nonebot-plugin-zxui/main/docs_image/1.png) 123 | ![x](https://raw.githubusercontent.com/HibiKier/nonebot-plugin-zxui/main/docs_image/2.png) 124 | ![x](https://raw.githubusercontent.com/HibiKier/nonebot-plugin-zxui/main/docs_image/3.png) 125 | 126 | ![x](https://raw.githubusercontent.com/HibiKier/nonebot-plugin-zxui/main/docs_image/5.png) 127 | ![x](https://raw.githubusercontent.com/HibiKier/nonebot-plugin-zxui/main/docs_image/6.png) 128 | ![x](https://raw.githubusercontent.com/HibiKier/nonebot-plugin-zxui/main/docs_image/7.png) 129 | 130 |
131 | 132 | ## ❤ 感谢 133 | 134 | - 可爱的小真寻 Bot [`zhenxun_bot`](https://github.com/HibiKier/zhenxun_bot): 我谢我自己,桀桀桀 135 | -------------------------------------------------------------------------------- /docs_image/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HibiKier/nonebot-plugin-zxui/956c88ddeef433865176dbc90f27f8ada004c381/docs_image/0.png -------------------------------------------------------------------------------- /docs_image/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HibiKier/nonebot-plugin-zxui/956c88ddeef433865176dbc90f27f8ada004c381/docs_image/1.png -------------------------------------------------------------------------------- /docs_image/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HibiKier/nonebot-plugin-zxui/956c88ddeef433865176dbc90f27f8ada004c381/docs_image/2.png -------------------------------------------------------------------------------- /docs_image/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HibiKier/nonebot-plugin-zxui/956c88ddeef433865176dbc90f27f8ada004c381/docs_image/3.png -------------------------------------------------------------------------------- /docs_image/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HibiKier/nonebot-plugin-zxui/956c88ddeef433865176dbc90f27f8ada004c381/docs_image/4.png -------------------------------------------------------------------------------- /docs_image/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HibiKier/nonebot-plugin-zxui/956c88ddeef433865176dbc90f27f8ada004c381/docs_image/5.png -------------------------------------------------------------------------------- /docs_image/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HibiKier/nonebot-plugin-zxui/956c88ddeef433865176dbc90f27f8ada004c381/docs_image/6.png -------------------------------------------------------------------------------- /docs_image/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HibiKier/nonebot-plugin-zxui/956c88ddeef433865176dbc90f27f8ada004c381/docs_image/7.png -------------------------------------------------------------------------------- /docs_image/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HibiKier/nonebot-plugin-zxui/956c88ddeef433865176dbc90f27f8ada004c381/docs_image/8.png -------------------------------------------------------------------------------- /docs_image/tt.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HibiKier/nonebot-plugin-zxui/956c88ddeef433865176dbc90f27f8ada004c381/docs_image/tt.jpg -------------------------------------------------------------------------------- /docs_image/tt1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HibiKier/nonebot-plugin-zxui/956c88ddeef433865176dbc90f27f8ada004c381/docs_image/tt1.png -------------------------------------------------------------------------------- /docs_image/tt2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HibiKier/nonebot-plugin-zxui/956c88ddeef433865176dbc90f27f8ada004c381/docs_image/tt2.png -------------------------------------------------------------------------------- /docs_image/tt3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HibiKier/nonebot-plugin-zxui/956c88ddeef433865176dbc90f27f8ada004c381/docs_image/tt3.png -------------------------------------------------------------------------------- /nonebot_plugin_zxui/__init__.py: -------------------------------------------------------------------------------- 1 | import nonebot 2 | from nonebot import require 3 | 4 | require("nonebot_plugin_localstore") 5 | require("nonebot_plugin_alconna") 6 | require("nonebot_plugin_session") 7 | require("nonebot_plugin_uninfo") 8 | require("nonebot_plugin_apscheduler") 9 | 10 | from nonebot.plugin import PluginMetadata, inherit_supported_adapters 11 | from zhenxun_db_client import client_db 12 | from zhenxun_utils.enum import PluginType 13 | 14 | from .config import Config 15 | from .config import config as PluginConfig 16 | from .stat import * # noqa: F403 17 | from .web_ui import * # noqa: F403 18 | from .zxpm import * # noqa: F403 19 | 20 | driver = nonebot.get_driver() 21 | 22 | 23 | @driver.on_startup 24 | async def _(): 25 | await client_db(PluginConfig.zxui_db_url) 26 | 27 | 28 | __plugin_meta__ = PluginMetadata( 29 | name="小真寻的WebUi", 30 | description="小真寻的WebUi", 31 | usage="", 32 | type="application", 33 | homepage="https://github.com/HibiKier/nonebot-plugin-zxui", 34 | config=Config, 35 | supported_adapters=inherit_supported_adapters( 36 | "nonebot_plugin_alconna", 37 | "nonebot_plugin_uninfo", 38 | "nonebot_plugin_session", 39 | ), 40 | extra={"author": "HibiKier", "plugin_type": PluginType.HIDDEN}, 41 | ) 42 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/config.py: -------------------------------------------------------------------------------- 1 | import nonebot 2 | import nonebot_plugin_localstore as store 3 | from pydantic import BaseModel 4 | 5 | store.get_plugin_data_dir() 6 | 7 | 8 | class Config(BaseModel): 9 | zxui_db_url: str = "" 10 | """数据库连接地址""" 11 | 12 | zxui_username: str 13 | """用户名""" 14 | 15 | zxui_password: str 16 | """密码""" 17 | 18 | zxui_enable_chat_history: bool = True 19 | """是否开启消息存储""" 20 | 21 | zxui_enable_call_history: bool = True 22 | """是否开启调用记录存储""" 23 | 24 | zxpm_notice_info_cd: int = 300 25 | """群/用户权限检测等各种检测提示信息cd,为0时不提醒""" 26 | zxpm_ban_reply: str = "才不会给你发消息." 27 | """用户被ban时回复消息,为空时不回复""" 28 | zxpm_ban_level: int = 5 29 | """使用ban功能的对应权限""" 30 | zxpm_switch_level: int = 1 31 | """群组插件开关管理对应权限""" 32 | zxpm_admin_default_auth: int = 5 33 | """群组管理员默认权限""" 34 | 35 | 36 | config = nonebot.get_plugin_config(Config) 37 | 38 | DATA_PATH = store.get_plugin_data_dir() 39 | 40 | if not config.zxui_db_url: 41 | db_path = DATA_PATH / "db" / "zhenxun.db" 42 | db_path.parent.mkdir(parents=True, exist_ok=True) 43 | config.zxui_db_url = f"sqlite:{db_path.absolute()}" 44 | 45 | 46 | SQL_TYPE = config.zxui_db_url.split(":")[0] 47 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/models/ban_console.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from tortoise import fields 4 | from typing_extensions import Self 5 | from zhenxun_db_client import Model 6 | from zhenxun_utils.exception import UserAndGroupIsNone 7 | from zhenxun_utils.log import logger 8 | 9 | 10 | class BanConsole(Model): 11 | id = fields.IntField(pk=True, generated=True, auto_increment=True) 12 | """自增id""" 13 | user_id = fields.CharField(255, null=True) 14 | """用户id""" 15 | group_id = fields.CharField(255, null=True) 16 | """群组id""" 17 | ban_level = fields.IntField() 18 | """使用ban命令的用户等级""" 19 | ban_time = fields.BigIntField() 20 | """ban开始的时间""" 21 | duration = fields.BigIntField() 22 | """ban时长""" 23 | operator = fields.CharField(255) 24 | """使用Ban命令的用户""" 25 | 26 | class Meta: # type: ignore 27 | table = "ban_console" 28 | table_description = "封禁人员/群组数据表" 29 | 30 | @classmethod 31 | async def _get_data(cls, user_id: str | None, group_id: str | None) -> Self | None: 32 | """获取数据 33 | 34 | 参数: 35 | user_id: 用户id 36 | group_id: 群组id 37 | 38 | 异常: 39 | UserAndGroupIsNone: 用户id和群组id都为空 40 | 41 | 返回: 42 | Self | None: Self 43 | """ 44 | if not user_id and not group_id: 45 | raise UserAndGroupIsNone() 46 | if user_id: 47 | return ( 48 | await cls.get_or_none(user_id=user_id, group_id=group_id) 49 | if group_id 50 | else await cls.get_or_none(user_id=user_id, group_id__isnull=True) 51 | ) 52 | else: 53 | return await cls.get_or_none(user_id="", group_id=group_id) 54 | 55 | @classmethod 56 | async def check_ban_level( 57 | cls, user_id: str | None, group_id: str | None, level: int 58 | ) -> bool: 59 | """检测ban掉目标的用户与unban用户的权限等级大小 60 | 61 | 参数: 62 | user_id: 用户id 63 | group_id: 群组id 64 | level: 权限等级 65 | 66 | 返回: 67 | bool: 权限判断,能否unban 68 | """ 69 | user = await cls._get_data(user_id, group_id) 70 | if user: 71 | logger.debug( 72 | f"检测用户被ban等级,user_level: {user.ban_level},level: {level}", 73 | target=f"{group_id}:{user_id}", 74 | ) 75 | return user.ban_level <= level 76 | return False 77 | 78 | @classmethod 79 | async def check_ban_time( 80 | cls, user_id: str | None, group_id: str | None = None 81 | ) -> int: 82 | """检测用户被ban时长 83 | 84 | 参数: 85 | user_id: 用户id 86 | 87 | 返回: 88 | int: ban剩余时长,-1时为永久ban,0表示未被ban 89 | """ 90 | logger.debug("获取用户ban时长", target=f"{group_id}:{user_id}") 91 | user = await cls._get_data(user_id, group_id) 92 | if not user and user_id: 93 | user = await cls._get_data(user_id, None) 94 | if user: 95 | if user.duration == -1: 96 | return -1 97 | _time = time.time() - (user.ban_time + user.duration) 98 | return 0 if _time > 0 else int(time.time() - user.ban_time - user.duration) 99 | return 0 100 | 101 | @classmethod 102 | async def is_ban(cls, user_id: str | None, group_id: str | None = None) -> bool: 103 | """判断用户是否被ban 104 | 105 | 参数: 106 | user_id: 用户id 107 | 108 | 返回: 109 | bool: 是否被ban 110 | """ 111 | logger.debug("检测是否被ban", target=f"{group_id}:{user_id}") 112 | if await cls.check_ban_time(user_id, group_id): 113 | return True 114 | else: 115 | await cls.unban(user_id, group_id) 116 | return False 117 | 118 | @classmethod 119 | async def ban( 120 | cls, 121 | user_id: str | None, 122 | group_id: str | None, 123 | ban_level: int, 124 | duration: int, 125 | operator: str | None = None, 126 | ): 127 | """ban掉目标用户 128 | 129 | 参数: 130 | user_id: 用户id 131 | group_id: 群组id 132 | ban_level: 使用命令者的权限等级 133 | duration: 时长,分钟,-1时为永久 134 | operator: 操作者id 135 | """ 136 | logger.debug( 137 | f"封禁用户/群组,等级:{ban_level},时长: {duration}", 138 | target=f"{group_id}:{user_id}", 139 | ) 140 | target = await cls._get_data(user_id, group_id) 141 | if target: 142 | await cls.unban(user_id, group_id) 143 | await cls.create( 144 | user_id=user_id, 145 | group_id=group_id, 146 | ban_level=ban_level, 147 | ban_time=int(time.time()), 148 | duration=duration, 149 | operator=operator or 0, 150 | ) 151 | 152 | @classmethod 153 | async def unban(cls, user_id: str | None, group_id: str | None = None) -> bool: 154 | """unban用户 155 | 156 | 参数: 157 | user_id: 用户id 158 | group_id: 群组id 159 | 160 | 返回: 161 | bool: 是否被ban 162 | """ 163 | user = await cls._get_data(user_id, group_id) 164 | if user: 165 | logger.debug("解除封禁", target=f"{group_id}:{user_id}") 166 | await user.delete() 167 | return True 168 | return False 169 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/models/bot_connect_log.py: -------------------------------------------------------------------------------- 1 | from tortoise import fields 2 | from zhenxun_db_client import Model 3 | 4 | 5 | class BotConnectLog(Model): 6 | id = fields.IntField(pk=True, generated=True, auto_increment=True) 7 | """自增id""" 8 | bot_id = fields.CharField(255, description="Bot id") 9 | """Bot id""" 10 | platform = fields.CharField(255, null=True, description="平台") 11 | """平台""" 12 | connect_time = fields.DatetimeField(description="连接时间") 13 | """日期""" 14 | type = fields.IntField(null=True, description="1: 连接, 0: 断开") 15 | """1: 连接, 0: 断开""" 16 | create_time = fields.DatetimeField(auto_now_add=True) 17 | """创建时间""" 18 | 19 | class Meta: # type: ignore 20 | table = "bot_connect_log" 21 | table_description = "bot连接表" 22 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/models/chat_history.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from typing import Literal 3 | from typing_extensions import Self 4 | 5 | from tortoise import fields 6 | from tortoise.functions import Count 7 | from zhenxun_db_client import Model 8 | 9 | 10 | class ChatHistory(Model): 11 | id = fields.IntField(pk=True, generated=True, auto_increment=True) 12 | """自增id""" 13 | user_id = fields.CharField(255) 14 | """用户id""" 15 | group_id = fields.CharField(255, null=True) 16 | """群聊id""" 17 | text = fields.TextField(null=True) 18 | """文本内容""" 19 | plain_text = fields.TextField(null=True) 20 | """纯文本""" 21 | create_time = fields.DatetimeField(auto_now_add=True) 22 | """创建时间""" 23 | bot_id = fields.CharField(255, null=True) 24 | """bot记录id""" 25 | platform = fields.CharField(255, null=True) 26 | """平台""" 27 | 28 | class Meta: # type: ignore 29 | table = "chat_history" 30 | table_description = "聊天记录数据表" 31 | 32 | @classmethod 33 | async def get_group_msg_rank( 34 | cls, 35 | gid: str | None, 36 | limit: int = 10, 37 | order: str = "DESC", 38 | date_scope: tuple[datetime, datetime] | None = None, 39 | ) -> list[Self]: 40 | """获取排行数据 41 | 42 | 参数: 43 | gid: 群号 44 | limit: 获取数量 45 | order: 排序类型,desc,des 46 | date_scope: 日期范围 47 | """ 48 | o = "-" if order == "DESC" else "" 49 | query = cls.filter(group_id=gid) if gid else cls 50 | if date_scope: 51 | query = query.filter(create_time__range=date_scope) 52 | return list( 53 | await query.annotate(count=Count("user_id")) 54 | .order_by(f"{o}count") 55 | .group_by("user_id") 56 | .limit(limit) 57 | .values_list("user_id", "count") 58 | ) # type: ignore 59 | 60 | @classmethod 61 | async def get_group_first_msg_datetime( 62 | cls, group_id: str | None 63 | ) -> datetime | None: 64 | """获取群第一条记录消息时间 65 | 66 | 参数: 67 | group_id: 群组id 68 | """ 69 | if group_id: 70 | message = ( 71 | await cls.filter(group_id=group_id).order_by("create_time").first() 72 | ) 73 | else: 74 | message = await cls.all().order_by("create_time").first() 75 | return message.create_time if message else None 76 | 77 | @classmethod 78 | async def get_message( 79 | cls, 80 | uid: str, 81 | gid: str, 82 | type_: Literal["user", "group"], 83 | msg_type: Literal["private", "group"] | None = None, 84 | days: int | tuple[datetime, datetime] | None = None, 85 | ) -> list[Self]: 86 | """获取消息查询query 87 | 88 | 参数: 89 | uid: 用户id 90 | gid: 群聊id 91 | type_: 类型,私聊或群聊 92 | msg_type: 消息类型,用户或群聊 93 | days: 限制日期 94 | """ 95 | if type_ == "user": 96 | query = cls.filter(user_id=uid) 97 | if msg_type == "private": 98 | query = query.filter(group_id__isnull=True) 99 | elif msg_type == "group": 100 | query = query.filter(group_id__not_isnull=True) 101 | else: 102 | query = cls.filter(group_id=gid) 103 | if uid: 104 | query = query.filter(user_id=uid) 105 | if days: 106 | if isinstance(days, int): 107 | query = query.filter( 108 | create_time__gte=datetime.now() - timedelta(days=days) 109 | ) 110 | elif isinstance(days, tuple): 111 | query = query.filter(create_time__range=days) 112 | return await query.all() # type: ignore 113 | 114 | @classmethod 115 | async def _run_script(cls): 116 | return [] 117 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/models/fg_request.py: -------------------------------------------------------------------------------- 1 | from nonebot.adapters import Bot 2 | from tortoise import fields 3 | from zhenxun_db_client import Model 4 | from zhenxun_utils.enum import RequestHandleType, RequestType 5 | from zhenxun_utils.exception import NotFoundError 6 | 7 | from .group_console import GroupConsole 8 | 9 | 10 | class FgRequest(Model): 11 | id = fields.IntField(pk=True, generated=True, auto_increment=True) 12 | """自增id""" 13 | request_type = fields.CharEnumField( 14 | RequestType, default=None, description="请求类型" 15 | ) 16 | """请求类型""" 17 | platform = fields.CharField(255, description="平台") 18 | """平台""" 19 | bot_id = fields.CharField(255, description="Bot Id") 20 | """botId""" 21 | flag = fields.CharField(max_length=255, default="", description="flag") 22 | """flag""" 23 | user_id = fields.CharField(max_length=255, description="请求用户id") 24 | """请求用户id""" 25 | group_id = fields.CharField(max_length=255, null=True, description="邀请入群id") 26 | """邀请入群id""" 27 | nickname = fields.CharField(max_length=255, description="请求人名称") 28 | """对象名称""" 29 | comment = fields.CharField(max_length=255, null=True, description="验证信息") 30 | """验证信息""" 31 | handle_type = fields.CharEnumField( 32 | RequestHandleType, null=True, description="处理类型" 33 | ) 34 | """处理类型""" 35 | 36 | class Meta: # type: ignore 37 | table = "fg_request" 38 | table_description = "好友群组请求" 39 | 40 | @classmethod 41 | async def approve(cls, bot: Bot, id: int): 42 | """同意请求 43 | 44 | 参数: 45 | bot: Bot 46 | id: 请求id 47 | 48 | 异常: 49 | NotFoundError: 未发现请求 50 | """ 51 | await cls._handle_request(bot, id, RequestHandleType.APPROVE) 52 | 53 | @classmethod 54 | async def refused(cls, bot: Bot, id: int): 55 | """拒绝请求 56 | 57 | 参数: 58 | bot: Bot 59 | id: 请求id 60 | 61 | 异常: 62 | NotFoundError: 未发现请求 63 | """ 64 | await cls._handle_request(bot, id, RequestHandleType.REFUSED) 65 | 66 | @classmethod 67 | async def ignore(cls, id: int): 68 | """忽略请求 69 | 70 | 参数: 71 | id: 请求id 72 | 73 | 异常: 74 | NotFoundError: 未发现请求 75 | """ 76 | await cls._handle_request(None, id, RequestHandleType.IGNORE) 77 | 78 | @classmethod 79 | async def expire(cls, id: int): 80 | """忽略请求 81 | 82 | 参数: 83 | id: 请求id 84 | 85 | 异常: 86 | NotFoundError: 未发现请求 87 | """ 88 | await cls._handle_request(None, id, RequestHandleType.EXPIRE) 89 | 90 | @classmethod 91 | async def _handle_request( 92 | cls, 93 | bot: Bot | None, 94 | id: int, 95 | handle_type: RequestHandleType, 96 | ): 97 | """处理请求 98 | 99 | 参数: 100 | bot: Bot 101 | id: 请求id 102 | handle_type: 处理类型 103 | 104 | 异常: 105 | NotFoundError: 未发现请求 106 | """ 107 | req = await cls.get_or_none(id=id) 108 | if not req: 109 | raise NotFoundError 110 | req.handle_type = handle_type 111 | await req.save(update_fields=["handle_type"]) 112 | if bot and handle_type not in [ 113 | RequestHandleType.IGNORE, 114 | RequestHandleType.EXPIRE, 115 | ]: 116 | if req.request_type == RequestType.FRIEND: 117 | await bot.set_friend_add_request( 118 | flag=req.flag, approve=handle_type == RequestHandleType.APPROVE 119 | ) 120 | else: 121 | await GroupConsole.update_or_create( 122 | group_id=req.group_id, defaults={"group_flag": 1} 123 | ) 124 | await bot.set_group_add_request( 125 | flag=req.flag, 126 | sub_type="invite", 127 | approve=handle_type == RequestHandleType.APPROVE, 128 | ) 129 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/models/level_user.py: -------------------------------------------------------------------------------- 1 | from tortoise import fields 2 | from zhenxun_db_client import Model 3 | 4 | 5 | class LevelUser(Model): 6 | id = fields.IntField(pk=True, generated=True, auto_increment=True) 7 | """自增id""" 8 | user_id = fields.CharField(255) 9 | """用户id""" 10 | group_id = fields.CharField(255) 11 | """群聊id""" 12 | user_level = fields.BigIntField() 13 | """用户权限等级""" 14 | group_flag = fields.IntField(default=0) 15 | """特殊标记,是否随群管理员变更而设置权限""" 16 | 17 | class Meta: # type: ignore 18 | table = "level_users" 19 | table_description = "用户权限数据库" 20 | unique_together = ("user_id", "group_id") 21 | 22 | @classmethod 23 | async def get_user_level(cls, user_id: str, group_id: str | None) -> int: 24 | """获取用户在群内的等级 25 | 26 | 参数: 27 | user_id: 用户id 28 | group_id: 群组id 29 | 30 | 返回: 31 | int: 权限等级 32 | """ 33 | if not group_id: 34 | return 0 35 | if user := await cls.get_or_none(user_id=user_id, group_id=group_id): 36 | return user.user_level 37 | return 0 38 | 39 | @classmethod 40 | async def set_level( 41 | cls, 42 | user_id: str, 43 | group_id: str, 44 | level: int, 45 | group_flag: int = 0, 46 | ): 47 | """设置用户在群内的权限 48 | 49 | 参数: 50 | user_id: 用户id 51 | group_id: 群组id 52 | level: 权限等级 53 | group_flag: 是否被自动更新刷新权限 0:是, 1:否. 54 | """ 55 | await cls.update_or_create( 56 | user_id=user_id, 57 | group_id=group_id, 58 | defaults={ 59 | "user_level": level, 60 | "group_flag": group_flag, 61 | }, 62 | ) 63 | 64 | @classmethod 65 | async def delete_level(cls, user_id: str, group_id: str) -> bool: 66 | """删除用户权限 67 | 68 | 参数: 69 | user_id: 用户id 70 | group_id: 群组id 71 | 72 | 返回: 73 | bool: 是否含有用户权限 74 | """ 75 | if user := await cls.get_or_none(user_id=user_id, group_id=group_id): 76 | await user.delete() 77 | return True 78 | return False 79 | 80 | @classmethod 81 | async def check_level(cls, user_id: str, group_id: str | None, level: int) -> bool: 82 | """检查用户权限等级是否大于 level 83 | 84 | 参数: 85 | user_id: 用户id 86 | group_id: 群组id 87 | level: 权限等级 88 | 89 | 返回: 90 | bool: 是否大于level 91 | """ 92 | if group_id: 93 | if user := await cls.get_or_none(user_id=user_id, group_id=group_id): 94 | return user.user_level >= level 95 | else: 96 | if user_list := await cls.filter(user_id=user_id).all(): 97 | user = max(user_list, key=lambda x: x.user_level) 98 | return user.user_level >= level 99 | return False 100 | 101 | @classmethod 102 | async def is_group_flag(cls, user_id: str, group_id: str) -> bool: 103 | """检测是否会被自动更新刷新权限 104 | 105 | 参数: 106 | user_id: 用户id 107 | group_id: 群组id 108 | 109 | 返回: 110 | bool: 是否会被自动更新权限刷新 111 | """ 112 | if user := await cls.get_or_none(user_id=user_id, group_id=group_id): 113 | return user.group_flag == 1 114 | return False 115 | 116 | @classmethod 117 | async def _run_script(cls): 118 | return [] 119 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/models/plugin_info.py: -------------------------------------------------------------------------------- 1 | from typing_extensions import Self 2 | 3 | from tortoise import fields 4 | from zhenxun_db_client import Model 5 | from zhenxun_utils.enum import BlockType, PluginType 6 | 7 | from .plugin_limit import PluginLimit # noqa: F401 8 | 9 | 10 | class PluginInfo(Model): 11 | id = fields.IntField(pk=True, generated=True, auto_increment=True) 12 | """自增id""" 13 | module = fields.CharField(255, description="模块名") 14 | """模块名""" 15 | module_path = fields.CharField(255, description="模块路径", unique=True) 16 | """模块路径""" 17 | name = fields.CharField(255, description="插件名称") 18 | """插件名称""" 19 | status = fields.BooleanField(default=True, description="全局开关状态") 20 | """全局开关状态""" 21 | block_type: BlockType | None = fields.CharEnumField( 22 | BlockType, default=None, null=True, description="禁用类型" 23 | ) 24 | """禁用类型""" 25 | load_status = fields.BooleanField(default=True, description="加载状态") 26 | """加载状态""" 27 | author = fields.CharField(255, null=True, description="作者") 28 | """作者""" 29 | version = fields.CharField(max_length=255, null=True, description="版本") 30 | """版本""" 31 | level = fields.IntField(default=5, description="所需群权限") 32 | """所需群权限""" 33 | default_status = fields.BooleanField(default=True, description="进群默认开关状态") 34 | """进群默认开关状态""" 35 | limit_superuser = fields.BooleanField(default=False, description="是否限制超级用户") 36 | """是否限制超级用户""" 37 | menu_type = fields.CharField(max_length=255, default="", description="菜单类型") 38 | """菜单类型""" 39 | plugin_type = fields.CharEnumField(PluginType, null=True, description="插件类型") 40 | """插件类型""" 41 | cost_gold = fields.IntField(default=0, description="调用插件所需金币") 42 | """调用插件所需金币""" 43 | plugin_limit = fields.ReverseRelation["PluginLimit"] 44 | """插件限制""" 45 | admin_level = fields.IntField(default=0, null=True, description="调用所需权限等级") 46 | """调用所需权限等级""" 47 | is_delete = fields.BooleanField(default=False, description="是否删除") 48 | """是否删除""" 49 | parent = fields.CharField(max_length=255, null=True, description="父插件") 50 | """父插件""" 51 | is_show = fields.BooleanField(default=True, description="是否显示在帮助中") 52 | """是否显示在帮助中""" 53 | 54 | class Meta: # type: ignore 55 | table = "plugin_info" 56 | table_description = "插件基本信息" 57 | 58 | @classmethod 59 | async def get_plugin(cls, load_status: bool = True, **kwargs) -> Self | None: 60 | """获取插件列表 61 | 62 | 参数: 63 | load_status: 加载状态. 64 | 65 | 返回: 66 | Self | None: 插件 67 | """ 68 | return await cls.get_or_none(load_status=load_status, **kwargs) 69 | 70 | @classmethod 71 | async def get_plugins(cls, load_status: bool = True, **kwargs) -> list[Self]: 72 | """获取插件列表 73 | 74 | 参数: 75 | load_status: 加载状态. 76 | 77 | 返回: 78 | list[Self]: 插件列表 79 | """ 80 | return await cls.filter(load_status=load_status, **kwargs).all() 81 | 82 | @classmethod 83 | async def _run_script(cls): 84 | return [] 85 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/models/plugin_limit.py: -------------------------------------------------------------------------------- 1 | from tortoise import fields 2 | from zhenxun_db_client import Model 3 | from zhenxun_utils.enum import LimitCheckType, LimitWatchType, PluginLimitType 4 | 5 | 6 | class PluginLimit(Model): 7 | id = fields.IntField(pk=True, generated=True, auto_increment=True) 8 | """自增id""" 9 | module = fields.CharField(255, description="模块名") 10 | """模块名""" 11 | module_path = fields.CharField(255, description="模块路径") 12 | """模块路径""" 13 | plugin = fields.ForeignKeyField( 14 | "models.PluginInfo", 15 | related_name="plugin_limit", 16 | on_delete=fields.CASCADE, 17 | description="所属插件", 18 | ) 19 | """所属插件""" 20 | limit_type = fields.CharEnumField(PluginLimitType, description="限制类型") 21 | """限制类型""" 22 | watch_type = fields.CharEnumField(LimitWatchType, description="监听类型") 23 | """监听类型""" 24 | status = fields.BooleanField(default=True, description="限制的开关状态") 25 | """限制的开关状态""" 26 | check_type = fields.CharEnumField( 27 | LimitCheckType, default=LimitCheckType.ALL, description="检查类型" 28 | ) 29 | """检查类型""" 30 | result = fields.CharField(max_length=255, null=True, description="返回信息") 31 | """返回信息""" 32 | cd = fields.IntField(null=True, description="cd") 33 | """cd""" 34 | max_count = fields.IntField(null=True, description="最大调用次数") 35 | """最大调用次数""" 36 | 37 | class Meta: # type: ignore 38 | table = "plugin_limit" 39 | table_description = "插件限制" 40 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/models/statistics.py: -------------------------------------------------------------------------------- 1 | from tortoise import fields 2 | from zhenxun_db_client import Model 3 | 4 | 5 | class Statistics(Model): 6 | id = fields.IntField(pk=True, generated=True, auto_increment=True) 7 | """自增id""" 8 | user_id = fields.CharField(255) 9 | """用户id""" 10 | group_id = fields.CharField(255, null=True) 11 | """群聊id""" 12 | plugin_name = fields.CharField(255) 13 | """插件名称""" 14 | create_time = fields.DatetimeField(auto_now=True) 15 | """添加日期""" 16 | bot_id = fields.CharField(255, null=True) 17 | """Bot Id""" 18 | 19 | class Meta: # type: ignore 20 | table = "statistics" 21 | table_description = "插件调用统计数据库" 22 | 23 | @classmethod 24 | async def _run_script(cls): 25 | return [] 26 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/stat/__init__.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | from datetime import datetime 3 | 4 | import nonebot 5 | from nonebot.adapters import Bot 6 | from nonebot.drivers import Driver 7 | from tortoise.exceptions import IntegrityError 8 | from zhenxun_utils.log import logger 9 | from zhenxun_utils.platform import PlatformUtils 10 | 11 | from ..models.bot_connect_log import BotConnectLog 12 | from ..models.bot_console import BotConsole 13 | 14 | driver: Driver = nonebot.get_driver() 15 | 16 | 17 | @driver.on_bot_connect 18 | async def _(bot: Bot): 19 | logger.debug(f"Bot: {bot.self_id} 建立连接...") 20 | await BotConnectLog.create( 21 | bot_id=bot.self_id, platform=bot.adapter, connect_time=datetime.now(), type=1 22 | ) 23 | if not await BotConsole.exists(bot_id=bot.self_id): 24 | try: 25 | await BotConsole.create( 26 | bot_id=bot.self_id, platform=PlatformUtils.get_platform(bot) 27 | ) 28 | except IntegrityError: 29 | pass 30 | 31 | 32 | @driver.on_bot_disconnect 33 | async def _(bot: Bot): 34 | logger.debug(f"Bot: {bot.self_id} 断开连接...") 35 | await BotConnectLog.create( 36 | bot_id=bot.self_id, platform=bot.adapter, connect_time=datetime.now(), type=0 37 | ) 38 | 39 | 40 | from .chat_history import * # noqa: F403 41 | from .statistics import * # noqa: F403 42 | 43 | with contextlib.suppress(ImportError): 44 | from nonebot.adapters.onebot.v11 import GroupIncreaseNoticeEvent # noqa: F401 45 | 46 | from .record_request import * # noqa: F403 47 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/stat/chat_history/__init__.py: -------------------------------------------------------------------------------- 1 | from .chat_message import * # noqa: F403 2 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/stat/chat_history/chat_message.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_message 2 | from nonebot.plugin import PluginMetadata 3 | from nonebot_plugin_alconna import UniMsg 4 | from nonebot_plugin_apscheduler import scheduler 5 | from nonebot_plugin_uninfo import Uninfo 6 | from zhenxun_utils.enum import PluginType 7 | from zhenxun_utils.log import logger 8 | 9 | from ...config import config 10 | from ...models.chat_history import ChatHistory 11 | from ...zxpm.extra import PluginExtraData 12 | 13 | __plugin_meta__ = PluginMetadata( 14 | name="功能调用统计", 15 | description="功能调用统计", 16 | usage="""""".strip(), 17 | extra=PluginExtraData( 18 | author="HibiKier", version="0.1", plugin_type=PluginType.HIDDEN 19 | ).dict(), 20 | ) 21 | 22 | 23 | def rule(message: UniMsg) -> bool: 24 | return config.zxui_enable_chat_history and bool(message) 25 | 26 | 27 | chat_history = on_message(rule=rule, priority=1, block=False) 28 | 29 | 30 | TEMP_LIST = [] 31 | 32 | 33 | @chat_history.handle() 34 | async def _(message: UniMsg, session: Uninfo): 35 | group_id = session.group.id if session.group else None 36 | TEMP_LIST.append( 37 | ChatHistory( 38 | user_id=session.user.id, 39 | group_id=group_id, 40 | text=str(message), 41 | plain_text=message.extract_plain_text(), 42 | bot_id=session.self_id, 43 | platform=session.platform, 44 | ) 45 | ) 46 | 47 | 48 | @scheduler.scheduled_job( 49 | "interval", 50 | minutes=1, 51 | ) 52 | async def _(): 53 | try: 54 | message_list = TEMP_LIST.copy() 55 | TEMP_LIST.clear() 56 | if message_list: 57 | await ChatHistory.bulk_create(message_list) 58 | logger.debug(f"批量添加聊天记录 {len(message_list)} 条", "定时任务") 59 | except Exception as e: 60 | logger.error("定时批量添加聊天记录", "定时任务", e=e) 61 | 62 | 63 | # @test.handle() 64 | # async def _(event: MessageEvent): 65 | # print(await ChatHistory.get_user_msg(event.user_id, "private")) 66 | # print(await ChatHistory.get_user_msg_count(event.user_id, "private")) 67 | # print(await ChatHistory.get_user_msg(event.user_id, "group")) 68 | # print(await ChatHistory.get_user_msg_count(event.user_id, "group")) 69 | # print(await ChatHistory.get_group_msg(event.group_id)) 70 | # print(await ChatHistory.get_group_msg_count(event.group_id)) 71 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/stat/record_request.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from nonebot import on_message, on_request 4 | from nonebot.adapters.onebot.v11 import ( 5 | ActionFailed, 6 | FriendRequestEvent, 7 | GroupRequestEvent, 8 | ) 9 | from nonebot.adapters.onebot.v11 import Bot as v11Bot 10 | from nonebot.adapters.onebot.v12 import Bot as v12Bot 11 | from nonebot.plugin import PluginMetadata 12 | from nonebot_plugin_apscheduler import scheduler 13 | from nonebot_plugin_session import EventSession 14 | from zhenxun_utils.enum import PluginType, RequestHandleType, RequestType 15 | from zhenxun_utils.log import logger 16 | 17 | from ..models.fg_request import FgRequest 18 | from ..models.group_console import GroupConsole 19 | from ..zxpm.extra import PluginExtraData 20 | 21 | __plugin_meta__ = PluginMetadata( 22 | name="记录请求", 23 | description="记录 好友/群组 请求", 24 | usage="", 25 | extra=PluginExtraData( 26 | author="HibiKier", 27 | version="0.1", 28 | plugin_type=PluginType.HIDDEN, 29 | ).dict(), 30 | ) 31 | 32 | 33 | class Timer: 34 | data: dict[str, float] = {} # noqa: RUF012 35 | 36 | @classmethod 37 | def check(cls, uid: int | str): 38 | return True if uid not in cls.data else time.time() - cls.data[uid] > 5 * 60 39 | 40 | @classmethod 41 | def clear(cls): 42 | now = time.time() 43 | cls.data = {k: v for k, v in cls.data.items() if v - now < 5 * 60} 44 | 45 | 46 | # TODO: 其他平台请求 47 | 48 | friend_req = on_request(priority=5, block=True) 49 | group_req = on_request(priority=5, block=True) 50 | _t = on_message(priority=999, block=False, rule=lambda: False) 51 | 52 | 53 | @friend_req.handle() 54 | async def _(bot: v12Bot | v11Bot, event: FriendRequestEvent, session: EventSession): 55 | if event.user_id and Timer.check(event.user_id): 56 | logger.debug("收录好友请求...", "好友请求", target=event.user_id) 57 | user = await bot.get_stranger_info(user_id=event.user_id) 58 | nickname = user["nickname"] 59 | # sex = user["sex"] 60 | # age = str(user["age"]) 61 | comment = event.comment 62 | # 旧请求全部设置为过期 63 | await FgRequest.filter( 64 | request_type=RequestType.FRIEND, 65 | user_id=str(event.user_id), 66 | handle_type__isnull=True, 67 | ).update(handle_type=RequestHandleType.EXPIRE) 68 | await FgRequest.create( 69 | request_type=RequestType.FRIEND, 70 | platform=session.platform, 71 | bot_id=bot.self_id, 72 | flag=event.flag, 73 | user_id=event.user_id, 74 | nickname=nickname, 75 | comment=comment, 76 | ) 77 | else: 78 | logger.debug("好友请求五分钟内重复, 已忽略", "好友请求", target=event.user_id) 79 | 80 | 81 | @group_req.handle() 82 | async def _(bot: v12Bot | v11Bot, event: GroupRequestEvent, session: EventSession): 83 | if event.sub_type != "invite": 84 | return 85 | if str(event.user_id) in bot.config.superusers: 86 | try: 87 | logger.debug( 88 | "超级用户自动同意加入群聊", 89 | "群聊请求", 90 | session=event.user_id, 91 | target=event.group_id, 92 | ) 93 | group, _ = await GroupConsole.update_or_create( 94 | group_id=str(event.group_id), 95 | defaults={ 96 | "group_name": "", 97 | "max_member_count": 0, 98 | "member_count": 0, 99 | "group_flag": 1, 100 | }, 101 | ) 102 | await bot.set_group_add_request( 103 | flag=event.flag, sub_type="invite", approve=True 104 | ) 105 | if isinstance(bot, v11Bot): 106 | group_info = await bot.get_group_info(group_id=event.group_id) 107 | max_member_count = group_info["max_member_count"] 108 | member_count = group_info["member_count"] 109 | else: 110 | group_info = await bot.get_group_info(group_id=str(event.group_id)) 111 | max_member_count = 0 112 | member_count = 0 113 | group.max_member_count = max_member_count 114 | group.member_count = member_count 115 | group.group_name = group_info["group_name"] 116 | await group.save( 117 | update_fields=["group_name", "max_member_count", "member_count"] 118 | ) 119 | except ActionFailed as e: 120 | logger.error( 121 | "超级用户自动同意加入群聊发生错误", 122 | "群聊请求", 123 | session=event.user_id, 124 | target=event.group_id, 125 | e=e, 126 | ) 127 | elif Timer.check(f"{event.user_id}:{event.group_id}"): 128 | logger.debug( 129 | f"收录 用户[{event.user_id}] 群聊[{event.group_id}] 群聊请求", 130 | "群聊请求", 131 | target=event.group_id, 132 | ) 133 | # 旧请求全部设置为过期 134 | await FgRequest.filter( 135 | request_type=RequestType.GROUP, 136 | user_id=str(event.user_id), 137 | group_id=str(event.group_id), 138 | handle_type__isnull=True, 139 | ).update(handle_type=RequestHandleType.EXPIRE) 140 | await FgRequest.create( 141 | request_type=RequestType.GROUP, 142 | platform=session.platform, 143 | bot_id=bot.self_id, 144 | flag=event.flag, 145 | user_id=str(event.user_id), 146 | nickname="", 147 | group_id=str(event.group_id), 148 | ) 149 | else: 150 | logger.debug( 151 | "群聊请求五分钟内重复, 已忽略", 152 | "群聊请求", 153 | target=f"{event.user_id}:{event.group_id}", 154 | ) 155 | 156 | 157 | @scheduler.scheduled_job( 158 | "interval", 159 | minutes=5, 160 | ) 161 | async def _(): 162 | Timer.clear() 163 | 164 | 165 | async def _(): 166 | Timer.clear() 167 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/stat/statistics/__init__.py: -------------------------------------------------------------------------------- 1 | from .statistics_hook import * # noqa: F403 2 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/stat/statistics/statistics_hook.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from nonebot.adapters import Bot, Event 4 | from nonebot.matcher import Matcher 5 | from nonebot.message import run_postprocessor 6 | from nonebot.plugin import PluginMetadata 7 | from nonebot_plugin_apscheduler import scheduler 8 | from nonebot_plugin_uninfo import Uninfo 9 | from zhenxun_utils.enum import PluginType 10 | from zhenxun_utils.log import logger 11 | 12 | from ...config import config 13 | from ...models.plugin_info import PluginInfo 14 | from ...models.statistics import Statistics 15 | from ...zxpm.extra import PluginExtraData 16 | 17 | TEMP_LIST = [] 18 | 19 | __plugin_meta__ = PluginMetadata( 20 | name="功能调用统计", 21 | description="功能调用统计", 22 | usage="""""".strip(), 23 | extra=PluginExtraData( 24 | author="HibiKier", version="0.1", plugin_type=PluginType.HIDDEN 25 | ).dict(), 26 | ) 27 | 28 | 29 | @run_postprocessor 30 | async def _( 31 | matcher: Matcher, 32 | exception: Exception | None, 33 | bot: Bot, 34 | session: Uninfo, 35 | event: Event, 36 | ): 37 | if matcher.type in ["request", "notice"]: 38 | return 39 | if not config.zxui_enable_call_history: 40 | return 41 | if matcher.plugin: 42 | plugin = await PluginInfo.get_plugin(module_path=matcher.plugin.module_name) 43 | plugin_type = plugin.plugin_type if plugin else None 44 | if plugin_type == PluginType.NORMAL: 45 | logger.debug(f"提交调用记录: {matcher.plugin_name}...", session=session) 46 | TEMP_LIST.append( 47 | Statistics( 48 | user_id=session.user.id, 49 | group_id=session.group.id if session.group else None, 50 | plugin_name=matcher.plugin_name, 51 | create_time=datetime.now(), 52 | bot_id=bot.self_id, 53 | ) 54 | ) 55 | 56 | 57 | @scheduler.scheduled_job( 58 | "interval", 59 | minutes=1, 60 | ) 61 | async def _(): 62 | try: 63 | call_list = TEMP_LIST.copy() 64 | TEMP_LIST.clear() 65 | if call_list: 66 | await Statistics.bulk_create(call_list) 67 | logger.debug(f"批量添加调用记录 {len(call_list)} 条", "定时任务") 68 | except Exception as e: 69 | logger.error("定时批量添加调用记录", "定时任务", e=e) 70 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/web_ui/__init__.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import nonebot 4 | from zhenxun_utils.log import logger, logger_ 5 | 6 | try: 7 | from fastapi import APIRouter, FastAPI 8 | from fastapi.middleware.cors import CORSMiddleware 9 | 10 | app = nonebot.get_app() 11 | if app and isinstance(app, FastAPI): 12 | app.add_middleware( 13 | CORSMiddleware, 14 | allow_origins=["*"], 15 | allow_credentials=True, 16 | allow_methods=["*"], 17 | allow_headers=["*"], 18 | ) 19 | except Exception as e: 20 | logger.warning("加载FastAPI失败...", "WebUi", e=e) 21 | else: 22 | from nonebot.log import default_filter, default_format 23 | 24 | from .api.logs import router as ws_log_routes 25 | from .api.logs.log_manager import LOG_STORAGE 26 | from .api.menu import router as menu_router 27 | from .api.tabs.dashboard import router as dashboard_router 28 | from .api.tabs.database import router as database_router 29 | from .api.tabs.main import router as main_router 30 | from .api.tabs.main import ws_router as status_routes 31 | from .api.tabs.manage import router as manage_router 32 | from .api.tabs.manage.chat import ws_router as chat_routes 33 | from .api.tabs.plugin_manage import router as plugin_router 34 | from .api.tabs.system import router as system_router 35 | from .auth import router as auth_router 36 | from .public import init_public 37 | 38 | driver = nonebot.get_driver() 39 | 40 | BaseApiRouter = APIRouter(prefix="/zhenxun/api") 41 | 42 | BaseApiRouter.include_router(auth_router) 43 | BaseApiRouter.include_router(dashboard_router) 44 | BaseApiRouter.include_router(main_router) 45 | BaseApiRouter.include_router(manage_router) 46 | BaseApiRouter.include_router(database_router) 47 | BaseApiRouter.include_router(plugin_router) 48 | BaseApiRouter.include_router(system_router) 49 | BaseApiRouter.include_router(menu_router) 50 | 51 | WsApiRouter = APIRouter(prefix="/zhenxun/socket") 52 | 53 | WsApiRouter.include_router(ws_log_routes) 54 | WsApiRouter.include_router(status_routes) 55 | WsApiRouter.include_router(chat_routes) 56 | 57 | @driver.on_startup 58 | async def _(): 59 | try: 60 | 61 | async def log_sink(message: str): 62 | loop = None 63 | if not loop: 64 | try: 65 | loop = asyncio.get_running_loop() 66 | except Exception as e: 67 | logger.warning("Web Ui log_sink", e=e) 68 | if not loop: 69 | loop = asyncio.new_event_loop() 70 | loop.create_task(LOG_STORAGE.add(message.rstrip("\n"))) # noqa: RUF006 71 | 72 | logger_.add( 73 | log_sink, colorize=True, filter=default_filter, format=default_format 74 | ) 75 | 76 | app: FastAPI = nonebot.get_app() 77 | app.include_router(BaseApiRouter) 78 | app.include_router(WsApiRouter) 79 | await init_public(app) 80 | logger.info("API启动成功", "WebUi") 81 | except Exception as e: 82 | logger.error("API启动失败", "WebUi", e=e) 83 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/web_ui/api/__init__.py: -------------------------------------------------------------------------------- 1 | from .menu import * # noqa: F403f 2 | from .tabs import * # noqa: F403f 3 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/web_ui/api/logs/__init__.py: -------------------------------------------------------------------------------- 1 | from .logs import * # noqa: F403 2 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/web_ui/api/logs/log_manager.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from collections.abc import Awaitable, Callable 3 | from typing import Generic, TypeVar 4 | 5 | _T = TypeVar("_T") 6 | LogListener = Callable[[_T], Awaitable[None]] 7 | 8 | 9 | class LogStorage(Generic[_T]): 10 | """ 11 | 日志存储 12 | """ 13 | 14 | def __init__(self, rotation: float = 5 * 60): 15 | self.count, self.rotation = 0, rotation 16 | self.logs: dict[int, str] = {} 17 | self.listeners: set[LogListener[str]] = set() 18 | 19 | async def add(self, log: str): 20 | seq = self.count = self.count + 1 21 | self.logs[seq] = log 22 | asyncio.get_running_loop().call_later(self.rotation, self.remove, seq) 23 | await asyncio.gather( 24 | *(listener(log) for listener in self.listeners), 25 | return_exceptions=True, 26 | ) 27 | return seq 28 | 29 | def remove(self, seq: int): 30 | del self.logs[seq] 31 | 32 | 33 | LOG_STORAGE: LogStorage[str] = LogStorage[str]() 34 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/web_ui/api/logs/logs.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from loguru import logger 3 | from nonebot.utils import escape_tag 4 | from starlette.websockets import WebSocket, WebSocketDisconnect, WebSocketState 5 | 6 | from .log_manager import LOG_STORAGE 7 | 8 | router = APIRouter() 9 | 10 | 11 | @router.websocket("/logs") 12 | async def system_logs_realtime(websocket: WebSocket): 13 | await websocket.accept() 14 | 15 | async def log_listener(log: str): 16 | await websocket.send_text(log) 17 | 18 | LOG_STORAGE.listeners.add(log_listener) 19 | try: 20 | while websocket.client_state == WebSocketState.CONNECTED: 21 | recv = await websocket.receive() 22 | logger.trace( 23 | f"{system_logs_realtime.__name__!r} received " 24 | f"{escape_tag(repr(recv))}" 25 | ) 26 | except WebSocketDisconnect: 27 | pass 28 | finally: 29 | LOG_STORAGE.listeners.remove(log_listener) 30 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/web_ui/api/menu/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from fastapi.responses import JSONResponse 3 | from nonebot import logger 4 | 5 | from ...base_model import Result 6 | from ...utils import authentication 7 | from .data_source import menu_manage 8 | from .model import MenuData 9 | 10 | router = APIRouter(prefix="/menu") 11 | 12 | 13 | @router.get( 14 | "/get_menus", 15 | dependencies=[authentication()], 16 | response_model=Result[MenuData], 17 | response_class=JSONResponse, 18 | deprecated="获取菜单列表", # type: ignore 19 | ) 20 | async def _() -> Result[MenuData]: 21 | try: 22 | return Result.ok(menu_manage.get_menus(), "拿到菜单了哦!") 23 | except Exception as e: 24 | logger.error(f"WebUi {router.prefix}/get_menus 调用错误 {type(e)}:{e}") 25 | return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") 26 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/web_ui/api/menu/data_source.py: -------------------------------------------------------------------------------- 1 | import ujson as json 2 | from nonebot import logger 3 | 4 | from ....config import DATA_PATH 5 | from .model import MenuData, MenuItem 6 | 7 | 8 | class MenuManage: 9 | def __init__(self) -> None: 10 | self.file = DATA_PATH / "menu.json" 11 | self.menu = [] 12 | if self.file.exists(): 13 | try: 14 | self.menu = json.load(self.file.open(encoding="utf8")) 15 | except Exception as e: 16 | logger.warning("菜单文件损坏,已重新生成...", "WebUi", e=e) 17 | if not self.menu: 18 | self.menu = [ 19 | MenuItem( 20 | name="仪表盘", 21 | module="dashboard", 22 | router="/dashboard", 23 | icon="dashboard", 24 | default=True, 25 | ), 26 | MenuItem( 27 | name="Bot控制台", 28 | module="command", 29 | router="/command", 30 | icon="command", 31 | ), 32 | MenuItem( 33 | name="插件列表", module="plugin", router="/plugin", icon="plugin" 34 | ), 35 | MenuItem( 36 | name="好友/群组", module="manage", router="/manage", icon="user" 37 | ), 38 | MenuItem( 39 | name="数据库管理", 40 | module="database", 41 | router="/database", 42 | icon="database", 43 | ), 44 | MenuItem( 45 | name="系统信息", module="system", router="/system", icon="system" 46 | ), 47 | ] 48 | self.save() 49 | 50 | def get_menus(self): 51 | return MenuData(menus=self.menu) 52 | 53 | def save(self): 54 | self.file.parent.mkdir(parents=True, exist_ok=True) 55 | temp = [menu.dict() for menu in self.menu] 56 | with self.file.open("w", encoding="utf8") as f: 57 | json.dump(temp, f, ensure_ascii=False, indent=4) 58 | 59 | 60 | menu_manage = MenuManage() 61 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/web_ui/api/menu/model.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class MenuItem(BaseModel): 5 | module: str 6 | """模块名称""" 7 | name: str 8 | """菜单名称""" 9 | router: str 10 | """路由""" 11 | icon: str 12 | """图标""" 13 | default: bool = False 14 | """默认选中""" 15 | 16 | 17 | class MenuData(BaseModel): 18 | bot_type: str = "nonebot" 19 | """bot类型""" 20 | menus: list[MenuItem] 21 | """菜单列表""" 22 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/web_ui/api/tabs/__init__.py: -------------------------------------------------------------------------------- 1 | from .database import * # noqa: F403 2 | from .main import * # noqa: F403 3 | from .manage import * # noqa: F403 4 | from .plugin_manage import * # noqa: F403 5 | from .system import * # noqa: F403 6 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/web_ui/api/tabs/dashboard/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from fastapi.responses import JSONResponse 3 | import nonebot 4 | from nonebot import logger 5 | from nonebot.config import Config 6 | 7 | from ....base_model import BaseResultModel, QueryModel, Result 8 | from ....utils import authentication 9 | from .data_source import ApiDataSource 10 | from .model import AllChatAndCallCount, BotInfo, ChatCallMonthCount, QueryChatCallCount 11 | 12 | router = APIRouter(prefix="/dashboard") 13 | 14 | driver = nonebot.get_driver() 15 | 16 | 17 | @router.get( 18 | "/get_bot_list", 19 | dependencies=[authentication()], 20 | response_model=Result[list[BotInfo]], 21 | response_class=JSONResponse, 22 | deprecated="获取bot列表", # type: ignore 23 | ) 24 | async def _() -> Result[list[BotInfo]]: 25 | try: 26 | return Result.ok(await ApiDataSource.get_bot_list(), "拿到信息啦!") 27 | except Exception as e: 28 | logger.error(f"WebUi {router.prefix}/get_bot_list 调用错误 {type(e)}:{e}") 29 | return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") 30 | 31 | 32 | @router.get( 33 | "/get_chat_and_call_count", 34 | dependencies=[authentication()], 35 | response_model=Result[QueryChatCallCount], 36 | response_class=JSONResponse, 37 | description="获取聊天/调用记录的全部和今日数量", 38 | ) 39 | async def _(bot_id: str | None = None) -> Result[QueryChatCallCount]: 40 | try: 41 | return Result.ok( 42 | await ApiDataSource.get_chat_and_call_count(bot_id), "拿到信息啦!" 43 | ) 44 | except Exception as e: 45 | logger.error( 46 | f"WebUi {router.prefix}/get_chat_and_call_count 调用错误 {type(e)}:{e}" 47 | ) 48 | return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") 49 | 50 | 51 | @router.get( 52 | "/get_all_chat_and_call_count", 53 | dependencies=[authentication()], 54 | response_model=Result[AllChatAndCallCount], 55 | response_class=JSONResponse, 56 | description="获取聊天/调用记录的全部数据次数", 57 | ) 58 | async def _(bot_id: str | None = None) -> Result[AllChatAndCallCount]: 59 | try: 60 | return Result.ok( 61 | await ApiDataSource.get_all_chat_and_call_count(bot_id), "拿到信息啦!" 62 | ) 63 | except Exception as e: 64 | logger.error( 65 | f"WebUi {router.prefix}/get_all_chat_and_call_count 调用错误 {type(e)}:{e}" 66 | ) 67 | return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") 68 | 69 | 70 | @router.get( 71 | "/get_chat_and_call_month", 72 | dependencies=[authentication()], 73 | response_model=Result[ChatCallMonthCount], 74 | response_class=JSONResponse, 75 | deprecated="获取聊天/调用记录的一个月数量", # type: ignore 76 | ) 77 | async def _(bot_id: str | None = None) -> Result[ChatCallMonthCount]: 78 | try: 79 | return Result.ok( 80 | await ApiDataSource.get_chat_and_call_month(bot_id), "拿到信息啦!" 81 | ) 82 | except Exception as e: 83 | logger.error( 84 | f"WebUi {router.prefix}/get_chat_and_call_month 调用错误 {type(e)}:{e}" 85 | ) 86 | return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") 87 | 88 | 89 | @router.post( 90 | "/get_connect_log", 91 | dependencies=[authentication()], 92 | response_model=Result[BaseResultModel], 93 | response_class=JSONResponse, 94 | deprecated="获取Bot连接记录", # type: ignore 95 | ) 96 | async def _(query: QueryModel) -> Result[BaseResultModel]: 97 | try: 98 | return Result.ok(await ApiDataSource.get_connect_log(query), "拿到信息啦!") 99 | except Exception as e: 100 | logger.error(f"WebUi {router.prefix}/get_connect_log 调用错误 {type(e)}:{e}") 101 | return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") 102 | 103 | 104 | @router.get( 105 | "/get_nonebot_config", 106 | dependencies=[authentication()], 107 | response_model=Result[Config], 108 | response_class=JSONResponse, 109 | deprecated="获取nb配置", # type: ignore 110 | ) 111 | async def _() -> Result[Config]: 112 | return Result.ok(driver.config) 113 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/web_ui/api/tabs/dashboard/data_source.py: -------------------------------------------------------------------------------- 1 | import time 2 | from datetime import datetime, timedelta 3 | 4 | import nonebot 5 | from nonebot.adapters import Bot 6 | from nonebot.drivers import Driver 7 | from tortoise.expressions import RawSQL 8 | from tortoise.functions import Count 9 | from zhenxun_utils.platform import PlatformUtils 10 | 11 | from .....models.bot_connect_log import BotConnectLog 12 | from .....models.chat_history import ChatHistory 13 | from .....models.statistics import Statistics 14 | from ....base_model import BaseResultModel, QueryModel 15 | from ..main.data_source import bot_live 16 | from .model import ( 17 | AllChatAndCallCount, 18 | BotConnectLogInfo, 19 | BotInfo, 20 | ChatCallMonthCount, 21 | QueryChatCallCount, 22 | ) 23 | 24 | driver: Driver = nonebot.get_driver() 25 | 26 | 27 | CONNECT_TIME = 0 28 | 29 | 30 | @driver.on_startup 31 | async def _(): 32 | global CONNECT_TIME 33 | CONNECT_TIME = int(time.time()) 34 | 35 | 36 | class ApiDataSource: 37 | @classmethod 38 | async def __build_bot_info(cls, bot: Bot) -> BotInfo: 39 | """构建Bot信息 40 | 41 | 参数: 42 | bot: Bot 43 | 44 | 返回: 45 | BotInfo: Bot信息 46 | """ 47 | now = datetime.now() 48 | platform = PlatformUtils.get_platform(bot) or "" 49 | if platform == "qq": 50 | login_info = await bot.get_login_info() 51 | nickname = login_info["nickname"] 52 | ava_url = PlatformUtils.get_user_avatar_url(bot.self_id, "qq") or "" 53 | else: 54 | nickname = bot.self_id 55 | ava_url = "" 56 | bot_info = BotInfo( 57 | self_id=bot.self_id, nickname=nickname, ava_url=ava_url, platform=platform 58 | ) 59 | group_list, _ = await PlatformUtils.get_group_list(bot, True) 60 | friend_list, _ = await PlatformUtils.get_friend_list(bot) 61 | bot_info.group_count = len(group_list) 62 | bot_info.friend_count = len(friend_list) 63 | bot_info.day_call = await Statistics.filter( 64 | create_time__gte=now - timedelta(hours=now.hour, minutes=now.minute), 65 | bot_id=bot.self_id, 66 | ).count() 67 | bot_info.received_messages = await ChatHistory.filter( 68 | bot_id=bot_info.self_id, 69 | create_time__gte=now - timedelta(hours=now.hour, minutes=now.minute), 70 | ).count() 71 | bot_info.connect_time = bot_live.get(bot.self_id) or 0 72 | if bot_info.connect_time: 73 | connect_date = datetime.fromtimestamp(CONNECT_TIME) 74 | bot_info.connect_date = connect_date.strftime("%Y-%m-%d %H:%M:%S") 75 | return bot_info 76 | 77 | @classmethod 78 | async def get_bot_list(cls) -> list[BotInfo]: 79 | """获取bot列表 80 | 81 | 返回: 82 | list[BotInfo]: Bot列表 83 | """ 84 | bot_list: list[BotInfo] = [] 85 | for _, bot in nonebot.get_bots().items(): 86 | bot_list.append(await cls.__build_bot_info(bot)) 87 | return bot_list 88 | 89 | @classmethod 90 | async def get_chat_and_call_count(cls, bot_id: str | None) -> QueryChatCallCount: 91 | """获取今日聊天和调用次数 92 | 93 | 参数: 94 | bot_id: bot id 95 | 96 | 返回: 97 | QueryChatCallCount: 数据内容 98 | """ 99 | now = datetime.now() 100 | query = ChatHistory 101 | if bot_id: 102 | query = query.filter(bot_id=bot_id) 103 | chat_all_count = await query.annotate().count() 104 | chat_day_count = await query.filter( 105 | create_time__gte=now - timedelta(hours=now.hour, minutes=now.minute) 106 | ).count() 107 | query = Statistics 108 | if bot_id: 109 | query = query.filter(bot_id=bot_id) 110 | call_all_count = await query.annotate().count() 111 | call_day_count = await query.filter( 112 | create_time__gte=now - timedelta(hours=now.hour, minutes=now.minute) 113 | ).count() 114 | return QueryChatCallCount( 115 | chat_num=chat_all_count, 116 | chat_day=chat_day_count, 117 | call_num=call_all_count, 118 | call_day=call_day_count, 119 | ) 120 | 121 | @classmethod 122 | async def get_all_chat_and_call_count( 123 | cls, bot_id: str | None 124 | ) -> AllChatAndCallCount: 125 | """获取全部聊天和调用记录 126 | 127 | 参数: 128 | bot_id: bot id 129 | 130 | 返回: 131 | AllChatAndCallCount: 数据内容 132 | """ 133 | now = datetime.now() 134 | query = ChatHistory 135 | if bot_id: 136 | query = query.filter(bot_id=bot_id) 137 | chat_week_count = await query.filter( 138 | create_time__gte=now - timedelta(days=7, hours=now.hour, minutes=now.minute) 139 | ).count() 140 | chat_month_count = await query.filter( 141 | create_time__gte=now 142 | - timedelta(days=30, hours=now.hour, minutes=now.minute) 143 | ).count() 144 | chat_year_count = await query.filter( 145 | create_time__gte=now 146 | - timedelta(days=365, hours=now.hour, minutes=now.minute) 147 | ).count() 148 | query = Statistics 149 | if bot_id: 150 | query = query.filter(bot_id=bot_id) 151 | call_week_count = await query.filter( 152 | create_time__gte=now - timedelta(days=7, hours=now.hour, minutes=now.minute) 153 | ).count() 154 | call_month_count = await query.filter( 155 | create_time__gte=now 156 | - timedelta(days=30, hours=now.hour, minutes=now.minute) 157 | ).count() 158 | call_year_count = await query.filter( 159 | create_time__gte=now 160 | - timedelta(days=365, hours=now.hour, minutes=now.minute) 161 | ).count() 162 | return AllChatAndCallCount( 163 | chat_week=chat_week_count, 164 | chat_month=chat_month_count, 165 | chat_year=chat_year_count, 166 | call_week=call_week_count, 167 | call_month=call_month_count, 168 | call_year=call_year_count, 169 | ) 170 | 171 | @classmethod 172 | async def get_chat_and_call_month(cls, bot_id: str | None) -> ChatCallMonthCount: 173 | """获取一个月内的调用/消息记录次数,并根据日期对数据填充0 174 | 175 | 参数: 176 | bot_id: bot id 177 | 178 | 返回: 179 | ChatCallMonthCount: 数据内容 180 | """ 181 | now = datetime.now() 182 | filter_date = now - timedelta(days=30, hours=now.hour, minutes=now.minute) 183 | chat_query = ChatHistory 184 | call_query = Statistics 185 | if bot_id: 186 | chat_query = chat_query.filter(bot_id=bot_id) 187 | call_query = call_query.filter(bot_id=bot_id) 188 | chat_date_list = ( 189 | await chat_query.filter(create_time__gte=filter_date) 190 | .annotate(date=RawSQL("DATE(create_time)"), count=Count("id")) 191 | .group_by("date") 192 | .values("date", "count") 193 | ) 194 | call_date_list = ( 195 | await call_query.filter(create_time__gte=filter_date) 196 | .annotate(date=RawSQL("DATE(create_time)"), count=Count("id")) 197 | .group_by("date") 198 | .values("date", "count") 199 | ) 200 | date_list = [] 201 | chat_count_list = [] 202 | call_count_list = [] 203 | chat_date2cnt = {str(date["date"]): date["count"] for date in chat_date_list} 204 | call_date2cnt = {str(date["date"]): date["count"] for date in call_date_list} 205 | date = now.date() 206 | for _ in range(30): 207 | if str(date) in chat_date2cnt: 208 | chat_count_list.append(chat_date2cnt[str(date)]) 209 | else: 210 | chat_count_list.append(0) 211 | if str(date) in call_date2cnt: 212 | call_count_list.append(call_date2cnt[str(date)]) 213 | else: 214 | call_count_list.append(0) 215 | date_list.append(str(date)[5:]) 216 | date -= timedelta(days=1) 217 | chat_count_list.reverse() 218 | call_count_list.reverse() 219 | date_list.reverse() 220 | return ChatCallMonthCount( 221 | chat=chat_count_list, call=call_count_list, date=date_list 222 | ) 223 | 224 | @classmethod 225 | async def get_connect_log(cls, query: QueryModel) -> BaseResultModel: 226 | """获取bot连接日志 227 | 228 | 参数: 229 | query: 查询模型 230 | 231 | 返回: 232 | BaseResultModel: 数据内容 233 | """ 234 | total = await BotConnectLog.all().count() 235 | if total % query.size: 236 | total += 1 237 | data = ( 238 | await BotConnectLog.all() 239 | .order_by("-id") 240 | .offset((query.index - 1) * query.size) 241 | .limit(query.size) 242 | ) 243 | result_list = [] 244 | for v in data: 245 | v.connect_time = v.connect_time.replace(tzinfo=None).replace(microsecond=0) 246 | result_list.append( 247 | BotConnectLogInfo( 248 | bot_id=v.bot_id, connect_time=v.connect_time, type=v.type 249 | ) 250 | ) 251 | return BaseResultModel(total=total, data=result_list) 252 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/web_ui/api/tabs/dashboard/model.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class BotConnectLogInfo(BaseModel): 7 | bot_id: str 8 | """机器人ID""" 9 | connect_time: datetime 10 | """连接日期""" 11 | type: int 12 | """连接类型""" 13 | 14 | 15 | class BotInfo(BaseModel): 16 | self_id: str 17 | """SELF ID""" 18 | nickname: str 19 | """昵称""" 20 | ava_url: str 21 | """头像url""" 22 | platform: str 23 | """平台""" 24 | friend_count: int = 0 25 | """好友数量""" 26 | group_count: int = 0 27 | """群聊数量""" 28 | received_messages: int = 0 29 | """今日消息接收""" 30 | day_call: int = 0 31 | """今日调用插件次数""" 32 | connect_time: int = 0 33 | """连接时间""" 34 | connect_date: str | None = None 35 | """连接日期""" 36 | 37 | 38 | class QueryChatCallCount(BaseModel): 39 | """ 40 | 查询聊天/调用记录次数 41 | """ 42 | 43 | chat_num: int 44 | """聊天记录总数""" 45 | chat_day: int 46 | """今日消息""" 47 | call_num: int 48 | """调用记录总数""" 49 | call_day: int 50 | """今日调用""" 51 | 52 | 53 | class ChatCallMonthCount(BaseModel): 54 | """ 55 | 查询聊天/调用一个月记录次数 56 | """ 57 | 58 | chat: list[int] 59 | """一个月内聊天总数""" 60 | call: list[int] 61 | """一个月内调用数据""" 62 | date: list[str] 63 | """日期""" 64 | 65 | 66 | class AllChatAndCallCount(BaseModel): 67 | """ 68 | 查询聊天/调用记录次数 69 | """ 70 | 71 | chat_week: int 72 | """一周内聊天次数""" 73 | chat_month: int 74 | """一月内聊天次数""" 75 | chat_year: int 76 | """一年内聊天次数""" 77 | call_week: int 78 | """一周内调用次数""" 79 | call_month: int 80 | """一月内调用次数""" 81 | call_year: int 82 | """一年内调用次数""" 83 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/web_ui/api/tabs/database/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Request 2 | from fastapi.responses import JSONResponse 3 | import nonebot 4 | from nonebot import logger 5 | from nonebot.drivers import Driver 6 | from tortoise import Tortoise 7 | 8 | from .....config import SQL_TYPE 9 | from .....models.plugin_info import PluginInfo 10 | from ....base_model import BaseResultModel, QueryModel, Result 11 | from ....utils import authentication 12 | from .data_source import ApiDataSource, type2sql 13 | from .models.model import Column, SqlLogInfo, SqlText 14 | from .models.sql_log import SqlLog 15 | 16 | router = APIRouter(prefix="/database") 17 | 18 | 19 | driver: Driver = nonebot.get_driver() 20 | 21 | 22 | @driver.on_startup 23 | async def _(): 24 | if ApiDataSource.SQL_DICT: 25 | result = await PluginInfo.filter( 26 | module__in=ApiDataSource.SQL_DICT.keys() 27 | ).values_list("module", "name") 28 | module2name = {r[0]: r[1] for r in result} 29 | for s in ApiDataSource.SQL_DICT: 30 | module = ApiDataSource.SQL_DICT[s].module 31 | ApiDataSource.SQL_DICT[s].name = module2name.get(module, module) 32 | 33 | 34 | @router.get( 35 | "/get_table_list", 36 | dependencies=[authentication()], 37 | response_model=Result[list[dict]], 38 | response_class=JSONResponse, 39 | description="获取数据库表", 40 | ) 41 | async def _() -> Result[list[dict]]: 42 | try: 43 | db = Tortoise.get_connection("default") 44 | query = await db.execute_query_dict(type2sql[SQL_TYPE]) 45 | return Result.ok(query) 46 | except Exception as e: 47 | logger.error(f"WebUi {router.prefix}/get_table_list 调用错误 {type(e)}:{e}") 48 | return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") 49 | 50 | 51 | @router.get( 52 | "/get_table_column", 53 | dependencies=[authentication()], 54 | response_model=Result[list[Column]], 55 | response_class=JSONResponse, 56 | description="获取表字段", 57 | ) 58 | async def _(table_name: str) -> Result[list[Column]]: 59 | try: 60 | return Result.ok( 61 | await ApiDataSource.get_table_column(table_name), "拿到信息啦!" 62 | ) 63 | except Exception as e: 64 | logger.error(f"WebUi {router.prefix}/get_table_column 调用错误 {type(e)}:{e}") 65 | return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") 66 | 67 | 68 | @router.post( 69 | "/exec_sql", 70 | dependencies=[authentication()], 71 | response_model=Result[list[dict]], 72 | response_class=JSONResponse, 73 | description="执行sql", 74 | ) 75 | async def _(sql: SqlText, request: Request) -> Result[list[dict]]: 76 | ip = request.client.host if request.client else "unknown" 77 | try: 78 | if sql.sql.lower().startswith("select"): 79 | db = Tortoise.get_connection("default") 80 | res = await db.execute_query_dict(sql.sql) 81 | await SqlLog.add(ip or "0.0.0.0", sql.sql, "") 82 | return Result.ok(res, "执行成功啦!") 83 | else: 84 | result = await PluginInfo.raw(sql.sql) 85 | await SqlLog.add(ip or "0.0.0.0", sql.sql, str(result)) 86 | return Result.ok(info="执行成功啦!") 87 | except Exception as e: 88 | logger.error(f"WebUi {router.prefix}/exec_sql 调用错误 {type(e)}:{e}") 89 | await SqlLog.add(ip or "0.0.0.0", sql.sql, str(e), False) 90 | return Result.warning_(f"sql执行错误: {e}") 91 | 92 | 93 | @router.post( 94 | "/get_sql_log", 95 | dependencies=[authentication()], 96 | response_model=Result[BaseResultModel], 97 | response_class=JSONResponse, 98 | description="sql日志列表", 99 | ) 100 | async def _(query: QueryModel) -> Result[BaseResultModel]: 101 | try: 102 | total = await SqlLog.all().count() 103 | if total % query.size: 104 | total += 1 105 | data = ( 106 | await SqlLog.all() 107 | .order_by("-id") 108 | .offset((query.index - 1) * query.size) 109 | .limit(query.size) 110 | ) 111 | result_list = [SqlLogInfo(sql=e.sql) for e in data] 112 | return Result.ok(BaseResultModel(total=total, data=result_list)) 113 | except Exception as e: 114 | logger.error(f"WebUi {router.prefix}/get_sql_log 调用错误 {type(e)}:{e}") 115 | return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") 116 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/web_ui/api/tabs/database/data_source.py: -------------------------------------------------------------------------------- 1 | from tortoise import Tortoise 2 | 3 | from .....config import SQL_TYPE 4 | from .models.model import Column 5 | 6 | SELECT_TABLE_MYSQL_SQL = """ 7 | SELECT table_name AS name, table_comment AS `desc` 8 | FROM information_schema.tables 9 | WHERE table_schema = DATABASE(); 10 | """ 11 | 12 | SELECT_TABLE_SQLITE_SQL = """ 13 | SELECT name FROM sqlite_master WHERE type='table'; 14 | """ 15 | 16 | SELECT_TABLE_PSQL_SQL = """ 17 | select a.tablename as name,d.description as desc from pg_tables a 18 | left join pg_class c on relname=tablename 19 | left join pg_description d on oid=objoid and objsubid=0 where a.schemaname='public' 20 | """ 21 | 22 | SELECT_TABLE_COLUMN_PSQL_SQL = """ 23 | SELECT column_name, data_type, character_maximum_length as max_length, is_nullable 24 | FROM information_schema.columns 25 | WHERE table_name = '{}'; 26 | """ 27 | 28 | SELECT_TABLE_COLUMN_MYSQL_SQL = """ 29 | SHOW COLUMNS FROM {}; 30 | """ 31 | 32 | SELECT_TABLE_COLUMN_SQLITE_SQL = """ 33 | PRAGMA table_info({}); 34 | """ 35 | 36 | type2sql = { 37 | "mysql": SELECT_TABLE_MYSQL_SQL, 38 | "sqlite": SELECT_TABLE_SQLITE_SQL, 39 | "postgres": SELECT_TABLE_PSQL_SQL, 40 | } 41 | 42 | type2sql_column = { 43 | "mysql": SELECT_TABLE_COLUMN_MYSQL_SQL, 44 | "sqlite": SELECT_TABLE_COLUMN_SQLITE_SQL, 45 | "postgres": SELECT_TABLE_COLUMN_PSQL_SQL, 46 | } 47 | 48 | 49 | class ApiDataSource: 50 | SQL_DICT = {} # noqa: RUF012 51 | 52 | @classmethod 53 | async def get_table_column(cls, table_name: str) -> list[Column]: 54 | """获取表字段信息 55 | 56 | 参数: 57 | table_name: 表名 58 | 59 | 返回: 60 | list[Column]: 字段数据 61 | """ 62 | db = Tortoise.get_connection("default") 63 | sql = type2sql_column[SQL_TYPE] 64 | query = await db.execute_query_dict(sql.format(table_name)) 65 | result_list = [] 66 | if SQL_TYPE == "sqlite": 67 | result_list.extend( 68 | Column( 69 | column_name=result["name"], 70 | data_type=result["type"], 71 | max_length=-1, 72 | is_nullable="YES" if result["notnull"] == 1 else "NO", 73 | ) 74 | for result in query 75 | ) 76 | elif SQL_TYPE == "mysql": 77 | result_list.extend( 78 | Column( 79 | column_name=result["Field"], 80 | data_type=result["Type"], 81 | max_length=-1, 82 | is_nullable=result["Null"], 83 | ) 84 | for result in query 85 | ) 86 | else: 87 | result_list.extend(Column(**result) for result in query) 88 | return result_list 89 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/web_ui/api/tabs/database/models/model.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class SqlLogInfo(BaseModel): 5 | sql: str 6 | """sql语句""" 7 | 8 | 9 | class SqlText(BaseModel): 10 | """ 11 | sql语句 12 | """ 13 | 14 | sql: str 15 | 16 | 17 | class Column(BaseModel): 18 | """ 19 | 列 20 | """ 21 | 22 | column_name: str 23 | """列名""" 24 | data_type: str 25 | """数据类型""" 26 | max_length: int | None 27 | """最大长度""" 28 | is_nullable: str 29 | """是否可为空""" 30 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/web_ui/api/tabs/database/models/sql_log.py: -------------------------------------------------------------------------------- 1 | from tortoise import fields 2 | from zhenxun_db_client import Model 3 | 4 | 5 | class SqlLog(Model): 6 | id = fields.IntField(pk=True, generated=True, auto_increment=True) 7 | """自增id""" 8 | ip = fields.CharField(255) 9 | """ip""" 10 | sql = fields.CharField(255) 11 | """sql""" 12 | result = fields.CharField(255, null=True) 13 | """结果""" 14 | is_suc = fields.BooleanField(default=True) 15 | """是否成功""" 16 | create_time = fields.DatetimeField(auto_now_add=True) 17 | """创建时间""" 18 | 19 | class Meta: # type: ignore 20 | table = "sql_log" 21 | table_description = "sql执行日志" 22 | 23 | @classmethod 24 | async def add( 25 | cls, ip: str, sql: str, result: str | None = None, is_suc: bool = True 26 | ): 27 | """获取用户在群内的等级 28 | 29 | 参数: 30 | ip: ip 31 | sql: sql 32 | result: 返回结果 33 | is_suc: 是否成功 34 | """ 35 | await cls.create(ip=ip, sql=sql, result=result, is_suc=is_suc) 36 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/web_ui/api/tabs/main/__init__.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import contextlib 3 | import time 4 | 5 | from fastapi import APIRouter 6 | from fastapi.responses import JSONResponse 7 | import nonebot 8 | from nonebot import logger 9 | from nonebot.config import Config 10 | from starlette.websockets import WebSocket, WebSocketDisconnect, WebSocketState 11 | from websockets.exceptions import ConnectionClosedError, ConnectionClosedOK 12 | from zhenxun_utils.common_utils import CommonUtils 13 | from zhenxun_utils.platform import PlatformUtils 14 | 15 | from .....models.bot_console import BotConsole 16 | from ....base_model import Result 17 | from ....config import QueryDateType 18 | from ....utils import authentication, get_system_status 19 | from .data_source import ApiDataSource 20 | from .model import ( 21 | ActiveGroup, 22 | BaseInfo, 23 | BotBlockModule, 24 | BotManageUpdateParam, 25 | BotStatusParam, 26 | HotPlugin, 27 | NonebotData, 28 | QueryCount, 29 | ) 30 | 31 | driver = nonebot.get_driver() 32 | run_time = time.time() 33 | 34 | ws_router = APIRouter() 35 | router = APIRouter(prefix="/main") 36 | 37 | 38 | @router.get( 39 | "/get_base_info", 40 | dependencies=[authentication()], 41 | response_model=Result[list[BaseInfo]], 42 | response_class=JSONResponse, 43 | description="基础信息", 44 | ) 45 | async def _(bot_id: str | None = None) -> Result[list[BaseInfo]]: 46 | """获取Bot基础信息 47 | 48 | 参数: 49 | bot_id (Optional[str], optional): bot_id. Defaults to None. 50 | 51 | 返回: 52 | Result: 获取指定bot信息与bot列表 53 | """ 54 | try: 55 | result = await ApiDataSource.get_base_info(bot_id) 56 | if not result: 57 | Result.warning_("无Bot连接...") 58 | return Result.ok(result, "拿到信息啦!") 59 | except Exception as e: 60 | logger.error(f"WebUi {router.prefix}/get_base_info 调用错误 {type(e)}:{e}") 61 | return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") 62 | 63 | 64 | @router.get( 65 | "/get_all_chat_count", 66 | dependencies=[authentication()], 67 | response_model=Result[QueryCount], 68 | response_class=JSONResponse, 69 | description="获取接收消息数量", 70 | ) 71 | async def _(bot_id: str | None = None) -> Result[QueryCount]: 72 | try: 73 | return Result.ok(await ApiDataSource.get_all_chat_count(bot_id), "拿到信息啦!") 74 | except Exception as e: 75 | logger.error(f"WebUi {router.prefix}/get_all_chat_count 调用错误 {type(e)}:{e}") 76 | return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") 77 | 78 | 79 | @router.get( 80 | "/get_all_call_count", 81 | dependencies=[authentication()], 82 | response_model=Result[QueryCount], 83 | response_class=JSONResponse, 84 | description="获取调用次数", 85 | ) 86 | async def _(bot_id: str | None = None) -> Result[QueryCount]: 87 | try: 88 | return Result.ok(await ApiDataSource.get_all_call_count(bot_id), "拿到信息啦!") 89 | except Exception as e: 90 | logger.error(f"WebUi {router.prefix}/get_all_call_count 调用错误 {type(e)}:{e}") 91 | return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") 92 | 93 | 94 | @router.get( 95 | "get_fg_count", 96 | dependencies=[authentication()], 97 | response_model=Result[dict[str, int]], 98 | response_class=JSONResponse, 99 | description="好友/群组数量", 100 | ) 101 | async def _(bot_id: str) -> Result[dict[str, int]]: 102 | try: 103 | bot = nonebot.get_bot(bot_id) 104 | data = { 105 | "friend_count": len(await PlatformUtils.get_friend_list(bot)), 106 | "group_count": len(await PlatformUtils.get_group_list(bot)), 107 | } 108 | return Result.ok(data, "拿到信息啦!") 109 | except (ValueError, KeyError): 110 | return Result.warning_("指定Bot未连接...") 111 | except Exception as e: 112 | logger.error(f"WebUi {router.prefix}/get_fg_count 调用错误 {type(e)}:{e}") 113 | return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") 114 | 115 | 116 | @router.get( 117 | "/get_nb_data", 118 | dependencies=[authentication()], 119 | response_model=Result[NonebotData], 120 | response_class=JSONResponse, 121 | description="获取nb数据", 122 | ) 123 | async def _() -> Result[NonebotData]: 124 | global run_time 125 | return Result.ok(NonebotData(config=driver.config, run_time=int(run_time))) 126 | 127 | 128 | @router.get( 129 | "/get_nb_config", 130 | dependencies=[authentication()], 131 | response_model=Result[Config], 132 | response_class=JSONResponse, 133 | description="获取nb配置", 134 | ) 135 | async def _() -> Result[Config]: 136 | return Result.ok(driver.config) 137 | 138 | 139 | @router.get( 140 | "/get_run_time", 141 | dependencies=[authentication()], 142 | response_model=Result[int], 143 | response_class=JSONResponse, 144 | description="获取nb运行时间", 145 | ) 146 | async def _() -> Result[int]: 147 | global run_time 148 | return Result.ok(int(run_time)) 149 | 150 | 151 | @router.get( 152 | "/get_active_group", 153 | dependencies=[authentication()], 154 | response_model=Result[list[ActiveGroup]], 155 | response_class=JSONResponse, 156 | description="获取活跃群聊", 157 | ) 158 | async def _( 159 | date_type: QueryDateType | None = None, bot_id: str | None = None 160 | ) -> Result[list[ActiveGroup]]: 161 | try: 162 | return Result.ok( 163 | await ApiDataSource.get_active_group(date_type, bot_id), "拿到信息啦!" 164 | ) 165 | except Exception as e: 166 | logger.error(f"WebUi {router.prefix}/get_active_group 调用错误 {type(e)}:{e}") 167 | return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") 168 | 169 | 170 | @router.get( 171 | "/get_hot_plugin", 172 | dependencies=[authentication()], 173 | response_model=Result[list[HotPlugin]], 174 | response_class=JSONResponse, 175 | description="获取热门插件", 176 | ) 177 | async def _( 178 | date_type: QueryDateType | None = None, bot_id: str | None = None 179 | ) -> Result[list[HotPlugin]]: 180 | try: 181 | return Result.ok( 182 | await ApiDataSource.get_hot_plugin(date_type, bot_id), "拿到信息啦!" 183 | ) 184 | except Exception as e: 185 | logger.error(f"WebUi {router.prefix}/get_hot_plugin 调用错误 {type(e)}:{e}") 186 | return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") 187 | 188 | 189 | @router.post( 190 | "/change_bot_status", 191 | dependencies=[authentication()], 192 | response_model=Result, 193 | response_class=JSONResponse, 194 | description="修改bot全局开关", 195 | ) 196 | async def _(param: BotStatusParam): 197 | try: 198 | await BotConsole.set_bot_status(param.status, param.bot_id) 199 | return Result.ok(info="修改bot全局开关成功!") 200 | except (ValueError, KeyError): 201 | return Result.fail("Bot未初始化...") 202 | 203 | 204 | @router.get( 205 | "/get_bot_block_module", 206 | dependencies=[authentication()], 207 | response_model=Result[BotBlockModule], 208 | response_class=JSONResponse, 209 | description="获取bot层面的禁用模块", 210 | ) 211 | async def _(bot_id: str) -> Result[BotBlockModule]: 212 | try: 213 | return Result.ok( 214 | await ApiDataSource.get_bot_block_module(bot_id), "拿到信息啦!" 215 | ) 216 | except Exception as e: 217 | logger.error( 218 | f"WebUi {router.prefix}/get_bot_block_module 调用错误 {type(e)}:{e}" 219 | ) 220 | return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") 221 | 222 | 223 | @router.post( 224 | "/update_bot_manage", 225 | dependencies=[authentication()], 226 | response_model=Result, 227 | response_class=JSONResponse, 228 | description="修改bot全局开关", 229 | ) 230 | async def _(param: BotManageUpdateParam): 231 | try: 232 | bot_data = await BotConsole.get_or_none(bot_id=param.bot_id) 233 | if not bot_data: 234 | return Result.fail("Bot数据不存在...") 235 | bot_data.block_plugins = CommonUtils.convert_module_format(param.block_plugins) 236 | bot_data.block_tasks = CommonUtils.convert_module_format(param.block_tasks) 237 | await bot_data.save(update_fields=["block_plugins", "block_tasks"]) 238 | return Result.ok() 239 | except Exception as e: 240 | logger.error(f"WebUi {router.prefix}/update_bot_manage 调用错误 {type(e)}:{e}") 241 | return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") 242 | 243 | 244 | @ws_router.websocket("/system_status") 245 | async def system_logs_realtime(websocket: WebSocket, sleep: int = 5): 246 | await websocket.accept() 247 | logger.debug("ws system_status is connect") 248 | with contextlib.suppress( 249 | WebSocketDisconnect, ConnectionClosedError, ConnectionClosedOK 250 | ): 251 | while websocket.client_state == WebSocketState.CONNECTED: 252 | system_status = await get_system_status() 253 | await websocket.send_text(system_status.json()) 254 | await asyncio.sleep(sleep) 255 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/web_ui/api/tabs/main/model.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from nonebot.adapters import Bot 4 | from nonebot.compat import PYDANTIC_V2 5 | from nonebot.config import Config 6 | from pydantic import BaseModel 7 | 8 | 9 | class BotManageUpdateParam(BaseModel): 10 | """bot更新参数""" 11 | 12 | bot_id: str 13 | """bot id""" 14 | block_plugins: list[str] 15 | """禁用插件""" 16 | block_tasks: list[str] 17 | """禁用被动""" 18 | 19 | 20 | class BotStatusParam(BaseModel): 21 | """bot状态参数""" 22 | 23 | bot_id: str 24 | """bot id""" 25 | status: bool 26 | """状态""" 27 | 28 | 29 | class BotBlockModule(BaseModel): 30 | """bot禁用模块参数""" 31 | 32 | bot_id: str 33 | """bot id""" 34 | block_plugins: list[str] 35 | """禁用插件""" 36 | block_tasks: list[str] 37 | """禁用被动""" 38 | all_plugins: list[dict[str, Any]] 39 | """所有插件""" 40 | all_tasks: list[dict[str, Any]] 41 | """所有被动""" 42 | 43 | 44 | class SystemStatus(BaseModel): 45 | """ 46 | 系统状态 47 | """ 48 | 49 | cpu: float 50 | memory: float 51 | disk: float 52 | 53 | 54 | class BaseInfo(BaseModel): 55 | """ 56 | 基础信息 57 | """ 58 | 59 | self_id: str 60 | """SELF ID""" 61 | nickname: str 62 | """昵称""" 63 | ava_url: str 64 | """头像url""" 65 | friend_count: int = 0 66 | """好友数量""" 67 | group_count: int = 0 68 | """群聊数量""" 69 | received_messages: int = 0 70 | """今日 累计接收消息""" 71 | connect_time: int = 0 72 | """连接时间""" 73 | connect_date: str | None = None 74 | """连接日期""" 75 | connect_count: int = 0 76 | """连接次数""" 77 | status: bool = False 78 | """全局状态""" 79 | 80 | is_select: bool = False 81 | """当前选择""" 82 | day_call: int = 0 83 | """今日调用插件次数""" 84 | version: str = "unknown" 85 | """真寻版本""" 86 | 87 | class Config: 88 | arbitrary_types_allowed = True 89 | 90 | def to_dict(self): 91 | return self.model_dump() if PYDANTIC_V2 else self.dict() 92 | 93 | 94 | class TemplateBaseInfo(BaseInfo): 95 | """ 96 | 基础信息 97 | """ 98 | 99 | bot: Bot 100 | """bot""" 101 | 102 | 103 | class QueryCount(BaseModel): 104 | """ 105 | 聊天记录数量 106 | """ 107 | 108 | num: int 109 | """总数""" 110 | day: int 111 | """一天内""" 112 | week: int 113 | """一周内""" 114 | month: int 115 | """一月内""" 116 | year: int 117 | """一年内""" 118 | 119 | 120 | class ActiveGroup(BaseModel): 121 | """ 122 | 活跃群聊数据 123 | """ 124 | 125 | group_id: str 126 | """群组id""" 127 | name: str 128 | """群组名称""" 129 | chat_num: int 130 | """发言数量""" 131 | ava_img: str 132 | """群组头像""" 133 | 134 | 135 | class HotPlugin(BaseModel): 136 | """ 137 | 热门插件 138 | """ 139 | 140 | module: str 141 | """模块名""" 142 | name: str 143 | """插件名称""" 144 | count: int 145 | """调用次数""" 146 | 147 | 148 | class NonebotData(BaseModel): 149 | config: Config 150 | """nb配置""" 151 | run_time: int 152 | """运行时间""" 153 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/web_ui/api/tabs/manage/chat.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | import nonebot 3 | from nonebot import on_message 4 | from nonebot.adapters import Bot, Event 5 | from nonebot_plugin_alconna import At, Hyper, Image, Text, UniMsg 6 | from nonebot_plugin_uninfo import Uninfo 7 | from starlette.websockets import WebSocket, WebSocketDisconnect, WebSocketState 8 | 9 | from ....config import AVA_URL 10 | from .model import Message, MessageItem 11 | 12 | driver = nonebot.get_driver() 13 | 14 | ws_conn: WebSocket | None = None 15 | 16 | ID2NAME = {} 17 | 18 | ID_LIST = [] 19 | 20 | ws_router = APIRouter() 21 | 22 | 23 | matcher = on_message(block=False, priority=1, rule=lambda: bool(ws_conn)) 24 | 25 | 26 | @driver.on_shutdown 27 | async def _(): 28 | if ws_conn and ws_conn.client_state == WebSocketState.CONNECTED: 29 | await ws_conn.close() 30 | 31 | 32 | @ws_router.websocket("/chat") 33 | async def _(websocket: WebSocket): 34 | global ws_conn 35 | await websocket.accept() 36 | if not ws_conn or ws_conn.client_state != WebSocketState.CONNECTED: 37 | ws_conn = websocket 38 | try: 39 | while websocket.client_state == WebSocketState.CONNECTED: 40 | await websocket.receive() 41 | except WebSocketDisconnect: 42 | ws_conn = None 43 | 44 | 45 | async def message_handle( 46 | user_id: str, 47 | message: UniMsg, 48 | group_id: str | None, 49 | ): 50 | messages = [] 51 | for m in message: 52 | if isinstance(m, Text | str): 53 | messages.append(MessageItem(type="text", msg=str(m))) 54 | elif isinstance(m, Image): 55 | if m.url: 56 | messages.append(MessageItem(type="img", msg=m.url)) 57 | elif isinstance(m, At): 58 | if group_id: 59 | if m.target == "0": 60 | uname = "全体成员" 61 | else: 62 | uname = m.target 63 | if group_id not in ID2NAME: 64 | ID2NAME[group_id] = {} 65 | if m.target in ID2NAME[group_id]: 66 | uname = ID2NAME[group_id][m.target] 67 | else: 68 | uname = f"@{user_id}" 69 | if m.target not in ID2NAME[group_id]: 70 | ID2NAME[group_id][m.target] = uname 71 | messages.append(MessageItem(type="at", msg=f"@{uname}")) 72 | elif isinstance(m, Hyper): 73 | messages.append(MessageItem(type="text", msg="[分享消息]")) 74 | return messages 75 | 76 | 77 | @matcher.handle() 78 | async def _(message: UniMsg, event: Event, bot: Bot, session: Uninfo): 79 | global ws_conn, ID2NAME, ID_LIST 80 | if ws_conn and ws_conn.client_state == WebSocketState.CONNECTED: 81 | name = session.user.name or session.user.nick or "" 82 | msg_id = message.get_message_id(event=event, bot=bot) 83 | if msg_id in ID_LIST: 84 | return 85 | ID_LIST.append(msg_id) 86 | if len(ID_LIST) > 50: 87 | ID_LIST = ID_LIST[40:] 88 | gid = session.group.id if session.group else None 89 | messages = await message_handle(session.user.id, message, gid) 90 | data = Message( 91 | object_id=gid or session.user.id, 92 | user_id=session.user.id, 93 | group_id=gid, 94 | message=messages, 95 | name=name, 96 | ava_url=AVA_URL.format(session.user.id), 97 | ) 98 | await ws_conn.send_json(data.dict()) 99 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/web_ui/api/tabs/manage/data_source.py: -------------------------------------------------------------------------------- 1 | import nonebot 2 | from tortoise.functions import Count 3 | from zhenxun_utils.common_utils import CommonUtils 4 | from zhenxun_utils.enum import RequestType 5 | from zhenxun_utils.platform import PlatformUtils 6 | 7 | from .....models.ban_console import BanConsole 8 | from .....models.chat_history import ChatHistory 9 | from .....models.fg_request import FgRequest 10 | from .....models.group_console import GroupConsole 11 | from .....models.plugin_info import PluginInfo 12 | from .....models.statistics import Statistics 13 | from ....config import AVA_URL, GROUP_AVA_URL 14 | from .model import ( 15 | FriendRequestResult, 16 | GroupDetail, 17 | GroupRequestResult, 18 | Plugin, 19 | ReqResult, 20 | UpdateGroup, 21 | UserDetail, 22 | ) 23 | 24 | 25 | class ApiDataSource: 26 | @classmethod 27 | async def update_group(cls, group: UpdateGroup): 28 | """更新群组数据 29 | 30 | 参数: 31 | group: UpdateGroup 32 | """ 33 | db_group = await GroupConsole.get_group(group.group_id) or GroupConsole( 34 | group_id=group.group_id 35 | ) 36 | db_group.level = group.level 37 | db_group.status = group.status 38 | if group.close_plugins: 39 | db_group.block_plugin = CommonUtils.convert_module_format( 40 | group.close_plugins 41 | ) 42 | else: 43 | db_group.block_plugin = "" 44 | await db_group.save() 45 | 46 | @classmethod 47 | async def get_request_list(cls) -> ReqResult: 48 | """获取好友与群组请求列表 49 | 50 | 返回: 51 | ReqResult: 数据内容 52 | """ 53 | req_result = ReqResult() 54 | data_list = await FgRequest.filter(handle_type__isnull=True).all() 55 | for req in data_list: 56 | if req.request_type == RequestType.FRIEND: 57 | req_result.friend.append( 58 | FriendRequestResult( 59 | oid=req.id, 60 | bot_id=req.bot_id, 61 | id=req.user_id, 62 | flag=req.flag, 63 | nickname=req.nickname, 64 | comment=req.comment, 65 | ava_url=AVA_URL.format(req.user_id), 66 | type=str(req.request_type).lower(), 67 | ) 68 | ) 69 | else: 70 | req_result.group.append( 71 | GroupRequestResult( 72 | oid=req.id, 73 | bot_id=req.bot_id, 74 | id=req.user_id, 75 | flag=req.flag, 76 | nickname=req.nickname, 77 | comment=req.comment, 78 | ava_url=GROUP_AVA_URL.format(req.group_id, req.group_id), 79 | type=str(req.request_type).lower(), 80 | invite_group=req.group_id, 81 | group_name=None, 82 | ) 83 | ) 84 | req_result.friend.reverse() 85 | req_result.group.reverse() 86 | return req_result 87 | 88 | @classmethod 89 | async def get_friend_detail(cls, bot_id: str, user_id: str) -> UserDetail | None: 90 | """获取好友详情 91 | 92 | 参数: 93 | bot_id: bot id 94 | user_id: 用户id 95 | 96 | 返回: 97 | UserDetail | None: 详情数据 98 | """ 99 | bot = nonebot.get_bot(bot_id) 100 | friend_list, _ = await PlatformUtils.get_friend_list(bot) 101 | fd = [x for x in friend_list if x.user_id == user_id] 102 | if not fd: 103 | return None 104 | like_plugin_list = ( 105 | await Statistics.filter(user_id=user_id) 106 | .annotate(count=Count("id")) 107 | .group_by("plugin_name") 108 | .order_by("-count") 109 | .limit(5) 110 | .values_list("plugin_name", "count") 111 | ) 112 | like_plugin = {} 113 | module_list = [x[0] for x in like_plugin_list] 114 | plugins = await PluginInfo.filter(module__in=module_list).all() 115 | module2name = {p.module: p.name for p in plugins} 116 | for data in like_plugin_list: 117 | name = module2name.get(data[0]) or data[0] 118 | like_plugin[name] = data[1] 119 | user = fd[0] 120 | return UserDetail( 121 | user_id=user_id, 122 | ava_url=AVA_URL.format(user_id), 123 | nickname=user.name or "", 124 | remark="", 125 | is_ban=await BanConsole.is_ban(user_id), 126 | chat_count=await ChatHistory.filter(user_id=user_id).count(), 127 | call_count=await Statistics.filter(user_id=user_id).count(), 128 | like_plugin=like_plugin, 129 | ) 130 | 131 | @classmethod 132 | async def __get_group_detail_like_plugin(cls, group_id: str) -> dict[str, int]: 133 | """获取群组喜爱的插件 134 | 135 | 参数: 136 | group_id: 群组id 137 | 138 | 返回: 139 | dict[str, int]: 插件与调用次数 140 | """ 141 | like_plugin_list = ( 142 | await Statistics.filter(group_id=group_id) 143 | .annotate(count=Count("id")) 144 | .group_by("plugin_name") 145 | .order_by("-count") 146 | .limit(5) 147 | .values_list("plugin_name", "count") 148 | ) 149 | like_plugin = {} 150 | plugins = await PluginInfo.get_plugins() 151 | module2name = {p.module: p.name for p in plugins} 152 | for data in like_plugin_list: 153 | name = module2name.get(data[0]) or data[0] 154 | like_plugin[name] = data[1] 155 | return like_plugin 156 | 157 | @classmethod 158 | async def __get_group_detail_disable_plugin( 159 | cls, group: GroupConsole 160 | ) -> list[Plugin]: 161 | """获取群组禁用插件 162 | 163 | 参数: 164 | group: GroupConsole 165 | 166 | 返回: 167 | list[Plugin]: 禁用插件数据列表 168 | """ 169 | disable_plugins: list[Plugin] = [] 170 | plugins = await PluginInfo.get_plugins() 171 | module2name = {p.module: p.name for p in plugins} 172 | if group.block_plugin: 173 | for module in CommonUtils.convert_module_format(group.block_plugin): 174 | if module: 175 | plugin = Plugin( 176 | module=module, 177 | plugin_name=module, 178 | is_super_block=False, 179 | ) 180 | plugin.plugin_name = module2name.get(module) or module 181 | disable_plugins.append(plugin) 182 | exists_modules = [p.module for p in disable_plugins] 183 | if group.superuser_block_plugin: 184 | for module in CommonUtils.convert_module_format( 185 | group.superuser_block_plugin 186 | ): 187 | if module and module not in exists_modules: 188 | plugin = Plugin( 189 | module=module, 190 | plugin_name=module, 191 | is_super_block=True, 192 | ) 193 | plugin.plugin_name = module2name.get(module) or module 194 | disable_plugins.append(plugin) 195 | return disable_plugins 196 | 197 | @classmethod 198 | async def get_group_detail(cls, group_id: str) -> GroupDetail | None: 199 | """获取群组详情 200 | 201 | 参数: 202 | group_id: 群组id 203 | 204 | 返回: 205 | GroupDetail | None: 群组详情数据 206 | """ 207 | group = await GroupConsole.get_or_none(group_id=group_id) 208 | if not group: 209 | return None 210 | like_plugin = await cls.__get_group_detail_like_plugin(group_id) 211 | disable_plugins: list[Plugin] = await cls.__get_group_detail_disable_plugin( 212 | group 213 | ) 214 | return GroupDetail( 215 | group_id=group_id, 216 | ava_url=GROUP_AVA_URL.format(group_id, group_id), 217 | name=group.group_name, 218 | member_count=group.member_count, 219 | max_member_count=group.max_member_count, 220 | chat_count=await ChatHistory.filter(group_id=group_id).count(), 221 | call_count=await Statistics.filter(group_id=group_id).count(), 222 | like_plugin=like_plugin, 223 | level=group.level, 224 | status=group.status, 225 | close_plugins=disable_plugins, 226 | task=[], 227 | ) 228 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/web_ui/api/tabs/manage/model.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | from zhenxun_utils.enum import RequestType 3 | 4 | 5 | class Group(BaseModel): 6 | """ 7 | 群组信息 8 | """ 9 | 10 | group_id: str 11 | """群组id""" 12 | group_name: str 13 | """群组名称""" 14 | member_count: int 15 | """成员人数""" 16 | max_member_count: int 17 | """群组最大人数""" 18 | 19 | 20 | class Task(BaseModel): 21 | """ 22 | 被动技能 23 | """ 24 | 25 | name: str 26 | """被动名称""" 27 | zh_name: str 28 | """被动中文名称""" 29 | status: bool 30 | """状态""" 31 | is_super_block: bool 32 | """是否超级用户禁用""" 33 | 34 | 35 | class Plugin(BaseModel): 36 | """ 37 | 插件 38 | """ 39 | 40 | module: str 41 | """模块名""" 42 | plugin_name: str 43 | """中文名""" 44 | is_super_block: bool 45 | """是否超级用户禁用""" 46 | 47 | 48 | class GroupResult(BaseModel): 49 | """ 50 | 群组返回数据 51 | """ 52 | 53 | group_id: str 54 | """群组id""" 55 | group_name: str 56 | """群组名称""" 57 | ava_url: str 58 | """群组头像""" 59 | 60 | 61 | class Friend(BaseModel): 62 | """ 63 | 好友数据 64 | """ 65 | 66 | user_id: str 67 | """用户id""" 68 | nickname: str = "" 69 | """昵称""" 70 | remark: str = "" 71 | """备注""" 72 | ava_url: str = "" 73 | """头像url""" 74 | 75 | 76 | class UpdateGroup(BaseModel): 77 | """ 78 | 更新群组信息 79 | """ 80 | 81 | group_id: str 82 | """群号""" 83 | status: bool 84 | """状态""" 85 | level: int 86 | """群权限""" 87 | task: list[str] 88 | """被动状态""" 89 | close_plugins: list[str] 90 | """关闭插件""" 91 | 92 | 93 | class FriendRequestResult(BaseModel): 94 | """ 95 | 好友/群组请求管理 96 | """ 97 | 98 | bot_id: str 99 | """bot_id""" 100 | oid: int 101 | """排序""" 102 | id: str 103 | """id""" 104 | flag: str 105 | """flag""" 106 | nickname: str | None 107 | """昵称""" 108 | comment: str | None 109 | """备注信息""" 110 | ava_url: str 111 | """头像""" 112 | type: str 113 | """类型 private group""" 114 | 115 | 116 | class GroupRequestResult(FriendRequestResult): 117 | """ 118 | 群聊邀请请求 119 | """ 120 | 121 | invite_group: str 122 | """邀请群聊""" 123 | group_name: str | None 124 | """群聊名称""" 125 | 126 | 127 | class ClearRequest(BaseModel): 128 | """ 129 | 清空请求 130 | """ 131 | 132 | request_type: RequestType 133 | 134 | 135 | class HandleRequest(BaseModel): 136 | """ 137 | 操作请求接收数据 138 | """ 139 | 140 | bot_id: str | None = None 141 | """bot_id""" 142 | id: int 143 | """数据id""" 144 | 145 | 146 | class LeaveGroup(BaseModel): 147 | """ 148 | 退出群聊 149 | """ 150 | 151 | bot_id: str 152 | """bot_id""" 153 | group_id: str 154 | """群聊id""" 155 | 156 | 157 | class DeleteFriend(BaseModel): 158 | """ 159 | 删除好友 160 | """ 161 | 162 | bot_id: str 163 | """bot_id""" 164 | user_id: str 165 | """用户id""" 166 | 167 | 168 | class ReqResult(BaseModel): 169 | """ 170 | 好友/群组请求列表 171 | """ 172 | 173 | friend: list[FriendRequestResult] = [] 174 | """好友请求列表""" 175 | group: list[GroupRequestResult] = [] 176 | """群组请求列表""" 177 | 178 | 179 | class UserDetail(BaseModel): 180 | """ 181 | 用户详情 182 | """ 183 | 184 | user_id: str 185 | """用户id""" 186 | ava_url: str 187 | """头像url""" 188 | nickname: str 189 | """昵称""" 190 | remark: str 191 | """备注""" 192 | is_ban: bool 193 | """是否被ban""" 194 | chat_count: int 195 | """发言次数""" 196 | call_count: int 197 | """功能调用次数""" 198 | like_plugin: dict[str, int] 199 | """最喜爱的功能""" 200 | 201 | 202 | class GroupDetail(BaseModel): 203 | """ 204 | 用户详情 205 | """ 206 | 207 | group_id: str 208 | """群组id""" 209 | ava_url: str 210 | """头像url""" 211 | name: str 212 | """名称""" 213 | member_count: int 214 | """成员数""" 215 | max_member_count: int 216 | """最大成员数""" 217 | chat_count: int 218 | """发言次数""" 219 | call_count: int 220 | """功能调用次数""" 221 | like_plugin: dict[str, int] 222 | """最喜爱的功能""" 223 | level: int 224 | """群权限""" 225 | status: bool 226 | """状态(睡眠)""" 227 | close_plugins: list[Plugin] 228 | """关闭的插件""" 229 | task: list[Task] 230 | """被动列表""" 231 | 232 | 233 | class MessageItem(BaseModel): 234 | type: str 235 | """消息类型""" 236 | msg: str 237 | """内容""" 238 | 239 | 240 | class Message(BaseModel): 241 | """ 242 | 消息 243 | """ 244 | 245 | object_id: str 246 | """主体id user_id 或 group_id""" 247 | user_id: str 248 | """用户id""" 249 | group_id: str | None = None 250 | """群组id""" 251 | message: list[MessageItem] 252 | """消息""" 253 | name: str 254 | """用户名称""" 255 | ava_url: str 256 | """用户头像""" 257 | 258 | 259 | class SendMessageParam(BaseModel): 260 | """ 261 | 发送消息 262 | """ 263 | 264 | bot_id: str 265 | """bot id""" 266 | user_id: str | None = None 267 | """用户id""" 268 | group_id: str | None = None 269 | """群组id""" 270 | message: str 271 | """消息""" 272 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/web_ui/api/tabs/plugin_manage/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Query 2 | from fastapi.responses import JSONResponse 3 | from nonebot import logger 4 | from zhenxun_utils.enum import BlockType, PluginType 5 | 6 | from .....models.plugin_info import PluginInfo as DbPluginInfo 7 | from ....base_model import Result 8 | from ....utils import authentication 9 | from .data_source import ApiDataSource 10 | from .model import ( 11 | PluginCount, 12 | PluginDetail, 13 | PluginInfo, 14 | PluginSwitch, 15 | UpdatePlugin, 16 | ) 17 | 18 | router = APIRouter(prefix="/plugin") 19 | 20 | 21 | @router.get( 22 | "/get_plugin_list", 23 | dependencies=[authentication()], 24 | response_model=Result[list[PluginInfo]], 25 | response_class=JSONResponse, 26 | deprecated="获取插件列表", # type: ignore 27 | ) 28 | async def _( 29 | plugin_type: list[PluginType] = Query(None), menu_type: str | None = None 30 | ) -> Result[list[PluginInfo]]: 31 | try: 32 | return Result.ok( 33 | await ApiDataSource.get_plugin_list(plugin_type, menu_type), "拿到信息啦!" 34 | ) 35 | except Exception as e: 36 | logger.error(f"WebUi {router.prefix}/get_plugin_list 调用错误 {type(e)}:{e}") 37 | return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") 38 | 39 | 40 | @router.get( 41 | "/get_plugin_count", 42 | dependencies=[authentication()], 43 | response_model=Result[PluginCount], 44 | response_class=JSONResponse, 45 | deprecated="获取插件数量", # type: ignore 46 | ) 47 | async def _() -> Result[PluginCount]: 48 | try: 49 | plugin_count = PluginCount() 50 | plugin_count.normal = await DbPluginInfo.filter( 51 | plugin_type=PluginType.NORMAL, load_status=True 52 | ).count() 53 | plugin_count.admin = await DbPluginInfo.filter( 54 | plugin_type__in=[PluginType.ADMIN, PluginType.SUPER_AND_ADMIN], 55 | load_status=True, 56 | ).count() 57 | plugin_count.superuser = await DbPluginInfo.filter( 58 | plugin_type__in=[PluginType.SUPERUSER, PluginType.SUPER_AND_ADMIN], 59 | load_status=True, 60 | ).count() 61 | plugin_count.other = await DbPluginInfo.filter( 62 | plugin_type__in=[PluginType.HIDDEN, PluginType.DEPENDANT], load_status=True 63 | ).count() 64 | return Result.ok(plugin_count, "拿到信息啦!") 65 | except Exception as e: 66 | logger.error(f"WebUi {router.prefix}/get_plugin_count 调用错误 {type(e)}:{e}") 67 | return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") 68 | 69 | 70 | @router.post( 71 | "/update_plugin", 72 | dependencies=[authentication()], 73 | response_model=Result, 74 | response_class=JSONResponse, 75 | description="更新插件参数", 76 | ) 77 | async def _(param: UpdatePlugin) -> Result: 78 | try: 79 | await ApiDataSource.update_plugin(param) 80 | return Result.ok(info="已经帮你写好啦!") 81 | except (ValueError, KeyError): 82 | return Result.fail("插件数据不存在...") 83 | except Exception as e: 84 | logger.error(f"WebUi {router.prefix}/update_plugin 调用错误 {type(e)}:{e}") 85 | return Result.fail(f"{type(e)}: {e}") 86 | 87 | 88 | @router.post( 89 | "/change_switch", 90 | dependencies=[authentication()], 91 | response_model=Result, 92 | response_class=JSONResponse, 93 | description="开关插件", 94 | ) 95 | async def _(param: PluginSwitch) -> Result: 96 | try: 97 | db_plugin = await DbPluginInfo.get_plugin(module=param.module) 98 | if not db_plugin: 99 | return Result.fail("插件不存在...") 100 | if not param.status: 101 | db_plugin.block_type = BlockType.ALL 102 | db_plugin.status = False 103 | else: 104 | db_plugin.block_type = None 105 | db_plugin.status = True 106 | await db_plugin.save() 107 | return Result.ok(info="成功改变了开关状态!") 108 | except Exception as e: 109 | logger.error(f"WebUi {router.prefix}/change_switch 调用错误 {type(e)}:{e}") 110 | return Result.fail(f"{type(e)}: {e}") 111 | 112 | 113 | @router.get( 114 | "/get_plugin_menu_type", 115 | dependencies=[authentication()], 116 | response_model=Result[list[str]], 117 | response_class=JSONResponse, 118 | description="获取插件类型", 119 | ) 120 | async def _() -> Result[list[str]]: 121 | try: 122 | menu_type_list = [] 123 | result = ( 124 | await DbPluginInfo.filter(load_status=True) 125 | .annotate() 126 | .values_list("menu_type", flat=True) 127 | ) 128 | for r in result: 129 | if r not in menu_type_list and r: 130 | menu_type_list.append(r) 131 | return Result.ok(menu_type_list) 132 | except Exception as e: 133 | logger.error( 134 | f"WebUi {router.prefix}/get_plugin_menu_type 调用错误 {type(e)}:{e}" 135 | ) 136 | return Result.fail(f"{type(e)}: {e}") 137 | 138 | 139 | @router.get( 140 | "/get_plugin", 141 | dependencies=[authentication()], 142 | response_model=Result[PluginDetail], 143 | response_class=JSONResponse, 144 | description="获取插件详情", 145 | ) 146 | async def _(module: str) -> Result[PluginDetail]: 147 | try: 148 | return Result.ok( 149 | await ApiDataSource.get_plugin_detail(module), "已经帮你写好啦!" 150 | ) 151 | except (ValueError, KeyError): 152 | return Result.fail("插件数据不存在...") 153 | except Exception as e: 154 | logger.error(f"WebUi {router.prefix}/get_plugin 调用错误 {type(e)}:{e}") 155 | return Result.fail(f"{type(e)}: {e}") 156 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/web_ui/api/tabs/plugin_manage/data_source.py: -------------------------------------------------------------------------------- 1 | from fastapi import Query 2 | from zhenxun_utils.enum import BlockType, PluginType 3 | 4 | from .....models.plugin_info import PluginInfo as DbPluginInfo 5 | from .model import PluginDetail, PluginInfo, UpdatePlugin 6 | 7 | 8 | class ApiDataSource: 9 | @classmethod 10 | async def get_plugin_list( 11 | cls, plugin_type: list[PluginType] = Query(None), menu_type: str | None = None 12 | ) -> list[PluginInfo]: 13 | """获取插件列表 14 | 15 | 参数: 16 | plugin_type: 插件类型. 17 | menu_type: 菜单类型. 18 | 19 | 返回: 20 | list[PluginInfo]: 插件数据列表 21 | """ 22 | plugin_list: list[PluginInfo] = [] 23 | query = DbPluginInfo 24 | if plugin_type: 25 | query = query.filter(plugin_type__in=plugin_type, load_status=True) 26 | if menu_type: 27 | query = query.filter(menu_type=menu_type, load_status=True) 28 | plugins = await query.all() 29 | for plugin in plugins: 30 | plugin_info = PluginInfo( 31 | module=plugin.module, 32 | plugin_name=plugin.name, 33 | default_status=plugin.default_status, 34 | limit_superuser=plugin.limit_superuser, 35 | cost_gold=plugin.cost_gold, 36 | menu_type=plugin.menu_type, 37 | version=plugin.version or "0", 38 | level=plugin.level, 39 | status=plugin.status, 40 | author=plugin.author, 41 | ) 42 | plugin_list.append(plugin_info) 43 | return plugin_list 44 | 45 | @classmethod 46 | async def update_plugin(cls, param: UpdatePlugin) -> DbPluginInfo: 47 | """更新插件数据 48 | 49 | 参数: 50 | param: UpdatePlugin 51 | 52 | 返回: 53 | DbPluginInfo | None: 插件数据 54 | """ 55 | db_plugin = await DbPluginInfo.get_plugin(module=param.module) 56 | if not db_plugin: 57 | raise ValueError("插件不存在") 58 | db_plugin.default_status = param.default_status 59 | db_plugin.limit_superuser = param.limit_superuser 60 | db_plugin.cost_gold = param.cost_gold 61 | db_plugin.level = param.level 62 | db_plugin.menu_type = param.menu_type 63 | db_plugin.block_type = param.block_type 64 | db_plugin.status = param.block_type != BlockType.ALL 65 | await db_plugin.save() 66 | return db_plugin 67 | 68 | @classmethod 69 | async def get_plugin_detail(cls, module: str) -> PluginDetail: 70 | """获取插件详情 71 | 72 | 参数: 73 | module: 模块名 74 | 75 | 异常: 76 | ValueError: 插件不存在 77 | 78 | 返回: 79 | PluginDetail: 插件详情数据 80 | """ 81 | db_plugin = await DbPluginInfo.get_plugin(module=module) 82 | if not db_plugin: 83 | raise ValueError("插件不存在") 84 | return PluginDetail( 85 | module=module, 86 | plugin_name=db_plugin.name, 87 | default_status=db_plugin.default_status, 88 | limit_superuser=db_plugin.limit_superuser, 89 | cost_gold=db_plugin.cost_gold, 90 | menu_type=db_plugin.menu_type, 91 | version=db_plugin.version or "0", 92 | level=db_plugin.level, 93 | status=db_plugin.status, 94 | author=db_plugin.author, 95 | config_list=[], 96 | block_type=db_plugin.block_type, 97 | ) 98 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/web_ui/api/tabs/plugin_manage/model.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from pydantic import BaseModel 4 | from zhenxun_utils.enum import BlockType 5 | 6 | 7 | class PluginSwitch(BaseModel): 8 | """ 9 | 插件开关 10 | """ 11 | 12 | module: str 13 | """模块""" 14 | status: bool 15 | """开关状态""" 16 | 17 | 18 | class UpdateConfig(BaseModel): 19 | """ 20 | 配置项修改参数 21 | """ 22 | 23 | module: str 24 | """模块""" 25 | key: str 26 | """配置项key""" 27 | value: Any 28 | """配置项值""" 29 | 30 | 31 | class UpdatePlugin(BaseModel): 32 | """ 33 | 插件修改参数 34 | """ 35 | 36 | module: str 37 | """模块""" 38 | default_status: bool 39 | """默认开关""" 40 | limit_superuser: bool 41 | """限制超级用户""" 42 | cost_gold: int 43 | """金币花费""" 44 | menu_type: str 45 | """插件菜单类型""" 46 | level: int 47 | """插件所需群权限""" 48 | block_type: BlockType | None = None 49 | """禁用类型""" 50 | configs: dict[str, Any] | None = None 51 | """配置项""" 52 | 53 | 54 | class PluginInfo(BaseModel): 55 | """ 56 | 基本插件信息 57 | """ 58 | 59 | module: str 60 | """插件名称""" 61 | plugin_name: str 62 | """插件中文名称""" 63 | default_status: bool 64 | """默认开关""" 65 | limit_superuser: bool 66 | """限制超级用户""" 67 | cost_gold: int 68 | """花费金币""" 69 | menu_type: str 70 | """插件菜单类型""" 71 | version: str 72 | """插件版本""" 73 | level: int 74 | """群权限""" 75 | status: bool 76 | """当前状态""" 77 | author: str | None = None 78 | """作者""" 79 | block_type: BlockType | None = None 80 | """禁用类型""" 81 | 82 | 83 | class PluginConfig(BaseModel): 84 | """ 85 | 插件配置项 86 | """ 87 | 88 | module: str 89 | """模块""" 90 | key: str 91 | """键""" 92 | value: Any 93 | """值""" 94 | help: str | None = None 95 | """帮助""" 96 | default_value: Any 97 | """默认值""" 98 | type: Any = None 99 | """值类型""" 100 | type_inner: list[str] | None = None 101 | """List Tuple等内部类型检验""" 102 | 103 | 104 | class PluginCount(BaseModel): 105 | """ 106 | 插件数量 107 | """ 108 | 109 | normal: int = 0 110 | """普通插件""" 111 | admin: int = 0 112 | """管理员插件""" 113 | superuser: int = 0 114 | """超级用户插件""" 115 | other: int = 0 116 | """其他插件""" 117 | 118 | 119 | class PluginDetail(PluginInfo): 120 | """ 121 | 插件详情 122 | """ 123 | 124 | config_list: list[PluginConfig] 125 | 126 | 127 | class PluginIr(BaseModel): 128 | id: int 129 | """插件id""" 130 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/web_ui/api/tabs/system/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | import shutil 4 | 5 | import aiofiles 6 | from fastapi import APIRouter 7 | from fastapi.responses import JSONResponse 8 | from zhenxun_utils._build_image import BuildImage 9 | 10 | from ....base_model import Result, SystemFolderSize 11 | from ....utils import authentication, get_system_disk 12 | from .model import AddFile, DeleteFile, DirFile, RenameFile, SaveFile 13 | 14 | router = APIRouter(prefix="/system") 15 | 16 | IMAGE_TYPE = ["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"] 17 | 18 | 19 | @router.get( 20 | "/get_dir_list", 21 | dependencies=[authentication()], 22 | response_model=Result[list[DirFile]], 23 | response_class=JSONResponse, 24 | description="获取文件列表", 25 | ) 26 | async def _(path: str | None = None) -> Result[list[DirFile]]: 27 | base_path = Path(path) if path else Path() 28 | data_list = [] 29 | for file in os.listdir(base_path): 30 | file_path = base_path / file 31 | is_image = any(file.endswith(f".{t}") for t in IMAGE_TYPE) 32 | data_list.append( 33 | DirFile( 34 | is_file=not file_path.is_dir(), 35 | is_image=is_image, 36 | name=file, 37 | parent=path, 38 | ) 39 | ) 40 | return Result.ok(data_list) 41 | 42 | 43 | @router.get( 44 | "/get_resources_size", 45 | dependencies=[authentication()], 46 | response_model=Result[list[SystemFolderSize]], 47 | response_class=JSONResponse, 48 | description="获取文件列表", 49 | ) 50 | async def _(full_path: str | None = None) -> Result[list[SystemFolderSize]]: 51 | return Result.ok(await get_system_disk(full_path)) 52 | 53 | 54 | @router.post( 55 | "/delete_file", 56 | dependencies=[authentication()], 57 | response_model=Result, 58 | response_class=JSONResponse, 59 | description="删除文件", 60 | ) 61 | async def _(param: DeleteFile) -> Result: 62 | path = Path(param.full_path) 63 | if not path or not path.exists(): 64 | return Result.warning_("文件不存在...") 65 | try: 66 | path.unlink() 67 | return Result.ok("删除成功!") 68 | except Exception as e: 69 | return Result.warning_(f"删除失败: {e!s}") 70 | 71 | 72 | @router.post( 73 | "/delete_folder", 74 | dependencies=[authentication()], 75 | response_model=Result, 76 | response_class=JSONResponse, 77 | description="删除文件夹", 78 | ) 79 | async def _(param: DeleteFile) -> Result: 80 | path = Path(param.full_path) 81 | if not path or not path.exists() or path.is_file(): 82 | return Result.warning_("文件夹不存在...") 83 | try: 84 | shutil.rmtree(path.absolute()) 85 | return Result.ok("删除成功!") 86 | except Exception as e: 87 | return Result.warning_(f"删除失败: {e!s}") 88 | 89 | 90 | @router.post( 91 | "/rename_file", 92 | dependencies=[authentication()], 93 | response_model=Result, 94 | response_class=JSONResponse, 95 | description="重命名文件", 96 | ) 97 | async def _(param: RenameFile) -> Result: 98 | path = ( 99 | (Path(param.parent) / param.old_name) if param.parent else Path(param.old_name) 100 | ) 101 | if not path or not path.exists(): 102 | return Result.warning_("文件不存在...") 103 | try: 104 | path.rename(path.parent / param.name) 105 | return Result.ok("重命名成功!") 106 | except Exception as e: 107 | return Result.warning_(f"重命名失败: {e!s}") 108 | 109 | 110 | @router.post( 111 | "/rename_folder", 112 | dependencies=[authentication()], 113 | response_model=Result, 114 | response_class=JSONResponse, 115 | description="重命名文件夹", 116 | ) 117 | async def _(param: RenameFile) -> Result: 118 | path = ( 119 | (Path(param.parent) / param.old_name) if param.parent else Path(param.old_name) 120 | ) 121 | if not path or not path.exists() or path.is_file(): 122 | return Result.warning_("文件夹不存在...") 123 | try: 124 | new_path = path.parent / param.name 125 | shutil.move(path.absolute(), new_path.absolute()) 126 | return Result.ok("重命名成功!") 127 | except Exception as e: 128 | return Result.warning_(f"重命名失败: {e!s}") 129 | 130 | 131 | @router.post( 132 | "/add_file", 133 | dependencies=[authentication()], 134 | response_model=Result, 135 | response_class=JSONResponse, 136 | description="新建文件", 137 | ) 138 | async def _(param: AddFile) -> Result: 139 | path = (Path(param.parent) / param.name) if param.parent else Path(param.name) 140 | if path.exists(): 141 | return Result.warning_("文件已存在...") 142 | try: 143 | path.open("w") 144 | return Result.ok("新建文件成功!") 145 | except Exception as e: 146 | return Result.warning_(f"新建文件失败: {e!s}") 147 | 148 | 149 | @router.post( 150 | "/add_folder", 151 | dependencies=[authentication()], 152 | response_model=Result, 153 | response_class=JSONResponse, 154 | description="新建文件夹", 155 | ) 156 | async def _(param: AddFile) -> Result: 157 | path = (Path(param.parent) / param.name) if param.parent else Path(param.name) 158 | if path.exists(): 159 | return Result.warning_("文件夹已存在...") 160 | try: 161 | path.mkdir() 162 | return Result.ok("新建文件夹成功!") 163 | except Exception as e: 164 | return Result.warning_(f"新建文件夹失败: {e!s}") 165 | 166 | 167 | @router.get( 168 | "/read_file", 169 | dependencies=[authentication()], 170 | response_model=Result[str], 171 | response_class=JSONResponse, 172 | description="读取文件", 173 | ) 174 | async def _(full_path: str) -> Result: 175 | path = Path(full_path) 176 | if not path.exists(): 177 | return Result.warning_("文件不存在...") 178 | try: 179 | text = path.read_text(encoding="utf-8") 180 | return Result.ok(text) 181 | except Exception as e: 182 | return Result.warning_(f"读取文件失败: {e!s}") 183 | 184 | 185 | @router.post( 186 | "/save_file", 187 | dependencies=[authentication()], 188 | response_model=Result[str], 189 | response_class=JSONResponse, 190 | description="读取文件", 191 | ) 192 | async def _(param: SaveFile) -> Result[str]: 193 | path = Path(param.full_path) 194 | try: 195 | async with aiofiles.open(path, "w", encoding="utf-8") as f: 196 | await f.write(param.content) 197 | return Result.ok("更新成功!") 198 | except Exception as e: 199 | return Result.warning_(f"保存文件失败: {e!s}") 200 | 201 | 202 | @router.get( 203 | "/get_image", 204 | dependencies=[authentication()], 205 | response_model=Result[str], 206 | response_class=JSONResponse, 207 | description="读取图片base64", 208 | ) 209 | async def _(full_path: str) -> Result[str]: 210 | path = Path(full_path) 211 | if not path.exists(): 212 | return Result.warning_("文件不存在...") 213 | try: 214 | return Result.ok(BuildImage.open(path).pic2bs4()) 215 | except Exception as e: 216 | return Result.warning_(f"获取图片失败: {e!s}") 217 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/web_ui/api/tabs/system/model.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class DirFile(BaseModel): 5 | """ 6 | 文件或文件夹 7 | """ 8 | 9 | is_file: bool 10 | """是否为文件""" 11 | is_image: bool 12 | """是否为图片""" 13 | name: str 14 | """文件夹或文件名称""" 15 | parent: str | None = None 16 | """父级""" 17 | 18 | 19 | class DeleteFile(BaseModel): 20 | """ 21 | 删除文件 22 | """ 23 | 24 | full_path: str 25 | """文件全路径""" 26 | 27 | 28 | class RenameFile(BaseModel): 29 | """ 30 | 删除文件 31 | """ 32 | 33 | parent: str | None 34 | """父路径""" 35 | old_name: str 36 | """旧名称""" 37 | name: str 38 | """新名称""" 39 | 40 | 41 | class AddFile(BaseModel): 42 | """ 43 | 新建文件 44 | """ 45 | 46 | parent: str | None = None 47 | """父路径""" 48 | name: str 49 | """新名称""" 50 | 51 | 52 | class SaveFile(BaseModel): 53 | """ 54 | 保存文件 55 | """ 56 | 57 | full_path: str 58 | """全路径""" 59 | content: str 60 | """内容""" 61 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/web_ui/auth/__init__.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | import json 3 | 4 | import aiofiles 5 | from fastapi import APIRouter, Depends 6 | from fastapi.security import OAuth2PasswordRequestForm 7 | import nonebot 8 | 9 | from ...config import config 10 | from ..base_model import Result 11 | from ..utils import ( 12 | ACCESS_TOKEN_EXPIRE_MINUTES, 13 | create_token, 14 | get_user, 15 | token_data, 16 | token_file, 17 | ) 18 | 19 | app = nonebot.get_app() 20 | 21 | 22 | router = APIRouter() 23 | 24 | 25 | @router.post("/login") 26 | async def login_get_token(form_data: OAuth2PasswordRequestForm = Depends()): 27 | username = config.zxui_username 28 | password = config.zxui_password 29 | if not username or not password: 30 | return Result.fail("你滴配置文件里用户名密码配置项为空", 998) 31 | if username != form_data.username or str(password) != form_data.password: 32 | return Result.fail("真笨, 账号密码都能记错!", 999) 33 | user = get_user(form_data.username) 34 | if not user: 35 | return Result.fail("用户不存在...", 997) 36 | access_token = create_token( 37 | user=user, 38 | expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES), 39 | ) 40 | token_data["token"].append(access_token) 41 | if len(token_data["token"]) > 3: 42 | token_data["token"] = token_data["token"][1:] 43 | async with aiofiles.open(token_file, "w", encoding="utf8") as f: 44 | await f.write(json.dumps(token_data, ensure_ascii=False, indent=4)) 45 | return Result.ok( 46 | {"access_token": access_token, "token_type": "bearer"}, "欢迎回家, 欧尼酱!" 47 | ) 48 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/web_ui/base_model.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Any, Generic, TypeVar 3 | 4 | from pydantic import BaseModel, validator 5 | 6 | T = TypeVar("T") 7 | 8 | RT = TypeVar("RT") 9 | 10 | 11 | class User(BaseModel): 12 | username: str 13 | password: str 14 | 15 | 16 | class Token(BaseModel): 17 | access_token: str 18 | token_type: str 19 | 20 | 21 | class Result(BaseModel, Generic[RT]): 22 | """ 23 | 总体返回 24 | """ 25 | 26 | suc: bool 27 | """调用状态""" 28 | code: int = 200 29 | """code""" 30 | info: str = "操作成功" 31 | """info""" 32 | warning: str | None = None 33 | """警告信息""" 34 | data: RT | None = None 35 | """返回数据""" 36 | 37 | @classmethod 38 | def warning_(cls, info: str, code: int = 200) -> "Result[RT]": 39 | return cls(suc=True, warning=info, code=code) 40 | 41 | @classmethod 42 | def fail(cls, info: str = "异常错误", code: int = 500) -> "Result[RT]": 43 | return cls(suc=False, info=info, code=code) 44 | 45 | @classmethod 46 | def ok( 47 | cls, data: Any = None, info: str = "操作成功", code: int = 200 48 | ) -> "Result[RT]": 49 | return cls(suc=True, info=info, code=code, data=data) 50 | 51 | 52 | class QueryModel(BaseModel, Generic[T]): 53 | """ 54 | 基本查询条件 55 | """ 56 | 57 | index: int 58 | """页数""" 59 | size: int 60 | """每页数量""" 61 | data: T | None = None 62 | """携带数据""" 63 | 64 | @validator("index") 65 | def index_validator(cls, index): 66 | if index < 1: 67 | raise ValueError("查询下标小于1...") 68 | return index 69 | 70 | @validator("size") 71 | def size_validator(cls, size): 72 | if size < 1: 73 | raise ValueError("每页数量小于1...") 74 | return size 75 | 76 | 77 | class BaseResultModel(BaseModel): 78 | """ 79 | 基础返回 80 | """ 81 | 82 | total: int 83 | """总页数""" 84 | data: Any 85 | """数据""" 86 | 87 | 88 | class SystemStatus(BaseModel): 89 | """ 90 | 系统状态 91 | """ 92 | 93 | cpu: float 94 | memory: float 95 | disk: float 96 | check_time: datetime 97 | 98 | 99 | class SystemFolderSize(BaseModel): 100 | """ 101 | 资源文件占比 102 | """ 103 | 104 | name: str 105 | """名称""" 106 | size: float 107 | """大小""" 108 | full_path: str | None 109 | """完整路径""" 110 | is_dir: bool 111 | """是否为文件夹""" 112 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/web_ui/config.py: -------------------------------------------------------------------------------- 1 | from strenum import StrEnum 2 | 3 | from ..config import DATA_PATH 4 | 5 | WEBUI_STRING = "web_ui" 6 | PUBLIC_STRING = "public" 7 | 8 | WEBUI_DATA_PATH = DATA_PATH / WEBUI_STRING 9 | PUBLIC_PATH = WEBUI_DATA_PATH / PUBLIC_STRING 10 | TMP_PATH = DATA_PATH / "tmp" / WEBUI_STRING 11 | TMP_PATH.mkdir(parents=True, exist_ok=True) 12 | 13 | WEBUI_DIST_GITHUB_URL = "https://github.com/HibiKier/zhenxun_bot_webui/tree/dist" 14 | 15 | 16 | AVA_URL = "http://q1.qlogo.cn/g?b=qq&nk={}&s=160" 17 | 18 | GROUP_AVA_URL = "http://p.qlogo.cn/gh/{}/{}/640/" 19 | 20 | 21 | class QueryDateType(StrEnum): 22 | """ 23 | 查询日期类型 24 | """ 25 | 26 | DAY = "day" 27 | """日""" 28 | WEEK = "week" 29 | """周""" 30 | MONTH = "month" 31 | """月""" 32 | YEAR = "year" 33 | """年""" 34 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/web_ui/public/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, FastAPI 2 | from fastapi.responses import FileResponse 3 | from fastapi.staticfiles import StaticFiles 4 | from nonebot import logger 5 | 6 | from ..config import PUBLIC_PATH 7 | from .data_source import COMMAND_NAME, update_webui_assets 8 | 9 | router = APIRouter() 10 | 11 | 12 | @router.get("/") 13 | async def index(): 14 | return FileResponse(PUBLIC_PATH / "index.html") 15 | 16 | 17 | @router.get("/favicon.ico") 18 | async def favicon(): 19 | return FileResponse(PUBLIC_PATH / "favicon.ico") 20 | 21 | 22 | @router.get("/79edfa81f3308a9f.jfif") 23 | async def _(): 24 | return FileResponse(PUBLIC_PATH / "79edfa81f3308a9f.jfif") 25 | 26 | 27 | async def init_public(app: FastAPI): 28 | try: 29 | if not PUBLIC_PATH.exists(): 30 | folders = await update_webui_assets() 31 | else: 32 | folders = [x.name for x in PUBLIC_PATH.iterdir() if x.is_dir()] 33 | app.include_router(router) 34 | for pathname in folders: 35 | logger.debug(f"挂载文件夹: {pathname}") 36 | app.mount( 37 | f"/{pathname}", 38 | StaticFiles(directory=PUBLIC_PATH / pathname, check_dir=True), 39 | name=f"public_{pathname}", 40 | ) 41 | except Exception as e: 42 | logger.error("初始化 WebUI资源 失败", COMMAND_NAME, e=e) 43 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/web_ui/public/data_source.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import shutil 3 | import zipfile 4 | 5 | from nonebot import logger 6 | from nonebot.utils import run_sync 7 | from zhenxun_utils.github_utils import GithubUtils 8 | from zhenxun_utils.http_utils import AsyncHttpx 9 | 10 | from ..config import PUBLIC_PATH, TMP_PATH, WEBUI_DIST_GITHUB_URL 11 | 12 | COMMAND_NAME = "WebUI资源管理" 13 | 14 | 15 | async def update_webui_assets(): 16 | webui_assets_path = TMP_PATH / "webui_assets.zip" 17 | download_url = await GithubUtils.parse_github_url( 18 | WEBUI_DIST_GITHUB_URL 19 | ).get_archive_download_urls() 20 | if await AsyncHttpx.download_file( 21 | download_url, webui_assets_path, follow_redirects=True 22 | ): 23 | logger.info("下载 webui_assets 成功...", COMMAND_NAME) 24 | return await _file_handle(webui_assets_path) 25 | raise Exception("下载 webui_assets 失败", COMMAND_NAME) 26 | 27 | 28 | @run_sync 29 | def _file_handle(webui_assets_path: Path): 30 | logger.debug("开始解压 webui_assets...", COMMAND_NAME) 31 | if webui_assets_path.exists(): 32 | tf = zipfile.ZipFile(webui_assets_path) 33 | tf.extractall(TMP_PATH) 34 | logger.debug("解压 webui_assets 成功...", COMMAND_NAME) 35 | else: 36 | raise Exception("解压 webui_assets 失败,文件不存在...", COMMAND_NAME) 37 | download_file_path = next(f for f in TMP_PATH.iterdir() if f.is_dir()) 38 | shutil.rmtree(PUBLIC_PATH, ignore_errors=True) 39 | shutil.copytree(download_file_path / "dist", PUBLIC_PATH, dirs_exist_ok=True) 40 | logger.debug("复制 webui_assets 成功...", COMMAND_NAME) 41 | shutil.rmtree(TMP_PATH, ignore_errors=True) 42 | return [x.name for x in PUBLIC_PATH.iterdir() if x.is_dir()] 43 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/web_ui/utils.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import os 3 | import secrets 4 | from datetime import datetime, timedelta, timezone 5 | from pathlib import Path 6 | 7 | import psutil 8 | import ujson as json 9 | from fastapi import Depends, HTTPException 10 | from fastapi.security import OAuth2PasswordBearer 11 | from jose import JWTError, jwt 12 | from nonebot.utils import run_sync 13 | 14 | from ..config import DATA_PATH, config 15 | from .base_model import SystemFolderSize, SystemStatus, User 16 | 17 | ALGORITHM = "HS256" 18 | ACCESS_TOKEN_EXPIRE_MINUTES = 30 19 | 20 | SECRET_FILE = DATA_PATH / "secret.txt" 21 | 22 | oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/login") 23 | 24 | token_file = DATA_PATH / "token.json" 25 | token_file.parent.mkdir(parents=True, exist_ok=True) 26 | token_data = {"token": []} 27 | if token_file.exists(): 28 | with contextlib.suppress(json.JSONDecodeError): 29 | token_data = json.load(open(token_file, encoding="utf8")) 30 | 31 | if not SECRET_FILE.exists(): 32 | with SECRET_FILE.open("w", encoding="utf8") as f: 33 | f.write(secrets.token_urlsafe(32)) 34 | 35 | 36 | def get_user(uname: str) -> User | None: 37 | """获取账号密码 38 | 39 | 参数: 40 | uname: uname 41 | 42 | 返回: 43 | Optional[User]: 用户信息 44 | """ 45 | username = config.zxui_username 46 | password = config.zxui_password 47 | if username and password and uname == username: 48 | return User(username=username, password=password) 49 | 50 | 51 | def create_token(user: User, expires_delta: timedelta | None = None): 52 | """创建token 53 | 54 | 参数: 55 | user: 用户信息 56 | expires_delta: 过期时间. 57 | """ 58 | with SECRET_FILE.open(encoding="utf8") as f: 59 | secret = f.read().strip() 60 | expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=15)) 61 | return jwt.encode( 62 | claims={"sub": user.username, "exp": expire}, 63 | key=secret, 64 | algorithm=ALGORITHM, 65 | ) 66 | 67 | 68 | def authentication(): 69 | """权限验证 70 | 71 | 异常: 72 | JWTError: JWTError 73 | HTTPException: HTTPException 74 | """ 75 | 76 | # if token not in token_data["token"]: 77 | def inner(token: str = Depends(oauth2_scheme)): 78 | try: 79 | with SECRET_FILE.open(encoding="utf8") as f: 80 | secret = f.read().strip() 81 | payload = jwt.decode(token, secret, algorithms=[ALGORITHM]) 82 | username, _ = payload.get("sub"), payload.get("exp") 83 | user = get_user(username) # type: ignore 84 | if user is None: 85 | raise JWTError 86 | except JWTError: 87 | raise HTTPException( 88 | status_code=400, detail="登录验证失败或已失效, 踢出房间!" 89 | ) 90 | 91 | return Depends(inner) 92 | 93 | 94 | def _get_dir_size(dir_path: Path) -> float: 95 | """获取文件夹大小 96 | 97 | 参数: 98 | dir_path: 文件夹路径 99 | """ 100 | return sum( 101 | sum(os.path.getsize(os.path.join(root, name)) for name in files) 102 | for root, dirs, files in os.walk(dir_path) 103 | ) 104 | 105 | 106 | @run_sync 107 | def get_system_status() -> SystemStatus: 108 | """获取系统信息等""" 109 | cpu = psutil.cpu_percent() 110 | memory = psutil.virtual_memory().percent 111 | disk = psutil.disk_usage("/").percent 112 | return SystemStatus( 113 | cpu=cpu, 114 | memory=memory, 115 | disk=disk, 116 | check_time=datetime.now().replace(microsecond=0), 117 | ) 118 | 119 | 120 | @run_sync 121 | def get_system_disk( 122 | full_path: str | None, 123 | ) -> list[SystemFolderSize]: 124 | """获取资源文件大小等""" 125 | base_path = Path(full_path) if full_path else Path() 126 | other_size = 0 127 | data_list = [] 128 | for file in os.listdir(base_path): 129 | f = base_path / file 130 | if f.is_dir(): 131 | size = _get_dir_size(f) / 1024 / 1024 132 | data_list.append( 133 | SystemFolderSize(name=file, size=size, full_path=str(f), is_dir=True) 134 | ) 135 | else: 136 | other_size += f.stat().st_size / 1024 / 1024 137 | if other_size: 138 | data_list.append( 139 | SystemFolderSize( 140 | name="other_file", size=other_size, full_path=full_path, is_dir=False 141 | ) 142 | ) 143 | return data_list 144 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/zxpm/__init__.py: -------------------------------------------------------------------------------- 1 | from nonebot.plugin import PluginMetadata, inherit_supported_adapters 2 | from zhenxun_utils.enum import PluginType 3 | 4 | from .commands import * # noqa: F403 5 | from .config import Config 6 | from .extra import PluginExtraData 7 | 8 | __plugin_meta__ = PluginMetadata( 9 | name="ZXPM插件管理", 10 | description="真寻的插件管理系统", 11 | usage=""" 12 | 包含了插件功能开关,群组/用户ban,群管监测,设置权限功能 13 | 并提供一个简单的帮助接口,可以通过 zxpm [名称] 来获取帮助 14 | 可以通过 -s 参数来获取该功能超级用户帮助 15 | 例如: 16 | zxpm ban 17 | zxpm ban -s 18 | """, 19 | type="application", 20 | homepage="https://github.com/HibiKier/nonebot-plugin-zxpm", 21 | config=Config, 22 | supported_adapters=inherit_supported_adapters( 23 | "nonebot_plugin_alconna", 24 | "nonebot_plugin_session", 25 | "nonebot_plugin_uninfo", 26 | ), 27 | extra=PluginExtraData(plugin_type=PluginType.PARENT).to_dict(), 28 | ) 29 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/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 | from zhenxun_utils.log import logger 7 | 8 | from ...models.group_console import GroupConsole 9 | from ...models.plugin_info import PluginInfo 10 | from .zxpm_ban import * # noqa: F403 11 | from .zxpm_bot_manage 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 | 18 | driver = nonebot.get_driver() 19 | 20 | with contextlib.suppress(ImportError): 21 | from nonebot.adapters.onebot.v11 import GroupIncreaseNoticeEvent # noqa: F401 22 | 23 | from .zxpm_add_group import * # noqa: F403 24 | from .zxpm_admin_watch import * # noqa: F403 25 | 26 | 27 | @driver.on_bot_connect 28 | async def _(bot: Bot): 29 | """更新bot群组信息 30 | 31 | 参数: 32 | bot: Bot 33 | """ 34 | try: 35 | if interface := get_interface(bot): 36 | scens = await interface.get_scenes() 37 | group_list = [(s.id, s.name) for s in scens if s.is_group] 38 | db_group_list = await GroupConsole.all().values_list("group_id", flat=True) 39 | block_modules = await PluginInfo.filter( 40 | load_status=True, default_status=False 41 | ).values_list("module", flat=True) 42 | block_modules = [f"<{module}" for module in block_modules] 43 | create_list = [] 44 | for gid, name in group_list: 45 | if gid not in db_group_list: 46 | group = GroupConsole(group_id=gid, group_name=name) 47 | if block_modules: 48 | group.block_plugin = ",".join(block_modules) + "," 49 | logger.debug(f"Bot: {bot.self_id} 添加创建群组Id: {group.group_id}") 50 | create_list.append(group) 51 | if create_list: 52 | await GroupConsole.bulk_create(create_list, 10) 53 | logger.debug( 54 | f"更新Bot: {bot.self_id} 共创建 {len(create_list)} 条群组数据..." 55 | ) 56 | except Exception as e: 57 | logger.error(f"获取Bot: {bot.self_id} 群组发生错误...", e=e) 58 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/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 | from zhenxun_utils.enum import PluginType 7 | 8 | from ....models.group_console import GroupConsole 9 | from ...extra import PluginExtraData 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_zxui/zxpm/commands/zxpm_add_group/data_source.py: -------------------------------------------------------------------------------- 1 | from nonebot.adapters import Bot 2 | from zhenxun_utils.log import logger 3 | 4 | from ....models.group_console import GroupConsole 5 | from ....models.level_user import LevelUser 6 | from ....models.plugin_info import PluginInfo 7 | from ...config import ZxpmConfig 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_zxui/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 | from zhenxun_utils.enum import PluginType 5 | from zhenxun_utils.log import logger 6 | 7 | from ....models.level_user import LevelUser 8 | from ...config import ZxpmConfig 9 | from ...extra import PluginExtraData 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_zxui/zxpm/commands/zxpm_ban/_data_source.py: -------------------------------------------------------------------------------- 1 | import time 2 | from typing import Literal 3 | 4 | from nonebot_plugin_session import EventSession 5 | from zhenxun_utils._image_template import ImageTemplate 6 | 7 | from ....models.ban_console import BanConsole 8 | from ....models.level_user import LevelUser 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 | None: 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_zxui/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 # noqa: E402 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_zxui/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_zxui/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_zxui/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_zxui/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_zxui/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 | from zhenxun_utils.enum import PluginType 14 | from zhenxun_utils.log import logger 15 | from zhenxun_utils.message import MessageUtils 16 | 17 | from ...extra import PluginExtraData 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_zxui/zxpm/commands/zxpm_help/_data_source.py: -------------------------------------------------------------------------------- 1 | import nonebot 2 | from zhenxun_utils._image_template import ImageTemplate 3 | from zhenxun_utils.enum import PluginType 4 | 5 | from ....models.level_user import LevelUser 6 | from ....models.plugin_info import PluginInfo 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_zxui/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_zxui/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_zxui/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 | from zhenxun_utils.enum import PluginType 9 | from zhenxun_utils.log import logger 10 | from zhenxun_utils.message import MessageUtils 11 | 12 | from ....models.ban_console import BanConsole 13 | from ....models.group_console import GroupConsole 14 | from ...config import ZxpmConfig 15 | from ...extra.limit import FreqLimiter 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_zxui/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 | from zhenxun_utils.enum import PluginType 7 | from zhenxun_utils.log import logger 8 | 9 | from ....models.plugin_info import PluginInfo 10 | from ....models.plugin_limit import PluginLimit 11 | from ...extra import PluginExtraData, PluginSetting 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 extra_data.author: 48 | extra_data.author = str(extra_data.author) 49 | if plugin.sub_plugins: 50 | extra_data.plugin_type = PluginType.PARENT 51 | plugin_list.append( 52 | PluginInfo( 53 | module=plugin.name, 54 | module_path=plugin.module_name, 55 | name=metadata.name, 56 | author=extra_data.author, 57 | version=extra_data.version, 58 | level=setting.level, 59 | default_status=setting.default_status, 60 | limit_superuser=setting.limit_superuser, 61 | menu_type=extra_data.menu_type, 62 | cost_gold=setting.cost_gold, 63 | plugin_type=extra_data.plugin_type, 64 | admin_level=extra_data.admin_level, 65 | parent=(plugin.parent_plugin.module_name if plugin.parent_plugin else None), 66 | ) 67 | ) 68 | if extra_data.limits: 69 | limit_list.extend( 70 | PluginLimit( 71 | module=plugin.name, 72 | module_path=plugin.module_name, 73 | limit_type=limit._type, 74 | watch_type=limit.watch_type, 75 | status=limit.status, 76 | check_type=limit.check_type, 77 | result=limit.result, 78 | cd=getattr(limit, "cd", None), 79 | max_count=getattr(limit, "max_count", None), 80 | ) 81 | for limit in extra_data.limits 82 | ) 83 | 84 | 85 | @driver.on_startup 86 | async def _(): 87 | """ 88 | 初始化插件数据配置 89 | """ 90 | plugin_list: list[PluginInfo] = [] 91 | limit_list: list[PluginLimit] = [] 92 | module2id = {} 93 | load_plugin = [] 94 | if module_list := await PluginInfo.all().values("id", "module_path"): 95 | module2id = {m["module_path"]: m["id"] for m in module_list} 96 | for plugin in get_loaded_plugins(): 97 | load_plugin.append(plugin.module_name) 98 | await _handle_setting(plugin, plugin_list, limit_list) 99 | create_list = [] 100 | update_list = [] 101 | for plugin in plugin_list: 102 | if plugin.module_path not in module2id: 103 | create_list.append(plugin) 104 | else: 105 | plugin.id = module2id[plugin.module_path] 106 | await plugin.save( 107 | update_fields=[ 108 | "name", 109 | "author", 110 | "version", 111 | "admin_level", 112 | "plugin_type", 113 | ] 114 | ) 115 | update_list.append(plugin) 116 | if create_list: 117 | await PluginInfo.bulk_create(create_list, 10) 118 | await PluginInfo.filter(module_path__in=load_plugin).update(load_status=True) 119 | await PluginInfo.filter(module_path__not_in=load_plugin).update(load_status=False) 120 | manager.init() 121 | if limit_list: 122 | for limit in limit_list: 123 | if not manager.exist(limit.module_path, limit.limit_type): 124 | """不存在,添加""" 125 | manager.add(limit.module_path, limit) 126 | manager.save_file() 127 | await manager.load_to_db() 128 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/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 | from zhenxun_utils.enum import BlockType, PluginType 6 | from zhenxun_utils.log import logger 7 | from zhenxun_utils.message import MessageUtils 8 | 9 | from ...config import ZxpmConfig 10 | from ...extra import PluginExtraData 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_zxui/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_zxui/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 | from zhenxun_utils.enum import PluginType 15 | from zhenxun_utils.log import logger 16 | from zhenxun_utils.message import MessageUtils 17 | 18 | from ....models.level_user import LevelUser 19 | from ...extra import PluginExtraData 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_zxui/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 | from zhenxun_utils.enum import PluginType 16 | from zhenxun_utils.message import MessageUtils 17 | 18 | from ....models.group_console import GroupConsole 19 | from ...extra import PluginExtraData 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_zxui/zxpm/config.py: -------------------------------------------------------------------------------- 1 | import nonebot 2 | from pydantic import BaseModel 3 | 4 | from ..config import DATA_PATH 5 | 6 | 7 | class Config(BaseModel): 8 | zxpm_notice_info_cd: int = 300 9 | """群/用户权限检测等各种检测提示信息cd,为0时不提醒""" 10 | zxpm_ban_reply: str = "才不会给你发消息." 11 | """用户被ban时回复消息,为空时不回复""" 12 | zxpm_ban_level: int = 5 13 | """使用ban功能的对应权限""" 14 | zxpm_switch_level: int = 1 15 | """群组插件开关管理对应权限""" 16 | zxpm_admin_default_auth: int = 5 17 | """群组管理员默认权限""" 18 | zxpm_limit_superuser: bool = False 19 | """是否限制超管权限""" 20 | 21 | 22 | ZxpmConfig = nonebot.get_plugin_config(Config) 23 | 24 | zxpm_data_path = DATA_PATH / "zxpm" 25 | zxpm_data_path.mkdir(parents=True, exist_ok=True) 26 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/zxpm/extra/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from nonebot.compat import PYDANTIC_V2 4 | from pydantic import BaseModel 5 | from zhenxun_utils.enum import PluginType 6 | 7 | from .limit import BaseBlock, PluginCdBlock, PluginCountBlock 8 | 9 | 10 | class PluginSetting(BaseModel): 11 | """ 12 | 插件基本配置 13 | """ 14 | 15 | level: int = 5 16 | """群权限等级""" 17 | default_status: bool = True 18 | """进群默认开关状态""" 19 | limit_superuser: bool = False 20 | """是否限制超级用户""" 21 | cost_gold: int = 0 22 | """调用插件花费金币""" 23 | 24 | 25 | class PluginExtraData(BaseModel): 26 | """ 27 | 插件扩展信息 28 | """ 29 | 30 | author: Any = None 31 | """作者""" 32 | version: str | None = None 33 | """版本""" 34 | plugin_type: PluginType = PluginType.NORMAL 35 | """插件类型""" 36 | menu_type: str = "功能" 37 | """菜单类型""" 38 | admin_level: int | None = None 39 | """管理员插件所需权限等级""" 40 | setting: PluginSetting | None = None 41 | """插件基本配置""" 42 | limits: list[BaseBlock | PluginCdBlock | PluginCountBlock] | None = None 43 | """插件限制""" 44 | superuser_help: str | None = None 45 | """超级用户帮助""" 46 | 47 | def to_dict(self): 48 | return self.model_dump() if PYDANTIC_V2 else self.dict() 49 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/zxpm/extra/limit.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from datetime import datetime 3 | import time 4 | from typing import Any 5 | 6 | from pydantic import BaseModel 7 | import pytz 8 | from zhenxun_utils.enum import BlockType, LimitWatchType, PluginLimitType 9 | 10 | 11 | class BaseBlock(BaseModel): 12 | """ 13 | 插件阻断基本类(插件阻断限制) 14 | """ 15 | 16 | status: bool = True 17 | """限制状态""" 18 | check_type: BlockType = BlockType.ALL 19 | """检查类型""" 20 | watch_type: LimitWatchType = LimitWatchType.USER 21 | """监听对象""" 22 | result: str | None = None 23 | """阻断时回复内容""" 24 | _type: PluginLimitType = PluginLimitType.BLOCK 25 | """类型""" 26 | 27 | 28 | class PluginCdBlock(BaseBlock): 29 | """ 30 | 插件cd限制 31 | """ 32 | 33 | cd: int = 5 34 | """cd""" 35 | _type: PluginLimitType = PluginLimitType.CD 36 | """类型""" 37 | 38 | 39 | class PluginCountBlock(BaseBlock): 40 | """ 41 | 插件次数限制 42 | """ 43 | 44 | max_count: int 45 | """最大调用次数""" 46 | _type: PluginLimitType = PluginLimitType.COUNT 47 | """类型""" 48 | 49 | 50 | class CountLimiter: 51 | """ 52 | 每日调用命令次数限制 53 | """ 54 | 55 | tz = pytz.timezone("Asia/Shanghai") 56 | 57 | def __init__(self, max_num): 58 | self.today = -1 59 | self.count = defaultdict(int) 60 | self.max = max_num 61 | 62 | def check(self, key) -> bool: 63 | day = datetime.now(self.tz).day 64 | if day != self.today: 65 | self.today = day 66 | self.count.clear() 67 | return self.count[key] < self.max 68 | 69 | def get_num(self, key): 70 | return self.count[key] 71 | 72 | def increase(self, key, num=1): 73 | self.count[key] += num 74 | 75 | def reset(self, key): 76 | self.count[key] = 0 77 | 78 | 79 | class UserBlockLimiter: 80 | """ 81 | 检测用户是否正在调用命令 82 | """ 83 | 84 | def __init__(self): 85 | self.flag_data = defaultdict(bool) 86 | self.time = time.time() 87 | 88 | def set_true(self, key: Any): 89 | self.time = time.time() 90 | self.flag_data[key] = True 91 | 92 | def set_false(self, key: Any): 93 | self.flag_data[key] = False 94 | 95 | def check(self, key: Any) -> bool: 96 | if time.time() - self.time > 30: 97 | self.set_false(key) 98 | return not self.flag_data[key] 99 | 100 | 101 | class FreqLimiter: 102 | """ 103 | 命令冷却,检测用户是否处于冷却状态 104 | """ 105 | 106 | def __init__(self, default_cd_seconds: int): 107 | self.next_time = defaultdict(float) 108 | self.default_cd = default_cd_seconds 109 | 110 | def check(self, key: Any) -> bool: 111 | return time.time() >= self.next_time[key] 112 | 113 | def start_cd(self, key: Any, cd_time: int = 0): 114 | self.next_time[key] = time.time() + ( 115 | cd_time if cd_time > 0 else self.default_cd 116 | ) 117 | 118 | def left_time(self, key: Any) -> float: 119 | return self.next_time[key] - time.time() 120 | -------------------------------------------------------------------------------- /nonebot_plugin_zxui/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 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "nonebot-plugin-zxui" 3 | version = "0.3.6" 4 | description = "" 5 | authors = ["HibiKier <775757368@qq.com>"] 6 | readme = "README.md" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.10" 10 | nonebot2 = "^2.3.3" 11 | zhenxun-db-client = "^0.1.2" 12 | zhenxun-utils = "^0.2.2" 13 | nonebot-plugin-alconna = "^0.54.1" 14 | nonebot-plugin-uninfo = "^0.6.2" 15 | nonebot-plugin-session = "^0.3.2" 16 | nonebot-plugin-apscheduler = "^0.5.0" 17 | ruamel-yaml = "^0.18.6" 18 | psutil = "^6.1.1" 19 | ujson = "^5.10.0" 20 | python-jose = "^3.3.0" 21 | python-multipart = "^0.0.20" 22 | fastapi = "^0.115.6" 23 | websockets = "^14.1" 24 | nonebot-plugin-localstore = "^0.7.3" 25 | 26 | 27 | [build-system] 28 | requires = ["poetry-core"] 29 | build-backend = "poetry.core.masonry.api" 30 | --------------------------------------------------------------------------------