From 4cbf46fd4d0b5f175e15a55f71398bf629172bfd Mon Sep 17 00:00:00 2001 From: lanvent Date: Thu, 20 Apr 2023 01:03:04 +0800 Subject: [PATCH 1/8] feat: add support for wechatcom channel --- app.py | 9 ++- channel/channel_factory.py | 4 + channel/wechat/wechat_channel.py | 4 +- channel/wechat/wechat_message.py | 2 +- channel/wechatcom/wechatcom_channel.py | 105 +++++++++++++++++++++++++ channel/wechatcom/wechatcom_message.py | 54 +++++++++++++ channel/wechatmp/wechatmp_channel.py | 6 +- config.py | 7 ++ requirements-optional.txt | 3 +- 9 files changed, 187 insertions(+), 7 deletions(-) create mode 100644 channel/wechatcom/wechatcom_channel.py create mode 100644 channel/wechatcom/wechatcom_message.py diff --git a/app.py b/app.py index 637b6e4..1a1bb5e 100644 --- a/app.py +++ b/app.py @@ -43,7 +43,14 @@ def run(): # os.environ['WECHATY_PUPPET_SERVICE_ENDPOINT'] = '127.0.0.1:9001' channel = channel_factory.create_channel(channel_name) - if channel_name in ["wx", "wxy", "terminal", "wechatmp", "wechatmp_service"]: + if channel_name in [ + "wx", + "wxy", + "terminal", + "wechatmp", + "wechatmp_service", + "wechatcom", + ]: PluginManager().load_plugins() # startup channel diff --git a/channel/channel_factory.py b/channel/channel_factory.py index ebd9732..fed3234 100644 --- a/channel/channel_factory.py +++ b/channel/channel_factory.py @@ -29,4 +29,8 @@ def create_channel(channel_type): from channel.wechatmp.wechatmp_channel import WechatMPChannel return WechatMPChannel(passive_reply=False) + elif channel_type == "wechatcom": + from channel.wechatcom.wechatcom_channel import WechatComChannel + + return WechatComChannel() raise RuntimeError diff --git a/channel/wechat/wechat_channel.py b/channel/wechat/wechat_channel.py index cf200b1..1a1e5ea 100644 --- a/channel/wechat/wechat_channel.py +++ b/channel/wechat/wechat_channel.py @@ -29,7 +29,7 @@ from plugins import * @itchat.msg_register([TEXT, VOICE, PICTURE, NOTE]) def handler_single_msg(msg): try: - cmsg = WeChatMessage(msg, False) + cmsg = WechatMessage(msg, False) except NotImplementedError as e: logger.debug("[WX]single message {} skipped: {}".format(msg["MsgId"], e)) return None @@ -40,7 +40,7 @@ def handler_single_msg(msg): @itchat.msg_register([TEXT, VOICE, PICTURE, NOTE], isGroupChat=True) def handler_group_msg(msg): try: - cmsg = WeChatMessage(msg, True) + cmsg = WechatMessage(msg, True) except NotImplementedError as e: logger.debug("[WX]group message {} skipped: {}".format(msg["MsgId"], e)) return None diff --git a/channel/wechat/wechat_message.py b/channel/wechat/wechat_message.py index 1888425..526b24f 100644 --- a/channel/wechat/wechat_message.py +++ b/channel/wechat/wechat_message.py @@ -8,7 +8,7 @@ from lib import itchat from lib.itchat.content import * -class WeChatMessage(ChatMessage): +class WechatMessage(ChatMessage): def __init__(self, itchat_msg, is_group=False): super().__init__(itchat_msg) self.msg_id = itchat_msg["MsgId"] diff --git a/channel/wechatcom/wechatcom_channel.py b/channel/wechatcom/wechatcom_channel.py new file mode 100644 index 0000000..1fbeccd --- /dev/null +++ b/channel/wechatcom/wechatcom_channel.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python +# -*- coding=utf-8 -*- +import web +from wechatpy.enterprise import WeChatClient, create_reply, parse_message +from wechatpy.enterprise.crypto import WeChatCrypto +from wechatpy.enterprise.exceptions import InvalidCorpIdException +from wechatpy.exceptions import InvalidSignatureException + +from bridge.context import Context +from bridge.reply import Reply, ReplyType +from channel.chat_channel import ChatChannel +from channel.wechatcom.wechatcom_message import WechatComMessage +from common.log import logger +from common.singleton import singleton +from config import conf + + +@singleton +class WechatComChannel(ChatChannel): + NOT_SUPPORT_REPLYTYPE = [ReplyType.IMAGE, ReplyType.VOICE] + + def __init__(self): + super().__init__() + self.corp_id = conf().get("wechatcom_corp_id") + self.secret = conf().get("wechatcom_secret") + self.agent_id = conf().get("wechatcom_agent_id") + self.token = conf().get("wechatcom_token") + self.aes_key = conf().get("wechatcom_aes_key") + print(self.corp_id, self.secret, self.agent_id, self.token, self.aes_key) + logger.info( + "[wechatcom] init: corp_id: {}, secret: {}, agent_id: {}, token: {}, aes_key: {}".format( + self.corp_id, self.secret, self.agent_id, self.token, self.aes_key + ) + ) + self.crypto = WeChatCrypto(self.token, self.aes_key, self.corp_id) + self.client = WeChatClient(self.corp_id, self.secret) # todo: 这里可能有线程安全问题 + + def startup(self): + # start message listener + urls = ("/wxcom", "channel.wechatcom.wechatcom_channel.Query") + app = web.application(urls, globals(), autoreload=False) + port = conf().get("wechatcom_port", 8080) + web.httpserver.runsimple(app.wsgifunc(), ("0.0.0.0", port)) + + def send(self, reply: Reply, context: Context): + print("send reply: ", reply.content, context["receiver"]) + receiver = context["receiver"] + reply_text = reply.content + self.client.message.send_text(self.agent_id, receiver, reply_text) + logger.info("[send] Do send to {}: {}".format(receiver, reply_text)) + + +class Query: + def GET(self): + channel = WechatComChannel() + params = web.input() + signature = params.msg_signature + timestamp = params.timestamp + nonce = params.nonce + echostr = params.echostr + print(params) + try: + echostr = channel.crypto.check_signature( + signature, timestamp, nonce, echostr + ) + except InvalidSignatureException: + raise web.Forbidden() + return echostr + + def POST(self): + channel = WechatComChannel() + params = web.input() + signature = params.msg_signature + timestamp = params.timestamp + nonce = params.nonce + try: + message = channel.crypto.decrypt_message( + web.data(), signature, timestamp, nonce + ) + except (InvalidSignatureException, InvalidCorpIdException): + raise web.Forbidden() + print(message) + msg = parse_message(message) + + print(msg) + if msg.type == "event": + if msg.event == "subscribe": + reply = create_reply("感谢关注", msg).render() + res = channel.crypto.encrypt_message(reply, nonce, timestamp) + return res + else: + try: + wechatcom_msg = WechatComMessage(msg, client=channel.client) + except NotImplementedError as e: + logger.debug("[wechatcom] " + str(e)) + return "success" + context = channel._compose_context( + wechatcom_msg.ctype, + wechatcom_msg.content, + isgroup=False, + msg=wechatcom_msg, + ) + if context: + channel.produce(context) + return "success" diff --git a/channel/wechatcom/wechatcom_message.py b/channel/wechatcom/wechatcom_message.py new file mode 100644 index 0000000..42de66e --- /dev/null +++ b/channel/wechatcom/wechatcom_message.py @@ -0,0 +1,54 @@ +import re + +import requests +from wechatpy.enterprise import WeChatClient + +from bridge.context import ContextType +from channel.chat_message import ChatMessage +from common.log import logger +from common.tmp_dir import TmpDir +from lib import itchat +from lib.itchat.content import * + + +class WechatComMessage(ChatMessage): + def __init__(self, msg, client: WeChatClient, is_group=False): + super().__init__(msg) + self.msg_id = msg.id + self.create_time = msg.time + self.is_group = is_group + + if msg.type == "text": + self.ctype = ContextType.TEXT + self.content = msg.content + elif msg.type == "voice": + self.ctype = ContextType.VOICE + self.content = ( + TmpDir().path() + msg.media_id + "." + msg.format + ) # content直接存临时目录路径 + + def download_voice(): + # 如果响应状态码是200,则将响应内容写入本地文件 + response = client.media.download(msg.media_id) + if response.status_code == 200: + with open(self.content, "wb") as f: + f.write(response.content) + else: + logger.info( + f"[wechatcom] Failed to download voice file, {response.content}" + ) + + self._prepare_fn = download_voice + elif msg.type == "image": + self.ctype = ContextType.IMAGE + self.content = msg.image # content直接存临时目录路径 + print(self.content) + # self._prepare_fn = lambda: itchat_msg.download(self.content) # TODO: download image + else: + raise NotImplementedError( + "Unsupported message type: Type:{} ".format(msg.type) + ) + + self.from_user_id = msg.source + self.to_user_id = msg.target + self.other_user_id = msg.source diff --git a/channel/wechatmp/wechatmp_channel.py b/channel/wechatmp/wechatmp_channel.py index ac3c3ac..3152124 100644 --- a/channel/wechatmp/wechatmp_channel.py +++ b/channel/wechatmp/wechatmp_channel.py @@ -98,7 +98,9 @@ class WechatMPChannel(ChatChannel): if self.passive_reply: receiver = context["receiver"] self.cache_dict[receiver] = reply.content - logger.info("[send] reply to {} saved to cache: {}".format(receiver, reply)) + logger.info( + "[wechatmp] reply to {} saved to cache: {}".format(receiver, reply) + ) else: receiver = context["receiver"] reply_text = reply.content @@ -115,7 +117,7 @@ class WechatMPChannel(ChatChannel): params=params, data=json.dumps(json_data, ensure_ascii=False).encode("utf8"), ) - logger.info("[send] Do send to {}: {}".format(receiver, reply_text)) + logger.info("[wechatmp] Do send to {}: {}".format(receiver, reply_text)) return def _success_callback(self, session_id, context, **kwargs): # 线程异常结束时的回调函数 diff --git a/config.py b/config.py index 8f5d2ca..f6c3983 100644 --- a/config.py +++ b/config.py @@ -75,6 +75,13 @@ available_setting = { "wechatmp_port": 8080, # 微信公众平台的端口,需要端口转发到80或443 "wechatmp_app_id": "", # 微信公众平台的appID,仅服务号需要 "wechatmp_app_secret": "", # 微信公众平台的appsecret,仅服务号需要 + # wechatcom的配置 + "wechatcom_token": "", # 企业微信的token + "wechatcom_port": 9898, # 企业微信的服务端口,不需要端口转发 + "wechatcom_corp_id": "", # 企业微信的corpID + "wechatcom_secret": "", # 企业微信的secret + "wechatcom_agent_id": "", # 企业微信的appID + "wechatcom_aes_key": "", # 企业微信的aes_key # chatgpt指令自定义触发词 "clear_memory_commands": ["#清除记忆"], # 重置会话指令,必须以#开头 # channel配置 diff --git a/requirements-optional.txt b/requirements-optional.txt index cfb52c9..d7c48ac 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -16,8 +16,9 @@ wechaty>=0.10.7 wechaty_puppet>=0.4.23 pysilk_mod>=1.6.0 # needed by send voice -# wechatmp +# wechatmp wechatcom web.py +wechatpy # chatgpt-tool-hub plugin From ab83dacb761c1ff2e718ef093ba6b9b75c8720fb Mon Sep 17 00:00:00 2001 From: lanvent Date: Thu, 20 Apr 2023 01:46:23 +0800 Subject: [PATCH 2/8] feat(wechatcom): add support for sending voice messages --- .pre-commit-config.yaml | 1 + channel/wechatcom/wechatcom_channel.py | 36 +++++++++++++++++++++----- voice/audio_convert.py | 18 +++++++++++++ 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5dd0d7d..0c39abf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,6 +18,7 @@ repos: hooks: - id: isort exclude: '(\/|^)lib\/' + args: [ -l, '88'] - repo: https://github.com/psf/black rev: 23.3.0 hooks: diff --git a/channel/wechatcom/wechatcom_channel.py b/channel/wechatcom/wechatcom_channel.py index 1fbeccd..6bb4f1f 100644 --- a/channel/wechatcom/wechatcom_channel.py +++ b/channel/wechatcom/wechatcom_channel.py @@ -1,10 +1,12 @@ #!/usr/bin/env python # -*- coding=utf-8 -*- +import os + import web from wechatpy.enterprise import WeChatClient, create_reply, parse_message from wechatpy.enterprise.crypto import WeChatCrypto from wechatpy.enterprise.exceptions import InvalidCorpIdException -from wechatpy.exceptions import InvalidSignatureException +from wechatpy.exceptions import InvalidSignatureException, WeChatClientException from bridge.context import Context from bridge.reply import Reply, ReplyType @@ -13,11 +15,12 @@ from channel.wechatcom.wechatcom_message import WechatComMessage from common.log import logger from common.singleton import singleton from config import conf +from voice.audio_convert import any_to_amr @singleton class WechatComChannel(ChatChannel): - NOT_SUPPORT_REPLYTYPE = [ReplyType.IMAGE, ReplyType.VOICE] + NOT_SUPPORT_REPLYTYPE = [ReplyType.IMAGE] def __init__(self): super().__init__() @@ -43,11 +46,32 @@ class WechatComChannel(ChatChannel): web.httpserver.runsimple(app.wsgifunc(), ("0.0.0.0", port)) def send(self, reply: Reply, context: Context): - print("send reply: ", reply.content, context["receiver"]) receiver = context["receiver"] - reply_text = reply.content - self.client.message.send_text(self.agent_id, receiver, reply_text) - logger.info("[send] Do send to {}: {}".format(receiver, reply_text)) + if reply.type in [ReplyType.TEXT, ReplyType.ERROR, ReplyType.INFO]: + self.client.message.send_text(self.agent_id, receiver, reply.content) + logger.info("[wechatcom] sendMsg={}, receiver={}".format(reply, receiver)) + elif reply.type == ReplyType.VOICE: + try: + file_path = reply.content + amr_file = os.path.splitext(file_path)[0] + ".amr" + any_to_amr(file_path, amr_file) + response = self.client.media.upload("voice", open(amr_file, "rb")) + logger.debug("[wechatcom] upload voice response: {}".format(response)) + except WeChatClientException as e: + logger.error("[wechatcom] upload voice failed: {}".format(e)) + return + try: + os.remove(file_path) + if amr_file != file_path: + os.remove(amr_file) + except Exception: + pass + self.client.message.send_voice( + self.agent_id, receiver, response["media_id"] + ) + logger.info( + "[wechatcom] sendVoice={}, receiver={}".format(reply.content, receiver) + ) class Query: diff --git a/voice/audio_convert.py b/voice/audio_convert.py index 241a3a6..4819b49 100644 --- a/voice/audio_convert.py +++ b/voice/audio_convert.py @@ -69,6 +69,24 @@ def any_to_sil(any_path, sil_path): raise NotImplementedError("Not support file type: {}".format(any_path)) +def any_to_amr(any_path, amr_path): + """ + 把任意格式转成amr文件 + """ + if any_path.endswith(".amr"): + shutil.copy2(any_path, amr_path) + return + if ( + any_path.endswith(".sil") + or any_path.endswith(".silk") + or any_path.endswith(".slk") + ): + raise NotImplementedError("Not support file type: {}".format(any_path)) + audio = AudioSegment.from_file(any_path) + audio = audio.set_frame_rate(8000) # only support 8000 + audio.export(amr_path, format="amr") + + def mp3_to_wav(mp3_path, wav_path): """ 把mp3格式转成pcm文件 From 3ea87813810e59922b654f8ebb4070ea878e7c29 Mon Sep 17 00:00:00 2001 From: lanvent Date: Thu, 20 Apr 2023 02:14:52 +0800 Subject: [PATCH 3/8] feat(wechatcom): add support for sending image --- channel/wechatcom/wechatcom_channel.py | 54 +++++++++++++++++++++++--- channel/wechatcom/wechatcom_message.py | 17 ++++++-- 2 files changed, 63 insertions(+), 8 deletions(-) diff --git a/channel/wechatcom/wechatcom_channel.py b/channel/wechatcom/wechatcom_channel.py index 6bb4f1f..90c7b22 100644 --- a/channel/wechatcom/wechatcom_channel.py +++ b/channel/wechatcom/wechatcom_channel.py @@ -1,7 +1,10 @@ #!/usr/bin/env python # -*- coding=utf-8 -*- +import io import os +import textwrap +import requests import web from wechatpy.enterprise import WeChatClient, create_reply, parse_message from wechatpy.enterprise.crypto import WeChatCrypto @@ -20,7 +23,7 @@ from voice.audio_convert import any_to_amr @singleton class WechatComChannel(ChatChannel): - NOT_SUPPORT_REPLYTYPE = [ReplyType.IMAGE] + NOT_SUPPORT_REPLYTYPE = [] def __init__(self): super().__init__() @@ -72,6 +75,38 @@ class WechatComChannel(ChatChannel): logger.info( "[wechatcom] sendVoice={}, receiver={}".format(reply.content, receiver) ) + elif reply.type == ReplyType.IMAGE_URL: # 从网络下载图片 + img_url = reply.content + pic_res = requests.get(img_url, stream=True) + image_storage = io.BytesIO() + for block in pic_res.iter_content(1024): + image_storage.write(block) + image_storage.seek(0) + try: + response = self.client.media.upload("image", image_storage) + logger.debug("[wechatcom] upload image response: {}".format(response)) + except WeChatClientException as e: + logger.error("[wechatcom] upload image failed: {}".format(e)) + return + self.client.message.send_image( + self.agent_id, receiver, response["media_id"] + ) + logger.info( + "[wechatcom] sendImage url={}, receiver={}".format(img_url, receiver) + ) + elif reply.type == ReplyType.IMAGE: # 从文件读取图片 + image_storage = reply.content + image_storage.seek(0) + try: + response = self.client.media.upload("image", image_storage) + logger.debug("[wechatcom] upload image response: {}".format(response)) + except WeChatClientException as e: + logger.error("[wechatcom] upload image failed: {}".format(e)) + return + self.client.message.send_image( + self.agent_id, receiver, response["media_id"] + ) + logger.info("[wechatcom] sendImage, receiver={}".format(receiver)) class Query: @@ -103,13 +138,22 @@ class Query: ) except (InvalidSignatureException, InvalidCorpIdException): raise web.Forbidden() - print(message) msg = parse_message(message) - - print(msg) + logger.debug("[wechatcom] receive message: {}, msg= {}".format(message, msg)) if msg.type == "event": if msg.event == "subscribe": - reply = create_reply("感谢关注", msg).render() + trigger_prefix = conf().get("single_chat_prefix", [""])[0] + reply_content = textwrap.dedent( + f"""\ + 感谢您的关注! + 这里是ChatGPT,可以自由对话。 + 支持语音对话。 + 支持通用表情输入。 + 支持图片输入输出。 + 支持角色扮演和文字冒险两种定制模式对话。 + 输入'{trigger_prefix}#help' 查看详细指令。""" + ) + reply = create_reply(reply_content, msg).render() res = channel.crypto.encrypt_message(reply, nonce, timestamp) return res else: diff --git a/channel/wechatcom/wechatcom_message.py b/channel/wechatcom/wechatcom_message.py index 42de66e..9b34f11 100644 --- a/channel/wechatcom/wechatcom_message.py +++ b/channel/wechatcom/wechatcom_message.py @@ -41,9 +41,20 @@ class WechatComMessage(ChatMessage): self._prepare_fn = download_voice elif msg.type == "image": self.ctype = ContextType.IMAGE - self.content = msg.image # content直接存临时目录路径 - print(self.content) - # self._prepare_fn = lambda: itchat_msg.download(self.content) # TODO: download image + self.content = TmpDir().path() + msg.media_id + ".png" # content直接存临时目录路径 + + def download_image(): + # 如果响应状态码是200,则将响应内容写入本地文件 + response = client.media.download(msg.media_id) + if response.status_code == 200: + with open(self.content, "wb") as f: + f.write(response.content) + else: + logger.info( + f"[wechatcom] Failed to download image file, {response.content}" + ) + + self._prepare_fn = download_image else: raise NotImplementedError( "Unsupported message type: Type:{} ".format(msg.type) From d2bf90c6c713f729e437c646c4b0a63c19d3a9a1 Mon Sep 17 00:00:00 2001 From: lanvent Date: Thu, 20 Apr 2023 08:31:42 +0800 Subject: [PATCH 4/8] refactor: rename WechatComChannel to WechatComAppChannel --- channel/channel_factory.py | 4 ++-- ...com_channel.py => wechatcomapp_channel.py} | 22 +++++++++---------- ...com_message.py => wechatcomapp_message.py} | 2 +- config.py | 15 +++++++------ 4 files changed, 22 insertions(+), 21 deletions(-) rename channel/wechatcom/{wechatcom_channel.py => wechatcomapp_channel.py} (91%) rename channel/wechatcom/{wechatcom_message.py => wechatcomapp_message.py} (98%) diff --git a/channel/channel_factory.py b/channel/channel_factory.py index fed3234..d6625d4 100644 --- a/channel/channel_factory.py +++ b/channel/channel_factory.py @@ -30,7 +30,7 @@ def create_channel(channel_type): return WechatMPChannel(passive_reply=False) elif channel_type == "wechatcom": - from channel.wechatcom.wechatcom_channel import WechatComChannel + from channel.wechatcom.wechatcomapp_channel import WechatComAppChannel - return WechatComChannel() + return WechatComAppChannel() raise RuntimeError diff --git a/channel/wechatcom/wechatcom_channel.py b/channel/wechatcom/wechatcomapp_channel.py similarity index 91% rename from channel/wechatcom/wechatcom_channel.py rename to channel/wechatcom/wechatcomapp_channel.py index 90c7b22..a791e41 100644 --- a/channel/wechatcom/wechatcom_channel.py +++ b/channel/wechatcom/wechatcomapp_channel.py @@ -14,7 +14,7 @@ from wechatpy.exceptions import InvalidSignatureException, WeChatClientException from bridge.context import Context from bridge.reply import Reply, ReplyType from channel.chat_channel import ChatChannel -from channel.wechatcom.wechatcom_message import WechatComMessage +from channel.wechatcom.wechatcomapp_message import WechatComAppMessage from common.log import logger from common.singleton import singleton from config import conf @@ -22,16 +22,16 @@ from voice.audio_convert import any_to_amr @singleton -class WechatComChannel(ChatChannel): +class WechatComAppChannel(ChatChannel): NOT_SUPPORT_REPLYTYPE = [] def __init__(self): super().__init__() self.corp_id = conf().get("wechatcom_corp_id") - self.secret = conf().get("wechatcom_secret") - self.agent_id = conf().get("wechatcom_agent_id") - self.token = conf().get("wechatcom_token") - self.aes_key = conf().get("wechatcom_aes_key") + self.secret = conf().get("wechatcomapp_secret") + self.agent_id = conf().get("wechatcomapp_agent_id") + self.token = conf().get("wechatcomapp_token") + self.aes_key = conf().get("wechatcomapp_aes_key") print(self.corp_id, self.secret, self.agent_id, self.token, self.aes_key) logger.info( "[wechatcom] init: corp_id: {}, secret: {}, agent_id: {}, token: {}, aes_key: {}".format( @@ -43,9 +43,9 @@ class WechatComChannel(ChatChannel): def startup(self): # start message listener - urls = ("/wxcom", "channel.wechatcom.wechatcom_channel.Query") + urls = ("/wxcom", "channel.wechatcom.wechatcomapp_channel.Query") app = web.application(urls, globals(), autoreload=False) - port = conf().get("wechatcom_port", 8080) + port = conf().get("wechatcomapp_port", 8080) web.httpserver.runsimple(app.wsgifunc(), ("0.0.0.0", port)) def send(self, reply: Reply, context: Context): @@ -111,7 +111,7 @@ class WechatComChannel(ChatChannel): class Query: def GET(self): - channel = WechatComChannel() + channel = WechatComAppChannel() params = web.input() signature = params.msg_signature timestamp = params.timestamp @@ -127,7 +127,7 @@ class Query: return echostr def POST(self): - channel = WechatComChannel() + channel = WechatComAppChannel() params = web.input() signature = params.msg_signature timestamp = params.timestamp @@ -158,7 +158,7 @@ class Query: return res else: try: - wechatcom_msg = WechatComMessage(msg, client=channel.client) + wechatcom_msg = WechatComAppMessage(msg, client=channel.client) except NotImplementedError as e: logger.debug("[wechatcom] " + str(e)) return "success" diff --git a/channel/wechatcom/wechatcom_message.py b/channel/wechatcom/wechatcomapp_message.py similarity index 98% rename from channel/wechatcom/wechatcom_message.py rename to channel/wechatcom/wechatcomapp_message.py index 9b34f11..f441a68 100644 --- a/channel/wechatcom/wechatcom_message.py +++ b/channel/wechatcom/wechatcomapp_message.py @@ -11,7 +11,7 @@ from lib import itchat from lib.itchat.content import * -class WechatComMessage(ChatMessage): +class WechatComAppMessage(ChatMessage): def __init__(self, msg, client: WeChatClient, is_group=False): super().__init__(msg) self.msg_id = msg.id diff --git a/config.py b/config.py index f6c3983..c5f09ab 100644 --- a/config.py +++ b/config.py @@ -75,13 +75,14 @@ available_setting = { "wechatmp_port": 8080, # 微信公众平台的端口,需要端口转发到80或443 "wechatmp_app_id": "", # 微信公众平台的appID,仅服务号需要 "wechatmp_app_secret": "", # 微信公众平台的appsecret,仅服务号需要 - # wechatcom的配置 - "wechatcom_token": "", # 企业微信的token - "wechatcom_port": 9898, # 企业微信的服务端口,不需要端口转发 - "wechatcom_corp_id": "", # 企业微信的corpID - "wechatcom_secret": "", # 企业微信的secret - "wechatcom_agent_id": "", # 企业微信的appID - "wechatcom_aes_key": "", # 企业微信的aes_key + # wechatcom的通用配置 + "wechatcom_corp_id": "", # 企业微信公司的corpID + # wechatcomapp的配置 + "wechatcomapp_token": "", # 企业微信app的token + "wechatcomapp_port": 9898, # 企业微信app的服务端口,不需要端口转发 + "wechatcomapp_secret": "", # 企业微信app的secret + "wechatcomapp_agent_id": "", # 企业微信app的agent_id + "wechatcomapp_aes_key": "", # 企业微信app的aes_key # chatgpt指令自定义触发词 "clear_memory_commands": ["#清除记忆"], # 重置会话指令,必须以#开头 # channel配置 From 3e9e8d442ac76630bc118facfc6ae57736847c6c Mon Sep 17 00:00:00 2001 From: lanvent Date: Thu, 20 Apr 2023 08:43:17 +0800 Subject: [PATCH 5/8] docs: add README.md for wechatcomapp channel --- channel/wechatcom/README.md | 33 +++++++++++++++++++++++ channel/wechatcom/wechatcomapp_channel.py | 2 +- 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 channel/wechatcom/README.md diff --git a/channel/wechatcom/README.md b/channel/wechatcom/README.md new file mode 100644 index 0000000..7e1d77a --- /dev/null +++ b/channel/wechatcom/README.md @@ -0,0 +1,33 @@ + +> 详细文档暂无 + +## 自建应用 + +- 在企业微信工作台自建应用 + +建立应用后点击通过API接收消息,设置服务器地址,服务器地址是`http://url:port/wxcomapp`的形式,也可以不用域名,比如 `http://ip:port/wxcomapp` + +- 修改配置 + +在主目录下的`config.json`中填写以下配置项 + +```python + # wechatcom的通用配置 + "wechatcom_corp_id": "", # 企业微信公司的corpID + # wechatcomapp的配置 + "wechatcomapp_token": "", # 企业微信app的token + "wechatcomapp_port": 9898, # 企业微信app的服务端口,不需要端口转发 + "wechatcomapp_secret": "", # 企业微信app的secret + "wechatcomapp_agent_id": "", # 企业微信app的agent_id + "wechatcomapp_aes_key": "", # 企业微信app的aes_key +``` + +- 运行程序 + +```python app.py``` + +在设置服务器页面点击保存 + +- 添加可信IP + +在自建应用管理页下方,将服务器的IP添加到可信IP diff --git a/channel/wechatcom/wechatcomapp_channel.py b/channel/wechatcom/wechatcomapp_channel.py index a791e41..0dad142 100644 --- a/channel/wechatcom/wechatcomapp_channel.py +++ b/channel/wechatcom/wechatcomapp_channel.py @@ -43,7 +43,7 @@ class WechatComAppChannel(ChatChannel): def startup(self): # start message listener - urls = ("/wxcom", "channel.wechatcom.wechatcomapp_channel.Query") + urls = ("/wxcomapp", "channel.wechatcom.wechatcomapp_channel.Query") app = web.application(urls, globals(), autoreload=False) port = conf().get("wechatcomapp_port", 8080) web.httpserver.runsimple(app.wsgifunc(), ("0.0.0.0", port)) From 665001732b4bd6a167088a99bd5703c422eefde5 Mon Sep 17 00:00:00 2001 From: lanvent Date: Fri, 21 Apr 2023 15:29:59 +0800 Subject: [PATCH 6/8] feat: add image compression Add image compression feature to WechatComAppChannel to compress images larger than 10MB before uploading to WeChat server. The compression is done using the `compress_imgfile` function in `utils.py`. The `fsize` function is also added to `utils.py` to calculate the size of a file or buffer. --- channel/wechatcom/wechatcomapp_channel.py | 18 ++++++++++++ common/utils.py | 34 +++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 common/utils.py diff --git a/channel/wechatcom/wechatcomapp_channel.py b/channel/wechatcom/wechatcomapp_channel.py index 0dad142..f62ab00 100644 --- a/channel/wechatcom/wechatcomapp_channel.py +++ b/channel/wechatcom/wechatcomapp_channel.py @@ -17,6 +17,7 @@ from channel.chat_channel import ChatChannel from channel.wechatcom.wechatcomapp_message import WechatComAppMessage from common.log import logger from common.singleton import singleton +from common.utils import compress_imgfile, fsize from config import conf from voice.audio_convert import any_to_amr @@ -81,6 +82,14 @@ class WechatComAppChannel(ChatChannel): image_storage = io.BytesIO() for block in pic_res.iter_content(1024): image_storage.write(block) + if (sz := fsize(image_storage)) >= 10 * 1024 * 1024: + logger.info( + "[wechatcom] image too large, ready to compress, sz={}".format(sz) + ) + image_storage = compress_imgfile(image_storage, 10 * 1024 * 1024 - 1) + logger.info( + "[wechatcom] image compressed, sz={}".format(fsize(image_storage)) + ) image_storage.seek(0) try: response = self.client.media.upload("image", image_storage) @@ -88,6 +97,7 @@ class WechatComAppChannel(ChatChannel): except WeChatClientException as e: logger.error("[wechatcom] upload image failed: {}".format(e)) return + self.client.message.send_image( self.agent_id, receiver, response["media_id"] ) @@ -96,6 +106,14 @@ class WechatComAppChannel(ChatChannel): ) elif reply.type == ReplyType.IMAGE: # 从文件读取图片 image_storage = reply.content + if (sz := fsize(image_storage)) >= 10 * 1024 * 1024: + logger.info( + "[wechatcom] image too large, ready to compress, sz={}".format(sz) + ) + image_storage = compress_imgfile(image_storage, 10 * 1024 * 1024 - 1) + logger.info( + "[wechatcom] image compressed, sz={}".format(fsize(image_storage)) + ) image_storage.seek(0) try: response = self.client.media.upload("image", image_storage) diff --git a/common/utils.py b/common/utils.py new file mode 100644 index 0000000..4d055f3 --- /dev/null +++ b/common/utils.py @@ -0,0 +1,34 @@ +import io +import os + +from PIL import Image + + +def fsize(file): + if isinstance(file, io.BytesIO): + return file.getbuffer().nbytes + elif isinstance(file, str): + return os.path.getsize(file) + elif hasattr(file, "seek") and hasattr(file, "tell"): + pos = file.tell() + file.seek(0, os.SEEK_END) + size = file.tell() + file.seek(pos) + return size + else: + raise TypeError("Unsupported type") + + +def compress_imgfile(file, max_size): + if fsize(file) <= max_size: + return file + file.seek(0) + img = Image.open(file) + rgb_image = img.convert("RGB") + quality = 95 + while True: + out_buf = io.BytesIO() + rgb_image.save(out_buf, "JPEG", quality=quality) + if fsize(out_buf) <= max_size: + return out_buf + quality -= 5 From 2ec53747658ecb9a5ee54b4bb77b08f5e5c0f052 Mon Sep 17 00:00:00 2001 From: lanvent Date: Sun, 23 Apr 2023 15:34:25 +0800 Subject: [PATCH 7/8] feat:modify wechatcom to wechatcom_app --- channel/channel_factory.py | 2 +- channel/wechatcom/wechatcomapp_channel.py | 17 +++++++++-------- config.py | 2 +- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/channel/channel_factory.py b/channel/channel_factory.py index d6625d4..96f3e5f 100644 --- a/channel/channel_factory.py +++ b/channel/channel_factory.py @@ -29,7 +29,7 @@ def create_channel(channel_type): from channel.wechatmp.wechatmp_channel import WechatMPChannel return WechatMPChannel(passive_reply=False) - elif channel_type == "wechatcom": + elif channel_type == "wechatcom_app": from channel.wechatcom.wechatcomapp_channel import WechatComAppChannel return WechatComAppChannel() diff --git a/channel/wechatcom/wechatcomapp_channel.py b/channel/wechatcom/wechatcomapp_channel.py index f62ab00..c655a3d 100644 --- a/channel/wechatcom/wechatcomapp_channel.py +++ b/channel/wechatcom/wechatcomapp_channel.py @@ -131,12 +131,12 @@ class Query: def GET(self): channel = WechatComAppChannel() params = web.input() - signature = params.msg_signature - timestamp = params.timestamp - nonce = params.nonce - echostr = params.echostr - print(params) + logger.info("[wechatcom] receive params: {}".format(params)) try: + signature = params.msg_signature + timestamp = params.timestamp + nonce = params.nonce + echostr = params.echostr echostr = channel.crypto.check_signature( signature, timestamp, nonce, echostr ) @@ -147,10 +147,11 @@ class Query: def POST(self): channel = WechatComAppChannel() params = web.input() - signature = params.msg_signature - timestamp = params.timestamp - nonce = params.nonce + logger.info("[wechatcom] receive params: {}".format(params)) try: + signature = params.msg_signature + timestamp = params.timestamp + nonce = params.nonce message = channel.crypto.decrypt_message( web.data(), signature, timestamp, nonce ) diff --git a/config.py b/config.py index c5f09ab..576432a 100644 --- a/config.py +++ b/config.py @@ -86,7 +86,7 @@ available_setting = { # chatgpt指令自定义触发词 "clear_memory_commands": ["#清除记忆"], # 重置会话指令,必须以#开头 # channel配置 - "channel_type": "wx", # 通道类型,支持:{wx,wxy,terminal,wechatmp,wechatmp_service} + "channel_type": "wx", # 通道类型,支持:{wx,wxy,terminal,wechatmp,wechatmp_service,wechatcom_app} "debug": False, # 是否开启debug模式,开启后会打印更多日志 "appdata_dir": "", # 数据目录 # 插件配置 From 9163ce71fd52566a716e258812411fc21c3589a3 Mon Sep 17 00:00:00 2001 From: lanvent Date: Sun, 23 Apr 2023 16:51:16 +0800 Subject: [PATCH 8/8] fix: enable plugins for wechatcom_app --- app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.py b/app.py index 1a1bb5e..ee4df44 100644 --- a/app.py +++ b/app.py @@ -49,7 +49,7 @@ def run(): "terminal", "wechatmp", "wechatmp_service", - "wechatcom", + "wechatcom_app", ]: PluginManager().load_plugins()