|
| 1 | +# -*- coding=utf-8 -*- |
| 2 | +import io |
| 3 | +import os |
| 4 | +import time |
| 5 | + |
| 6 | +import requests |
| 7 | +import web |
| 8 | +from wechatpy.enterprise import create_reply, parse_message |
| 9 | +from wechatpy.enterprise.crypto import WeChatCrypto |
| 10 | +from wechatpy.enterprise.exceptions import InvalidCorpIdException |
| 11 | +from wechatpy.exceptions import InvalidSignatureException, WeChatClientException |
| 12 | + |
| 13 | +from bridge.context import Context |
| 14 | +from bridge.reply import Reply, ReplyType |
| 15 | +from channel.chat_channel import ChatChannel |
| 16 | +from channel.wechatcom.wechatcomapp_client import WechatComAppClient |
| 17 | +from channel.wechatcom.wechatcomapp_message import WechatComAppMessage |
| 18 | +from common.log import logger |
| 19 | +from common.singleton import singleton |
| 20 | +from common.utils import compress_imgfile, fsize, split_string_by_utf8_length |
| 21 | +from config import conf, subscribe_msg |
| 22 | +from voice.audio_convert import any_to_amr |
| 23 | + |
| 24 | +MAX_UTF8_LEN = 2048 |
| 25 | + |
| 26 | + |
| 27 | +@singleton |
| 28 | +class WechatComAppChannel(ChatChannel): |
| 29 | + NOT_SUPPORT_REPLYTYPE = [] |
| 30 | + |
| 31 | + def __init__(self): |
| 32 | + super().__init__() |
| 33 | + self.corp_id = conf().get("wechatcom_corp_id") |
| 34 | + self.secret = conf().get("wechatcomapp_secret") |
| 35 | + self.agent_id = conf().get("wechatcomapp_agent_id") |
| 36 | + self.token = conf().get("wechatcomapp_token") |
| 37 | + self.aes_key = conf().get("wechatcomapp_aes_key") |
| 38 | + print(self.corp_id, self.secret, self.agent_id, self.token, self.aes_key) |
| 39 | + logger.info( |
| 40 | + "[wechatcom] init: corp_id: {}, secret: {}, agent_id: {}, token: {}, aes_key: {}".format(self.corp_id, self.secret, self.agent_id, self.token, self.aes_key) |
| 41 | + ) |
| 42 | + self.crypto = WeChatCrypto(self.token, self.aes_key, self.corp_id) |
| 43 | + self.client = WechatComAppClient(self.corp_id, self.secret) |
| 44 | + |
| 45 | + def startup(self): |
| 46 | + # start message listener |
| 47 | + urls = ("/wxcomapp", "channel.wechatcom.wechatcomapp_channel.Query") |
| 48 | + app = web.application(urls, globals(), autoreload=False) |
| 49 | + port = conf().get("wechatcomapp_port", 9898) |
| 50 | + web.httpserver.runsimple(app.wsgifunc(), ("0.0.0.0", port)) |
| 51 | + |
| 52 | + def send(self, reply: Reply, context: Context): |
| 53 | + receiver = context["receiver"] |
| 54 | + if reply.type in [ReplyType.TEXT, ReplyType.ERROR, ReplyType.INFO]: |
| 55 | + reply_text = reply.content |
| 56 | + texts = split_string_by_utf8_length(reply_text, MAX_UTF8_LEN) |
| 57 | + if len(texts) > 1: |
| 58 | + logger.info("[wechatcom] text too long, split into {} parts".format(len(texts))) |
| 59 | + for i, text in enumerate(texts): |
| 60 | + self.client.message.send_text(self.agent_id, receiver, text) |
| 61 | + if i != len(texts) - 1: |
| 62 | + time.sleep(0.5) # 休眠0.5秒,防止发送过快乱序 |
| 63 | + logger.info("[wechatcom] Do send text to {}: {}".format(receiver, reply_text)) |
| 64 | + elif reply.type == ReplyType.VOICE: |
| 65 | + try: |
| 66 | + file_path = reply.content |
| 67 | + amr_file = os.path.splitext(file_path)[0] + ".amr" |
| 68 | + any_to_amr(file_path, amr_file) |
| 69 | + response = self.client.media.upload("voice", open(amr_file, "rb")) |
| 70 | + logger.debug("[wechatcom] upload voice response: {}".format(response)) |
| 71 | + except WeChatClientException as e: |
| 72 | + logger.error("[wechatcom] upload voice failed: {}".format(e)) |
| 73 | + return |
| 74 | + try: |
| 75 | + os.remove(file_path) |
| 76 | + if amr_file != file_path: |
| 77 | + os.remove(amr_file) |
| 78 | + except Exception: |
| 79 | + pass |
| 80 | + self.client.message.send_voice(self.agent_id, receiver, response["media_id"]) |
| 81 | + logger.info("[wechatcom] sendVoice={}, receiver={}".format(reply.content, receiver)) |
| 82 | + elif reply.type == ReplyType.IMAGE_URL: # 从网络下载图片 |
| 83 | + img_url = reply.content |
| 84 | + pic_res = requests.get(img_url, stream=True) |
| 85 | + image_storage = io.BytesIO() |
| 86 | + for block in pic_res.iter_content(1024): |
| 87 | + image_storage.write(block) |
| 88 | + if (sz := fsize(image_storage)) >= 10 * 1024 * 1024: |
| 89 | + logger.info("[wechatcom] image too large, ready to compress, sz={}".format(sz)) |
| 90 | + image_storage = compress_imgfile(image_storage, 10 * 1024 * 1024 - 1) |
| 91 | + logger.info("[wechatcom] image compressed, sz={}".format(fsize(image_storage))) |
| 92 | + image_storage.seek(0) |
| 93 | + try: |
| 94 | + response = self.client.media.upload("image", image_storage) |
| 95 | + logger.debug("[wechatcom] upload image response: {}".format(response)) |
| 96 | + except WeChatClientException as e: |
| 97 | + logger.error("[wechatcom] upload image failed: {}".format(e)) |
| 98 | + return |
| 99 | + |
| 100 | + self.client.message.send_image(self.agent_id, receiver, response["media_id"]) |
| 101 | + logger.info("[wechatcom] sendImage url={}, receiver={}".format(img_url, receiver)) |
| 102 | + elif reply.type == ReplyType.IMAGE: # 从文件读取图片 |
| 103 | + image_storage = reply.content |
| 104 | + if (sz := fsize(image_storage)) >= 10 * 1024 * 1024: |
| 105 | + logger.info("[wechatcom] image too large, ready to compress, sz={}".format(sz)) |
| 106 | + image_storage = compress_imgfile(image_storage, 10 * 1024 * 1024 - 1) |
| 107 | + logger.info("[wechatcom] image compressed, sz={}".format(fsize(image_storage))) |
| 108 | + image_storage.seek(0) |
| 109 | + try: |
| 110 | + response = self.client.media.upload("image", image_storage) |
| 111 | + logger.debug("[wechatcom] upload image response: {}".format(response)) |
| 112 | + except WeChatClientException as e: |
| 113 | + logger.error("[wechatcom] upload image failed: {}".format(e)) |
| 114 | + return |
| 115 | + self.client.message.send_image(self.agent_id, receiver, response["media_id"]) |
| 116 | + logger.info("[wechatcom] sendImage, receiver={}".format(receiver)) |
| 117 | + |
| 118 | + |
| 119 | +class Query: |
| 120 | + def GET(self): |
| 121 | + channel = WechatComAppChannel() |
| 122 | + params = web.input() |
| 123 | + logger.info("[wechatcom] receive params: {}".format(params)) |
| 124 | + try: |
| 125 | + signature = params.msg_signature |
| 126 | + timestamp = params.timestamp |
| 127 | + nonce = params.nonce |
| 128 | + echostr = params.echostr |
| 129 | + echostr = channel.crypto.check_signature(signature, timestamp, nonce, echostr) |
| 130 | + except InvalidSignatureException: |
| 131 | + raise web.Forbidden() |
| 132 | + return echostr |
| 133 | + |
| 134 | + def POST(self): |
| 135 | + channel = WechatComAppChannel() |
| 136 | + params = web.input() |
| 137 | + logger.info("[wechatcom] receive params: {}".format(params)) |
| 138 | + try: |
| 139 | + signature = params.msg_signature |
| 140 | + timestamp = params.timestamp |
| 141 | + nonce = params.nonce |
| 142 | + message = channel.crypto.decrypt_message(web.data(), signature, timestamp, nonce) |
| 143 | + except (InvalidSignatureException, InvalidCorpIdException): |
| 144 | + raise web.Forbidden() |
| 145 | + msg = parse_message(message) |
| 146 | + logger.debug("[wechatcom] receive message: {}, msg= {}".format(message, msg)) |
| 147 | + if msg.type == "event": |
| 148 | + if msg.event == "subscribe": |
| 149 | + reply_content = subscribe_msg() |
| 150 | + if reply_content: |
| 151 | + reply = create_reply(reply_content, msg).render() |
| 152 | + res = channel.crypto.encrypt_message(reply, nonce, timestamp) |
| 153 | + return res |
| 154 | + else: |
| 155 | + try: |
| 156 | + wechatcom_msg = WechatComAppMessage(msg, client=channel.client) |
| 157 | + except NotImplementedError as e: |
| 158 | + logger.debug("[wechatcom] " + str(e)) |
| 159 | + return "success" |
| 160 | + context = channel._compose_context( |
| 161 | + wechatcom_msg.ctype, |
| 162 | + wechatcom_msg.content, |
| 163 | + isgroup=False, |
| 164 | + msg=wechatcom_msg, |
| 165 | + ) |
| 166 | + if context: |
| 167 | + channel.produce(context) |
| 168 | + return "success" |
0 commit comments