zhenxun_bot/zhenxun/configs/utils/__init__.py
HibiKier 4e33bf3a50
版本更新 (#1666)
*  父级插件加载

*  添加测试:更新与添加插件 (#1594)

*  测试更新与添加插件

*  Sourcery建议

* 👷 添加pytest

* 🎨 优化代码

* 🐛 bug修复

* 🐛修复添加插件返回403的问题 (#1595)

* 完善测试方法
* vscode测试配置
* 重构插件安装过程

* 🎨 修改readme

* Update README.md

* 🐛 修改bug与版本锁定

* 🐛 修复超级用户对群组功能开关

* 🐛 修复插件商店检查插件更新问题 (#1597)

* 🐛 修复插件商店检查插件更新问题

* 🐛 恶意命令检测问题

* 🐛 增加插件状态检查 (#1598)

*  优化测试用例

* 🐛 更改插件更新与安装逻辑

* 🐛 修复更新群组成员信息

* 🎨 代码优化

* 🚀 更新Dockerfile (#1599)

* 🎨 更新requirements

*  添加依赖aiocache

*  添加github镜像

*  添加仓库目录多获取渠道

* 🐛 修复测试用例

*  添加API缓存

* 🎨 采取Sourcery建议

* 🐛 文件下载逻辑修改

* 🎨 优化代码

* 🐛 修复插件开关有时出现错误

*  重构自检ui

* 🐛 自检html修正

* 修复签到逻辑bug,并使代码更灵活以适应签到好感度等级配置 (#1606)

* 修复签到功能已知问题

* 修复签到功能已知问题

* 修改参数名称

* 修改uid判断

---------

Co-authored-by: HibiKier <45528451+HibiKier@users.noreply.github.com>

* 🎨 代码结构优化

* 🐛 私聊时修改插件时删除私聊帮助

* 🐛 过滤父插件

* 🐛 修复自检在ARM上的问题 (#1607)

* 🐛 修复自检在ARM上的问题

*  优化测试

*  支持mysql,psql,sqlite随机函数

* 🔧 VSCode配置修改

* 🔧 VSCode配置修改

*  添加金币排行

Co-Authored-By: HibiKier <45528451+HibiKier@users.noreply.github.com>

* 📝 修改README

Co-Authored-By: HibiKier <45528451+HibiKier@users.noreply.github.com>

* 🔨 提取GitHub相关操作 (#1609)

* 🔨 提取GitHub相关操作

* 🔨 重构API策略

*  签到/金币排行限制最大数量 (#1616)

*  签到/金币排行限制最大数量

* 🐛 修复超级用户id获取问题

* 🐛 修复路径解压与挂载 (#1619)

* 🐛 修复功能少时zhenxun帮助图片排序问题 (#1620)

* 🐛 签到文本适应 (#1622)

* 🐛 好感度排行提供默认值 (#1624)

* 🎈 优先使用github api (#1625)

*  重构帮助,限制普通用户查询管理插件 (#1626)

* 🐛 修复群权限与插件等级匹配 (#1627)

*  当管理员尝试ban真寻时将被反杀 (#1628)

*  群组发言时间检测提供开关配置 (#1630)

* 🐳 chore: 支持自动修改版本号 (#1629)

* 🎈 perf(github_utils): 支持github url下载遍历 (#1632)

* 🎈 perf(github_utils): 支持github url下载遍历

* 🐞 fix(http_utils): 修复一些下载问题

* 🦄 refactor(http_utils): 部分重构

* chore(version): Update version to v0.2.2-e6f17c4

---------

Co-authored-by: AkashiCoin <AkashiCoin@users.noreply.github.com>

* 🧪 test(auto_update): 修复测试用例 (#1633)

* 🐛 修复商店商品为空时报错 (#1634)

* 🐛 修复群权限与插件等级匹配 (#1635)

*  message_build支持AtAll (#1639)

* 🎈 perf: 使用commit号下载插件 (#1641)

* 🎈 perf: 使用commit号下载插件

* chore(version): Update version to v0.2.2-f9c7360

---------

Co-authored-by: AkashiCoin <AkashiCoin@users.noreply.github.com>

* 🐳 chore: 修改运行检查触发路径 (#1642)

* 🐳 chore: 修改运行检查触发路径

* 🐳 chore: 添加tests目录

*  重构qq群事件处理 (#1643)

* 🐛 签到名称自适应 (#1644)

* 🎨  更新README (#1645)

* 🐛 fix(http_utils): 流式下载Content-Length错误 (#1647)

* 🐛 修复群组中帮助功能状态显示问题 (#1650)

* 🐛 修复群欢迎消息设置 (#1651)

* 🐛 修复webui下载后首次启动错误 (#1652)

* 🐛 修复webui下载后首次启动错误

* chore(version): Update version to v0.2.2-4a8ef85

---------

Co-authored-by: HibiKier <HibiKier@users.noreply.github.com>

*  移除默认图片文件夹:爬 (#1653)

*  安装/移除插件提供插件安装/卸载方法用于插件初始化 (#1654)

*  新增超级用户与管理员帮助模板 (#1655)

*  新增个人信息命令 (#1657)

*  修改个人信息菜单名称 (#1658)

*  新增插件商店api (#1659)

*  新增插件商店api

* chore(version): Update version to v0.2.2-7e15f20

---------

Co-authored-by: HibiKier <HibiKier@users.noreply.github.com>

*  将cd,block,count限制复原配置文件 (#1662)

* 🎨 修改README (#1663)

* 🎨 修改版本号 (#1664)

* 🎨 修改requirements (#1665)

---------

Co-authored-by: AkashiCoin <l1040186796@gmail.com>
Co-authored-by: fanyinrumeng <42991257+fanyinrumeng@users.noreply.github.com>
Co-authored-by: AkashiCoin <i@loli.vet>
Co-authored-by: Elaga <1728903318@qq.com>
Co-authored-by: AkashiCoin <AkashiCoin@users.noreply.github.com>
Co-authored-by: HibiKier <HibiKier@users.noreply.github.com>
2024-10-01 00:42:23 +08:00

481 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import copy
from typing import Any
from pathlib import Path
from collections.abc import Callable
import cattrs
from ruamel.yaml import YAML
from pydantic import BaseModel
from ruamel.yaml.scanner import ScannerError
from zhenxun.services.log import logger
from zhenxun.configs.path_config import DATA_PATH
from zhenxun.utils.enum import BlockType, PluginType, LimitWatchType, PluginLimitType
_yaml = YAML(pure=True)
_yaml.indent = 2
_yaml.allow_unicode = True
class Example(BaseModel):
"""
示例
"""
exec: str
"""执行命令"""
description: str = ""
"""命令描述"""
class Command(BaseModel):
"""
具体参数说明
"""
command: str
"""命令"""
params: list[str] = []
"""参数"""
description: str = ""
"""描述"""
examples: list[Example] = []
"""示例列表"""
class RegisterConfig(BaseModel):
"""
注册配置项
"""
key: str
"""配置项键"""
value: Any
"""配置项值"""
module: str | None = None
"""模块名"""
help: str | None
"""配置注解"""
default_value: Any | None = None
"""默认值"""
type: Any = None
"""参数类型"""
arg_parser: Callable | None = None
"""参数解析"""
class ConfigModel(BaseModel):
"""
配置项
"""
value: Any
"""配置项值"""
help: str | None
"""配置注解"""
default_value: Any | None = None
"""默认值"""
type: Any = None
"""参数类型"""
arg_parser: Callable | None = None
"""参数解析"""
class ConfigGroup(BaseModel):
"""
配置组
"""
module: str
"""模块名"""
name: str | None = None
"""插件名"""
configs: dict[str, ConfigModel] = {}
"""配置项列表"""
def get(self, c: str, default: Any = None) -> Any:
cfg = self.configs.get(c.upper())
if cfg is not None:
if cfg.value is not None:
return cfg.value
if cfg.default_value is not None:
return cfg.default_value
return default
class BaseBlock(BaseModel):
"""
插件阻断基本类(插件阻断限制)
"""
status: bool = True
"""限制状态"""
check_type: BlockType = BlockType.ALL
"""检查类型"""
watch_type: LimitWatchType = LimitWatchType.USER
"""监听对象"""
result: str | None = None
"""阻断时回复内容"""
_type: PluginLimitType = PluginLimitType.BLOCK
"""类型"""
class PluginCdBlock(BaseBlock):
"""
插件cd限制
"""
cd: int = 5
"""cd"""
_type: PluginLimitType = PluginLimitType.CD
"""类型"""
class PluginCountBlock(BaseBlock):
"""
插件次数限制
"""
max_count: int
"""最大调用次数"""
_type: PluginLimitType = PluginLimitType.COUNT
"""类型"""
class PluginSetting(BaseModel):
"""
插件基本配置
"""
level: int = 5
"""群权限等级"""
default_status: bool = True
"""进群默认开关状态"""
limit_superuser: bool = False
"""是否限制超级用户"""
cost_gold: int = 0
"""调用插件花费金币"""
class Task(BaseBlock):
module: str
"""被动技能模块名"""
name: str
"""被动技能名称"""
status: bool = True
"""全局开关状态"""
create_status: bool = False
"""初次加载默认开关状态"""
default_status: bool = True
"""进群时默认状态"""
run_time: str | None = None
"""运行时间"""
class PluginExtraData(BaseModel):
"""
插件扩展信息
"""
author: str | None = None
"""作者"""
version: str | None = None
"""版本"""
plugin_type: PluginType = PluginType.NORMAL
"""插件类型"""
menu_type: str = "功能"
"""菜单类型"""
admin_level: int | None = None
"""管理员插件所需权限等级"""
configs: list[RegisterConfig] | None = None
"""插件配置"""
setting: PluginSetting | None = None
"""插件基本配置"""
limits: list[BaseBlock | PluginCdBlock | PluginCountBlock] | None = None
"""插件限制"""
commands: list[Command] = []
"""命令列表,用于说明帮助"""
tasks: list[Task] | None = None
"""技能被动"""
superuser_help: str | None = None
"""超级用户帮助"""
aliases: set[str] = set()
"""额外名称"""
sql_list: list[str] | None = None
"""常用sql"""
class NoSuchConfig(Exception):
pass
class ConfigsManager:
"""
插件配置 与 资源 管理器
"""
def __init__(self, file: Path):
self._data: dict[str, ConfigGroup] = {}
self._simple_data: dict = {}
self._simple_file = DATA_PATH / "config.yaml"
_yaml = YAML()
if file:
file.parent.mkdir(exist_ok=True, parents=True)
self.file = file
self.load_data()
if self._simple_file.exists():
try:
with self._simple_file.open(encoding="utf8") as f:
self._simple_data = _yaml.load(f)
except ScannerError as e:
raise ScannerError(
f"{e}\n**********************************************\n"
f"****** 可能为config.yaml配置文件填写不规范 ******\n"
f"**********************************************"
) from e
def set_name(self, module: str, name: str):
"""设置插件配置中文名出
参数:
module: 模块名
name: 中文名称
异常:
ValueError: module不能为为空
"""
if not module:
raise ValueError("set_name: module不能为为空")
if data := self._data.get(module):
data.name = name
def add_plugin_config(
self,
module: str,
key: str,
value: Any,
*,
help: str | None = None,
default_value: Any = None,
type: type | None = None,
arg_parser: Callable | None = None,
_override: bool = False,
):
"""为插件添加一个配置,不会被覆盖,只有第一个生效
参数:
module: 模块
key: 键
value: 值
help: 配置注解.
default_value: 默认值.
type: 值类型.
arg_parser: 值解析器一般与webui配合使用.
_override: 强制覆盖值.
异常:
ValueError: module和key不能为为空
ValueError: 填写错误
"""
if not module or not key:
raise ValueError("add_plugin_config: module和key不能为为空")
if module in self._data and (config := self._data[module].configs.get(key)):
config.help = help
config.arg_parser = arg_parser
config.type = type
if _override:
config.value = value
config.default_value = default_value
else:
_module = None
if ":" in module:
module_split = module.split(":")
if len(module_split) < 2:
raise ValueError(f"module: {module} 填写错误")
_module = module_split[-1]
module = module_split[0]
key = key.upper()
if not self._data.get(module):
self._data[module] = ConfigGroup(module=module)
self._data[module].configs[key] = ConfigModel(
value=value,
help=help,
default_value=default_value,
type=type,
)
def set_config(
self,
module: str,
key: str,
value: Any,
auto_save: bool = False,
save_simple_data: bool = True,
):
"""设置配置值
参数:
module: 模块名
key: 配置名称
value: 值
auto_save: 自动保存.
save_simple_data: 保存至config.yaml.
"""
if module in self._data:
data = self._data[module].configs.get(key)
if data and data != value:
self._data[module].configs[key].value = value
self._simple_data[module][key] = value
if auto_save:
self.save(save_simple_data=save_simple_data)
def get_config(self, module: str, key: str, default: Any = None) -> Any:
"""获取指定配置值
参数:
module: 模块名
key: 配置键
default: 没有key值内容的默认返回值.
异常:
NoSuchConfig: 未查询到配置
返回:
Any: 配置值
"""
logger.debug(
f"尝试获取配置MODULE: [<u><y>{module}</y></u>] | KEY: [<u><y>{key}</y></u>]"
)
key = key.upper()
value = None
if module in self._data.keys():
config = self._data[module].configs.get(key) or self._data[
module
].configs.get(key)
if not config:
raise NoSuchConfig(
f"未查询到配置项 MODULE: [ {module} ] | KEY: [ {key} ]"
)
try:
if config.arg_parser:
value = config.arg_parser(value or config.default_value)
elif config.value is not None:
# try:
value = (
cattrs.structure(config.value, config.type)
if config.type
else config.value
)
elif config.default_value is not None:
value = (
cattrs.structure(config.default_value, config.type)
if config.type
else config.default_value
)
except Exception as e:
logger.warning(
f"配置项类型转换 MODULE: [<u><y>{module}</y></u>]"
" | KEY: [<u><y>{key}</y></u>]",
e=e,
)
value = config.value or config.default_value
if value is None:
value = default
logger.debug(
f"获取配置 MODULE: [<u><y>{module}</y></u>] | "
f" KEY: [<u><y>{key}</y></u>] -> [<u><c>{value}</c></u>]"
)
return value
def get(self, key: str) -> ConfigGroup:
"""获取插件配置数据
参数:
key: 键,一般为模块名
返回:
ConfigGroup: ConfigGroup
"""
return self._data.get(key) or ConfigGroup(module="")
def save(self, path: str | Path | None = None, save_simple_data: bool = False):
"""保存数据
参数:
path: 路径.
save_simple_data: 同时保存至config.yaml.
"""
if save_simple_data:
with open(self._simple_file, "w", encoding="utf8") as f:
_yaml.dump(self._simple_data, f)
path = path or self.file
data = {}
for module in self._data:
data[module] = {}
for config in self._data[module].configs:
value = self._data[module].configs[config].dict()
del value["type"]
del value["arg_parser"]
data[module][config] = value
with open(path, "w", encoding="utf8") as f:
_yaml.dump(data, f)
def reload(self):
"""重新加载配置文件"""
if self._simple_file.exists():
with open(self._simple_file, encoding="utf8") as f:
self._simple_data = _yaml.load(f)
for key in self._simple_data.keys():
for k in self._simple_data[key].keys():
self._data[key].configs[k].value = self._simple_data[key][k]
self.save()
def load_data(self):
"""加载数据
异常:
ValueError: 配置文件为空!
"""
if not self.file.exists():
return
with open(self.file, encoding="utf8") as f:
temp_data = _yaml.load(f)
if not temp_data:
self.file.unlink()
raise ValueError(
"配置文件为空!\n"
"***********************************************************\n"
"****** 配置文件 plugins2config.yaml 为空,已删除,请重启 ******\n"
"***********************************************************"
)
count = 0
for module in temp_data:
config_group = ConfigGroup(module=module)
for config in temp_data[module]:
config_group.configs[config] = ConfigModel(**temp_data[module][config])
count += 1
self._data[module] = config_group
logger.info(
f"加载配置完成,共加载 <u><y>{len(temp_data)}</y></u> 个配置组及对应"
f" <u><y>{count}</y></u> 个配置项"
)
def get_data(self) -> dict[str, ConfigGroup]:
return copy.deepcopy(self._data)
def is_empty(self) -> bool:
return not bool(self._data)
def keys(self):
return self._data.keys()
def __str__(self):
return str(self._data)
def __setitem__(self, key, value):
self._data[key] = value
def __getitem__(self, key):
return self._data[key]