""" 退群提醒插件 当成员退出群聊时,发送提醒卡片 支持两种方式: 1. type=11099 群成员删除事件(如果 API 支持) 2. type=11058 系统消息解析(通用方式) """ import tomllib from pathlib import Path from loguru import logger from utils.plugin_base import PluginBase from utils.decorators import on_text_message # 定义事件装饰器 def on_chatroom_member_remove(priority=50): """群成员删除装饰器""" def decorator(func): setattr(func, '_event_type', 'chatroom_member_remove') setattr(func, '_priority', min(max(priority, 0), 99)) return func return decorator def on_system_message(priority=50): """系统消息装饰器""" def decorator(func): setattr(func, '_event_type', 'system_message') setattr(func, '_priority', min(max(priority, 0), 99)) return func return decorator def on_chatroom_info_change(priority=50): """群信息变化装饰器""" def decorator(func): setattr(func, '_event_type', 'chatroom_info_change') setattr(func, '_priority', min(max(priority, 0), 99)) return func return decorator class GroupLeave(PluginBase): """退群提醒插件""" # 插件元数据 description = "成员退群时发送提醒卡片" author = "ShiHao" version = "1.0.0" def __init__(self): super().__init__() self.config = None # 群成员缓存:{room_wxid: {wxid: nickname}} self.member_cache = {} async def async_init(self): """插件异步初始化""" # 读取配置 config_path = Path(__file__).parent / "config.toml" with open(config_path, "rb") as f: self.config = tomllib.load(f) logger.info("退群提醒插件已加载") @on_chatroom_member_remove(priority=50) async def on_chatroom_member_remove(self, bot, message: dict): """处理群成员删除事件(type=11099)""" logger.info(f"[GroupLeave] 收到群成员删除事件,原始消息: {message}") # 检查是否启用 if not self.config["behavior"]["enabled"]: logger.warning("[GroupLeave] 插件未启用,跳过处理") return room_wxid = message.get("RoomWxid", "") member_list = message.get("MemberList", []) logger.info(f"[GroupLeave] 解析结果 - 群ID: {room_wxid}, 成员列表: {member_list}") # 检查群聊过滤 if not self._should_notify(room_wxid): logger.info(f"[GroupLeave] 群 {room_wxid} 不在提醒列表中,跳过") return logger.success(f"[GroupLeave] 群 {room_wxid} 有成员退出: {len(member_list)} 人") await self._process_leave_members(bot, room_wxid, member_list) @on_system_message(priority=50) async def on_system_message(self, bot, message: dict): """处理系统消息(type=11058),解析退群通知""" # 检查是否启用 if not self.config["behavior"]["enabled"]: return raw_msg = message.get("Content", "") room_wxid = message.get("FromWxid", "") # 只处理群聊系统消息 if not room_wxid.endswith("@chatroom"): return logger.info(f"[GroupLeave] 收到系统消息: room={room_wxid}, msg={raw_msg}") # 解析退群消息 # 格式1: "你将xxxx移出了群聊" # 格式2: "xxxx将xxxx移出了群聊" # 格式3: "xxxx退出了群聊" import re # 匹配退群消息 patterns = [ r"(.+?)退出了群聊", # 主动退群 r"(?:你|.+?)将(.+?)移出了群聊", # 被踢出群 ] for pattern in patterns: match = re.search(pattern, raw_msg) if match: nickname = match.group(1).strip() logger.success(f"[GroupLeave] 从系统消息解析到退群成员: {nickname}") # 检查群聊过滤 if not self._should_notify(room_wxid): logger.info(f"[GroupLeave] 群 {room_wxid} 不在提醒列表中,跳过") return # 构造成员列表(只有昵称,没有 wxid) member_list = [{"wxid": "", "nickname": nickname}] await self._process_leave_members(bot, room_wxid, member_list) break @on_chatroom_info_change(priority=50) async def on_chatroom_info_change(self, bot, message: dict): """处理群信息变化事件(type=11100),检测退群""" if not self.config["behavior"]["enabled"]: return room_wxid = message.get("RoomWxid", "") if not self._should_notify(room_wxid): return try: # 获取当前群成员列表 current_members = await bot.get_chatroom_members(room_wxid) if not current_members: return # 构建当前成员字典 {wxid: nickname} current_member_dict = {m.get("wxid"): m.get("nickname", "未知成员") for m in current_members if m.get("wxid")} # 首次遇到该群,初始化缓存 if room_wxid not in self.member_cache: logger.info(f"[GroupLeave] 首次记录群成员: {room_wxid}, 成员数: {len(current_member_dict)}") self.member_cache[room_wxid] = current_member_dict return # 对比找出退群成员 cached_member_dict = self.member_cache[room_wxid] left_wxids = set(cached_member_dict.keys()) - set(current_member_dict.keys()) if left_wxids: logger.success(f"[GroupLeave] 检测到退群成员: {len(left_wxids)} 人") # 构造退群成员列表(使用缓存中的昵称) left_members = [{"wxid": wxid, "nickname": cached_member_dict.get(wxid, "未知成员")} for wxid in left_wxids] # 更新缓存 self.member_cache[room_wxid] = current_member_dict # 处理退群成员 await self._process_leave_members(bot, room_wxid, left_members) else: # 更新缓存 self.member_cache[room_wxid] = current_member_dict except Exception as e: logger.error(f"[GroupLeave] 处理群信息变化失败: {e}") async def _process_leave_members(self, bot, room_wxid: str, member_list: list): """处理退群成员列表""" from datetime import datetime # 为每个退出的成员发送提醒 for member in member_list: nickname = member.get("nickname", "某成员") try: # 获取当前时间 leave_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") # 发送文字提醒 await self._send_leave_message(bot, room_wxid, nickname, leave_time) except Exception as e: logger.error(f"处理退群成员 {nickname} 提醒失败: {e}") async def _send_leave_message(self, bot, room_wxid: str, nickname: str, leave_time: str): """发送退群提醒文字消息""" try: message = f"【退群提醒】\n成员:{nickname}\n时间:{leave_time}" await bot.send_text(room_wxid, message) logger.success(f"已发送退群提醒: {nickname}") except Exception as e: logger.error(f"发送退群提醒失败: {e}") def _should_notify(self, room_wxid: str) -> bool: """判断是否应该发送提醒""" enabled_groups = self.config["behavior"]["enabled_groups"] disabled_groups = self.config["behavior"]["disabled_groups"] # 如果在禁用列表中,不提醒 if room_wxid in disabled_groups: return False # 如果启用列表为空,对所有群生效 if not enabled_groups: return True # 否则只对启用列表中的群生效 return room_wxid in enabled_groups @on_text_message(priority=90) async def handle_test_command(self, bot, message: dict): """处理测试命令""" content = message.get("Content", "").strip() from_wxid = message.get("FromWxid", "") is_group = message.get("IsGroup", False) # 只处理群聊消息 if not is_group: return # 检查是否是测试退群命令 if content.startswith("/测试退群"): parts = content.split() if len(parts) < 2: await bot.send_text(from_wxid, "用法: /测试退群 wxid") return False test_wxid = parts[1].strip() logger.info(f"收到测试退群命令: wxid={test_wxid}") try: # 模拟群成员删除事件 test_message = { "RoomWxid": from_wxid, "MemberList": [{"wxid": test_wxid, "nickname": "测试用户"}], } await self.on_chatroom_member_remove(bot, test_message) await bot.send_text(from_wxid, f"已触发退群提醒测试: {test_wxid}") except Exception as e: logger.error(f"测试退群提醒失败: {e}") await bot.send_text(from_wxid, f"测试失败: {e}") return False # 阻止后续处理