From 68361cddd256b9d309696fd5a5abb2f11dbc91ad Mon Sep 17 00:00:00 2001 From: JS00000 Date: Tue, 18 Apr 2023 03:08:18 +0800 Subject: [PATCH] wechatmp_service: image and voice reply supported --- .../{ServiceAccount.py => service_account.py} | 6 +- ...bscribeAccount.py => subscribe_account.py} | 0 channel/wechatmp/wechatmp_channel.py | 134 ++++++++---------- channel/wechatmp/wechatmp_client.py | 125 ++++++++++++++++ voice/pytts/pytts_voice.py | 18 ++- 5 files changed, 200 insertions(+), 83 deletions(-) rename channel/wechatmp/{ServiceAccount.py => service_account.py} (93%) rename channel/wechatmp/{SubscribeAccount.py => subscribe_account.py} (100%) create mode 100644 channel/wechatmp/wechatmp_client.py diff --git a/channel/wechatmp/ServiceAccount.py b/channel/wechatmp/service_account.py similarity index 93% rename from channel/wechatmp/ServiceAccount.py rename to channel/wechatmp/service_account.py index 699581d..adaa754 100644 --- a/channel/wechatmp/ServiceAccount.py +++ b/channel/wechatmp/service_account.py @@ -23,7 +23,11 @@ class Query: webData = web.data() # logger.debug("[wechatmp] Receive request:\n" + webData.decode("utf-8")) wechatmp_msg = receive.parse_xml(webData) - if wechatmp_msg.msg_type == "text" or wechatmp_msg.msg_type == "voice": + if ( + wechatmp_msg.msg_type == "text" + or wechatmp_msg.msg_type == "voice" + # or wechatmp_msg.msg_type == "image" + ): from_user = wechatmp_msg.from_user_id message = wechatmp_msg.content.decode("utf-8") message_id = wechatmp_msg.msg_id diff --git a/channel/wechatmp/SubscribeAccount.py b/channel/wechatmp/subscribe_account.py similarity index 100% rename from channel/wechatmp/SubscribeAccount.py rename to channel/wechatmp/subscribe_account.py diff --git a/channel/wechatmp/wechatmp_channel.py b/channel/wechatmp/wechatmp_channel.py index ac3c3ac..453a1a4 100644 --- a/channel/wechatmp/wechatmp_channel.py +++ b/channel/wechatmp/wechatmp_channel.py @@ -1,26 +1,25 @@ # -*- coding: utf-8 -*- -import json -import threading -import time - -import requests import web - +import io +import imghdr +import requests from bridge.context import * from bridge.reply import * from channel.chat_channel import ChatChannel +from channel.wechatmp.wechatmp_client import WechatMPClient from channel.wechatmp.common import * from common.expired_dict import ExpiredDict from common.log import logger +from common.tmp_dir import TmpDir from common.singleton import singleton from config import conf # If using SSL, uncomment the following lines, and modify the certificate path. -# from cheroot.server import HTTPServer -# from cheroot.ssl.builtin import BuiltinSSLAdapter -# HTTPServer.ssl_adapter = BuiltinSSLAdapter( -# certificate='/ssl/cert.pem', -# private_key='/ssl/cert.key') +from cheroot.server import HTTPServer +from cheroot.ssl.builtin import BuiltinSSLAdapter +HTTPServer.ssl_adapter = BuiltinSSLAdapter( + certificate='/ssl/cert.pem', + private_key='/ssl/cert.key') @singleton @@ -30,6 +29,7 @@ class WechatMPChannel(ChatChannel): self.passive_reply = passive_reply self.running = set() self.received_msgs = ExpiredDict(60 * 60 * 24) + self.client = WechatMPClient() if self.passive_reply: self.NOT_SUPPORT_REPLYTYPE = [ReplyType.IMAGE, ReplyType.VOICE] self.cache_dict = dict() @@ -37,85 +37,65 @@ class WechatMPChannel(ChatChannel): self.query2 = dict() self.query3 = dict() else: - # TODO support image - self.NOT_SUPPORT_REPLYTYPE = [ReplyType.IMAGE, ReplyType.VOICE] - self.app_id = conf().get("wechatmp_app_id") - self.app_secret = conf().get("wechatmp_app_secret") - self.access_token = None - self.access_token_expires_time = 0 - self.access_token_lock = threading.Lock() - self.get_access_token() + self.NOT_SUPPORT_REPLYTYPE = [] + def startup(self): if self.passive_reply: - urls = ("/wx", "channel.wechatmp.SubscribeAccount.Query") + urls = ("/wx", "channel.wechatmp.subscribe_account.Query") else: - urls = ("/wx", "channel.wechatmp.ServiceAccount.Query") + urls = ("/wx", "channel.wechatmp.service_account.Query") app = web.application(urls, globals(), autoreload=False) port = conf().get("wechatmp_port", 8080) web.httpserver.runsimple(app.wsgifunc(), ("0.0.0.0", port)) - def wechatmp_request(self, method, url, **kwargs): - r = requests.request(method=method, url=url, **kwargs) - r.raise_for_status() - r.encoding = "utf-8" - ret = r.json() - if "errcode" in ret and ret["errcode"] != 0: - raise WeChatAPIException("{}".format(ret)) - return ret - - def get_access_token(self): - # return the access_token - if self.access_token: - if self.access_token_expires_time - time.time() > 60: - return self.access_token - - # Get new access_token - # Do not request access_token in parallel! Only the last obtained is valid. - if self.access_token_lock.acquire(blocking=False): - # Wait for other threads that have previously obtained access_token to complete the request - # This happens every 2 hours, so it doesn't affect the experience very much - time.sleep(1) - self.access_token = None - url = "https://api.weixin.qq.com/cgi-bin/token" - params = { - "grant_type": "client_credential", - "appid": self.app_id, - "secret": self.app_secret, - } - data = self.wechatmp_request(method="get", url=url, params=params) - self.access_token = data["access_token"] - self.access_token_expires_time = int(time.time()) + data["expires_in"] - logger.info("[wechatmp] access_token: {}".format(self.access_token)) - self.access_token_lock.release() - else: - # Wait for token update - while self.access_token_lock.locked(): - time.sleep(0.1) - return self.access_token def send(self, reply: Reply, context: Context): + receiver = context["receiver"] 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 cached reply to {}: {}".format(receiver, reply)) else: - receiver = context["receiver"] - reply_text = reply.content - url = "https://api.weixin.qq.com/cgi-bin/message/custom/send" - params = {"access_token": self.get_access_token()} - json_data = { - "touser": receiver, - "msgtype": "text", - "text": {"content": reply_text}, - } - self.wechatmp_request( - method="post", - url=url, - params=params, - data=json.dumps(json_data, ensure_ascii=False).encode("utf8"), - ) - logger.info("[send] Do send to {}: {}".format(receiver, reply_text)) + if reply.type == ReplyType.TEXT or reply.type == ReplyType.INFO or reply.type == ReplyType.ERROR: + reply_text = reply.content + self.client.send_text(receiver, reply_text) + logger.info("[wechatmp] Do send to {}: {}".format(receiver, reply_text)) + + elif reply.type == ReplyType.VOICE: + voice_file_path = reply.content + logger.info("[wechatmp] voice file path {}".format(voice_file_path)) + + with open(voice_file_path, 'rb') as f: + filename = receiver + "-" + context["msg"].msg_id + ".mp3" + media_id = self.client.upload_media("voice", (filename, f, "audio/mpeg")) + self.client.send_voice(receiver, media_id) + logger.info("[wechatmp] Do send voice to {}".format(receiver)) + + elif reply.type == ReplyType.IMAGE_URL: # 从网络下载图片 + img_url = reply.content + pic_res = requests.get(img_url, stream=True) + print(pic_res.headers) + image_storage = io.BytesIO() + for block in pic_res.iter_content(1024): + image_storage.write(block) + image_storage.seek(0) + image_type = imghdr.what(image_storage) + filename = receiver + "-" + context["msg"].msg_id + "." + image_type + content_type = "image/" + image_type + # content_type = pic_res.headers.get('content-type') + media_id = self.client.upload_media("image", (filename, image_storage, content_type)) + self.client.send_image(receiver, media_id) + logger.info("[wechatmp] sendImage url={}, receiver={}".format(img_url, receiver)) + elif reply.type == ReplyType.IMAGE: # 从文件读取图片 + image_storage = reply.content + image_storage.seek(0) + image_type = imghdr.what(image_storage) + filename = receiver + "-" + context["msg"].msg_id + "." + image_type + content_type = "image/" + image_type + media_id = self.client.upload_media("image", (filename, image_storage, content_type)) + self.client.send_image(receiver, media_id) + logger.info("[wechatmp] sendImage, receiver={}".format(receiver)) + return def _success_callback(self, session_id, context, **kwargs): # 线程异常结束时的回调函数 diff --git a/channel/wechatmp/wechatmp_client.py b/channel/wechatmp/wechatmp_client.py new file mode 100644 index 0000000..7a2cd02 --- /dev/null +++ b/channel/wechatmp/wechatmp_client.py @@ -0,0 +1,125 @@ +import time +import json +import requests +import threading +from channel.wechatmp.common import * +from common.log import logger +from config import conf + + +class WechatMPClient: + def __init__(self): + self.app_id = conf().get("wechatmp_app_id") + self.app_secret = conf().get("wechatmp_app_secret") + self.access_token = None + self.access_token_expires_time = 0 + self.access_token_lock = threading.Lock() + self.get_access_token() + + + def wechatmp_request(self, method, url, **kwargs): + r = requests.request(method=method, url=url, **kwargs) + r.raise_for_status() + r.encoding = "utf-8" + ret = r.json() + if "errcode" in ret and ret["errcode"] != 0: + raise WeChatAPIException("{}".format(ret)) + return ret + + def get_access_token(self): + # return the access_token + if self.access_token: + if self.access_token_expires_time - time.time() > 60: + return self.access_token + + # Get new access_token + # Do not request access_token in parallel! Only the last obtained is valid. + if self.access_token_lock.acquire(blocking=False): + # Wait for other threads that have previously obtained access_token to complete the request + # This happens every 2 hours, so it doesn't affect the experience very much + time.sleep(1) + self.access_token = None + url = "https://api.weixin.qq.com/cgi-bin/token" + params = { + "grant_type": "client_credential", + "appid": self.app_id, + "secret": self.app_secret, + } + ret = self.wechatmp_request(method="get", url=url, params=params) + self.access_token = ret["access_token"] + self.access_token_expires_time = int(time.time()) + ret["expires_in"] + logger.info("[wechatmp] access_token: {}".format(self.access_token)) + self.access_token_lock.release() + else: + # Wait for token update + while self.access_token_lock.locked(): + time.sleep(0.1) + return self.access_token + + + def send_text(self, receiver, reply_text): + url = "https://api.weixin.qq.com/cgi-bin/message/custom/send" + params = {"access_token": self.get_access_token()} + json_data = { + "touser": receiver, + "msgtype": "text", + "text": {"content": reply_text}, + } + self.wechatmp_request( + method="post", + url=url, + params=params, + data=json.dumps(json_data, ensure_ascii=False).encode("utf8"), + ) + + + def send_voice(self, receiver, media_id): + url="https://api.weixin.qq.com/cgi-bin/message/custom/send" + params = {"access_token": self.get_access_token()} + json_data = { + "touser": receiver, + "msgtype": "voice", + "voice": { + "media_id": media_id + } + } + self.wechatmp_request( + method="post", + url=url, + params=params, + data=json.dumps(json_data, ensure_ascii=False).encode("utf8"), + ) + + def send_image(self, receiver, media_id): + url="https://api.weixin.qq.com/cgi-bin/message/custom/send" + params = {"access_token": self.get_access_token()} + json_data = { + "touser": receiver, + "msgtype": "image", + "image": { + "media_id": media_id + } + } + self.wechatmp_request( + method="post", + url=url, + params=params, + data=json.dumps(json_data, ensure_ascii=False).encode("utf8"), + ) + + + def upload_media(self, media_type, media_file): + url="https://api.weixin.qq.com/cgi-bin/media/upload" + params={ + "access_token": self.get_access_token(), + "type": media_type + } + files={"media": media_file} + logger.info("[wechatmp] media {} uploaded".format(media_file)) + ret = self.wechatmp_request( + method="post", + url=url, + params=params, + files=files + ) + return ret["media_id"] diff --git a/voice/pytts/pytts_voice.py b/voice/pytts/pytts_voice.py index 072e28b..57d0cfb 100644 --- a/voice/pytts/pytts_voice.py +++ b/voice/pytts/pytts_voice.py @@ -10,7 +10,7 @@ from bridge.reply import Reply, ReplyType from common.log import logger from common.tmp_dir import TmpDir from voice.voice import Voice - +import os class PyttsVoice(Voice): engine = pyttsx3.init() @@ -24,15 +24,23 @@ class PyttsVoice(Voice): if "Chinese" in voice.name: self.engine.setProperty("voice", voice.id) + self.engine.setProperty("voice", "zh") + def textToVoice(self, text): try: - wavFile = TmpDir().path() + "reply-" + str(int(time.time())) + ".wav" - self.engine.save_to_file(text, wavFile) + mp3FileName = "reply-" + str(int(time.time())) + ".mp3" + mp3File = TmpDir().path() + mp3FileName + self.engine.save_to_file(text, mp3File) self.engine.runAndWait() + + # engine.runAndWait() will return before the file created + while mp3FileName not in os.listdir(TmpDir().path()): + time.sleep(0.1) + logger.info( - "[Pytts] textToVoice text={} voice file name={}".format(text, wavFile) + "[Pytts] textToVoice text={} voice file name={}".format(text, mp3File) ) - reply = Reply(ReplyType.VOICE, wavFile) + reply = Reply(ReplyType.VOICE, mp3File) except Exception as e: reply = Reply(ReplyType.ERROR, str(e)) finally: