├── .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 |
11 |
12 |
13 | # nonebot-plugin-zxui
14 |
15 | _✨ 基于 [NoneBot2](https://github.com/nonebot/nonebot2) 的 小真寻WebUi API实现 ✨_
16 |
17 | 
18 | 
19 | 
20 | [](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 | 
121 | 
122 | 
123 | 
124 | 
125 |
126 | 
127 | 
128 | 
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 |
--------------------------------------------------------------------------------