Files
zhenxun_bot/zhenxun/builtin_plugins/init/manager.py
T
molanp eab79bdb52 feat(init): 添加自动清理无效插件和群组数据功能 (#2116)
* feat(init): 添加自动清理无效插件和群组数据功能

- 添加nonebot_plugin_apscheduler依赖用于定时任务
- 导入ChatHistory模型用于清理聊天记录
- 移除CacheException相关异常处理代码
- 重写群组同步逻辑,支持创建新群组记录和删除已退出群组
- 添加定时清理已退出群组聊天记录的功能
- 优化日志输出格式和信息内容

* refactor(manager): 添加E501忽略注释以修复代码风格问题

在Manager类的方法中添加noqa: E501注释,解决docstring行长超过限制的代码检查警告

* feat(init): 添加自动清理聊天记录配置选项

- 引入Config配置模块
- 新增auto_clean.CLEAN_CHAT_HISTORY配置项,控制是否自动清理已退出群聊的聊天记录
- 根据配置决定是否添加定时清理任务,实现可配置的聊天历史清理功能
2026-04-11 21:21:08 +08:00

439 lines
16 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.
from copy import deepcopy
from ruamel.yaml import YAML
from zhenxun.configs.path_config import DATA_PATH
from zhenxun.configs.utils import BaseBlock, PluginCdBlock, PluginCountBlock
from zhenxun.models.plugin_info import PluginInfo
from zhenxun.models.plugin_limit import PluginLimit
from zhenxun.services.log import logger
from zhenxun.utils.enum import BlockType, LimitCheckType, PluginLimitType
_yaml = YAML(pure=True)
_yaml.indent = 2
_yaml.allow_unicode = True
CD_TEST = """需要cd的功能
自定义的功能需要cd也可以在此配置
key:模块名称
cd:cd 时长(秒)
status:此限制的开关状态
check_type:'PRIVATE'/'GROUP'/'ALL',限制私聊/群聊/全部
watch_type:监听对象,以user_id或group_id作为键来限制,'USER':用户id,'GROUP':群id
示例:'USER':用户N秒内触发1次,'GROUP':群N秒内触发1次
result:回复的话,可以添加[at],[uname],[nickname]来对应艾特,用户群名称,昵称系统昵称
result 为 "" 或 None 时则不回复
result示例:"[uname]你冲的太快了,[nickname]先生,请稍后再冲[at]"
result回复:"老色批你冲的太快了,欧尼酱先生,请稍后再冲@老色批"
用户昵称↑ 昵称系统的昵称↑ 艾特用户↑"""
BLOCK_TEST = """用户调用阻塞
即 当用户调用此功能还未结束时
用发送消息阻止用户重复调用此命令直到该命令结束
key:模块名称
status:此限制的开关状态
check_type:'PRIVATE'/'GROUP'/'ALL',限制私聊/群聊/全部
watch_type:监听对象,以user_id或group_id作为键来限制,'USER':用户id,'GROUP':群id
示例:'USER':阻塞用户,'group':阻塞群聊
result:回复的话,可以添加[at],[uname],[nickname]来对应艾特,用户群名称,昵称系统昵称
result 为 "" 或 None 时则不回复
result示例:"[uname]你冲的太快了,[nickname]先生,请稍后再冲[at]"
result回复:"老色批你冲的太快了,欧尼酱先生,请稍后再冲@老色批"
用户昵称↑ 昵称系统的昵称↑ 艾特用户↑"""
COUNT_TEST = """命令每日次数限制
即 用户/群聊 每日可调用命令的次数 [数据内存存储,重启将会重置]
每日调用直到 00:00 刷新
key:模块名称
max_count: 每日调用上限
status:此限制的开关状态
watch_type:监听对象,以user_id或group_id作为键来限制,'USER':用户id,'GROUP':群id
示例:'USER':用户上限,'group':群聊上限
result:回复的话,可以添加[at],[uname],[nickname]来对应艾特,用户群名称,昵称系统昵称
result 为 "" 或 None 时则不回复
result示例:"[uname]你冲的太快了,[nickname]先生,请稍后再冲[at]"
result回复:"老色批你冲的太快了,欧尼酱先生,请稍后再冲@老色批"
用户昵称↑ 昵称系统的昵称↑ 艾特用户↑"""
class Manager:
"""
插件命令 cd 管理器
"""
def __init__(self):
self.cd_file = DATA_PATH / "configs" / "plugins2cd.yaml"
self.block_file = DATA_PATH / "configs" / "plugins2block.yaml"
self.count_file = DATA_PATH / "configs" / "plugins2count.yaml"
self.cd_data = {}
self.block_data = {}
self.count_data = {}
def add(
self,
module: str,
data: BaseBlock | PluginCdBlock | PluginCountBlock | PluginLimit,
):
"""添加限制"""
if isinstance(data, PluginLimit):
check_type = BlockType.ALL
if LimitCheckType.GROUP == data.check_type:
check_type = BlockType.GROUP
elif LimitCheckType.PRIVATE == data.check_type:
check_type = BlockType.PRIVATE
if data.limit_type == PluginLimitType.CD:
data = PluginCdBlock(
status=data.status,
check_type=check_type,
watch_type=data.watch_type,
result=data.result,
cd=data.cd,
)
elif data.limit_type == PluginLimitType.BLOCK:
data = BaseBlock(
status=data.status,
check_type=check_type,
watch_type=data.watch_type,
result=data.result,
)
elif data.limit_type == PluginLimitType.COUNT:
data = PluginCountBlock(
status=data.status,
watch_type=data.watch_type,
result=data.result,
max_count=data.max_count,
)
if isinstance(data, PluginCdBlock):
self.cd_data[module] = data
elif isinstance(data, PluginCountBlock):
self.count_data[module] = data
elif isinstance(data, BaseBlock):
self.block_data[module] = data
def exists(self, module: str, type: PluginLimitType):
"""是否存在"""
if type == PluginLimitType.CD:
return module in self.cd_data
elif type == PluginLimitType.BLOCK:
return module in self.block_data
elif type == PluginLimitType.COUNT:
return module in self.count_data
def init(self):
if not self.cd_file.exists():
self.save_cd_file()
if not self.block_file.exists():
self.save_block_file()
if not self.count_file.exists():
self.save_count_file()
self.__load_file()
def __load_file(self):
self.__load_block_file()
self.__load_cd_file()
self.__load_count_file()
def save_file(self):
"""保存文件"""
self.save_cd_file()
self.save_block_file()
self.save_count_file()
def save_cd_file(self):
"""保存文件"""
self._extracted_from_save_file_3("PluginCdLimit", CD_TEST, self.cd_data)
def save_block_file(self):
"""保存文件"""
self._extracted_from_save_file_3(
"PluginBlockLimit", BLOCK_TEST, self.block_data
)
def save_count_file(self):
"""保存文件"""
self._extracted_from_save_file_3(
"PluginCountLimit", COUNT_TEST, self.count_data
)
def _extracted_from_save_file_3(self, type_: str, after: str, data: dict):
"""保存文件
参数:
type_: 类型参数
after: 备注
"""
temp_data = deepcopy(data)
if not temp_data:
temp_data = {
"test": {
"status": False,
"check_type": "ALL",
"limit_type": "USER",
"result": "你冲的太快了,请稍后再冲",
}
}
if type_ == "PluginCdLimit":
temp_data["test"]["cd"] = 5
elif type_ == "PluginCountLimit":
temp_data["test"]["max_count"] = 5
del temp_data["test"]["check_type"]
else:
for v in temp_data:
temp_data[v] = temp_data[v].to_dict()
if check_type := temp_data[v].get("check_type"):
temp_data[v]["check_type"] = str(check_type)
if watch_type := temp_data[v].get("watch_type"):
temp_data[v]["watch_type"] = str(watch_type)
if type_ == "PluginCountLimit":
del temp_data[v]["check_type"]
file = self.block_file
if type_ == "PluginCdLimit":
file = self.cd_file
elif type_ == "PluginCountLimit":
file = self.count_file
with open(file, "w", encoding="utf8") as f:
_yaml.dump({type_: temp_data}, f)
with open(file, encoding="utf8") as rf:
_data = _yaml.load(rf)
_data.yaml_set_comment_before_after_key(after=after, key=type_)
with open(file, "w", encoding="utf8") as wf:
_yaml.dump(_data, wf)
def __load_cd_file(self):
self.cd_data: dict[str, PluginCdBlock] = {}
if self.cd_file.exists():
with open(self.cd_file, encoding="utf8") as f:
temp = _yaml.load(f) or {}
if "PluginCdLimit" in temp.keys():
for k, v in temp["PluginCdLimit"].items():
if "." in k:
k = k.split(".")[-1]
self.cd_data[k] = PluginCdBlock.parse_obj(v)
def __load_block_file(self):
self.block_data: dict[str, BaseBlock] = {}
if self.block_file.exists():
with open(self.block_file, encoding="utf8") as f:
temp = _yaml.load(f) or {}
if "PluginBlockLimit" in temp.keys():
for k, v in temp["PluginBlockLimit"].items():
if "." in k:
k = k.split(".")[-1]
self.block_data[k] = BaseBlock.parse_obj(v)
def __load_count_file(self):
self.count_data: dict[str, PluginCountBlock] = {}
if self.count_file.exists():
with open(self.count_file, encoding="utf8") as f:
temp = _yaml.load(f) or {}
if "PluginCountLimit" in temp.keys():
for k, v in temp["PluginCountLimit"].items():
if "." in k:
k = k.split(".")[-1]
self.count_data[k] = PluginCountBlock.parse_obj(v)
def __replace_data(
self,
db_data: PluginLimit | None,
limit: PluginCdBlock | BaseBlock | PluginCountBlock,
) -> PluginLimit:
"""替换数据"""
if not db_data:
db_data = PluginLimit()
db_data.status = limit.status
check_type = LimitCheckType.ALL
if BlockType.GROUP == limit.check_type:
check_type = LimitCheckType.GROUP
elif BlockType.PRIVATE == limit.check_type:
check_type = LimitCheckType.PRIVATE
db_data.check_type = check_type
db_data.watch_type = limit.watch_type
db_data.result = limit.result or ""
return db_data
def __set_data(
self,
k: str,
db_data: PluginLimit | None,
limit: PluginCdBlock | BaseBlock | PluginCountBlock,
limit_type: PluginLimitType,
module2plugin: dict[str, PluginInfo],
) -> tuple[PluginLimit, bool]:
"""设置数据
参数:
k: 模块名
db_data: 数据库数据
limit: 文件数据
limit_type: 限制类型
module2plugin: 模块:插件信息
返回:
tuple[PluginLimit, bool]: PluginLimit,是否创建
"""
if not db_data:
return (
PluginLimit(
module=k,
module_path=module2plugin[k].module_path,
limit_type=limit_type,
plugin=module2plugin[k],
cd=getattr(limit, "cd", None),
max_count=getattr(limit, "max_count", None),
status=limit.status,
check_type=limit.check_type,
watch_type=limit.watch_type,
result=limit.result,
),
True,
)
db_data = self.__replace_data(db_data, limit)
if limit_type == PluginLimitType.CD:
db_data.cd = limit.cd # type: ignore
if limit_type == PluginLimitType.COUNT:
db_data.max_count = limit.max_count # type: ignore
return db_data, False
def __get_file_data(self, limit_type: PluginLimitType):
"""获取文件数据
参数:
limit_type: 限制类型
返回:
dict: 文件数据
"""
if limit_type == PluginLimitType.CD:
return self.cd_data
elif limit_type == PluginLimitType.COUNT:
return self.count_data
else:
return self.block_data
def __set_db_limits(
self,
db_limits: list[PluginLimit],
module2plugin: dict[str, PluginInfo],
limit_type: PluginLimitType,
) -> tuple[list[PluginLimit], list[PluginLimit], list[int]]:
"""更新cd限制数据
参数:
db_limits: 数据库limits
module2plugin: 模块:插件信息
limit_type: 插件限制类型
返回:
tuple[list[PluginLimit], list[PluginLimit]], list[int]: 创建列表,更新列表,删除列表
""" # noqa: E501
update_list: list[PluginLimit] = []
create_list: list[PluginLimit] = []
delete_list: list[int] = []
# 过滤出当前类型的所有 limit
db_type_limits = [
limit for limit in db_limits if limit.limit_type == limit_type
]
# module - PluginLimit 映射
module2limit: dict[str, PluginLimit] = {
limit.module: limit for limit in db_type_limits
}
# 如果没有任何文件数据,对应类型下的记录全部删掉
data = self.__get_file_data(limit_type)
if not data:
delete_list = [limit.id for limit in db_type_limits]
return create_list, update_list, delete_list
# 数据库中有,但文件里没有的模块,全部删掉
file_modules = set(data.keys())
for limit in db_type_limits:
if limit.module not in file_modules:
delete_list.append(limit.id)
# 遍历文件数据,生成 create / update / delete
for k, v in data.items():
db_data = module2limit.get(k)
# 插件未加载:删掉所有同模块的 limit
if k not in module2plugin:
if k != "test":
logger.warning(f"插件模块 {k} 未加载,已忽略当前 {v._type} 限制...")
if db_data:
delete_list.append(db_data.id)
continue
db_data, is_create = self.__set_data(
k, db_data, v, limit_type, module2plugin
)
if is_create:
create_list.append(db_data)
else:
update_list.append(db_data)
return create_list, update_list, delete_list
async def __set_all_limit(
self,
) -> tuple[list[PluginLimit], list[PluginLimit], list[int]]:
"""获取所有插件限制数据
返回:
tuple[list[PluginLimit], list[PluginLimit]]: 创建列表,更新列表
"""
db_limits = await PluginLimit.all()
modules = set(
list(self.cd_data.keys())
+ list(self.block_data.keys())
+ list(self.count_data.keys())
)
plugins = await PluginInfo.get_plugins(module__in=modules)
module2plugin = {p.module: p for p in plugins}
create_list, update_list, delete_list = self.__set_db_limits(
db_limits, module2plugin, PluginLimitType.CD
)
create_list1, update_list1, delete_list1 = self.__set_db_limits(
db_limits, module2plugin, PluginLimitType.COUNT
)
create_list2, update_list2, delete_list2 = self.__set_db_limits(
db_limits, module2plugin, PluginLimitType.BLOCK
)
all_create = create_list + create_list1 + create_list2
all_update = update_list + update_list1 + update_list2
all_delete = delete_list + delete_list1 + delete_list2
return all_create, all_update, all_delete
async def load_to_db(self):
"""读取配置文件"""
create_list, update_list, delete_list = await self.__set_all_limit()
if create_list:
await PluginLimit.bulk_create(create_list)
if update_list:
for limit in update_list:
await limit.save(
update_fields=[
"status",
"check_type",
"watch_type",
"result",
"cd",
"max_count",
]
)
# TODO: tortoise.exceptions.OperationalError:syntax error at or near "GROUP"
# await PluginLimit.bulk_update(
# update_list,
# ["status", "check_type", "watch_type", "result", "cd", "max_count"],
# )
if delete_list:
await PluginLimit.filter(id__in=delete_list).delete()
cnt = await PluginLimit.filter(status=True).count()
logger.info(f"已经加载 {cnt} 个插件限制.")
manager = Manager()