Просмотр исходного кода

refactor(wechatmp): use wechatpy to handle wechatmp messages

feat(wechatmp): add support for image and voice replies
master
lanvent 1 год назад
Родитель
Сommit
89dd8a1db6
7 измененных файлов: 270 добавлений и 340 удалений
  1. +26
    -31
      channel/wechatmp/active_reply.py
  2. +10
    -18
      channel/wechatmp/common.py
  3. +50
    -46
      channel/wechatmp/passive_reply.py
  4. +89
    -36
      channel/wechatmp/wechatmp_channel.py
  5. +26
    -171
      channel/wechatmp/wechatmp_client.py
  6. +53
    -38
      channel/wechatmp/wechatmp_message.py
  7. +16
    -0
      voice/audio_convert.py

+ 26
- 31
channel/wechatmp/active_reply.py Просмотреть файл

@@ -2,15 +2,14 @@ import time


import web import web


from channel.wechatmp.wechatmp_message import parse_xml
from channel.wechatmp.passive_reply_message import TextMsg
from channel.wechatmp.wechatmp_message import WeChatMPMessage
from bridge.context import * from bridge.context import *
from bridge.reply import ReplyType
from channel.wechatmp.common import * from channel.wechatmp.common import *
from channel.wechatmp.wechatmp_channel import WechatMPChannel from channel.wechatmp.wechatmp_channel import WechatMPChannel
from wechatpy import parse_message
from common.log import logger from common.log import logger
from config import conf from config import conf
from wechatpy.replies import create_reply


# This class is instantiated once per query # This class is instantiated once per query
class Query: class Query:
@@ -21,16 +20,22 @@ class Query:
# Make sure to return the instance that first created, @singleton will do that. # Make sure to return the instance that first created, @singleton will do that.
channel = WechatMPChannel() channel = WechatMPChannel()
try: try:
webData = web.data()
message = web.data() # todo crypto
# logger.debug("[wechatmp] Receive request:\n" + webData.decode("utf-8")) # logger.debug("[wechatmp] Receive request:\n" + webData.decode("utf-8"))
wechatmp_msg = parse_xml(webData)
if (
wechatmp_msg.msg_type == "text"
or wechatmp_msg.msg_type == "voice"
# or wechatmp_msg.msg_type == "image"
):
msg = parse_message(message)
if msg.type == "event":
logger.info(
"[wechatmp] Event {} from {}".format(
msg.event, msg.source
)
)
reply_text = subscribe_msg()
replyPost = create_reply(reply_text, msg)
return replyPost.render()
wechatmp_msg = WeChatMPMessage(msg, client=channel.client)
if wechatmp_msg.ctype in [ContextType.TEXT, ContextType.IMAGE, ContextType.VOICE]:
from_user = wechatmp_msg.from_user_id from_user = wechatmp_msg.from_user_id
message = wechatmp_msg.content
content = wechatmp_msg.content
message_id = wechatmp_msg.msg_id message_id = wechatmp_msg.msg_id


logger.info( logger.info(
@@ -39,16 +44,18 @@ class Query:
web.ctx.env.get("REMOTE_PORT"), web.ctx.env.get("REMOTE_PORT"),
from_user, from_user,
message_id, message_id,
message,
content,
) )
) )
if (wechatmp_msg.msg_type == "voice" and conf().get("voice_reply_voice") == True):
rtype = ReplyType.VOICE
if (msg.type == "voice" and wechatmp_msg.ctype == ContextType.TEXT):
origin_ctype = ContextType.VOICE
context = channel._compose_context(
wechatmp_msg.ctype, content, isgroup=False, origin_ctype=origin_ctype, msg=wechatmp_msg
)
else: else:
rtype = None
context = channel._compose_context(
ContextType.TEXT, message, isgroup=False, desire_rtype=rtype, msg=wechatmp_msg
)
context = channel._compose_context(
wechatmp_msg.ctype, content, isgroup=False, msg=wechatmp_msg
)
if context: if context:
# set private openai_api_key # set private openai_api_key
# if from_user is not changed in itchat, this can be placed at chat_channel # if from_user is not changed in itchat, this can be placed at chat_channel
@@ -59,18 +66,6 @@ class Query:
channel.produce(context) channel.produce(context)
# The reply will be sent by channel.send() in another thread # The reply will be sent by channel.send() in another thread
return "success" return "success"

elif wechatmp_msg.msg_type == "event":
logger.info(
"[wechatmp] Event {} from {}".format(
wechatmp_msg.Event, wechatmp_msg.from_user_id
)
)
content = subscribe_msg()
replyMsg = TextMsg(
wechatmp_msg.from_user_id, wechatmp_msg.to_user_id, content
)
return replyMsg.send()
else: else:
logger.info("暂且不处理") logger.info("暂且不处理")
return "success" return "success"


+ 10
- 18
channel/wechatmp/common.py Просмотреть файл

@@ -1,8 +1,10 @@
import hashlib
import textwrap import textwrap
import web


from config import conf from config import conf

from wechatpy.utils import check_signature
from wechatpy.crypto import WeChatCrypto
from wechatpy.exceptions import InvalidSignatureException
MAX_UTF8_LEN = 2048 MAX_UTF8_LEN = 2048




@@ -12,27 +14,17 @@ class WeChatAPIException(Exception):


def verify_server(data): def verify_server(data):
try: try:
if len(data) == 0:
return "None"
signature = data.signature signature = data.signature
timestamp = data.timestamp timestamp = data.timestamp
nonce = data.nonce nonce = data.nonce
echostr = data.echostr echostr = data.echostr
token = conf().get("wechatmp_token") # 请按照公众平台官网\基本配置中信息填写 token = conf().get("wechatmp_token") # 请按照公众平台官网\基本配置中信息填写

data_list = [token, timestamp, nonce]
data_list.sort()
sha1 = hashlib.sha1()
# map(sha1.update, data_list) #python2
sha1.update("".join(data_list).encode("utf-8"))
hashcode = sha1.hexdigest()
print("handle/GET func: hashcode, signature: ", hashcode, signature)
if hashcode == signature:
return echostr
else:
return ""
except Exception as Argument:
return Argument
check_signature(token, signature, timestamp, nonce)
return echostr
except InvalidSignatureException:
raise web.Forbidden("Invalid signature")
except Exception as e:
raise web.Forbidden(str(e))




def subscribe_msg(): def subscribe_msg():


+ 50
- 46
channel/wechatmp/passive_reply.py Просмотреть файл

@@ -3,15 +3,14 @@ import asyncio


import web import web


from channel.wechatmp.wechatmp_message import parse_xml
from channel.wechatmp.passive_reply_message import TextMsg, VoiceMsg, ImageMsg
from channel.wechatmp.wechatmp_message import WeChatMPMessage
from bridge.context import * from bridge.context import *
from bridge.reply import ReplyType
from channel.wechatmp.common import * from channel.wechatmp.common import *
from channel.wechatmp.wechatmp_channel import WechatMPChannel from channel.wechatmp.wechatmp_channel import WechatMPChannel
from common.log import logger from common.log import logger
from config import conf from config import conf

from wechatpy import parse_message
from wechatpy.replies import create_reply, ImageReply, VoiceReply


# This class is instantiated once per query # This class is instantiated once per query
class Query: class Query:
@@ -22,36 +21,49 @@ class Query:
try: try:
request_time = time.time() request_time = time.time()
channel = WechatMPChannel() channel = WechatMPChannel()
webData = web.data()
logger.debug("[wechatmp] Receive post data:\n" + webData.decode("utf-8"))
wechatmp_msg = parse_xml(webData)
if wechatmp_msg.msg_type == "text" or wechatmp_msg.msg_type == "voice":
message = web.data() # todo crypto
msg = parse_message(message)
logger.debug("[wechatmp] Receive post data:\n" + message.decode("utf-8"))
if msg.type == "event":
logger.info(
"[wechatmp] Event {} from {}".format(
msg.event, msg.source
)
)
reply_text = subscribe_msg()
replyPost = create_reply(reply_text, msg)
return replyPost.render()
wechatmp_msg = WeChatMPMessage(msg, client=channel.client)
if wechatmp_msg.ctype in [ContextType.TEXT, ContextType.IMAGE, ContextType.VOICE]:
from_user = wechatmp_msg.from_user_id from_user = wechatmp_msg.from_user_id
to_user = wechatmp_msg.to_user_id
message = wechatmp_msg.content
content = wechatmp_msg.content
message_id = wechatmp_msg.msg_id message_id = wechatmp_msg.msg_id


supported = True supported = True
if "【收到不支持的消息类型,暂无法显示】" in message:
if "【收到不支持的消息类型,暂无法显示】" in content:
supported = False # not supported, used to refresh supported = False # not supported, used to refresh


# New request # New request
if ( if (
from_user not in channel.cache_dict from_user not in channel.cache_dict
and from_user not in channel.running and from_user not in channel.running
or message.startswith("#")
or content.startswith("#")
and message_id not in channel.request_cnt # insert the godcmd and message_id not in channel.request_cnt # insert the godcmd
): ):
# The first query begin # The first query begin
if (wechatmp_msg.msg_type == "voice" and conf().get("voice_reply_voice") == True):
rtype = ReplyType.VOICE
if (msg.type == "voice" and wechatmp_msg.ctype == ContextType.TEXT):
origin_ctype = ContextType.VOICE
context = channel._compose_context(
wechatmp_msg.ctype, content, isgroup=False, origin_ctype=origin_ctype, msg=wechatmp_msg
)
else: else:
rtype = None
context = channel._compose_context(
ContextType.TEXT, message, isgroup=False, desire_rtype=rtype, msg=wechatmp_msg
)
context = channel._compose_context(
wechatmp_msg.ctype, content, isgroup=False, msg=wechatmp_msg
)
logger.debug( logger.debug(
"[wechatmp] context: {} {}".format(context, wechatmp_msg)
"[wechatmp] context: {} {} {}".format(context, wechatmp_msg, supported)
) )


if supported and context: if supported and context:
@@ -65,26 +77,27 @@ class Query:
trigger_prefix = conf().get("single_chat_prefix", [""]) trigger_prefix = conf().get("single_chat_prefix", [""])
if trigger_prefix or not supported: if trigger_prefix or not supported:
if trigger_prefix: if trigger_prefix:
content = textwrap.dedent(
reply_text = textwrap.dedent(
f"""\ f"""\
请输入'{trigger_prefix}'接你想说的话跟我说话。 请输入'{trigger_prefix}'接你想说的话跟我说话。
例如: 例如:
{trigger_prefix}你好,很高兴见到你。""" {trigger_prefix}你好,很高兴见到你。"""
) )
else: else:
content = textwrap.dedent(
reply_text = textwrap.dedent(
"""\ """\
你好,很高兴见到你。 你好,很高兴见到你。
请跟我说话吧。""" 请跟我说话吧。"""
) )
else: else:
logger.error(f"[wechatmp] unknown error") logger.error(f"[wechatmp] unknown error")
content = textwrap.dedent(
reply_text = textwrap.dedent(
"""\ """\
未知错误,请稍后再试""" 未知错误,请稍后再试"""
) )
replyPost = TextMsg(wechatmp_msg.from_user_id, wechatmp_msg.to_user_id, content).send()
return replyPost
replyPost = create_reply(reply_text, msg)
return replyPost.render()




# Wechat official server will request 3 times (5 seconds each), with the same message_id. # Wechat official server will request 3 times (5 seconds each), with the same message_id.
@@ -96,7 +109,7 @@ class Query:
request_cnt, request_cnt,
from_user, from_user,
message_id, message_id,
message,
content,
web.ctx.env.get("REMOTE_ADDR"), web.ctx.env.get("REMOTE_ADDR"),
web.ctx.env.get("REMOTE_PORT"), web.ctx.env.get("REMOTE_PORT"),
) )
@@ -121,8 +134,8 @@ class Query:
else: # request_cnt == 3: else: # request_cnt == 3:
# return timeout message # return timeout message
reply_text = "【正在思考中,回复任意文字尝试获取回复】" reply_text = "【正在思考中,回复任意文字尝试获取回复】"
replyPost = TextMsg(from_user, to_user, reply_text).send()
return replyPost
replyPost = create_reply(reply_text, msg)
return replyPost.render()


# reply is ready # reply is ready
channel.request_cnt.pop(message_id) channel.request_cnt.pop(message_id)
@@ -158,36 +171,27 @@ class Query:
request_cnt, request_cnt,
from_user, from_user,
message_id, message_id,
message,
content,
reply_text, reply_text,
) )
) )
replyPost = TextMsg(from_user, to_user, reply_text).send()
return replyPost
replyPost = create_reply(reply_text, msg)
return replyPost.render()



elif (reply_type == "voice"): elif (reply_type == "voice"):
media_id = content media_id = content
asyncio.run_coroutine_threadsafe(channel.delete_media(media_id), channel.delete_media_loop) asyncio.run_coroutine_threadsafe(channel.delete_media(media_id), channel.delete_media_loop)
replyPost = VoiceMsg(from_user, to_user, media_id).send()
return replyPost
replyPost = VoiceReply(message=msg)
replyPost.media_id = media_id
return replyPost.render()


elif (reply_type == "image"): elif (reply_type == "image"):
media_id = content media_id = content
asyncio.run_coroutine_threadsafe(channel.delete_media(media_id), channel.delete_media_loop) asyncio.run_coroutine_threadsafe(channel.delete_media(media_id), channel.delete_media_loop)
replyPost = ImageMsg(from_user, to_user, media_id).send()
return replyPost

elif wechatmp_msg.msg_type == "event":
logger.info(
"[wechatmp] Event {} from {}".format(
wechatmp_msg.content, wechatmp_msg.from_user_id
)
)
content = subscribe_msg()
replyMsg = TextMsg(
wechatmp_msg.from_user_id, wechatmp_msg.to_user_id, content
)
return replyMsg.send()
replyPost = ImageReply(message=msg)
replyPost.media_id = media_id
return replyPost.render()
else: else:
logger.info("暂且不处理") logger.info("暂且不处理")
return "success" return "success"


+ 89
- 36
channel/wechatmp/wechatmp_channel.py Просмотреть файл

@@ -13,10 +13,13 @@ from common.log import logger
from common.singleton import singleton from common.singleton import singleton
from config import conf from config import conf


from wechatpy.exceptions import WeChatClientException
import asyncio import asyncio
from threading import Thread from threading import Thread


import web import web

from voice.audio_convert import any_to_mp3
# If using SSL, uncomment the following lines, and modify the certificate path. # If using SSL, uncomment the following lines, and modify the certificate path.
# from cheroot.server import HTTPServer # from cheroot.server import HTTPServer
# from cheroot.ssl.builtin import BuiltinSSLAdapter # from cheroot.ssl.builtin import BuiltinSSLAdapter
@@ -31,7 +34,9 @@ class WechatMPChannel(ChatChannel):
super().__init__() super().__init__()
self.passive_reply = passive_reply self.passive_reply = passive_reply
self.NOT_SUPPORT_REPLYTYPE = [] self.NOT_SUPPORT_REPLYTYPE = []
self.client = WechatMPClient()
appid = conf().get("wechatmp_app_id")
secret = conf().get("wechatmp_app_secret")
self.client = WechatMPClient(appid, secret)
if self.passive_reply: if self.passive_reply:
# Cache the reply to the user's first message # Cache the reply to the user's first message
self.cache_dict = dict() self.cache_dict = dict()
@@ -62,7 +67,7 @@ class WechatMPChannel(ChatChannel):
async def delete_media(self, media_id): async def delete_media(self, media_id):
logger.debug("[wechatmp] permanent media {} will be deleted in 10s".format(media_id)) logger.debug("[wechatmp] permanent media {} will be deleted in 10s".format(media_id))
await asyncio.sleep(10) await asyncio.sleep(10)
self.client.delete_permanent_media(media_id)
self.client.material.delete(media_id)
logger.info("[wechatmp] permanent media {} has been deleted".format(media_id)) logger.info("[wechatmp] permanent media {} has been deleted".format(media_id))


def send(self, reply: Reply, context: Context): def send(self, reply: Reply, context: Context):
@@ -73,17 +78,17 @@ class WechatMPChannel(ChatChannel):
logger.info("[wechatmp] reply to {} cached:\n{}".format(receiver, reply_text)) logger.info("[wechatmp] reply to {} cached:\n{}".format(receiver, reply_text))
self.cache_dict[receiver] = ("text", reply_text) self.cache_dict[receiver] = ("text", reply_text)
elif reply.type == ReplyType.VOICE: 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_permanent_media("voice", (filename, f, "audio/mpeg"))
# 根据文件大小估计一个微信自动审核的时间,审核结束前返回将会导致语音无法播放,这个估计有待验证
f_size = os.fstat(f.fileno()).st_size
print(f_size)
time.sleep(1.0 + 2 * f_size / 1024 / 1024)
logger.info("[wechatmp] voice reply to {} uploaded: {}".format(receiver, media_id))
self.cache_dict[receiver] = ("voice", media_id)
try:
file_path = reply.content
response = self.client.material.add("voice", open(file_path, "rb"))
logger.debug("[wechatmp] upload voice response: {}".format(response))
except WeChatClientException as e:
logger.error("[wechatmp] upload voice failed: {}".format(e))
return
time.sleep(3) # todo check media_id
media_id = response["media_id"]
logger.info("[wechatmp] voice reply to {} uploaded: {}".format(receiver, media_id))
self.cache_dict[receiver] = ("voice", media_id)
elif reply.type == ReplyType.IMAGE_URL: # 从网络下载图片 elif reply.type == ReplyType.IMAGE_URL: # 从网络下载图片
img_url = reply.content img_url = reply.content
pic_res = requests.get(img_url, stream=True) pic_res = requests.get(img_url, stream=True)
@@ -93,33 +98,64 @@ class WechatMPChannel(ChatChannel):
image_storage.write(block) image_storage.write(block)
image_storage.seek(0) image_storage.seek(0)
image_type = imghdr.what(image_storage) image_type = imghdr.what(image_storage)
filename = receiver + "-" + context["msg"].msg_id + "." + image_type
filename = receiver + "-" + str(context['msg'].msg_id) + "." + image_type
content_type = "image/" + image_type content_type = "image/" + image_type
media_id = self.client.upload_permanent_media("image", (filename, image_storage, content_type))
logger.info("[wechatmp] image reply to {} uploaded: {}".format(receiver, media_id))
try:
response = self.client.material.add("image", (filename, image_storage, content_type))
logger.debug("[wechatmp] upload image response: {}".format(response))
except WeChatClientException as e:
logger.error("[wechatmp] upload image failed: {}".format(e))
return
media_id = response["media_id"]
logger.info(
"[wechatmp] image reply url={}, receiver={}".format(img_url, receiver)
)
self.cache_dict[receiver] = ("image", media_id) self.cache_dict[receiver] = ("image", media_id)
elif reply.type == ReplyType.IMAGE: # 从文件读取图片 elif reply.type == ReplyType.IMAGE: # 从文件读取图片
image_storage = reply.content image_storage = reply.content
image_storage.seek(0) image_storage.seek(0)
image_type = imghdr.what(image_storage) image_type = imghdr.what(image_storage)
filename = receiver + "-" + context["msg"].msg_id + "." + image_type
filename = receiver + "-" + str(context['msg'].msg_id) + "." + image_type
content_type = "image/" + image_type content_type = "image/" + image_type
media_id = self.client.upload_permanent_media("image", (filename, image_storage, content_type))
logger.info("[wechatmp] image reply to {} uploaded: {}".format(receiver, media_id))
try:
response = self.client.material.add("image", (filename, image_storage, content_type))
logger.debug("[wechatmp] upload image response: {}".format(response))
except WeChatClientException as e:
logger.error("[wechatmp] upload image failed: {}".format(e))
return
media_id = response["media_id"]
logger.info(
"[wechatmp] image reply url={}, receiver={}".format(img_url, receiver)
)
self.cache_dict[receiver] = ("image", media_id) self.cache_dict[receiver] = ("image", media_id)
else: else:
if reply.type == ReplyType.TEXT or reply.type == ReplyType.INFO or reply.type == ReplyType.ERROR: if reply.type == ReplyType.TEXT or reply.type == ReplyType.INFO or reply.type == ReplyType.ERROR:
reply_text = reply.content reply_text = reply.content
self.client.send_text(receiver, reply_text)
self.client.message.send_text(receiver, reply_text)
logger.info("[wechatmp] Do send to {}: {}".format(receiver, reply_text)) logger.info("[wechatmp] Do send to {}: {}".format(receiver, reply_text))
elif reply.type == ReplyType.VOICE: 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))
try:
file_path = reply.content
file_name = os.path.basename(file_path)
file_type = os.path.splitext(file_name)[1]
if file_type == ".mp3":
file_type = "audio/mpeg"
elif file_type == ".amr":
file_type = "audio/amr"
else:
mp3_file = os.path.splitext(file_path)[0] + ".mp3"
any_to_mp3(file_path, mp3_file)
file_path = mp3_file
file_name = os.path.basename(file_path)
file_type = "audio/mpeg"
logger.info("[wechatmp] file_name: {}, file_type: {} ".format(file_name, file_type))
response = self.client.media.upload("voice", (file_name, open(file_path, "rb"), file_type))
logger.debug("[wechatmp] upload voice response: {}".format(response))
except WeChatClientException as e:
logger.error("[wechatmp] upload voice failed: {}".format(e))
return
self.client.message.send_voice(receiver, response["media_id"])
logger.info("[wechatmp] Do send voice to {}".format(receiver))
elif reply.type == ReplyType.IMAGE_URL: # 从网络下载图片 elif reply.type == ReplyType.IMAGE_URL: # 从网络下载图片
img_url = reply.content img_url = reply.content
pic_res = requests.get(img_url, stream=True) pic_res = requests.get(img_url, stream=True)
@@ -129,21 +165,38 @@ class WechatMPChannel(ChatChannel):
image_storage.write(block) image_storage.write(block)
image_storage.seek(0) image_storage.seek(0)
image_type = imghdr.what(image_storage) image_type = imghdr.what(image_storage)
filename = receiver + "-" + context["msg"].msg_id + "." + image_type
filename = receiver + "-" + str(context['msg'].msg_id) + "." + image_type
content_type = "image/" + 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))
try:
response = self.client.media.upload("image", (filename, image_storage, content_type))
logger.debug("[wechatmp] upload image response: {}".format(response))
except WeChatClientException as e:
logger.error("[wechatmp] upload image failed: {}".format(e))
return
self.client.message.send_image(
receiver, response["media_id"]
)
logger.info(
"[wechatmp] sendImage url={}, receiver={}".format(img_url, receiver)
)
elif reply.type == ReplyType.IMAGE: # 从文件读取图片 elif reply.type == ReplyType.IMAGE: # 从文件读取图片
image_storage = reply.content image_storage = reply.content
image_storage.seek(0) image_storage.seek(0)
image_type = imghdr.what(image_storage) image_type = imghdr.what(image_storage)
filename = receiver + "-" + context["msg"].msg_id + "." + image_type
filename = receiver + "-" + str(context['msg'].msg_id) + "." + image_type
content_type = "image/" + 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))
try:
response = self.client.media.upload("image", (filename, image_storage, content_type))
logger.debug("[wechatmp] upload image response: {}".format(response))
except WeChatClientException as e:
logger.error("[wechatmp] upload image failed: {}".format(e))
return
self.client.message.send_image(
receiver, response["media_id"]
)
logger.info(
"[wechatmp] sendImage, receiver={}".format(receiver)
)
return return


def _success_callback(self, session_id, context, **kwargs): # 线程异常结束时的回调函数 def _success_callback(self, session_id, context, **kwargs): # 线程异常结束时的回调函数


+ 26
- 171
channel/wechatmp/wechatmp_client.py Просмотреть файл

@@ -3,178 +3,33 @@ import json
import requests import requests
import threading import threading
from channel.wechatmp.common import * from channel.wechatmp.common import *
from wechatpy.client import WeChatClient
from common.log import logger from common.log import logger
from config import conf 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:
if ret["errcode"] == 45009:
self.clear_quota_v2()
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}
ret = self.wechatmp_request(
method="post",
url=url,
params=params,
files=files
)
logger.debug("[wechatmp] media {} uploaded".format(media_file))
return ret["media_id"]


def upload_permanent_media(self, media_type, media_file):
url="https://api.weixin.qq.com/cgi-bin/material/add_material"
params={
"access_token": self.get_access_token(),
"type": media_type
}
files={"media": media_file}
ret = self.wechatmp_request(
method="post",
url=url,
params=params,
files=files
)
logger.debug("[wechatmp] permanent media {} uploaded".format(media_file))
return ret["media_id"]


def delete_permanent_media(self, media_id):
url="https://api.weixin.qq.com/cgi-bin/material/del_material"
params={
"access_token": self.get_access_token()
}
self.wechatmp_request(
method="post",
url=url,
params=params,
data=json.dumps({"media_id": media_id}, ensure_ascii=False).encode("utf8")
)
logger.debug("[wechatmp] permanent media {} deleted".format(media_id))

def clear_quota(self):
url="https://api.weixin.qq.com/cgi-bin/clear_quota"
params = {
"access_token": self.get_access_token()
}
self.wechatmp_request(
method="post",
url=url,
params=params,
data={"appid": self.app_id}
)
logger.debug("[wechatmp] API quata has been cleard")

def clear_quota_v2(self):
url="https://api.weixin.qq.com/cgi-bin/clear_quota/v2"
self.wechatmp_request(
method="post",
url=url,
data={"appid": self.app_id, "appsecret": self.app_secret}
)
logger.debug("[wechatmp] API quata has been cleard")
class WechatMPClient(WeChatClient):
def __init__(self, appid, secret, access_token=None,
session=None, timeout=None, auto_retry=True):
super(WechatMPClient, self).__init__(
appid, secret, access_token, session, timeout, auto_retry
)
self.fetch_access_token_lock = threading.Lock()

def fetch_access_token(self):
"""
获取 access token
详情请参考 http://mp.weixin.qq.com/wiki/index.php?title=通用接口文档

:return: 返回的 JSON 数据包
"""
with self.fetch_access_token_lock:
access_token = self.session.get(self.access_token_key)
if access_token:
if not self.expires_at:
return access_token
timestamp = time.time()
if self.expires_at - timestamp > 60:
return access_token
return super().fetch_access_token()

+ 53
- 38
channel/wechatmp/wechatmp_message.py Просмотреть файл

@@ -1,50 +1,65 @@
# -*- coding: utf-8 -*-# # -*- coding: utf-8 -*-#
# filename: receive.py
import xml.etree.ElementTree as ET


from bridge.context import ContextType from bridge.context import ContextType
from channel.chat_message import ChatMessage from channel.chat_message import ChatMessage
from common.log import logger from common.log import logger
from common.tmp_dir import TmpDir




def parse_xml(web_data):
if len(web_data) == 0:
return None
xmlData = ET.fromstring(web_data)
return WeChatMPMessage(xmlData)



class WeChatMPMessage(ChatMessage): class WeChatMPMessage(ChatMessage):
def __init__(self, xmlData):
super().__init__(xmlData)
self.to_user_id = xmlData.find("ToUserName").text
self.from_user_id = xmlData.find("FromUserName").text
self.create_time = xmlData.find("CreateTime").text
self.msg_type = xmlData.find("MsgType").text
try:
self.msg_id = xmlData.find("MsgId").text
except:
self.msg_id = self.from_user_id + self.create_time
def __init__(self, msg, client=None):
super().__init__(msg)
self.msg_id = msg.id
self.create_time = msg.time
self.is_group = False self.is_group = False


# reply to other_user_id
self.other_user_id = self.from_user_id

if self.msg_type == "text":
if msg.type == "text":
self.ctype = ContextType.TEXT self.ctype = ContextType.TEXT
self.content = xmlData.find("Content").text
elif self.msg_type == "voice":
self.ctype = ContextType.TEXT
self.content = xmlData.find("Recognition").text # 接收语音识别结果
# other voice_to_text method not implemented yet
if self.content == None:
self.content = "你好"
elif self.msg_type == "image":
# not implemented yet
self.pic_url = xmlData.find("PicUrl").text
self.media_id = xmlData.find("MediaId").text
elif self.msg_type == "event":
self.content = xmlData.find("Event").text
else: # video, shortvideo, location, link
# not implemented
pass
self.content = msg.content
elif msg.type == "voice":
if msg.recognition == None:
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"[wechatmp] Failed to download voice file, {response.content}"
)

self._prepare_fn = download_voice
else:
self.ctype = ContextType.TEXT
self.content = msg.recognition
elif msg.type == "image":
self.ctype = ContextType.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"[wechatmp] Failed to download image file, {response.content}"
)

self._prepare_fn = 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

+ 16
- 0
voice/audio_convert.py Просмотреть файл

@@ -33,6 +33,22 @@ def get_pcm_from_wav(wav_path):
wav = wave.open(wav_path, "rb") wav = wave.open(wav_path, "rb")
return wav.readframes(wav.getnframes()) return wav.readframes(wav.getnframes())


def any_to_mp3(any_path, mp3_path):
"""
把任意格式转成mp3文件
"""
if any_path.endswith(".mp3"):
shutil.copy2(any_path, mp3_path)
return
if (
any_path.endswith(".sil")
or any_path.endswith(".silk")
or any_path.endswith(".slk")
):
sil_to_wav(any_path, any_path)
any_path = mp3_path
audio = AudioSegment.from_file(any_path)
audio.export(mp3_path, format="mp3")


def any_to_wav(any_path, wav_path): def any_to_wav(any_path, wav_path):
""" """


Загрузка…
Отмена
Сохранить