import requests import websocket import json from langbot.libs.wechatpad_api.client import WeChatPadClient import typing import asyncio import traceback import re import base64 import copy import threading import quart from langbot.pkg.platform.logger import EventLogger from langbot.pkg.utils import bounded_executor, httpclient import xml.etree.ElementTree as ET from typing import Optional, Tuple from functools import partial import logging import langbot_plugin.api.entities.builtin.platform.message as platform_message import langbot_plugin.api.entities.builtin.platform.events as platform_events import langbot_plugin.api.entities.builtin.platform.entities as platform_entities import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter import langbot_plugin.api.definition.abstract.platform.event_logger as abstract_platform_logger _MAX_GATEWAY_MESSAGE_CHARS = 1024 * 1024 class WeChatPadMessageConverter(abstract_platform_adapter.AbstractMessageConverter): def __init__(self, config: dict, logger: abstract_platform_logger.AbstractEventLogger): self.bot = WeChatPadClient(config['wechatpad_url'], config['token']) self.config = config self.logger = logger # super().__init__( # config = config, # bot = bot, # logger = logger, # ) @staticmethod async def yiri2target(message_chain: platform_message.MessageChain) -> list[dict]: content_list = [] for component in message_chain: if isinstance(component, platform_message.AtAll): content_list.append({'type': 'at', 'target': 'all'}) elif isinstance(component, platform_message.At): content_list.append({'type': 'at', 'target': component.target}) elif isinstance(component, platform_message.Plain): content_list.append({'type': 'text', 'content': component.text}) elif isinstance(component, platform_message.Image): if component.url: session = httpclient.get_session() async with session.get(component.url) as response: if response.status == 200: file_bytes = await httpclient.read_limited(response) base64_str = (await asyncio.to_thread(base64.b64encode, file_bytes)).decode('utf-8') else: raise Exception('获取文件失败') # pass content_list.append({'type': 'image', 'image': base64_str}) elif component.base64: content_list.append({'type': 'image', 'image': component.base64}) elif isinstance(component, platform_message.WeChatEmoji): content_list.append( {'type': 'WeChatEmoji', 'emoji_md5': component.emoji_md5, 'emoji_size': component.emoji_size} ) elif isinstance(component, platform_message.Voice): content_list.append({'type': 'voice', 'data': component.url, 'duration': component.length, 'forma': 0}) elif isinstance(component, platform_message.WeChatAppMsg): content_list.append({'type': 'WeChatAppMsg', 'app_msg': component.app_msg}) elif isinstance(component, platform_message.Forward): for node in component.node_list: if node.message_chain: content_list.extend(await WeChatPadMessageConverter.yiri2target(node.message_chain)) return content_list async def target2yiri( self, message: dict, bot_account_id: str, ) -> platform_message.MessageChain: """外部消息转平台消息""" # 数据预处理 message_list = [] bot_wxid = self.config['wxid'] ats_bot = False # 是否被@ content = message['content']['str'] content_no_preifx = content # 群消息则去掉前缀 is_group_message = self._is_group_message(message) if is_group_message: ats_bot = self._ats_bot(message, bot_account_id) self.logger.info(f'ats_bot: {ats_bot}; bot_account_id: {bot_account_id}; bot_wxid: {bot_wxid}') if '@所有人' in content: message_list.append(platform_message.AtAll()) if ats_bot: message_list.append(platform_message.At(target=bot_account_id)) # 解析@信息并生成At组件 at_targets = self._extract_at_targets(message) for target_id in at_targets: if target_id != bot_wxid: # 避免重复添加机器人的At message_list.append(platform_message.At(target=target_id)) content_no_preifx, _ = self._extract_content_and_sender(content) msg_type = message['msg_type'] # 映射消息类型到处理器方法 handler_map = { 1: self._handler_text, 3: self._handler_image, 34: self._handler_voice, 49: self._handler_compound, # 复合类型 } # 分派处理 handler = handler_map.get(msg_type, self._handler_default) handler_result = await handler( message=message, # 原始的message content_no_preifx=content_no_preifx, # 处理后的content ) if handler_result and len(handler_result) > 0: message_list.extend(handler_result) return platform_message.MessageChain(message_list) async def _handler_text(self, message: Optional[dict], content_no_preifx: str) -> platform_message.MessageChain: """处理文本消息 (msg_type=1)""" if message and self._is_group_message(message): pattern = r'@\S{1,20}' content_no_preifx = re.sub(pattern, '', content_no_preifx) return platform_message.MessageChain([platform_message.Plain(text=content_no_preifx)]) async def _handler_image(self, message: Optional[dict], content_no_preifx: str) -> platform_message.MessageChain: """处理图像消息 (msg_type=3)""" try: image_xml = content_no_preifx if not image_xml: return platform_message.MessageChain([platform_message.Unknown('[图片内容为空]')]) root = ET.fromstring(image_xml) # 提取img标签的属性 img_tag = root.find('img') if img_tag is not None: aeskey = img_tag.get('aeskey') cdnthumburl = img_tag.get('cdnthumburl') # cdnmidimgurl = img_tag.get('cdnmidimgurl') image_data = await asyncio.to_thread( self.bot.cdn_download, aeskey=aeskey, file_type=1, file_url=cdnthumburl, ) if image_data['Data']['FileData'] == '': image_data = await asyncio.to_thread( self.bot.cdn_download, aeskey=aeskey, file_type=2, file_url=cdnthumburl, ) base64_str = image_data['Data']['FileData'] # self.logger.info(f"data:image/png;base64,{base64_str}") elements = [ platform_message.Image(base64=f'data:image/png;base64,{base64_str}'), # platform_message.WeChatForwardImage(xml_data=image_xml) # 微信消息转发 ] return platform_message.MessageChain(elements) except Exception as e: self.logger.error(f'处理图片失败: {str(e)}') return platform_message.MessageChain([platform_message.Unknown('[图片处理失败]')]) async def _handler_voice(self, message: Optional[dict], content_no_preifx: str) -> platform_message.MessageChain: """处理语音消息 (msg_type=34)""" message_List = [] try: # 从消息中提取语音数据(需根据实际数据结构调整字段名) # audio_base64 = message["img_buf"]["buffer"] voice_xml = content_no_preifx new_msg_id = message['new_msg_id'] root = ET.fromstring(voice_xml) # 提取voicemsg标签的属性 voicemsg = root.find('voicemsg') if voicemsg is not None: bufid = voicemsg.get('bufid') length = voicemsg.get('voicelength') voice_data = await asyncio.to_thread( self.bot.get_msg_voice, buf_id=str(bufid), length=int(length), msgid=str(new_msg_id), ) audio_base64 = voice_data['Data']['Base64'] # 验证语音数据有效性 if not audio_base64: message_List.append(platform_message.Unknown(text='[语音内容为空]')) return platform_message.MessageChain(message_List) # 转换为平台支持的语音格式(如 Silk 格式) voice_element = platform_message.Voice(base64=f'data:audio/silk;base64,{audio_base64}') message_List.append(voice_element) except KeyError as e: self.logger.error(f'语音数据字段缺失: {str(e)}') message_List.append(platform_message.Unknown(text='[语音数据解析失败]')) except Exception as e: self.logger.error(f'处理语音消息异常: {str(e)}') message_List.append(platform_message.Unknown(text='[语音处理失败]')) return platform_message.MessageChain(message_List) async def _handler_compound(self, message: Optional[dict], content_no_preifx: str) -> platform_message.MessageChain: """处理复合消息 (msg_type=49),根据子类型分派""" try: xml_data = ET.fromstring(content_no_preifx) appmsg_data = xml_data.find('.//appmsg') if appmsg_data: data_type = appmsg_data.findtext('.//type', '') # 二次分派处理器 sub_handler_map = { '57': self._handler_compound_quote, '5': self._handler_compound_link, '6': self._handler_compound_file, '74': self._handler_compound_file, '33': self._handler_compound_mini_program, '36': self._handler_compound_mini_program, '2000': partial(self._handler_compound_unsupported, text='[转账消息]'), '2001': partial(self._handler_compound_unsupported, text='[红包消息]'), '51': partial(self._handler_compound_unsupported, text='[视频号消息]'), } handler = sub_handler_map.get(data_type, self._handler_compound_unsupported) return await handler( message=message, # 原始msg xml_data=xml_data, # xml数据 ) else: return platform_message.MessageChain([platform_message.Unknown(text=content_no_preifx)]) except Exception as e: self.logger.error(f'解析复合消息失败: {str(e)}') return platform_message.MessageChain([platform_message.Unknown(text=content_no_preifx)]) async def _handler_compound_quote( self, message: Optional[dict], xml_data: ET.Element ) -> platform_message.MessageChain: """处理引用消息 (data_type=57)""" message_list = [] # self.logger.info("_handler_compound_quote", ET.tostring(xml_data, encoding='unicode')) appmsg_data = xml_data.find('.//appmsg') quote_data = '' # 引用原文 # quote_id = None # 引用消息的原发送者 # tousername = None # 接收方: 所属微信的wxid user_data = '' # 用户消息 sender_id = xml_data.findtext('.//fromusername') # 发送方:单聊用户/群member # 引用消息转发 if appmsg_data: user_data = appmsg_data.findtext('.//title') or '' quote_data = appmsg_data.find('.//refermsg').findtext('.//content') # quote_id = appmsg_data.find('.//refermsg').findtext('.//chatusr') message_list.append(platform_message.WeChatAppMsg(app_msg=ET.tostring(appmsg_data, encoding='unicode'))) # if message: # tousername = message['to_user_name']['str'] if quote_data: quote_data_message_list = platform_message.MessageChain() # 文本消息 try: if '' not in quote_data: quote_data_message_list.append(platform_message.Plain(text=quote_data)) else: # 引用消息展开 quote_data_xml = ET.fromstring(quote_data) if quote_data_xml.find('img'): quote_data_message_list.extend(await self._handler_image(None, quote_data)) elif quote_data_xml.find('voicemsg'): quote_data_message_list.extend(await self._handler_voice(None, quote_data)) elif quote_data_xml.find('videomsg'): quote_data_message_list.extend(await self._handler_default(None, quote_data)) # 先不处理 else: # appmsg quote_data_message_list.extend(await self._handler_compound(None, quote_data)) except Exception as e: self.logger.error(f'处理引用消息异常 expcetion:{e}') quote_data_message_list.append(platform_message.Plain(text=quote_data)) message_list.append( platform_message.Quote( sender_id=sender_id, origin=quote_data_message_list, ) ) if len(user_data) > 0: pattern = r'@\S{1,20}' user_data = re.sub(pattern, '', user_data) message_list.append(platform_message.Plain(text=user_data)) return platform_message.MessageChain(message_list) async def _handler_compound_file(self, message: dict, xml_data: ET.Element) -> platform_message.MessageChain: """处理文件消息 (data_type=6)""" file_data = xml_data.find('.//appmsg') if file_data.findtext('.//type', '') == '74': return None else: xml_data_str = ET.tostring(xml_data, encoding='unicode') # print(xml_data_str) # 提取img标签的属性 # print(xml_data) file_name = file_data.find('title').text file_id = file_data.find('md5').text # file_szie = file_data.find('totallen') # print(file_data) if file_data is not None: aeskey = xml_data.findtext('.//appattach/aeskey') cdnthumburl = xml_data.findtext('.//appattach/cdnattachurl') # cdnmidimgurl = img_tag.get('cdnmidimgurl') # print(aeskey,cdnthumburl) file_data = await asyncio.to_thread( self.bot.cdn_download, aeskey=aeskey, file_type=5, file_url=cdnthumburl, ) file_base64 = file_data['Data']['FileData'] # print(file_data) file_size = file_data['Data']['TotalSize'] # print(file_base64) return platform_message.MessageChain( [ platform_message.WeChatFile( file_id=file_id, file_name=file_name, file_size=file_size, file_base64=file_base64 ), platform_message.WeChatForwardFile(xml_data=xml_data_str), ] ) async def _handler_compound_link(self, message: dict, xml_data: ET.Element) -> platform_message.MessageChain: """处理链接消息(如公众号文章、外部网页)""" message_list = [] try: # 解析 XML 中的链接参数 appmsg = xml_data.find('.//appmsg') if appmsg is None: return platform_message.MessageChain() message_list.append( platform_message.WeChatLink( link_title=appmsg.findtext('title', ''), link_desc=appmsg.findtext('des', ''), link_url=appmsg.findtext('url', ''), link_thumb_url=appmsg.findtext('thumburl', ''), # 这个字段拿不到 ) ) # 还没有发链接的接口, 暂时还需要自己构造appmsg, 先用WeChatAppMsg。 message_list.append(platform_message.WeChatAppMsg(app_msg=ET.tostring(appmsg, encoding='unicode'))) except Exception as e: self.logger.error(f'解析链接消息失败: {str(e)}') return platform_message.MessageChain(message_list) async def _handler_compound_mini_program( self, message: dict, xml_data: ET.Element ) -> platform_message.MessageChain: """处理小程序消息(如小程序卡片、服务通知)""" xml_data_str = ET.tostring(xml_data, encoding='unicode') return platform_message.MessageChain([platform_message.WeChatForwardMiniPrograms(xml_data=xml_data_str)]) async def _handler_default(self, message: Optional[dict], content_no_preifx: str) -> platform_message.MessageChain: """处理未知消息类型""" if message: msg_type = message['msg_type'] else: msg_type = '' return platform_message.MessageChain([platform_message.Unknown(text=f'[未知消息类型 msg_type:{msg_type}]')]) def _handler_compound_unsupported( self, message: dict, xml_data: str, text: Optional[str] = None ) -> platform_message.MessageChain: """处理未支持复合消息类型(msg_type=49)子类型""" if not text: text = f'[xml_data={xml_data}]' content_list = [] content_list.append(platform_message.Unknown(text=f'[处理未支持复合消息类型[msg_type=49]|{text}')) return platform_message.MessageChain(content_list) # 返回是否被艾特 def _ats_bot(self, message: dict, bot_account_id: str) -> bool: ats_bot = False try: to_user_name = message['to_user_name']['str'] # 接收方: 所属微信的wxid raw_content = message['content']['str'] # 原始消息内容 content_no_prefix, _ = self._extract_content_and_sender(raw_content) # 直接艾特机器人(这个有bug,当被引用的消息里面有@bot,会套娃 # ats_bot = ats_bot or (f"@{bot_account_id}" in content_no_prefix) # 文本类@bot push_content = message.get('push_content', '') ats_bot = ats_bot or ('在群聊中@了你' in push_content) # 引用别人时@bot msg_source = message.get('msg_source', '') or '' if len(msg_source) > 0: msg_source_data = ET.fromstring(msg_source) at_user_list = msg_source_data.findtext('atuserlist') or '' ats_bot = ats_bot or (to_user_name in at_user_list) # 引用bot if message.get('msg_type', 0) == 49: xml_data = ET.fromstring(content_no_prefix) appmsg_data = xml_data.find('.//appmsg') tousername = message['to_user_name']['str'] if appmsg_data: # 接收方: 所属微信的wxid quote_id = appmsg_data.find('.//refermsg').findtext('.//chatusr') # 引用消息的原发送者 ats_bot = ats_bot or (quote_id == tousername) except Exception as e: self.logger.error(f'_ats_bot got except: {e}') finally: return ats_bot # 提取一下at的wxid列表 def _extract_at_targets(self, message: dict) -> list[str]: """从消息中提取被@用户的ID列表""" at_targets = [] try: # 从msg_source中解析atuserlist msg_source = message.get('msg_source', '') or '' if len(msg_source) > 0: msg_source_data = ET.fromstring(msg_source) at_user_list = msg_source_data.findtext('atuserlist') or '' if at_user_list: # atuserlist格式通常是逗号分隔的用户ID列表 at_targets = [user_id.strip() for user_id in at_user_list.split(',') if user_id.strip()] except Exception as e: self.logger.error(f'_extract_at_targets got except: {e}') return at_targets # 提取一下content前面的sender_id, 和去掉前缀的内容 def _extract_content_and_sender(self, raw_content: str) -> Tuple[str, Optional[str]]: try: # 检查消息开头,如果有 wxid_sbitaz0mt65n22:\n 则删掉 # add: 有些用户的wxid不是上述格式。换成user_name: regex = re.compile(r'^[a-zA-Z0-9_\-]{5,20}:') line_split = raw_content.split('\n') if len(line_split) > 0 and regex.match(line_split[0]): raw_content = '\n'.join(line_split[1:]) sender_id = line_split[0].strip(':') return raw_content, sender_id except Exception as e: self.logger.error(f'_extract_content_and_sender got except: {e}') finally: return raw_content, None # 是否是群消息 def _is_group_message(self, message: dict) -> bool: from_user_name = message['from_user_name']['str'] return from_user_name.endswith('@chatroom') class WeChatPadEventConverter(abstract_platform_adapter.AbstractEventConverter): def __init__(self, config: dict, logger: logging.Logger): self.config = config self.logger = logger self.message_converter = WeChatPadMessageConverter(self.config, self.logger) # super().__init__( # config=config, # message_converter=message_converter, # logger = logger, # ) @staticmethod async def yiri2target(event: platform_events.MessageEvent) -> dict: pass async def target2yiri( self, event: dict, bot_account_id: str, ) -> platform_events.MessageEvent: # 排除公众号以及微信团队消息 if ( event['from_user_name']['str'].startswith('gh_') or event['from_user_name']['str'] == 'weixin' or event['from_user_name']['str'] == 'newsapp' or event['from_user_name']['str'] == self.config['wxid'] ): return None message_chain = await self.message_converter.target2yiri(copy.deepcopy(event), bot_account_id) if not message_chain: return None if '@chatroom' in event['from_user_name']['str']: # 找出开头的 wxid_ 字符串,以:结尾 sender_wxid = event['content']['str'].split(':')[0] return platform_events.GroupMessage( sender=platform_entities.GroupMember( id=sender_wxid, member_name=event['from_user_name']['str'], permission=platform_entities.Permission.Member, group=platform_entities.Group( id=event['from_user_name']['str'], name=event['from_user_name']['str'], permission=platform_entities.Permission.Member, ), special_title='', ), message_chain=message_chain, time=event['create_time'], source_platform_object=event, ) else: return platform_events.FriendMessage( sender=platform_entities.Friend( id=event['from_user_name']['str'], nickname=event['from_user_name']['str'], remark='', ), message_chain=message_chain, time=event['create_time'], source_platform_object=event, ) class WeChatPadAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): name: str = 'WeChatPad' # 定义适配器名称 bot: WeChatPadClient quart_app: quart.Quart bot_account_id: str config: dict logger: EventLogger message_converter: WeChatPadMessageConverter event_converter: WeChatPadEventConverter listeners: typing.Dict[ typing.Type[platform_events.Event], typing.Callable[[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None], ] = {} _MAX_CALLBACK_FUTURES = 100 def __init__(self, config: dict, logger: EventLogger): quart_app = quart.Quart(__name__) message_converter = WeChatPadMessageConverter(config, logger) event_converter = WeChatPadEventConverter(config, logger) bot = WeChatPadClient(config['wechatpad_url'], config['token']) super().__init__( config=config, logger=logger, quart_app=quart_app, message_converter=message_converter, event_converter=event_converter, listeners={}, bot_account_id='', name='WeChatPad', bot=bot, ) self._event_loop: asyncio.AbstractEventLoop | None = None self._ws_app: websocket.WebSocketApp | None = None self._ws_thread: threading.Thread | None = None self._stop_event = threading.Event() self._callback_futures: set = set() self._callback_futures_lock = threading.Lock() async def ws_message(self, data): """处理接收到的消息""" try: event = await self.event_converter.target2yiri(data.copy(), self.bot_account_id) except Exception: await self.logger.error(f'Error in wechatpad callback: {traceback.format_exc()}') if event is not None and event.__class__ in self.listeners: await self.listeners[event.__class__](event, self) return 'ok' async def _handle_message(self, message: platform_message.MessageChain, target_id: str): """统一消息处理核心逻辑""" content_list = await self.message_converter.yiri2target(message) # print(content_list) at_targets = [item['target'] for item in content_list if item['type'] == 'at'] # print(at_targets) # 处理@逻辑 at_targets = at_targets or [] member_info = [] if at_targets: member_result = await asyncio.to_thread(self.bot.get_chatroom_member_detail, target_id) member_info = member_result['Data']['member_data']['chatroom_member_list'] # 处理消息组件 for msg in content_list: # 文本消息处理@ if msg['type'] == 'text' and at_targets: if 'all' in at_targets: msg['content'] = f'@所有人 {msg["content"]}' else: at_nick_name_list = [] for member in member_info: if member['user_name'] in at_targets: at_nick_name_list.append(f'@{member["nick_name"]}') msg['content'] = f'{" ".join(at_nick_name_list)} {msg["content"]}' # 统一消息派发 handler_map = { 'text': lambda msg: self.bot.send_text_message( to_wxid=target_id, message=msg['content'], ats=['notify@all'] if 'all' in at_targets else at_targets ), 'image': lambda msg: self.bot.send_image_message( to_wxid=target_id, img_url=msg['image'], ats=['notify@all'] if 'all' in at_targets else at_targets ), 'WeChatEmoji': lambda msg: self.bot.send_emoji_message( to_wxid=target_id, emoji_md5=msg['emoji_md5'], emoji_size=msg['emoji_size'] ), 'voice': lambda msg: self.bot.send_voice_message( to_wxid=target_id, voice_data=msg['data'], voice_duration=msg['duration'], voice_forma=msg['forma'], ), 'WeChatAppMsg': lambda msg: self.bot.send_app_message( to_wxid=target_id, app_message=msg['app_msg'], type=0, ), 'at': lambda msg: None, } if handler := handler_map.get(msg['type']): await asyncio.to_thread(handler, msg) else: await self.logger.warning(f'未处理的消息类型: {msg["type"]}') continue def _schedule_ws_message(self, data: dict) -> None: loop = self._event_loop if loop is None or loop.is_closed() or self._stop_event.is_set(): return with self._callback_futures_lock: if len(self._callback_futures) >= self._MAX_CALLBACK_FUTURES: return future = asyncio.run_coroutine_threadsafe(self.ws_message(data), loop) self._callback_futures.add(future) def done(completed) -> None: with self._callback_futures_lock: self._callback_futures.discard(completed) if completed.cancelled(): return try: completed.result() except asyncio.CancelledError: pass except Exception: logging.getLogger(__name__).exception('WeChatPad callback failed') future.add_done_callback(done) async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain): """主动发送消息""" return await self._handle_message(message, target_id) async def reply_message( self, message_source: platform_events.MessageEvent, message: platform_message.MessageChain, quote_origin: bool = False, ): """回复消息""" if message_source.source_platform_object: target_id = message_source.source_platform_object['from_user_name']['str'] return await self._handle_message(message, target_id) async def is_muted(self, group_id: int) -> bool: pass def register_listener( self, event_type: typing.Type[platform_events.Event], callback: typing.Callable[ [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None ], ): self.listeners[event_type] = callback def unregister_listener( self, event_type: typing.Type[platform_events.Event], callback: typing.Callable[ [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None ], ): pass async def run_async(self): self._event_loop = asyncio.get_running_loop() self._stop_event.clear() if not self.config['admin_key'] and not self.config['token']: raise RuntimeError('无wechatpad管理密匙,请填入配置文件后重启') else: if self.config['token']: self.bot = WeChatPadClient(self.config['wechatpad_url'], self.config['token']) data = await asyncio.to_thread(self.bot.get_login_status) if data['Code'] == 300 and data['Text'] == '你已退出微信': response = await asyncio.to_thread( requests.post, f'{self.config["wechatpad_url"]}/admin/GenAuthKey1?key={self.config["admin_key"]}', json={'Count': 1, 'Days': 365}, timeout=10, ) if response.status_code != 200: body = await httpclient.response_text(response) raise Exception(f'获取token失败: {body}') response_data = await httpclient.parse_json_response(response) self.config['token'] = response_data['Data'][0] elif not self.config['token']: response = await asyncio.to_thread( requests.post, f'{self.config["wechatpad_url"]}/admin/GenAuthKey1?key={self.config["admin_key"]}', json={'Count': 1, 'Days': 365}, timeout=10, ) if response.status_code != 200: body = await httpclient.response_text(response) raise Exception(f'获取token失败: {body}') response_data = await httpclient.parse_json_response(response) self.config['token'] = response_data['Data'][0] self.bot = WeChatPadClient(self.config['wechatpad_url'], self.config['token'], logger=self.logger) profile = await asyncio.to_thread(self.bot.get_profile) self.bot_account_id = profile['Data']['userInfo']['nickName']['str'] self.config['wxid'] = profile['Data']['userInfo']['userName']['str'] def connect_websocket_sync() -> None: uri = f'{self.config["wechatpad_ws"]}/GetSyncMsg?key={self.config["token"]}' def on_message(ws, message): try: if len(message) > _MAX_GATEWAY_MESSAGE_CHARS: logging.getLogger(__name__).warning('WeChatPad WebSocket message exceeds the size limit') return data = json.loads(message) self._schedule_ws_message(data) except json.JSONDecodeError: logging.getLogger(__name__).warning('WeChatPad received a non-JSON message') def on_error(ws, error): logging.getLogger(__name__).warning('WeChatPad WebSocket error: %s', str(error)[:200]) def on_close(ws, close_status_code, close_msg): logging.getLogger(__name__).info('WeChatPad WebSocket closed') def on_open(ws): logging.getLogger(__name__).info('WeChatPad WebSocket connected') while not self._stop_event.is_set(): ws = websocket.WebSocketApp( uri, on_message=on_message, on_error=on_error, on_close=on_close, on_open=on_open, ) self._ws_app = ws ws.run_forever(ping_interval=60, ping_timeout=20) self._ws_app = None if not self._stop_event.wait(5): logging.getLogger(__name__).info('Reconnecting WeChatPad WebSocket') self._ws_thread = threading.Thread( target=connect_websocket_sync, name='WebSocketClientThread', daemon=True, ) self._ws_thread.start() await self.logger.info('WebSocket client thread started') while not self._stop_event.is_set() and self._ws_thread.is_alive(): await asyncio.sleep(1) if not self._stop_event.is_set(): raise RuntimeError('WeChatPad WebSocket client thread exited unexpectedly') async def kill(self) -> bool: self._stop_event.set() ws = self._ws_app if ws is not None: await bounded_executor.run_blocking_cleanup(ws.close) with self._callback_futures_lock: futures = list(self._callback_futures) for future in futures: future.cancel() if futures: await asyncio.gather( *(asyncio.wrap_future(future) for future in futures), return_exceptions=True, ) thread = self._ws_thread if thread is not None and thread.is_alive(): await bounded_executor.run_blocking_cleanup(thread.join, 5) self._ws_thread = None self._ws_app = None self._event_loop = None return True