""" 入群欢迎插件 当新成员加入群聊时,发送欢迎卡片 支持两种方式: 1. type=11098 群成员新增事件(如果 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_add(priority=50): """群成员新增装饰器""" def decorator(func): setattr(func, '_event_type', 'chatroom_member_add') 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 GroupWelcome(PluginBase): """入群欢迎插件""" # 插件元数据 description = "新成员入群时发送欢迎卡片" author = "ShiHao" version = "1.0.0" def __init__(self): super().__init__() self.config = None # 群成员缓存:{room_wxid: set(member_wxids)} 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_add(priority=50) async def on_chatroom_member_add(self, bot, message: dict): """处理群成员新增事件(type=11098)""" logger.info(f"[GroupWelcome] 收到群成员新增事件,原始消息: {message}") # 检查是否启用 if not self.config["behavior"]["enabled"]: logger.warning("[GroupWelcome] 插件未启用,跳过处理") return room_wxid = message.get("RoomWxid", "") member_list = message.get("MemberList", []) logger.info(f"[GroupWelcome] 解析结果 - 群ID: {room_wxid}, 成员列表: {member_list}") # 检查群聊过滤 if not self._should_welcome(room_wxid): logger.info(f"[GroupWelcome] 群 {room_wxid} 不在欢迎列表中,跳过") return logger.success(f"[GroupWelcome] 群 {room_wxid} 有新成员加入: {len(member_list)} 人") await self._process_new_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"[GroupWelcome] 收到系统消息: room={room_wxid}, msg={raw_msg}") # 解析入群消息 # 格式1: "你邀请xxxx加入了群聊" # 格式2: "xxxx邀请xxxx加入了群聊" # 格式3: "xxxxx通过扫描你分享的二维码加入群聊" # 格式4: "xxxxx通过扫描xxxxxx分享的二维码加入群聊" 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"[GroupWelcome] 从系统消息解析到新成员: {nickname}") # 检查群聊过滤 if not self._should_welcome(room_wxid): logger.info(f"[GroupWelcome] 群 {room_wxid} 不在欢迎列表中,跳过") return # 构造成员列表(只有昵称,没有 wxid) member_list = [{"wxid": "", "nickname": nickname}] await self._process_new_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", "") total_member = message.get("TotalMember", 0) member_list = message.get("MemberList", []) logger.info(f"[GroupWelcome] 收到群信息变化事件: room={room_wxid}, total_member={total_member}, has_member_list={len(member_list) > 0}") # 检查群聊过滤 if not self._should_welcome(room_wxid): return # 如果有 member_list,直接处理(群创建或某些情况) if member_list: logger.info(f"[GroupWelcome] 群信息变化事件包含成员列表,直接处理") await self._process_new_members(bot, room_wxid, member_list) return # 没有 member_list,需要通过对比缓存找出新成员 try: # 获取当前群成员列表 current_members = await bot.get_chatroom_members(room_wxid) if not current_members: logger.warning(f"[GroupWelcome] 无法获取群成员列表: {room_wxid}") return current_wxids = {m.get("wxid") for m in current_members if m.get("wxid")} # 如果缓存中没有这个群,初始化缓存(首次) if room_wxid not in self.member_cache: logger.info(f"[GroupWelcome] 首次记录群成员: {room_wxid}, 成员数: {len(current_wxids)}") self.member_cache[room_wxid] = current_wxids return # 对比找出新成员 cached_wxids = self.member_cache[room_wxid] new_wxids = current_wxids - cached_wxids if new_wxids: logger.success(f"[GroupWelcome] 检测到新成员: {len(new_wxids)} 人") # 构造新成员列表 new_members = [m for m in current_members if m.get("wxid") in new_wxids] # 更新缓存 self.member_cache[room_wxid] = current_wxids # 处理新成员 await self._process_new_members(bot, room_wxid, new_members) else: # 成员数量没有增加,可能是其他信息变化 logger.debug(f"[GroupWelcome] 群信息变化但无新成员: {room_wxid}") # 更新缓存(可能有成员退出) self.member_cache[room_wxid] = current_wxids except Exception as e: logger.error(f"[GroupWelcome] 处理群信息变化失败: {e}") import traceback logger.error(f"详细错误: {traceback.format_exc()}") async def _process_new_members(self, bot, room_wxid: str, member_list: list): """处理新成员列表""" # 为每个新成员发送欢迎卡片 for member in member_list: wxid = member.get("wxid", "") nickname = member.get("nickname", "新成员") try: # 如果有 wxid,尝试获取详细信息(包括头像) if wxid: user_info = await bot.get_user_info_in_chatroom(room_wxid, wxid) if user_info: # 提取头像 URL big_head_img_url = user_info.get("bigHeadImgUrl", "") actual_nickname = user_info.get("nickName", {}).get("string", nickname) logger.info(f"获取到新成员信息: {actual_nickname} ({wxid}), 头像: {big_head_img_url}") # 发送欢迎卡片 await self._send_welcome_card( bot, room_wxid, actual_nickname, big_head_img_url ) else: logger.warning(f"无法获取新成员 {wxid} 的详细信息,使用默认配置") # 使用默认配置发送 await self._send_welcome_card(bot, room_wxid, nickname, "") else: # 没有 wxid(从系统消息解析),尝试通过昵称匹配获取 wxid 和头像 logger.info(f"[GroupWelcome] 尝试通过昵称匹配获取用户信息: {nickname}") # 获取群成员列表(可能需要等待成员真正加入) members = await bot.get_chatroom_members(room_wxid) # 如果没找到,等待1秒后重试一次 if not members or not any(nickname.strip('"') == m.get("nickname", "") for m in members): import asyncio await asyncio.sleep(1) members = await bot.get_chatroom_members(room_wxid) if members: # 通过昵称或群内昵称匹配 matched_member = None for member in members: member_nickname = member.get("nickname", "") member_display_name = member.get("display_name", "") # 匹配昵称(去除引号) if nickname.strip('"') == member_nickname or nickname.strip('"') == member_display_name: matched_member = member break if matched_member: member_wxid = matched_member.get("wxid", "") member_nickname = matched_member.get("nickname", nickname) logger.success(f"[GroupWelcome] 匹配成功: {member_nickname} ({member_wxid})") # 获取成员详细信息(包括头像) try: user_info = await bot.get_user_info_in_chatroom(room_wxid, member_wxid) if user_info: member_avatar = user_info.get("bigHeadImgUrl", "") logger.info(f"[GroupWelcome] 获取到成员头像: {member_avatar}") await self._send_welcome_card(bot, room_wxid, member_nickname, member_avatar) else: logger.warning(f"[GroupWelcome] 无法获取成员详细信息,使用默认头像") await self._send_welcome_card(bot, room_wxid, member_nickname, "") except Exception as e: logger.error(f"[GroupWelcome] 获取成员详细信息失败: {e}") await self._send_welcome_card(bot, room_wxid, member_nickname, "") else: logger.warning(f"[GroupWelcome] 未找到匹配的成员: {nickname}") await self._send_welcome_card(bot, room_wxid, nickname, "") else: logger.warning(f"[GroupWelcome] 无法获取群成员列表") await self._send_welcome_card(bot, room_wxid, nickname, "") except Exception as e: logger.error(f"处理新成员 {nickname} 欢迎失败: {e}") async def _send_welcome_card( self, bot, room_wxid: str, nickname: str, image_url: str ): """发送欢迎卡片""" welcome_config = self.config["welcome"] # 替换变量 title = welcome_config["title"].replace("{nickname}", nickname) desc = welcome_config["desc"].replace("{nickname}", nickname) url = welcome_config["url"] # 使用新成员的头像作为卡片图片 card_image_url = image_url if image_url else "" try: await bot.send_link_card( to_wxid=room_wxid, title=title, desc=desc, url=url, image_url=card_image_url, ) logger.success(f"已向 {nickname} 发送欢迎卡片") except Exception as e: logger.error(f"发送欢迎卡片失败: {e}") def _should_welcome(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_add(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 # 阻止后续处理