diff --git a/libs/wecom_ai_bot_api/WXBizMsgCrypt3.py b/libs/wecom_ai_bot_api/WXBizMsgCrypt3.py new file mode 100644 index 00000000..96e9367f --- /dev/null +++ b/libs/wecom_ai_bot_api/WXBizMsgCrypt3.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python +# -*- encoding:utf-8 -*- + +"""对企业微信发送给企业后台的消息加解密示例代码. +@copyright: Copyright (c) 1998-2014 Tencent Inc. + +""" + +# ------------------------------------------------------------------------ +import logging +import base64 +import random +import hashlib +import time +import struct +from Crypto.Cipher import AES +import xml.etree.cElementTree as ET +import socket +from libs.wecom_ai_bot_api import ierror + + +""" +Crypto.Cipher包已不再维护,开发者可以通过以下命令下载安装最新版的加解密工具包 + pip install pycryptodome +""" + + +class FormatException(Exception): + pass + + +def throw_exception(message, exception_class=FormatException): + """my define raise exception function""" + raise exception_class(message) + + +class SHA1: + """计算企业微信的消息签名接口""" + + def getSHA1(self, token, timestamp, nonce, encrypt): + """用SHA1算法生成安全签名 + @param token: 票据 + @param timestamp: 时间戳 + @param encrypt: 密文 + @param nonce: 随机字符串 + @return: 安全签名 + """ + try: + sortlist = [token, timestamp, nonce, encrypt] + sortlist.sort() + sha = hashlib.sha1() + sha.update(''.join(sortlist).encode()) + return ierror.WXBizMsgCrypt_OK, sha.hexdigest() + except Exception as e: + logger = logging.getLogger() + logger.error(e) + return ierror.WXBizMsgCrypt_ComputeSignature_Error, None + + +class XMLParse: + """提供提取消息格式中的密文及生成回复消息格式的接口""" + + # xml消息模板 + AES_TEXT_RESPONSE_TEMPLATE = """ + + +%(timestamp)s + +""" + + def extract(self, xmltext): + """提取出xml数据包中的加密消息 + @param xmltext: 待提取的xml字符串 + @return: 提取出的加密消息字符串 + """ + try: + xml_tree = ET.fromstring(xmltext) + encrypt = xml_tree.find('Encrypt') + return ierror.WXBizMsgCrypt_OK, encrypt.text + except Exception as e: + logger = logging.getLogger() + logger.error(e) + return ierror.WXBizMsgCrypt_ParseXml_Error, None + + def generate(self, encrypt, signature, timestamp, nonce): + """生成xml消息 + @param encrypt: 加密后的消息密文 + @param signature: 安全签名 + @param timestamp: 时间戳 + @param nonce: 随机字符串 + @return: 生成的xml字符串 + """ + resp_dict = { + 'msg_encrypt': encrypt, + 'msg_signaturet': signature, + 'timestamp': timestamp, + 'nonce': nonce, + } + resp_xml = self.AES_TEXT_RESPONSE_TEMPLATE % resp_dict + return resp_xml + + +class PKCS7Encoder: + """提供基于PKCS7算法的加解密接口""" + + block_size = 32 + + def encode(self, text): + """对需要加密的明文进行填充补位 + @param text: 需要进行填充补位操作的明文 + @return: 补齐明文字符串 + """ + text_length = len(text) + # 计算需要填充的位数 + amount_to_pad = self.block_size - (text_length % self.block_size) + if amount_to_pad == 0: + amount_to_pad = self.block_size + # 获得补位所用的字符 + pad = chr(amount_to_pad) + return text + (pad * amount_to_pad).encode() + + def decode(self, decrypted): + """删除解密后明文的补位字符 + @param decrypted: 解密后的明文 + @return: 删除补位字符后的明文 + """ + pad = ord(decrypted[-1]) + if pad < 1 or pad > 32: + pad = 0 + return decrypted[:-pad] + + +class Prpcrypt(object): + """提供接收和推送给企业微信消息的加解密接口""" + + def __init__(self, key): + # self.key = base64.b64decode(key+"=") + self.key = key + # 设置加解密模式为AES的CBC模式 + self.mode = AES.MODE_CBC + + def encrypt(self, text, receiveid): + """对明文进行加密 + @param text: 需要加密的明文 + @return: 加密得到的字符串 + """ + # 16位随机字符串添加到明文开头 + text = text.encode() + text = self.get_random_str() + struct.pack('I', socket.htonl(len(text))) + text + receiveid.encode() + + # 使用自定义的填充方式对明文进行补位填充 + pkcs7 = PKCS7Encoder() + text = pkcs7.encode(text) + # 加密 + cryptor = AES.new(self.key, self.mode, self.key[:16]) + try: + ciphertext = cryptor.encrypt(text) + # 使用BASE64对加密后的字符串进行编码 + return ierror.WXBizMsgCrypt_OK, base64.b64encode(ciphertext) + except Exception as e: + logger = logging.getLogger() + logger.error(e) + return ierror.WXBizMsgCrypt_EncryptAES_Error, None + + def decrypt(self, text, receiveid): + """对解密后的明文进行补位删除 + @param text: 密文 + @return: 删除填充补位后的明文 + """ + try: + cryptor = AES.new(self.key, self.mode, self.key[:16]) + # 使用BASE64对密文进行解码,然后AES-CBC解密 + plain_text = cryptor.decrypt(base64.b64decode(text)) + except Exception as e: + logger = logging.getLogger() + logger.error(e) + return ierror.WXBizMsgCrypt_DecryptAES_Error, None + try: + pad = plain_text[-1] + # 去掉补位字符串 + # pkcs7 = PKCS7Encoder() + # plain_text = pkcs7.encode(plain_text) + # 去除16位随机字符串 + content = plain_text[16:-pad] + xml_len = socket.ntohl(struct.unpack('I', content[:4])[0]) + xml_content = content[4 : xml_len + 4] + from_receiveid = content[xml_len + 4 :] + except Exception as e: + logger = logging.getLogger() + logger.error(e) + return ierror.WXBizMsgCrypt_IllegalBuffer, None + + if from_receiveid.decode('utf8') != receiveid: + return ierror.WXBizMsgCrypt_ValidateCorpid_Error, None + return 0, xml_content + + def get_random_str(self): + """随机生成16位字符串 + @return: 16位字符串 + """ + return str(random.randint(1000000000000000, 9999999999999999)).encode() + + +class WXBizMsgCrypt(object): + # 构造函数 + def __init__(self, sToken, sEncodingAESKey, sReceiveId): + try: + self.key = base64.b64decode(sEncodingAESKey + '=') + assert len(self.key) == 32 + except Exception: + throw_exception('[error]: EncodingAESKey unvalid !', FormatException) + # return ierror.WXBizMsgCrypt_IllegalAesKey,None + self.m_sToken = sToken + self.m_sReceiveId = sReceiveId + + # 验证URL + # @param sMsgSignature: 签名串,对应URL参数的msg_signature + # @param sTimeStamp: 时间戳,对应URL参数的timestamp + # @param sNonce: 随机串,对应URL参数的nonce + # @param sEchoStr: 随机串,对应URL参数的echostr + # @param sReplyEchoStr: 解密之后的echostr,当return返回0时有效 + # @return:成功0,失败返回对应的错误码 + + def VerifyURL(self, sMsgSignature, sTimeStamp, sNonce, sEchoStr): + sha1 = SHA1() + ret, signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, sEchoStr) + if ret != 0: + return ret, None + if not signature == sMsgSignature: + return ierror.WXBizMsgCrypt_ValidateSignature_Error, None + pc = Prpcrypt(self.key) + ret, sReplyEchoStr = pc.decrypt(sEchoStr, self.m_sReceiveId) + return ret, sReplyEchoStr + + def EncryptMsg(self, sReplyMsg, sNonce, timestamp=None): + # 将企业回复用户的消息加密打包 + # @param sReplyMsg: 企业号待回复用户的消息,xml格式的字符串 + # @param sTimeStamp: 时间戳,可以自己生成,也可以用URL参数的timestamp,如为None则自动用当前时间 + # @param sNonce: 随机串,可以自己生成,也可以用URL参数的nonce + # sEncryptMsg: 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串, + # return:成功0,sEncryptMsg,失败返回对应的错误码None + pc = Prpcrypt(self.key) + ret, encrypt = pc.encrypt(sReplyMsg, self.m_sReceiveId) + encrypt = encrypt.decode('utf8') + if ret != 0: + return ret, None + if timestamp is None: + timestamp = str(int(time.time())) + # 生成安全签名 + sha1 = SHA1() + ret, signature = sha1.getSHA1(self.m_sToken, timestamp, sNonce, encrypt) + if ret != 0: + return ret, None + xmlParse = XMLParse() + return ret, xmlParse.generate(encrypt, signature, timestamp, sNonce) + + def DecryptMsg(self, sPostData, sMsgSignature, sTimeStamp, sNonce): + # 检验消息的真实性,并且获取解密后的明文 + # @param sMsgSignature: 签名串,对应URL参数的msg_signature + # @param sTimeStamp: 时间戳,对应URL参数的timestamp + # @param sNonce: 随机串,对应URL参数的nonce + # @param sPostData: 密文,对应POST请求的数据 + # xml_content: 解密后的原文,当return返回0时有效 + # @return: 成功0,失败返回对应的错误码 + # 验证安全签名 + xmlParse = XMLParse() + ret, encrypt = xmlParse.extract(sPostData) + if ret != 0: + return ret, None + sha1 = SHA1() + ret, signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, encrypt) + if ret != 0: + return ret, None + if not signature == sMsgSignature: + return ierror.WXBizMsgCrypt_ValidateSignature_Error, None + pc = Prpcrypt(self.key) + ret, xml_content = pc.decrypt(encrypt, self.m_sReceiveId) + return ret, xml_content diff --git a/libs/wecom_ai_bot_api/api.py b/libs/wecom_ai_bot_api/api.py new file mode 100644 index 00000000..e6abc4ac --- /dev/null +++ b/libs/wecom_ai_bot_api/api.py @@ -0,0 +1,287 @@ +import json +import time +import uuid +import xml.etree.ElementTree as ET +from urllib.parse import unquote +import hashlib +import traceback + +import httpx +from libs.wecom_ai_bot_api.WXBizMsgCrypt3 import WXBizMsgCrypt +from quart import Quart, request, Response, jsonify +from pkg.platform.types import message as platform_message +import asyncio +from libs.wecom_ai_bot_api import wecombotevent +from typing import Callable +import base64 +from Crypto.Cipher import AES + + +class WecomBotClient: + def __init__(self,Token:str,EnCodingAESKey:str,Corpid:str,logger:None): + self.Token=Token + self.EnCodingAESKey=EnCodingAESKey + self.Corpid=Corpid + self.ReceiveId = '' + self.app = Quart(__name__) + self.app.add_url_rule( + '/callback/command', + 'handle_callback', + self.handle_callback_request, + methods=['POST','GET'] + ) + self._message_handlers = { + 'example': [], + } + self.user_stream_map = {} + self.logger = logger + self.generated_content = {} + self.msg_id_map = {} + + async def sha1_signature(token: str, timestamp: str, nonce: str, encrypt: str) -> str: + raw = "".join(sorted([token, timestamp, nonce, encrypt])) + return hashlib.sha1(raw.encode("utf-8")).hexdigest() + + async def handle_callback_request(self): + try: + self.wxcpt=WXBizMsgCrypt(self.Token,self.EnCodingAESKey,'') + + if request.method == "GET": + + msg_signature = unquote(request.args.get("msg_signature", "")) + timestamp = unquote(request.args.get("timestamp", "")) + nonce = unquote(request.args.get("nonce", "")) + echostr = unquote(request.args.get("echostr", "")) + + if not all([msg_signature, timestamp, nonce, echostr]): + await self.logger.error("请求参数缺失") + return Response("缺少参数", status=400) + + ret, decrypted_str = self.wxcpt.VerifyURL(msg_signature, timestamp, nonce, echostr) + if ret != 0: + + await self.logger.error("验证URL失败") + return Response("验证失败", status=403) + + return Response(decrypted_str, mimetype="text/plain") + + elif request.method == "POST": + msg_signature = unquote(request.args.get("msg_signature", "")) + timestamp = unquote(request.args.get("timestamp", "")) + nonce = unquote(request.args.get("nonce", "")) + + try: + timeout = 3.2 + interval = 0.1 + start_time = time.monotonic() + encrypted_json = await request.get_json() + encrypted_msg = encrypted_json.get("encrypt", "") + if not encrypted_msg: + await self.logger.error("请求体中缺少 'encrypt' 字段") + + xml_post_data = f"" + ret, decrypted_xml = self.wxcpt.DecryptMsg(xml_post_data, msg_signature, timestamp, nonce) + if ret != 0: + await self.logger.error("解密失败") + + + msg_json = json.loads(decrypted_xml) + + from_user_id = msg_json.get("from", {}).get("userid") + chatid = msg_json.get("chatid", "") + + message_data = await self.get_message(msg_json) + + + + if message_data: + try: + event = wecombotevent.WecomBotEvent(message_data) + if event: + await self._handle_message(event) + except Exception as e: + await self.logger.error(traceback.format_exc()) + print(traceback.format_exc()) + + start_time = time.time() + try: + if msg_json.get('chattype','') == 'single': + if from_user_id in self.user_stream_map: + stream_id = self.user_stream_map[from_user_id] + else: + stream_id =str(uuid.uuid4()) + self.user_stream_map[from_user_id] = stream_id + + + else: + + if chatid in self.user_stream_map: + stream_id = self.user_stream_map[chatid] + else: + stream_id = str(uuid.uuid4()) + self.user_stream_map[chatid] = stream_id + except Exception as e: + await self.logger.error(traceback.format_exc()) + print(traceback.format_exc()) + while True: + content = self.generated_content.pop(msg_json['msgid'],None) + if content: + reply_plain = { + "msgtype": "stream", + "stream": { + "id": stream_id, + "finish": True, + "content": content + } + } + reply_plain_str = json.dumps(reply_plain, ensure_ascii=False) + + reply_timestamp = str(int(time.time())) + ret, encrypt_text = self.wxcpt.EncryptMsg(reply_plain_str, nonce, reply_timestamp) + if ret != 0: + await self.logger.error("加密失败") + + + root = ET.fromstring(encrypt_text) + encrypt = root.find("Encrypt").text + resp = { + "encrypt": encrypt, + } + return jsonify(resp), 200 + + if time.time() - start_time > timeout: + break + + await asyncio.sleep(interval) + + if self.msg_id_map.get(message_data['msgid'], 1) == 3: + print('请求失效:暂不支持智能机器人超过10秒的请求,如有需求,请联系 LangBot 团队。') + return '' + + except Exception as e: + await self.logger.error(traceback.format_exc()) + print(traceback.format_exc()) + + except Exception as e: + await self.logger.error(traceback.format_exc()) + print(traceback.format_exc()) + + + async def get_message(self,msg_json): + message_data = {} + + if msg_json.get('chattype','') == 'single': + message_data['type'] = 'single' + elif msg_json.get('chattype','') == 'group': + message_data['type'] = 'group' + + if msg_json.get('msgtype') == 'text': + message_data['content'] = msg_json.get('text',{}).get('content') + elif msg_json.get('msgtype') == 'image': + picurl = msg_json.get('image', {}).get('url','') + base64 = await self.download_url_to_base64(picurl,self.EnCodingAESKey) + message_data['picurl'] = base64 + elif msg_json.get('msgtype') == 'mixed': + items = msg_json.get('mixed', {}).get('msg_item', []) + texts = [] + picurl = None + for item in items: + if item.get('msgtype') == 'text': + texts.append(item.get('text', {}).get('content', '')) + elif item.get('msgtype') == 'image' and picurl is None: + picurl = item.get('image', {}).get('url') + + if texts: + message_data['content'] = "".join(texts) # 拼接所有 text + if picurl: + base64 = await self.download_url_to_base64(picurl,self.EnCodingAESKey) + message_data['picurl'] = base64 # 只保留第一个 image + + message_data['userid'] = msg_json.get('from', {}).get('userid', '') + message_data['msgid'] = msg_json.get('msgid', '') + + if msg_json.get('aibotid'): + message_data['aibotid'] = msg_json.get('aibotid', '') + + return message_data + + async def _handle_message(self, event: wecombotevent.WecomBotEvent): + """ + 处理消息事件。 + """ + try: + message_id = event.message_id + if message_id in self.msg_id_map.keys(): + self.msg_id_map[message_id] += 1 + return + self.msg_id_map[message_id] = 1 + msg_type = event.type + if msg_type in self._message_handlers: + for handler in self._message_handlers[msg_type]: + await handler(event) + except Exception: + print(traceback.format_exc()) + + async def set_message(self, msg_id: str, content: str): + self.generated_content[msg_id] = content + + def on_message(self, msg_type: str): + def decorator(func: Callable[[wecombotevent.WecomBotEvent], None]): + if msg_type not in self._message_handlers: + self._message_handlers[msg_type] = [] + self._message_handlers[msg_type].append(func) + return func + + return decorator + + + async def download_url_to_base64(self, download_url, encoding_aes_key): + async with httpx.AsyncClient() as client: + response = await client.get(download_url) + if response.status_code != 200: + await self.logger.error(f'failed to get file: {response.text}') + return None + + encrypted_bytes = response.content + + + aes_key = base64.b64decode(encoding_aes_key + "=") # base64 补齐 + iv = aes_key[:16] + + + cipher = AES.new(aes_key, AES.MODE_CBC, iv) + decrypted = cipher.decrypt(encrypted_bytes) + + + pad_len = decrypted[-1] + decrypted = decrypted[:-pad_len] + + + if decrypted.startswith(b"\xff\xd8"): # JPEG + mime_type = "image/jpeg" + elif decrypted.startswith(b"\x89PNG"): # PNG + mime_type = "image/png" + elif decrypted.startswith((b"GIF87a", b"GIF89a")): # GIF + mime_type = "image/gif" + elif decrypted.startswith(b"BM"): # BMP + mime_type = "image/bmp" + elif decrypted.startswith(b"II*\x00") or decrypted.startswith(b"MM\x00*"): # TIFF + mime_type = "image/tiff" + else: + mime_type = "application/octet-stream" + + # 转 base64 + base64_str = base64.b64encode(decrypted).decode("utf-8") + return f"data:{mime_type};base64,{base64_str}" + + + async def run_task(self, host: str, port: int, *args, **kwargs): + """ + 启动 Quart 应用。 + """ + await self.app.run_task(host=host, port=port, *args, **kwargs) + + + + + diff --git a/libs/wecom_ai_bot_api/ierror.py b/libs/wecom_ai_bot_api/ierror.py new file mode 100644 index 00000000..6c7ca122 --- /dev/null +++ b/libs/wecom_ai_bot_api/ierror.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +######################################################################### +# Author: jonyqin +# Created Time: Thu 11 Sep 2014 01:53:58 PM CST +# File Name: ierror.py +# Description:定义错误码含义 +######################################################################### +WXBizMsgCrypt_OK = 0 +WXBizMsgCrypt_ValidateSignature_Error = -40001 +WXBizMsgCrypt_ParseXml_Error = -40002 +WXBizMsgCrypt_ComputeSignature_Error = -40003 +WXBizMsgCrypt_IllegalAesKey = -40004 +WXBizMsgCrypt_ValidateCorpid_Error = -40005 +WXBizMsgCrypt_EncryptAES_Error = -40006 +WXBizMsgCrypt_DecryptAES_Error = -40007 +WXBizMsgCrypt_IllegalBuffer = -40008 +WXBizMsgCrypt_EncodeBase64_Error = -40009 +WXBizMsgCrypt_DecodeBase64_Error = -40010 +WXBizMsgCrypt_GenReturnXml_Error = -40011 diff --git a/libs/wecom_ai_bot_api/wecombotevent.py b/libs/wecom_ai_bot_api/wecombotevent.py new file mode 100644 index 00000000..f2edeac7 --- /dev/null +++ b/libs/wecom_ai_bot_api/wecombotevent.py @@ -0,0 +1,60 @@ +from typing import Dict, Any, Optional + + +class WecomBotEvent(dict): + @staticmethod + def from_payload(payload: Dict[str, Any]) -> Optional['WecomBotEvent']: + try: + event = WecomBotEvent(payload) + return event + except KeyError: + return None + + @property + def type(self) -> str: + """ + 事件类型 + """ + return self.get('type', '') + + @property + def userid(self) -> str: + """ + 用户id + """ + return self.get('from', {}).get('userid', '') + + @property + def content(self) -> str: + """ + 内容 + """ + return self.get('content', '') + + @property + def picurl(self) -> str: + """ + 图片url + """ + return self.get('picurl', '') + + @property + def chatid(self) -> str: + """ + 群组id + """ + return self.get('chatid', {}) + + @property + def message_id(self) -> str: + """ + 消息id + """ + return self.get('msgid', '') + + @property + def ai_bot_id(self) -> str: + """ + AI Bot ID + """ + return self.get('aibotid', '') diff --git a/pkg/platform/sources/wecombot.png b/pkg/platform/sources/wecombot.png new file mode 100644 index 00000000..0734efaf Binary files /dev/null and b/pkg/platform/sources/wecombot.png differ diff --git a/pkg/platform/sources/wecombot.py b/pkg/platform/sources/wecombot.py new file mode 100644 index 00000000..b2380120 --- /dev/null +++ b/pkg/platform/sources/wecombot.py @@ -0,0 +1,158 @@ +from __future__ import annotations +import typing +import asyncio +import traceback + +import datetime +from pkg.platform.adapter import MessagePlatformAdapter +from pkg.platform.types import events as platform_events, message as platform_message +from libs.wecom_ai_bot_api.wecombotevent import WecomBotEvent +from libs.wecom_ai_bot_api.api import WecomBotClient +from .. import adapter +from ...core import app +from ..types import entities as platform_entities +from ...command.errors import ParamNotEnoughError +from ..logger import EventLogger + +class WecomBotMessageConverter(adapter.MessageConverter): + @staticmethod + async def yiri2target(message_chain: platform_message.MessageChain): + content = '' + for msg in message_chain: + if type(msg) is platform_message.Plain: + content += msg.text + return content + + @staticmethod + async def target2yiri(event: WecomBotEvent): + yiri_msg_list = [] + if event.type == 'group': + yiri_msg_list.append(platform_message.At(target=event.ai_bot_id)) + yiri_msg_list.append(platform_message.Source(id=event.message_id, time=datetime.datetime.now())) + yiri_msg_list.append(platform_message.Plain(text=event.content)) + if event.picurl != '': + yiri_msg_list.append(platform_message.Image(base64=event.picurl)) + chain = platform_message.MessageChain(yiri_msg_list) + + return chain + +class WecomBotEventConverter(adapter.EventConverter): + + @staticmethod + async def yiri2target(event:platform_events.MessageEvent): + return event.source_platform_object + + @staticmethod + async def target2yiri(event:WecomBotEvent): + message_chain = await WecomBotMessageConverter.target2yiri(event) + if event.type == 'single': + return platform_events.FriendMessage( + sender=platform_entities.Friend( + id=event.userid, + nickname='', + remark='', + ), + message_chain=message_chain, + time=datetime.datetime.now().timestamp(), + source_platform_object=event, + ) + elif event.type == 'group': + try: + sender = platform_entities.GroupMember( + id=event.userid, + permission='MEMBER', + member_name=event.userid, + group=platform_entities.Group( + id=str(event.chatid), + name='', + permission=platform_entities.Permission.Member, + ), + special_title='', + join_timestamp=0, + last_speak_timestamp=0, + mute_time_remaining=0, + ) + time = datetime.datetime.now().timestamp() + return platform_events.GroupMessage( + sender=sender, + message_chain=message_chain, + time=time, + source_platform_object=event, + ) + except Exception: + print(traceback.format_exc()) + +class WecomBotAdapter(adapter.MessagePlatformAdapter): + bot : WecomBotClient + app: app.Application + message_converter = WecomBotMessageConverter() + event_converter = WecomBotEventConverter() + config:dict + bot_account_id:str + + def __init__(self, config:dict, ap:app.Application, logger:EventLogger): + self.config = config + self.app = ap + self.logger = logger + required_keys = ['Token', 'EncodingAESKey', 'Corpid'] + missing_keys = [key for key in required_keys if key not in config] + if missing_keys: + raise ParamNotEnoughError('缺少相关配置项,请查看文档或联系管理员') + self.bot_account_id = self.config['BotId'] + self.bot = WecomBotClient( + Token=self.config['Token'], + EnCodingAESKey=self.config['EncodingAESKey'], + Corpid=self.config['Corpid'], + logger=self.logger + ) + + async def reply_message(self, message_source:platform_events.MessageEvent, message:platform_message.MessageChain,quote_origin: bool = False): + + content = await self.message_converter.yiri2target(message) + await self.bot.set_message(message_source.source_platform_object.message_id, content) + + async def send_message(self, target_type, target_id, message): + pass + + def register_listener( + self, + event_type: typing.Type[platform_events.Event], + callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None], + ): + async def on_message(event: WecomBotEvent): + try: + return await callback(await self.event_converter.target2yiri(event), self) + except Exception: + await self.logger.error(f'Error in wecombot callback: {traceback.format_exc()}') + print(traceback.format_exc()) + try: + if event_type == platform_events.FriendMessage: + self.bot.on_message('single')(on_message) + elif event_type == platform_events.GroupMessage: + self.bot.on_message('group')(on_message) + except Exception: + print(traceback.format_exc()) + + + async def run_async(self): + async def shutdown_trigger_placeholder(): + while True: + await asyncio.sleep(1) + + await self.bot.run_task( + host=self.config['host'], + port=self.config['port'], + shutdown_trigger=shutdown_trigger_placeholder, + ) + + async def kill(self) -> bool: + return False + + async def unregister_listener( + self, + event_type: type, + callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None], + ): + return super().unregister_listener(event_type, callback) + + diff --git a/pkg/platform/sources/wecombot.yaml b/pkg/platform/sources/wecombot.yaml new file mode 100644 index 00000000..766bd904 --- /dev/null +++ b/pkg/platform/sources/wecombot.yaml @@ -0,0 +1,62 @@ +apiVersion: v1 +kind: MessagePlatformAdapter +metadata: + name: wecombot + label: + en_US: WeComBot + zh_Hans: 企业微信智能机器人 + description: + en_US: WeComBot Adapter + zh_Hans: 企业微信智能机器人适配器,请查看文档了解使用方式 + icon: wecombot.png +spec: + config: + - name: host + label: + en_US: Host + zh_Hans: 监听主机 + description: + en_US: Webhook host, unless you know what you're doing, please write 0.0.0.0 + zh_Hans: Webhook 监听主机,除非你知道自己在做什么,否则请写 0.0.0.0 + type: string + required: true + default: "0.0.0.0" + - name: port + label: + en_US: Port + zh_Hans: 监听端口 + type: integer + required: true + default: 2291 + - name: Corpid + label: + en_US: Corpid + zh_Hans: 企业ID + type: string + required: true + default: "" + - name: Token + label: + en_US: Token + zh_Hans: 令牌 + type: string + required: true + default: "" + - name: EncodingAESKey + label: + en_US: EncodingAESKey + zh_Hans: 消息加解密密钥 + type: string + required: true + default: "" + - name: BotId + label: + en_US: BotId + zh_Hans: 机器人ID + type: string + required: true + default: "" +execution: + python: + path: ./wecombot.py + attr: WecomBotAdapter \ No newline at end of file