diff --git a/poetry.lock b/poetry.lock index 0a43ca1a..e3a0dab3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3000,6 +3000,22 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "py-cpuinfo" +version = "9.0.0" +description = "Get CPU info with pure Python" +optional = false +python-versions = "*" +files = [ + {file = "py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690"}, + {file = "py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5"}, +] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "pyasn1" version = "0.6.0" @@ -4909,4 +4925,4 @@ reference = "ali" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "706cc73807663c91c3eed8d689e7f04f49fd6f0b6d2b3a2e7e4e078a2c3f89c0" +content-hash = "e66133515a2b17715ddf5f887ec31f128751acda1ea67736e9d27b29a9b16b65" diff --git a/pyproject.toml b/pyproject.toml index 7be8b329..d582538b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,6 +50,7 @@ python-multipart = "^0.0.9" nonebot-plugin-alconna = "0.51.1" arclet-alconna = "1.8.23" aiocache = "^0.12.2" +py-cpuinfo = "^9.0.0" [tool.poetry.dev-dependencies] nonebug = "^0.3.2" diff --git a/resources/template/check/main.css b/resources/template/check/main.css new file mode 100644 index 00000000..2a0e207f --- /dev/null +++ b/resources/template/check/main.css @@ -0,0 +1,179 @@ + + +@font-face { + font-family: fzrzFont; + /* 导入的字体文件 */ + src: url("./res/font/fzrzExtraBold.ttf"); +} + + + + +body { + position: absolute; + left: -8px; + top: -8px; +} + +.wrapper{ + height: 750px; + width: 395px; + background-color: antiquewhite; + position: relative; +} + +.top-image { + height: 215px; + width: 395px; +} + +.abs-image { + height: 426px; + width: 93px; + position: absolute; + right: 39px; + top: 240px; +} + +.bot-text { + height: 55px; + width: 335px; + position: relative; + padding: 0 30px; + margin-top: 10px; +} + +.main { + height: 448px; + width: 335px; + padding: 0 30px; + position: relative; +} + +.title { + background-color: #EB869D; + height: 32px; + font-family: 'fzrzFont'; + color: white; + width: max-content; + border-radius: 20px; + padding: 0 10px; + float: left; + margin-top: 10px; +} + +.title-n { + height: 23px; + width: 23px; + border-radius: 50%; + background-color: white; + position: absolute; + left: 36px; + top: 15px; + display: flex; + justify-content: center; + align-items: center; +} + +.title-a { + background-color: #ED859E; + height: 19px; + width: 19px; + border-radius: 50%; +} + +.network { + display: flex; + margin-left: 10px; +} + +.network-item { + font-family: 'fzrzFont'; + color: #EC859F; + display: flex; + justify-items: center; + align-items: center; + margin-left: 20px; +} + +.network-status { + height: 10px; + width: 10px; + border-radius: 50%; + margin-left: 5px; +} + +.network-text { + margin-left: 5px; +} + +.network-item { + margin-top: 10px; +} + +.data-status-item { + margin-top: 10px; +} + +.data-status { + width: 100%; + height: 240px; + /* background-color:blueviolet; */ + margin-top: 22px; +} + +.data-status-item { + display: flex; + font-family: 'fzrzFont'; + font-size: 15px; + line-height: 18.5px; + height: 50px; +} + +.process { + height: 28px; + width: 105px; + border: #ED859E 4px solid; + border-radius: 30px; + overflow: hidden; +} + +.process-bar { + height: 28px; + width: 30px; + background-color: #EB869D; + border-top-right-radius: 30px; + border-bottom-right-radius: 30px; +} + +.data-status-item-text { + margin-left: 10px; + color: #EC839D; +} + +.line { + background-color: #EC859F; + height: 2px; + width: 100%; + border-radius: 20px; +} + +.status-text { + font-family: 'fzrzFont'; + color: #EC859F; + height: 90px; + display: flex; +} + +.status-text-title { + height: 25%; +} + +.tip { + font-family: 'fzrzFont'; + color: #ecbac7; + font-size: 10px; + position: absolute; + right: 5px; + bottom: 1px; +} \ No newline at end of file diff --git a/resources/template/check/main.html b/resources/template/check/main.html new file mode 100644 index 00000000..7216e4bc --- /dev/null +++ b/resources/template/check/main.html @@ -0,0 +1,90 @@ + + + + + + + + + test + + + + +
+ + +
+
+
+
+
+ {{data.nickname}}自检 +
+
+
BaiDu
+
Google
+
+
+
+
+
+
+
+
+
+
CPU
+ +
{{data.cpu_info}}
+
+
+
+
+
+
+
+
RAM
+
{{data.ram_info}}
+
+
+
+
+
+
+
+
SWAP
+
{{data.swap_info}}
+
+
+
+
+
+
+
+
DISK
+
{{data.disk_info}}
+
+
+
+
+
+
+

CPU

+

SYSTEM

+

VERSION

+

PLUGINS

+
+
+

{{data.brand_raw}}

+

{{data.system}}

+

{{data.version}}

+

{{data.plugin_count}} loaded

+
+
+
+
Create By Zhenxun
+
+ + + \ No newline at end of file diff --git a/resources/template/check/main.js b/resources/template/check/main.js new file mode 100644 index 00000000..e69de29b diff --git a/resources/template/check/res/font/fzrzExtraBold.ttf b/resources/template/check/res/font/fzrzExtraBold.ttf new file mode 100644 index 00000000..8afeafca Binary files /dev/null and b/resources/template/check/res/font/fzrzExtraBold.ttf differ diff --git a/resources/template/check/res/img/bk.png b/resources/template/check/res/img/bk.png new file mode 100644 index 00000000..cf1a762f Binary files /dev/null and b/resources/template/check/res/img/bk.png differ diff --git a/resources/template/check/res/img/top.jpg b/resources/template/check/res/img/top.jpg new file mode 100644 index 00000000..4ea75eb8 Binary files /dev/null and b/resources/template/check/res/img/top.jpg differ diff --git a/zhenxun/builtin_plugins/check/__init__.py b/zhenxun/builtin_plugins/check/__init__.py new file mode 100644 index 00000000..a97ad026 --- /dev/null +++ b/zhenxun/builtin_plugins/check/__init__.py @@ -0,0 +1,53 @@ +from nonebot.rule import to_me +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot_plugin_session import EventSession +from nonebot_plugin_htmlrender import template_to_pic +from nonebot_plugin_alconna import Alconna, Arparma, on_alconna + +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils +from zhenxun.configs.utils import PluginExtraData +from zhenxun.configs.path_config import TEMPLATE_PATH + +from .data_source import get_status_info + +__plugin_meta__ = PluginMetadata( + name="服务器自我检查", + description="查看服务器当前状态", + usage=""" + 查看服务器当前状态 + 指令: + 自检 + """.strip(), + extra=PluginExtraData( + author="HibiKier", version="0.1", plugin_type=PluginType.SUPERUSER + ).dict(), +) + + +_matcher = on_alconna( + Alconna("自检"), rule=to_me(), permission=SUPERUSER, block=True, priority=1 +) + + +@_matcher.handle() +async def _(session: EventSession, arparma: Arparma): + try: + data = await get_status_info() + image = await template_to_pic( + template_path=str((TEMPLATE_PATH / "check").absolute()), + template_name="main.html", + templates={"data": data}, + pages={ + "viewport": {"width": 195, "height": 750}, + "base_url": f"file://{TEMPLATE_PATH}", + }, + wait=2, + ) + await MessageUtils.build_message(image).send() + logger.info("自检", arparma.header_result, session=session) + except Exception as e: + await MessageUtils.build_message(f"自检失败: {e}").send() + logger.error("自检失败", arparma.header_result, session=session, e=e) diff --git a/zhenxun/builtin_plugins/check/data_source.py b/zhenxun/builtin_plugins/check/data_source.py new file mode 100644 index 00000000..d7742944 --- /dev/null +++ b/zhenxun/builtin_plugins/check/data_source.py @@ -0,0 +1,163 @@ +import platform +from pathlib import Path +from dataclasses import dataclass + +import psutil +import cpuinfo +import nonebot +from pydantic import BaseModel +from nonebot.utils import run_sync +from httpx import NetworkError, ConnectTimeout + +from zhenxun.services.log import logger +from zhenxun.configs.config import BotConfig +from zhenxun.utils.http_utils import AsyncHttpx + +BAIDU_URL = "https://www.baidu.com/" +GOOGLE_URL = "https://www.google.com/" + +VERSION_FILE = Path() / "__version__" + + +@dataclass +class CPUInfo: + core: int + """CPU 物理核心数""" + usage: float + """CPU 占用百分比,取值范围(0,100]""" + freq: float + """CPU 的时钟速度(单位:GHz)""" + + @classmethod + def get_cpu_info(cls): + cpu_core = psutil.cpu_count(logical=False) + cpu_usage = psutil.cpu_percent(interval=1) + cpu_freq = round(psutil.cpu_freq().current / 1000, 2) + + return CPUInfo(core=cpu_core, usage=cpu_usage, freq=cpu_freq) + + +@dataclass +class RAMInfo: + """RAM 信息(单位:GB)""" + + total: float + """RAM 总量""" + usage: float + """当前 RAM 占用量/GB""" + + @classmethod + def get_ram_info(cls): + ram_total = round(psutil.virtual_memory().total / (1024**3), 2) + ram_usage = round(psutil.virtual_memory().used / (1024**3), 2) + + return RAMInfo(total=ram_total, usage=ram_usage) + + +@dataclass +class SwapMemory: + """Swap 信息(单位:GB)""" + + total: float + """Swap 总量""" + usage: float + """当前 Swap 占用量/GB""" + + @classmethod + def get_swap_info(cls): + swap_total = round(psutil.swap_memory().total / (1024**3), 2) + swap_usage = round(psutil.swap_memory().used / (1024**3), 2) + + return SwapMemory(total=swap_total, usage=swap_usage) + + +@dataclass +class DiskInfo: + """硬盘信息""" + + total: float + """硬盘总量""" + usage: float + """当前硬盘占用量/GB""" + + @classmethod + def get_disk_info(cls): + disk_total = round(psutil.disk_usage("/").total / (1024**3), 2) + disk_usage = round(psutil.disk_usage("/").used / (1024**3), 2) + + return DiskInfo(total=disk_total, usage=disk_usage) + + +class SystemInfo(BaseModel): + """系统信息""" + + cpu: CPUInfo + """CPU信息""" + ram: RAMInfo + """RAM信息""" + swap: SwapMemory + """SWAP信息""" + disk: DiskInfo + """DISK信息""" + + def get_system_info(self): + return { + "cpu_info": f"{self.cpu.usage}% - {self.cpu.freq}Ghz [{self.cpu.core} core]", + "cpu_process": psutil.cpu_percent(), + "ram_info": f"{self.ram.usage} / {self.ram.total} GB", + "ram_process": self.ram.usage / self.ram.total * 100, + "swap_info": f"{self.swap.usage} / {self.swap.total} GB", + "swap_process": self.swap.usage / self.swap.total * 100, + "disk_info": f"{self.disk.usage} / {self.disk.total} GB", + "disk_process": self.disk.usage / self.disk.total * 100, + } + + +@run_sync +def __build_status() -> dict: + """获取 `CPU` `RAM` `SWAP` `DISK` 信息""" + cpu = CPUInfo.get_cpu_info() + ram = RAMInfo.get_ram_info() + swap = SwapMemory.get_swap_info() + disk = DiskInfo.get_disk_info() + + return SystemInfo(cpu=cpu, ram=ram, swap=swap, disk=disk).get_system_info() + + +async def __get_network_info(): + """网络请求""" + baidu, google = True, True + try: + await AsyncHttpx.get(BAIDU_URL, timeout=5) + except Exception as e: + logger.warning("自检:百度无法访问...", e=e) + baidu = False + try: + await AsyncHttpx.get(GOOGLE_URL, timeout=5) + except Exception as e: + logger.warning("自检:谷歌无法访问...", e=e) + google = False + return baidu, google + + +def __get_version() -> str | None: + """获取版本信息""" + with open(VERSION_FILE, encoding="utf-8") as f: + if text := f.read(): + text.split(":")[-1] + return None + + +async def get_status_info() -> dict: + """获取信息""" + data = await __build_status() + baidu, google = await __get_network_info() + data["baidu"] = "#8CC265" if baidu else "red" + data["google"] = "#8CC265" if google else "red" + system = platform.uname() + data["system"] = f"{system.system} {system.release}" + data["version"] = __get_version() + data["brand_raw"] = cpuinfo.get_cpu_info()["brand_raw"] + data["plugin_count"] = len(nonebot.get_loaded_plugins()) + data["nickname"] = BotConfig.self_nickname + return data diff --git a/zhenxun/builtin_plugins/record_request.py b/zhenxun/builtin_plugins/record_request.py index 43b19fcb..730bf1db 100644 --- a/zhenxun/builtin_plugins/record_request.py +++ b/zhenxun/builtin_plugins/record_request.py @@ -1,27 +1,26 @@ import time from datetime import datetime +from nonebot.plugin import PluginMetadata from nonebot import on_message, on_request -from nonebot.adapters.onebot.v11 import ( - ActionFailed, - FriendRequestEvent, - GroupRequestEvent, -) +from nonebot_plugin_session import EventSession +from nonebot_plugin_apscheduler import scheduler from nonebot.adapters.onebot.v11 import Bot as v11Bot from nonebot.adapters.onebot.v12 import Bot as v12Bot -from nonebot.plugin import PluginMetadata -from nonebot_plugin_apscheduler import scheduler -from nonebot_plugin_session import EventSession +from nonebot.adapters.onebot.v11 import ( + ActionFailed, + GroupRequestEvent, + FriendRequestEvent, +) -from zhenxun.configs.config import BotConfig, Config -from zhenxun.configs.utils import PluginExtraData, RegisterConfig -from zhenxun.models.fg_request import FgRequest -from zhenxun.models.friend_user import FriendUser -from zhenxun.models.group_console import GroupConsole from zhenxun.services.log import logger -from zhenxun.utils.enum import PluginType, RequestHandleType, RequestType -from zhenxun.utils.message import MessageUtils +from zhenxun.models.fg_request import FgRequest from zhenxun.utils.platform import PlatformUtils +from zhenxun.models.friend_user import FriendUser +from zhenxun.configs.config import Config, BotConfig +from zhenxun.models.group_console import GroupConsole +from zhenxun.configs.utils import RegisterConfig, PluginExtraData +from zhenxun.utils.enum import PluginType, RequestType, RequestHandleType base_config = Config.get("invite_manager") @@ -119,91 +118,92 @@ async def _(bot: v12Bot | v11Bot, event: FriendRequestEvent, session: EventSessi @group_req.handle() async def _(bot: v12Bot | v11Bot, event: GroupRequestEvent, session: EventSession): - if event.sub_type == "invite": - if str(event.user_id) in bot.config.superusers: - try: - logger.debug( - "超级用户自动同意加入群聊", - "群聊请求", - session=event.user_id, - target=event.group_id, - ) - group, _ = await GroupConsole.update_or_create( - group_id=str(event.group_id), - defaults={ - "group_name": "", - "max_member_count": 0, - "member_count": 0, - "group_flag": 1, - }, - ) - await bot.set_group_add_request( - flag=event.flag, sub_type="invite", approve=True - ) - if isinstance(bot, v11Bot): - group_info = await bot.get_group_info(group_id=event.group_id) - max_member_count = group_info["max_member_count"] - member_count = group_info["member_count"] - else: - group_info = await bot.get_group_info(group_id=str(event.group_id)) - max_member_count = 0 - member_count = 0 - group.max_member_count = max_member_count - group.member_count = member_count - group.group_name = group_info["group_name"] - await group.save( - update_fields=["group_name", "max_member_count", "member_count"] - ) - except ActionFailed as e: - logger.error( - "超级用户自动同意加入群聊发生错误", - "群聊请求", - session=event.user_id, - target=event.group_id, - e=e, - ) - elif Timer.check(f"{event.user_id}:{event.group_id}"): + if event.sub_type != "invite": + return + if str(event.user_id) in bot.config.superusers: + try: logger.debug( - f"收录 用户[{event.user_id}] 群聊[{event.group_id}] 群聊请求", + "超级用户自动同意加入群聊", "群聊请求", + session=event.user_id, target=event.group_id, ) - nickname = await FriendUser.get_user_name(str(event.user_id)) - await bot.send_private_msg( - user_id=event.user_id, - message=f"想要邀请我偷偷入群嘛~已经提醒{BotConfig.self_nickname}的管理员大人了\n" - "请确保已经群主或群管理沟通过!\n" - "等待管理员处理吧!", - ) - # 旧请求全部设置为过期 - await FgRequest.filter( - request_type=RequestType.GROUP, - user_id=str(event.user_id), - group_id=str(event.group_id), - handle_type__isnull=True, - ).update(handle_type=RequestHandleType.EXPIRE) - f = await FgRequest.create( - request_type=RequestType.GROUP, - platform=session.platform, - bot_id=bot.self_id, - flag=event.flag, - user_id=str(event.user_id), - nickname=nickname, + group, _ = await GroupConsole.update_or_create( group_id=str(event.group_id), + defaults={ + "group_name": "", + "max_member_count": 0, + "member_count": 0, + "group_flag": 1, + }, ) - await PlatformUtils.send_superuser( - bot, - f"*****一份入群申请*****\n" - "ID:{f.id}\n" - "申请人:{nickname}({event.user_id})\n群聊:" - f"{event.group_id}\n邀请日期:{datetime.now().replace(microsecond=0)}", + await bot.set_group_add_request( + flag=event.flag, sub_type="invite", approve=True ) - else: - logger.debug( - "群聊请求五分钟内重复, 已忽略", + if isinstance(bot, v11Bot): + group_info = await bot.get_group_info(group_id=event.group_id) + max_member_count = group_info["max_member_count"] + member_count = group_info["member_count"] + else: + group_info = await bot.get_group_info(group_id=str(event.group_id)) + max_member_count = 0 + member_count = 0 + group.max_member_count = max_member_count + group.member_count = member_count + group.group_name = group_info["group_name"] + await group.save( + update_fields=["group_name", "max_member_count", "member_count"] + ) + except ActionFailed as e: + logger.error( + "超级用户自动同意加入群聊发生错误", "群聊请求", - target=f"{event.user_id}:{event.group_id}", + session=event.user_id, + target=event.group_id, + e=e, ) + elif Timer.check(f"{event.user_id}:{event.group_id}"): + logger.debug( + f"收录 用户[{event.user_id}] 群聊[{event.group_id}] 群聊请求", + "群聊请求", + target=event.group_id, + ) + nickname = await FriendUser.get_user_name(str(event.user_id)) + await bot.send_private_msg( + user_id=event.user_id, + message=f"想要邀请我偷偷入群嘛~已经提醒{BotConfig.self_nickname}的管理员大人了\n" + "请确保已经群主或群管理沟通过!\n" + "等待管理员处理吧!", + ) + # 旧请求全部设置为过期 + await FgRequest.filter( + request_type=RequestType.GROUP, + user_id=str(event.user_id), + group_id=str(event.group_id), + handle_type__isnull=True, + ).update(handle_type=RequestHandleType.EXPIRE) + f = await FgRequest.create( + request_type=RequestType.GROUP, + platform=session.platform, + bot_id=bot.self_id, + flag=event.flag, + user_id=str(event.user_id), + nickname=nickname, + group_id=str(event.group_id), + ) + await PlatformUtils.send_superuser( + bot, + f"*****一份入群申请*****\n" + f"ID:{f.id}\n" + f"申请人:{nickname}({event.user_id})\n群聊:" + f"{event.group_id}\n邀请日期:{datetime.now().replace(microsecond=0)}", + ) + else: + logger.debug( + "群聊请求五分钟内重复, 已忽略", + "群聊请求", + target=f"{event.user_id}:{event.group_id}", + ) @scheduler.scheduled_job( diff --git a/zhenxun/utils/http_utils.py b/zhenxun/utils/http_utils.py index da33264a..98f2b74e 100644 --- a/zhenxun/utils/http_utils.py +++ b/zhenxun/utils/http_utils.py @@ -173,6 +173,7 @@ class AsyncHttpx: cookies: dict[str, str] | None = None, timeout: int = 30, stream: bool = False, + follow_redirects: bool = True, **kwargs, ) -> bool: """下载文件 @@ -204,7 +205,7 @@ class AsyncHttpx: use_proxy=use_proxy, proxy=proxy, timeout=timeout, - follow_redirects=True, + follow_redirects=follow_redirects, **kwargs, ) response.raise_for_status()