├── __init__.py ├── config.json.template ├── README.MD └── hello_plus.py /__init__.py: -------------------------------------------------------------------------------- 1 | from .hello_plus import * 2 | -------------------------------------------------------------------------------- /config.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "group_welc_fixed_msg": { 3 | "群聊1": "群聊1的固定欢迎语", 4 | "群聊2": "群聊2的固定欢迎语" 5 | }, 6 | 7 | "group_welc_prompt": "请你随机使用一种风格说一句问候语来欢迎新用户\"{nickname}\"加入群聊。", 8 | 9 | "group_exit_prompt": "请你随机使用一种风格跟其他群用户说他违反规则\"{nickname}\"退出群聊。", 10 | 11 | "patpat_prompt": "请你随机使用一种风格介绍你自己,并告诉用户输入#help可以查看帮助信息。", 12 | 13 | "use_character_desc": false, 14 | "redirect_link":"https://baike.baidu.com/item/welcome/2135227", 15 | "exit_url": "https://baike.baidu.com/item/%E9%80%80%E5%87%BA/28909", 16 | "say_exit": "有缘再见", 17 | "sleep_time": 60, 18 | "auth_token":"12345679", 19 | "welc_text":false, 20 | "group_names":["测试bott","测试机器人"] 21 | } -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # HelloPlus Plugin 2 | 3 | HelloPlus是一个增强版的微信群管理插件,主要提供群聊欢迎、退群提醒等功能(原hello插件增加卡片欢迎)。 4 | 5 | ## 功能特点 6 | 7 | 1. 群聊欢迎 8 | - 支持自定义欢迎消息 9 | - 显示新成员头像和个性签名和加入时间 10 | - 支持自定义欢迎卡片跳转链接 11 | 12 | 2. 退群提醒 13 | - 可配置是否启用退群提醒 14 | - 支持自定义退群提示语 15 | 16 | 3. 基础对话(原hello功能) 17 | - 响应"Hello"命令:回复用户昵称 18 | - 响应"Hi"命令:简单回复"Hi" 19 | - 响应"End"命令:生成"The World"的图片 20 | 21 | ## 配置参数说明 22 | 23 | ### 基础配置参数 24 | 25 | | 参数名 | 类型 | 默认值 | 说明 | 26 | |--------|------|--------|------| 27 | | group_welc_fixed_msg | object | {} | 群固定欢迎语配置,格式为 {"群名": "欢迎语"} | 28 | | group_welc_prompt | string | "请你随机使用一种风格说一句问候语来欢迎新用户{nickname}加入群聊。" | AI欢迎语模板 | 29 | | group_exit_prompt | string | "请你随机使用一种风格介绍你自己,并告诉用户输入#help可以查看帮助信息。" | 退群提示语模板 | 30 | | patpat_prompt | string | "请你随机使用一种风格跟其他群用户说他违反规则{nickname}退出群聊。" | 拍一拍提示语模板 | 31 | | welc_text | boolean | false | 是否在欢迎卡片后追加文字欢迎 | 32 | | use_character_desc | boolean | true | 是否使用角色设定 | 33 | 34 | ### 链接配置参数 35 | 36 | | 参数名 | 类型 | 默认值 | 说明 | 37 | |--------|------|--------|------| 38 | | redirect_link | string | "https://baike.baidu.com/item/welcome/2135227" | 欢迎卡片跳转链接 | 39 | | exit_url | string | "https://baike.baidu.com/item/%E9%80%80%E5%87%BA/28909" | 退群卡片跳转链接 | 40 | 41 | ### 监控配置参数 42 | 43 | | 参数名 | 类型 | 默认值 | 说明 | 44 | |--------|------|--------|------| 45 | | sleep_time | integer | 60 | 群监控检查间隔(秒) | 46 | | auth_token | string | "12345679" | 管理员验证token | 47 | | say_exit | string | "有缘再见" | 退群卡片提示语 | 48 | | group_names | array | [] | 默认开启监控的群列表,格式为 ["群名1", "群名2"] | 49 | 50 | ### 示例配置 51 | 52 | ```json 53 | { 54 | "group_welc_fixed_msg": { 55 | "测试群": "欢迎加入测试群!" 56 | }, 57 | "group_welc_prompt": "热烈欢迎{nickname}加入我们的大家庭!", 58 | "redirect_link": "https://example.com/welcome", 59 | "exit_url": "https://example.com/goodbye", 60 | "sleep_time": 30, 61 | "auth_token": "your-secure-token", 62 | "welc_text": true, 63 | "use_character_desc": true 64 | } 65 | ``` 66 | 67 | ## 使用方法 68 | 69 | 1. 群成员加入时自动触发欢迎 70 | - 显示欢迎卡片,包含成员昵称、时间、签名等信息(失败使用普通欢迎) 71 | - 如果配置了固定欢迎语,优先使用固定欢迎语 72 | 73 | 2. 群成员退出时自动提醒 74 | - 如果启用了退群提醒,会发送相应消息 75 | - 可在配置中自定义退群提示语 76 | 77 | 3. 基础命令 78 | - 发送"Hello":机器人回复问候语 79 | - 发送"Hi":机器人简单回复 80 | - 发送"End":机器人生成图片 81 | - 发送"#help":查看帮助信息 82 | 83 | 4. 群监控操作 84 | - 发送"开启退群监控":开启当前群的退群监控功能 85 | - 发送"关闭退群监控":关闭当前群的退群监控功能 86 | - 发送"查看监控群列表":查看当前所有开启监控的群及其成员数量 87 | - 发送"群监控管理验证 [token]":验证成为群监控管理员(私聊) 88 | - 私聊发送"开启监控 群聊名":开启指定群的退群监控功能 89 | - 私聊发送"关闭监控 群聊名":关闭指定群的退群监控功能 90 | 91 | 注意: 92 | - 只有管理员可以操作群监控功能 93 | - 监控间隔可通过 sleep_time 参数配置 94 | - 管理员验证token需要在配置文件中设置 95 | - 建议在私聊环境下进行管理员验证 96 | - 私聊操作群监控需要(机器人)保存群到通讯录 97 | - 默认开启监控的群列表需要(机器人)保存群到通讯录 98 | 99 | ## 注意事项 100 | 101 | 1. 请确保已正确配置GE微信相关参数 102 | 2. 建议根据实际需求调整欢迎语和提示语模板 103 | 3. 如需禁用某些功能,可在配置文件中设置相应参数 104 | 105 | ## 作者 106 | 107 | 作者:wangcl 108 | 版本:0.1 109 | -------------------------------------------------------------------------------- /hello_plus.py: -------------------------------------------------------------------------------- 1 | # encoding:utf-8 2 | 3 | import plugins 4 | from bridge.context import ContextType 5 | from bridge.reply import Reply, ReplyType 6 | from channel.chat_message import ChatMessage 7 | from common.log import logger 8 | from plugins import * 9 | from config import conf 10 | 11 | 12 | @plugins.register( 13 | name="HelloPlus", 14 | desire_priority=1, 15 | hidden=True, 16 | desc="欢迎plus版", 17 | version="0.1", 18 | author="wangcl", 19 | ) 20 | 21 | 22 | class HelloPlus(Plugin): 23 | 24 | group_welc_prompt = "请你随机使用一种风格说一句问候语来欢迎新用户\"{nickname}\"加入群聊。" 25 | group_exit_prompt = "请你随机使用一种风格介绍你自己,并告诉用户输入#help可以查看帮助信息。" 26 | patpat_prompt = "请你随机使用一种风格跟其他群用户说他违反规则\"{nickname}\"退出群聊。" 27 | exit_url="https://baike.baidu.com/item/%E9%80%80%E5%87%BA/28909" 28 | redirect_link = "https://baike.baidu.com/item/welcome/2135227" 29 | say_exit = "有缘再见" 30 | sleep_time = 60 31 | welc_text = False 32 | auth_token = "12345679" 33 | admin_user = set() 34 | group_names=['群聊1','群聊2'] 35 | ql_list = {} 36 | def __init__(self): 37 | super().__init__() 38 | try: 39 | self.config = super().load_config() 40 | if not self.config: 41 | self.config = self._load_config_template() 42 | self.group_welc_fixed_msg = self.config.get("group_welc_fixed_msg", {}) 43 | self.group_welc_prompt = self.config.get("group_welc_prompt", self.group_welc_prompt) 44 | self.group_exit_prompt = self.config.get("group_exit_prompt", self.group_exit_prompt) 45 | self.group_exit_prompt_str="\"{nickname}\"退出群聊。请你随机使用一种风格跟他说再见。" 46 | self.patpat_prompt = self.config.get("patpat_prompt", self.patpat_prompt) 47 | self.redirect_link = self.config.get("redirect_link", self.redirect_link) 48 | self.exit_url = self.config.get("exit_url", self.exit_url) 49 | self.say_exit = self.config.get("say_exit", self.say_exit) 50 | self.sleep_time=self.config.get("sleep_time", self.sleep_time) 51 | self.auth_token=self.config.get("auth_token", self.auth_token) 52 | self.welc_text=self.config.get("welc_text", self.welc_text) 53 | self.group_names=self.config.get("group_names", self.group_names) 54 | self.appid = conf().get("gewechat_app_id", "") 55 | self.gewetk = conf().get("gewechat_token", "") 56 | self.base_url = conf().get("gewechat_base_url") 57 | self.group_members={} 58 | self.memberList = [] 59 | self.admin_user = [] 60 | self.monitor_threads = {} # 存储监控线程 61 | self.monitoring_groups = set() # 存储正在监控的群组ID 62 | self.monitoring_groups_name = {} # 存储正在监控的群组name 63 | self.headers = { 64 | 'X-GEWE-TOKEN': self.gewetk, 65 | 'Content-Type': 'application/json' 66 | } 67 | try: 68 | self.get_group_list() 69 | 70 | logger.info(f"[HelloPlus]默认群聊监控开启成功") 71 | except Exception as e: 72 | logger.error(f"[HelloPlus]默认群聊监控开启失败:{e}") 73 | logger.info("[HelloPlus] inited") 74 | self.handlers[Event.ON_HANDLE_CONTEXT] = self.on_handle_context 75 | except Exception as e: 76 | print(f"[HelloPlus]初始化异常-----:{e}") 77 | logger.error(f"[HelloPlus]初始化异常:{e}") 78 | raise "[HelloPlus] init failed, ignore " 79 | 80 | def on_handle_context(self, e_context: EventContext): 81 | if e_context["context"].type not in [ 82 | ContextType.TEXT, 83 | ContextType.JOIN_GROUP, 84 | ContextType.PATPAT, 85 | ContextType.EXIT_GROUP 86 | ]: 87 | return 88 | msg: ChatMessage = e_context["context"]["msg"] 89 | group_name = msg.from_user_nickname 90 | if e_context["context"].type == ContextType.JOIN_GROUP: 91 | if "group_welcome_msg" in conf() or group_name in self.group_welc_fixed_msg: 92 | 93 | reply = Reply() 94 | reply.type = ReplyType.TEXT 95 | if group_name in self.group_welc_fixed_msg: 96 | reply.content = self.group_welc_fixed_msg.get(group_name, "") 97 | else: 98 | reply.content = conf().get("group_welcome_msg", "") 99 | e_context["reply"] = reply 100 | e_context.action = EventAction.BREAK_PASS # 事件结束,并跳过处理context的默认逻辑 101 | return 102 | print('----welcome----') 103 | try: 104 | import time 105 | time.sleep(2) 106 | qm,imgurl,nickName=self.get_info(msg.other_user_id,msg.actual_user_nickname) 107 | if qm!=None or imgurl!=None: 108 | ret=self.welcome(msg,qm,imgurl) 109 | if ret!= 200: 110 | e_context["context"].type = ContextType.TEXT 111 | e_context["context"].content = self.group_welc_prompt.format(nickname=msg.actual_user_nickname) 112 | e_context.action = EventAction.BREAK # 事件结束,进入默认处理逻辑 113 | if self.welc_text: 114 | time.sleep(2) 115 | e_context["context"].type = ContextType.TEXT 116 | e_context["context"].content = self.group_welc_prompt.format(nickname=msg.actual_user_nickname) 117 | e_context.action = EventAction.BREAK 118 | else: 119 | e_context["context"].type = ContextType.TEXT 120 | e_context["context"].content = self.group_welc_prompt.format(nickname=msg.actual_user_nickname) 121 | e_context.action = EventAction.BREAK # 事件结束,进入默认处理逻辑 122 | except: 123 | e_context["context"].type = ContextType.TEXT 124 | e_context["context"].content = self.group_welc_prompt.format(nickname=msg.actual_user_nickname) 125 | e_context.action = EventAction.BREAK # 事件结束,进入默认处理逻辑 126 | if not self.config or not self.config.get("use_character_desc"): 127 | e_context["context"]["generate_breaked_by"] = EventAction.BREAK 128 | return 129 | 130 | if e_context["context"].type == ContextType.EXIT_GROUP: 131 | 132 | if "group_exit_msg" in conf(): 133 | reply = Reply() 134 | reply.type = ReplyType.TEXT 135 | reply.content = conf().get("group_exit_msg", "") 136 | e_context["reply"] = reply 137 | e_context.action = EventAction.BREAK_PASS # 事件结束,并跳过处理context的默认逻辑 138 | return 139 | if conf().get("group_chat_exit_group"): 140 | e_context["context"].type = ContextType.TEXT 141 | e_context["context"].content = self.group_exit_prompt.format(nickname=msg.actual_user_nickname) 142 | e_context.action = EventAction.BREAK # 事件结束,进入默认处理逻辑 143 | return 144 | e_context.action = EventAction.BREAK 145 | return 146 | 147 | if e_context["context"].type == ContextType.PATPAT: 148 | e_context["context"].type = ContextType.TEXT 149 | e_context["context"].content = self.patpat_prompt 150 | e_context.action = EventAction.BREAK # 事件结束,进入默认处理逻辑 151 | if not self.config or not self.config.get("use_character_desc"): 152 | e_context["context"]["generate_breaked_by"] = EventAction.BREAK 153 | return 154 | 155 | content = e_context["context"].content 156 | logger.debug("[Hello] on_handle_context. content: %s" % content) 157 | if content == "Hello": 158 | reply = Reply() 159 | reply.type = ReplyType.TEXT 160 | if e_context["context"]["isgroup"]: 161 | reply.content = f"Hello, {msg.actual_user_nickname} from {msg.from_user_nickname}" 162 | else: 163 | reply.content = f"Hello, {msg.from_user_nickname}" 164 | e_context["reply"] = reply 165 | e_context.action = EventAction.BREAK_PASS # 事件结束,并跳过处理context的默认逻辑 166 | user_id=msg.actual_user_id 167 | if content.startswith('群监控管理验证'): 168 | if e_context["context"]["isgroup"]: 169 | reply_cont="不支持群聊验证" 170 | reply = self.create_reply(ReplyType.TEXT, reply_cont) 171 | e_context["reply"] = reply 172 | e_context.action = EventAction.BREAK_PASS 173 | return 174 | tk = content[7:].strip() 175 | reply_cont="验证成功,已将您设为群监控管理员。" if self.add_admin_user(tk,user_id) else "验证失败" 176 | reply = self.create_reply(ReplyType.TEXT, reply_cont) 177 | e_context["reply"] = reply 178 | e_context.action = EventAction.BREAK_PASS 179 | if content =='查看监控群列表': 180 | if not self.is_admin(user_id): 181 | reply = Reply() 182 | reply.type = ReplyType.TEXT 183 | reply.content = "没权限啊" 184 | e_context["reply"] = reply 185 | e_context.action = EventAction.BREAK_PASS 186 | return 187 | # 获取监控群列表 188 | # 从self.group_members获取每个群的成员数量 189 | 190 | if len(self.group_members.keys())==0: 191 | reply = Reply() 192 | reply.type = ReplyType.TEXT 193 | reply.content = "监控群列表为空" 194 | e_context["reply"] = reply 195 | e_context.action = EventAction.BREAK_PASS 196 | return 197 | group_member_count = {} 198 | for group_id, members in self.group_members.items(): 199 | group_member_count[group_id] = len(members) 200 | reply = Reply() 201 | reply.type = ReplyType.TEXT 202 | reply.content = "监控群列表:\n" 203 | for group_id in self.monitoring_groups: 204 | reply.content += f" 💬{self.monitoring_groups_name[group_id]} -🙎‍♂️当前成员: {group_member_count[group_id]}人\n" 205 | e_context["reply"] = reply 206 | e_context.action = EventAction.BREAK_PASS 207 | return 208 | if content.startswith("开启监控"): 209 | if e_context["context"]["isgroup"]: 210 | reply_cont="不支持群聊开启" 211 | reply = self.create_reply(ReplyType.TEXT, reply_cont) 212 | e_context["reply"] = reply 213 | e_context.action = EventAction.BREAK_PASS 214 | return 215 | if not self.is_admin(user_id): 216 | reply = Reply() 217 | reply.type = ReplyType.TEXT 218 | reply.content = "没权限啊" 219 | e_context["reply"] = reply 220 | e_context.action = EventAction.BREAK_PASS 221 | return 222 | group_name=content[4:].strip() 223 | ret,msg=self.start_monitor(group_name) 224 | reply = Reply() 225 | reply.type = ReplyType.TEXT 226 | reply.content = msg 227 | e_context["reply"] = reply 228 | e_context.action = EventAction.BREAK_PASS 229 | if content.startswith("关闭监控"): 230 | group_name=content[4:].strip() 231 | if e_context["context"]["isgroup"]: 232 | reply_cont="不支持群聊关闭" 233 | reply = self.create_reply(ReplyType.TEXT, reply_cont) 234 | e_context["reply"] = reply 235 | e_context.action = EventAction.BREAK_PASS 236 | return 237 | if not self.is_admin(user_id): 238 | reply = Reply() 239 | reply.type = ReplyType.TEXT 240 | reply.content = "没权限啊" 241 | e_context["reply"] = reply 242 | e_context.action = EventAction.BREAK_PASS 243 | return 244 | flag=True 245 | for group_id, name in self.monitoring_groups_name.items(): 246 | if name == group_name: 247 | flag=False 248 | if group_id in self.monitoring_groups: 249 | self.monitoring_groups.remove(group_id) 250 | self.monitoring_groups_name.pop(group_id) 251 | reply = Reply() 252 | reply.type = ReplyType.TEXT 253 | reply.content = f"监控关闭成功: {group_name}" 254 | e_context["reply"] = reply 255 | else: 256 | reply = Reply() 257 | reply.type = ReplyType.TEXT 258 | reply.content = f"[{group_name}]未开启退群监控" 259 | e_context["reply"] = reply 260 | break 261 | if flag: 262 | reply = Reply() 263 | reply.type = ReplyType.TEXT 264 | reply.content = f"未找到群组:{group_name}" 265 | e_context["reply"] = reply 266 | e_context.action = EventAction.BREAK_PASS 267 | return 268 | if e_context["context"]["isgroup"]: 269 | 270 | if content =='开启退群监控': 271 | if not self.is_admin(user_id): 272 | reply = Reply() 273 | reply.type = ReplyType.TEXT 274 | reply.content = "没权限啊" 275 | e_context["reply"] = reply 276 | e_context.action = EventAction.BREAK_PASS 277 | return 278 | self.get_member_list(msg.other_user_id,msg.other_user_nickname) 279 | reply = Reply() 280 | reply.type = ReplyType.TEXT 281 | reply.content = f"当前群[{msg.other_user_nickname}]已开启退群监控" 282 | e_context["reply"] = reply 283 | e_context.action = EventAction.BREAK_PASS 284 | return 285 | if content == "关闭退群监控": 286 | if not self.is_admin(user_id): 287 | reply = Reply() 288 | reply.type = ReplyType.TEXT 289 | reply.content = "没权限啊" 290 | e_context["reply"] = reply 291 | e_context.action = EventAction.BREAK_PASS 292 | return 293 | group_id = msg.other_user_id 294 | if group_id in self.monitoring_groups: 295 | self.monitoring_groups.remove(group_id) 296 | self.monitoring_groups_name.pop(group_id) 297 | reply = Reply() 298 | reply.type = ReplyType.TEXT 299 | reply.content = f"当前群[{msg.other_user_nickname}]已关闭退群监控" 300 | e_context["reply"] = reply 301 | else: 302 | reply = Reply() 303 | reply.type = ReplyType.TEXT 304 | reply.content = "当前群未开启退群监控" 305 | e_context["reply"] = reply 306 | e_context.action = EventAction.BREAK_PASS 307 | return 308 | if content == "Hi": 309 | reply = Reply() 310 | reply.type = ReplyType.TEXT 311 | reply.content = "Hi" 312 | e_context["reply"] = reply 313 | e_context.action = EventAction.BREAK # 事件结束,进入默认处理逻辑,一般会覆写reply 314 | 315 | if content == "End": 316 | # 如果是文本消息"End",将请求转换成"IMAGE_CREATE",并将content设置为"The World" 317 | e_context["context"].type = ContextType.IMAGE_CREATE 318 | content = "The World" 319 | e_context.action = EventAction.CONTINUE # 事件继续,交付给下个插件或默认逻辑 320 | 321 | def get_help_text(self, **kwargs): 322 | help_text = "输入Hello,我会回复你的名字\n输入End,我会回复你世界的图片\n" 323 | return help_text 324 | 325 | def _load_config_template(self): 326 | logger.debug("No Hello plugin config.json, use plugins/hello/config.json.template") 327 | try: 328 | plugin_config_path = os.path.join(self.path, "config.json.template") 329 | if os.path.exists(plugin_config_path): 330 | with open(plugin_config_path, "r", encoding="utf-8") as f: 331 | plugin_conf = json.load(f) 332 | return plugin_conf 333 | except Exception as e: 334 | logger.exception(e) 335 | def welcome(self,msg,qm,imgurl): 336 | import requests 337 | import json 338 | from datetime import datetime 339 | post_url = f"{self.base_url}/message/postAppMsg" 340 | now = datetime.now().strftime("%Y年%m月%d日 %H时%M分%S秒") 341 | url=self.redirect_link 342 | payload = json.dumps({ 343 | "appId": self.appid, 344 | "toWxid": msg.other_user_id, 345 | "appmsg": ( 346 | f'' 347 | f'👏欢迎 {msg.actual_user_nickname} 加入群聊!🎉' 348 | f'⌚:{now}\n签名:{qm if qm else "这个人没有签名"}' 349 | f'view50' 350 | f'{url}' 351 | f'' 352 | f'{imgurl}' 353 | '' 354 | '0' 355 | '' 356 | '0' 357 | '0' 358 | '' 359 | ) 360 | }) 361 | response = requests.request("POST", post_url, data=payload, headers=self.headers) 362 | 363 | return response.json()['ret'] 364 | def exit(self,group_id,imgurl,nickName): 365 | import requests 366 | import json 367 | from datetime import datetime 368 | post_url = f"{self.base_url}/message/postAppMsg" 369 | now = datetime.now().strftime("%Y年%m月%d日 %H时%M分%S秒") 370 | payload = json.dumps({ 371 | "appId": self.appid, 372 | "toWxid": group_id, 373 | "appmsg": ( 374 | f'' 375 | f' {nickName} 离开群聊!' 376 | f'⌚:{now}\n{self.say_exit}' 377 | f'view50' 378 | f'{self.exit_url}' 379 | f'' 380 | f'{imgurl}' 381 | '' 382 | '0' 383 | '' 384 | '0' 385 | '0' 386 | '' 387 | ) 388 | }) 389 | response = requests.request("POST", post_url, data=payload, headers=self.headers) 390 | 391 | return response.json()['ret'] 392 | def get_info(self,group_id,nickname): 393 | import requests 394 | import json 395 | print('----get_info----') 396 | wxid=self.get_list(group_id,nickname) 397 | if wxid==None: 398 | return None 399 | payload = json.dumps({ 400 | "appId": self.appid, 401 | "chatroomId": group_id, 402 | "memberWxids": [ 403 | wxid 404 | ] 405 | }) 406 | data=requests.request("POST", f"{self.base_url}/group/getChatroomMemberDetail", data=payload, headers=self.headers).json() 407 | print('----get_info----',data["data"][0]["signature"],data["data"][0]["smallHeadImgUrl"],data["data"][0]["nickName"]) 408 | return data["data"][0]["signature"],data["data"][0]["smallHeadImgUrl"],data["data"][0]["nickName"] 409 | def get_list(self,group_id,nickname): 410 | print('----get_list----') 411 | print('----group_id----',group_id,nickname) 412 | import requests 413 | import json 414 | payload = json.dumps({ 415 | "appId": self.appid, 416 | "chatroomId": group_id, 417 | }) 418 | 419 | data=requests.request("POST", f"{self.base_url}/group/getChatroomMemberList", data=payload, headers=self.headers).json() 420 | ret=data['ret'] 421 | if ret!=200: 422 | return None 423 | wxid=None 424 | 425 | for member in data["data"]["memberList"]: 426 | if member["nickName"] == nickname: 427 | wxid=member["wxid"] 428 | print('----get_list----',wxid) 429 | return wxid 430 | def get_member_list(self, other_user_id, other_user_nickname): 431 | """ 432 | 获取群成员列表并监控退群行为 433 | """ 434 | print('----get_member_list----') 435 | import requests 436 | import json 437 | import time 438 | import threading 439 | 440 | # 清理已存在的监控线程 441 | if other_user_id in self.monitor_threads: 442 | # 从监控集合中移除 443 | if other_user_id in self.monitoring_groups: 444 | self.monitoring_groups.remove(other_user_id) 445 | if other_user_id in self.monitoring_groups_name: 446 | self.monitoring_groups_name.pop(other_user_id) 447 | # 等待旧线程结束 448 | if self.monitor_threads[other_user_id].is_alive(): 449 | time.sleep(self.sleep_time + 1) 450 | # 清理线程记录 451 | self.monitor_threads.pop(other_user_id) 452 | 453 | def monitor_group(group_id): 454 | while group_id in self.monitoring_groups: 455 | try: 456 | payload = json.dumps({ 457 | "appId": self.appid, 458 | "chatroomId": group_id, 459 | }) 460 | 461 | data = requests.request("POST", f"{self.base_url}/group/getChatroomMemberList", 462 | data=payload, headers=self.headers).json() 463 | 464 | if data.get('ret') != 200: 465 | logger.error(f"[HelloPlus] Failed to get member list for group {group_id}: {data}") 466 | time.sleep(self.sleep_time) 467 | continue 468 | 469 | current_members = data["data"]["memberList"] 470 | 471 | if group_id not in self.group_members: 472 | self.group_members[group_id] = current_members 473 | else: 474 | old_members = self.group_members[group_id] 475 | old_wxids = {m["wxid"] for m in old_members} 476 | new_wxids = {m["wxid"] for m in current_members} 477 | 478 | left_members = old_wxids - new_wxids 479 | if left_members: 480 | for wxid in left_members: 481 | member = next(m for m in old_members if m["wxid"] == wxid) 482 | logger.info(f"[HelloPlus] User {member['nickName']} left group {group_id}") 483 | self.exit(group_id, member['smallHeadImgUrl'], member['nickName']) 484 | 485 | self.group_members[group_id] = current_members 486 | 487 | self.memberList = current_members 488 | time.sleep(self.sleep_time) 489 | 490 | except Exception as e: 491 | logger.error(f"[HelloPlus] Error monitoring group {group_id}: {e}") 492 | if group_id not in self.monitoring_groups: 493 | break 494 | time.sleep(self.sleep_time) 495 | continue 496 | 497 | logger.info(f"[HelloPlus] Stopped monitoring group {group_id}") 498 | 499 | # 启动新的监控线程 500 | self.monitoring_groups.add(other_user_id) 501 | self.monitoring_groups_name[other_user_id] = other_user_nickname 502 | t = threading.Thread(target=monitor_group, args=(other_user_id,)) 503 | t.daemon = True 504 | t.start() 505 | self.monitor_threads[other_user_id] = t 506 | 507 | return self.memberList 508 | def is_admin(self,wxid): 509 | return wxid in self.admin_user 510 | def add_admin_user(self,token,wxid): 511 | if token==self.auth_token: 512 | print('--**验证成功') 513 | self.admin_user.append(wxid) 514 | print(self.admin_user) 515 | return True 516 | return False 517 | def create_reply(self, reply_type, content): 518 | reply = Reply() 519 | reply.type = reply_type 520 | reply.content = content 521 | return reply 522 | def get_group_list(self): 523 | import requests 524 | import json 525 | url = f"{self.base_url}/contacts/fetchContactsList" 526 | 527 | payload = json.dumps({ 528 | "appId": self.appid 529 | }) 530 | 531 | response = requests.request("POST", url, data=payload, headers=self.headers) 532 | response=response.json() 533 | if response['ret']!=200: 534 | return None 535 | rooms=response['data']['chatrooms'] 536 | self.rooms=rooms 537 | print(rooms) 538 | self.get_group_info(rooms) 539 | def get_group_info(self,rooms): 540 | import requests 541 | import json 542 | import time 543 | url = f"{self.base_url}/contacts/getDetailInfo" 544 | 545 | payload = json.dumps({ 546 | "appId": self.appid, 547 | "wxids": rooms 548 | }) 549 | 550 | response = requests.request("POST", url, data=payload, headers=self.headers) 551 | response=response.json() 552 | if response['ret']!=200: 553 | return None 554 | datas=response['data'] 555 | 556 | for group_name in self.group_names: 557 | found = False 558 | for data in datas: 559 | self.ql_list[data['userName']]=data['nickName'] 560 | if data['nickName'] == group_name: 561 | time.sleep(1) 562 | self.get_member_list(data['userName'], data['nickName']) 563 | found = True 564 | break 565 | if not found: 566 | print(f"群组 {group_name} 未找到") 567 | 568 | return self.ql_list 569 | def start_monitor(self, group_name): 570 | try: 571 | # 遍历self.ql_list找到group_name对应的group_id 572 | for group_id, name in self.ql_list.items(): 573 | if name == group_name: 574 | # 找到匹配的群组,调用get_member_list 575 | try: 576 | self.get_member_list(group_id, group_name) 577 | logger.info(f"监控启动成功: {group_name}") 578 | return True, f"监控启动成功: {group_name}" 579 | except Exception as e: 580 | error_msg = f"启动群监控失败: {group_name} " 581 | logger.error(f"启动群监控失败 {str(e)}") 582 | return False, error_msg 583 | 584 | return False, f"未找到群组: {group_name}" 585 | except Exception as e: 586 | error_msg = f"启动监控时发生错误: {group_name}" 587 | logger.error(f"[启动监控时发生错误] : {str(e)}") 588 | return False, error_msg --------------------------------------------------------------------------------