mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-02-16 08:16:06 +08:00
Merge remote-tracking branch 'origin/wechatmp' into wechatmp
This commit is contained in:
@@ -2,15 +2,15 @@ import time
|
||||
|
||||
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.reply import ReplyType
|
||||
from bridge.reply import *
|
||||
from channel.wechatmp.common import *
|
||||
from channel.wechatmp.wechatmp_channel import WechatMPChannel
|
||||
from wechatpy import parse_message
|
||||
from common.log import logger
|
||||
from config import conf
|
||||
|
||||
from wechatpy.replies import create_reply
|
||||
|
||||
# This class is instantiated once per query
|
||||
class Query:
|
||||
@@ -21,16 +21,25 @@ class Query:
|
||||
# Make sure to return the instance that first created, @singleton will do that.
|
||||
channel = WechatMPChannel()
|
||||
try:
|
||||
webData = web.data()
|
||||
message = web.data() # todo crypto
|
||||
# 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
|
||||
)
|
||||
)
|
||||
if msg.event in ["subscribe", "subscribe_scan"]:
|
||||
reply_text = subscribe_msg()
|
||||
replyPost = create_reply(reply_text, msg)
|
||||
return replyPost.render()
|
||||
else:
|
||||
return "success"
|
||||
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
|
||||
message = wechatmp_msg.content
|
||||
content = wechatmp_msg.content
|
||||
message_id = wechatmp_msg.msg_id
|
||||
|
||||
logger.info(
|
||||
@@ -39,16 +48,17 @@ class Query:
|
||||
web.ctx.env.get("REMOTE_PORT"),
|
||||
from_user,
|
||||
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 and conf().get("voice_reply_voice", False):
|
||||
context = channel._compose_context(
|
||||
wechatmp_msg.ctype, content, isgroup=False, desire_rtype=ReplyType.VOICE, msg=wechatmp_msg
|
||||
)
|
||||
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:
|
||||
# set private openai_api_key
|
||||
# if from_user is not changed in itchat, this can be placed at chat_channel
|
||||
@@ -59,18 +69,6 @@ class Query:
|
||||
channel.produce(context)
|
||||
# The reply will be sent by channel.send() in another thread
|
||||
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:
|
||||
logger.info("暂且不处理")
|
||||
return "success"
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import hashlib
|
||||
import textwrap
|
||||
import web
|
||||
|
||||
from config import conf
|
||||
|
||||
from wechatpy.utils import check_signature
|
||||
from wechatpy.crypto import WeChatCrypto
|
||||
from wechatpy.exceptions import InvalidSignatureException
|
||||
MAX_UTF8_LEN = 2048
|
||||
|
||||
|
||||
@@ -12,38 +14,28 @@ class WeChatAPIException(Exception):
|
||||
|
||||
def verify_server(data):
|
||||
try:
|
||||
if len(data) == 0:
|
||||
return "None"
|
||||
signature = data.signature
|
||||
timestamp = data.timestamp
|
||||
nonce = data.nonce
|
||||
echostr = data.echostr
|
||||
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():
|
||||
trigger_prefix = conf().get("single_chat_prefix", [""])
|
||||
trigger_prefix = conf().get("single_chat_prefix", [""])[0]
|
||||
msg = textwrap.dedent(
|
||||
f"""\
|
||||
感谢您的关注!
|
||||
这里是ChatGPT,可以自由对话。
|
||||
资源有限,回复较慢,请勿着急。
|
||||
支持语音对话。
|
||||
暂时不支持图片输入。
|
||||
支持图片输入。
|
||||
支持图片输出,画字开头的消息将按要求创作图片。
|
||||
支持tool、角色扮演和文字冒险等丰富的插件。
|
||||
输入'{trigger_prefix}#帮助' 查看详细指令。"""
|
||||
|
||||
@@ -3,15 +3,15 @@ import asyncio
|
||||
|
||||
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.reply import ReplyType
|
||||
from bridge.reply import *
|
||||
from channel.wechatmp.common import *
|
||||
from channel.wechatmp.wechatmp_channel import WechatMPChannel
|
||||
from common.log import logger
|
||||
from config import conf
|
||||
|
||||
from wechatpy import parse_message
|
||||
from wechatpy.replies import create_reply, ImageReply, VoiceReply
|
||||
|
||||
# This class is instantiated once per query
|
||||
class Query:
|
||||
@@ -22,36 +22,38 @@ class Query:
|
||||
try:
|
||||
request_time = time.time()
|
||||
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 in ["text", "voice", "image"]:
|
||||
wechatmp_msg = WeChatMPMessage(msg, client=channel.client)
|
||||
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
|
||||
|
||||
supported = True
|
||||
if "【收到不支持的消息类型,暂无法显示】" in message:
|
||||
if "【收到不支持的消息类型,暂无法显示】" in content:
|
||||
supported = False # not supported, used to refresh
|
||||
|
||||
# New request
|
||||
if (
|
||||
from_user not in channel.cache_dict
|
||||
and from_user not in channel.running
|
||||
or message.startswith("#")
|
||||
or content.startswith("#")
|
||||
and message_id not in channel.request_cnt # insert the godcmd
|
||||
):
|
||||
# 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 and conf().get("voice_reply_voice", False):
|
||||
context = channel._compose_context(
|
||||
wechatmp_msg.ctype, content, isgroup=False, desire_rtype=ReplyType.VOICE, msg=wechatmp_msg
|
||||
)
|
||||
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(
|
||||
"[wechatmp] context: {} {}".format(context, wechatmp_msg)
|
||||
"[wechatmp] context: {} {} {}".format(context, wechatmp_msg, supported)
|
||||
)
|
||||
|
||||
if supported and context:
|
||||
@@ -65,26 +67,27 @@ class Query:
|
||||
trigger_prefix = conf().get("single_chat_prefix", [""])[0]
|
||||
if trigger_prefix or not supported:
|
||||
if trigger_prefix:
|
||||
content = textwrap.dedent(
|
||||
reply_text = textwrap.dedent(
|
||||
f"""\
|
||||
请输入'{trigger_prefix}'接你想说的话跟我说话。
|
||||
例如:
|
||||
{trigger_prefix}你好,很高兴见到你。"""
|
||||
)
|
||||
else:
|
||||
content = textwrap.dedent(
|
||||
reply_text = textwrap.dedent(
|
||||
"""\
|
||||
你好,很高兴见到你。
|
||||
请跟我说话吧。"""
|
||||
)
|
||||
else:
|
||||
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.
|
||||
@@ -98,7 +101,7 @@ class Query:
|
||||
message_id,
|
||||
web.ctx.env.get("REMOTE_ADDR"),
|
||||
web.ctx.env.get("REMOTE_PORT"),
|
||||
message
|
||||
content
|
||||
)
|
||||
)
|
||||
|
||||
@@ -121,8 +124,8 @@ class Query:
|
||||
else: # request_cnt == 3:
|
||||
# return timeout message
|
||||
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
|
||||
channel.request_cnt.pop(message_id)
|
||||
@@ -136,76 +139,80 @@ class Query:
|
||||
|
||||
# Only one request can access to the cached data
|
||||
try:
|
||||
(reply_type, content) = channel.cache_dict.pop(from_user)
|
||||
(reply_type, reply_content) = channel.cache_dict.pop(from_user)
|
||||
except KeyError:
|
||||
return "success"
|
||||
|
||||
if (reply_type == "text"):
|
||||
if len(content.encode("utf8")) <= MAX_UTF8_LEN:
|
||||
reply_text = content
|
||||
if len(reply_content.encode("utf8")) <= MAX_UTF8_LEN:
|
||||
reply_text = reply_content
|
||||
else:
|
||||
continue_text = "\n【未完待续,回复任意文字以继续】"
|
||||
splits = split_string_by_utf8_length(
|
||||
content,
|
||||
reply_content,
|
||||
MAX_UTF8_LEN - len(continue_text.encode("utf-8")),
|
||||
max_split=1,
|
||||
)
|
||||
reply_text = splits[0] + continue_text
|
||||
channel.cache_dict[from_user] = ("text", splits[1])
|
||||
|
||||
|
||||
logger.info(
|
||||
"[wechatmp] Request {} do send to {} {}: {}\n{}".format(
|
||||
request_cnt,
|
||||
from_user,
|
||||
message_id,
|
||||
message,
|
||||
content,
|
||||
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"):
|
||||
media_id = content
|
||||
media_id = reply_content
|
||||
asyncio.run_coroutine_threadsafe(channel.delete_media(media_id), channel.delete_media_loop)
|
||||
logger.info(
|
||||
"[wechatmp] Request {} do send to {} {}: {} voice media_id {}".format(
|
||||
request_cnt,
|
||||
from_user,
|
||||
message_id,
|
||||
message,
|
||||
content,
|
||||
media_id,
|
||||
)
|
||||
)
|
||||
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"):
|
||||
media_id = content
|
||||
media_id = reply_content
|
||||
asyncio.run_coroutine_threadsafe(channel.delete_media(media_id), channel.delete_media_loop)
|
||||
logger.info(
|
||||
"[wechatmp] Request {} do send to {} {}: {} image media_id {}".format(
|
||||
request_cnt,
|
||||
from_user,
|
||||
message_id,
|
||||
message,
|
||||
content,
|
||||
media_id,
|
||||
)
|
||||
)
|
||||
replyPost = ImageMsg(from_user, to_user, media_id).send()
|
||||
return replyPost
|
||||
replyPost = ImageReply(message=msg)
|
||||
replyPost.media_id = media_id
|
||||
return replyPost.render()
|
||||
|
||||
elif wechatmp_msg.msg_type == "event":
|
||||
elif msg.type == "event":
|
||||
logger.info(
|
||||
"[wechatmp] Event {} from {}".format(
|
||||
wechatmp_msg.content, wechatmp_msg.from_user_id
|
||||
msg.event, msg.source
|
||||
)
|
||||
)
|
||||
content = subscribe_msg()
|
||||
replyMsg = TextMsg(
|
||||
wechatmp_msg.from_user_id, wechatmp_msg.to_user_id, content
|
||||
)
|
||||
return replyMsg.send()
|
||||
if msg.event in ["subscribe", "subscribe_scan"]:
|
||||
reply_text = subscribe_msg()
|
||||
replyPost = create_reply(reply_text, msg)
|
||||
return replyPost.render()
|
||||
else:
|
||||
return "success"
|
||||
|
||||
else:
|
||||
logger.info("暂且不处理")
|
||||
return "success"
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
# -*- coding: utf-8 -*-#
|
||||
# filename: reply.py
|
||||
import time
|
||||
|
||||
|
||||
class Msg(object):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def send(self):
|
||||
return "success"
|
||||
|
||||
|
||||
class TextMsg(Msg):
|
||||
def __init__(self, toUserName, fromUserName, content):
|
||||
self.__dict = dict()
|
||||
self.__dict["ToUserName"] = toUserName
|
||||
self.__dict["FromUserName"] = fromUserName
|
||||
self.__dict["CreateTime"] = int(time.time())
|
||||
self.__dict["Content"] = content
|
||||
|
||||
def send(self):
|
||||
XmlForm = """
|
||||
<xml>
|
||||
<ToUserName><![CDATA[{ToUserName}]]></ToUserName>
|
||||
<FromUserName><![CDATA[{FromUserName}]]></FromUserName>
|
||||
<CreateTime>{CreateTime}</CreateTime>
|
||||
<MsgType><![CDATA[text]]></MsgType>
|
||||
<Content><![CDATA[{Content}]]></Content>
|
||||
</xml>
|
||||
"""
|
||||
return XmlForm.format(**self.__dict)
|
||||
|
||||
|
||||
class VoiceMsg(Msg):
|
||||
def __init__(self, toUserName, fromUserName, mediaId):
|
||||
self.__dict = dict()
|
||||
self.__dict["ToUserName"] = toUserName
|
||||
self.__dict["FromUserName"] = fromUserName
|
||||
self.__dict["CreateTime"] = int(time.time())
|
||||
self.__dict["MediaId"] = mediaId
|
||||
|
||||
def send(self):
|
||||
XmlForm = """
|
||||
<xml>
|
||||
<ToUserName><![CDATA[{ToUserName}]]></ToUserName>
|
||||
<FromUserName><![CDATA[{FromUserName}]]></FromUserName>
|
||||
<CreateTime>{CreateTime}</CreateTime>
|
||||
<MsgType><![CDATA[voice]]></MsgType>
|
||||
<Voice>
|
||||
<MediaId><![CDATA[{MediaId}]]></MediaId>
|
||||
</Voice>
|
||||
</xml>
|
||||
"""
|
||||
return XmlForm.format(**self.__dict)
|
||||
|
||||
|
||||
class ImageMsg(Msg):
|
||||
def __init__(self, toUserName, fromUserName, mediaId):
|
||||
self.__dict = dict()
|
||||
self.__dict["ToUserName"] = toUserName
|
||||
self.__dict["FromUserName"] = fromUserName
|
||||
self.__dict["CreateTime"] = int(time.time())
|
||||
self.__dict["MediaId"] = mediaId
|
||||
|
||||
def send(self):
|
||||
XmlForm = """
|
||||
<xml>
|
||||
<ToUserName><![CDATA[{ToUserName}]]></ToUserName>
|
||||
<FromUserName><![CDATA[{FromUserName}]]></FromUserName>
|
||||
<CreateTime>{CreateTime}</CreateTime>
|
||||
<MsgType><![CDATA[image]]></MsgType>
|
||||
<Image>
|
||||
<MediaId><![CDATA[{MediaId}]]></MediaId>
|
||||
</Image>
|
||||
</xml>
|
||||
"""
|
||||
return XmlForm.format(**self.__dict)
|
||||
@@ -4,25 +4,26 @@ import os
|
||||
import time
|
||||
import imghdr
|
||||
import requests
|
||||
import asyncio
|
||||
import threading
|
||||
from config import conf
|
||||
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.log import logger
|
||||
from common.singleton import singleton
|
||||
from config import conf
|
||||
|
||||
import asyncio
|
||||
from threading import Thread
|
||||
from voice.audio_convert import any_to_mp3
|
||||
from channel.chat_channel import ChatChannel
|
||||
from channel.wechatmp.common import *
|
||||
from channel.wechatmp.wechatmp_client import WechatMPClient
|
||||
from wechatpy.exceptions import WeChatClientException
|
||||
|
||||
import web
|
||||
# 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
|
||||
@@ -31,7 +32,9 @@ class WechatMPChannel(ChatChannel):
|
||||
super().__init__()
|
||||
self.passive_reply = passive_reply
|
||||
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:
|
||||
# Cache the reply to the user's first message
|
||||
self.cache_dict = dict()
|
||||
@@ -41,7 +44,7 @@ class WechatMPChannel(ChatChannel):
|
||||
self.request_cnt = dict()
|
||||
# The permanent media need to be deleted to avoid media number limit
|
||||
self.delete_media_loop = asyncio.new_event_loop()
|
||||
t = Thread(target=self.start_loop, args=(self.delete_media_loop,))
|
||||
t = threading.Thread(target=self.start_loop, args=(self.delete_media_loop,))
|
||||
t.setDaemon(True)
|
||||
t.start()
|
||||
|
||||
@@ -62,7 +65,7 @@ class WechatMPChannel(ChatChannel):
|
||||
async def delete_media(self, media_id):
|
||||
logger.debug("[wechatmp] permanent media {} will be deleted in 10s".format(media_id))
|
||||
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))
|
||||
|
||||
def send(self, reply: Reply, context: Context):
|
||||
@@ -73,16 +76,23 @@ class WechatMPChannel(ChatChannel):
|
||||
logger.info("[wechatmp] text cached, receiver {}\n{}".format(receiver, reply_text))
|
||||
self.cache_dict[receiver] = ("text", reply_text)
|
||||
elif reply.type == ReplyType.VOICE:
|
||||
voice_file_path = reply.content
|
||||
logger.debug("[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
|
||||
time.sleep(1.0 + 2 * f_size / 1024 / 1024)
|
||||
logger.info("[wechatmp] voice uploaded, receiver {}, media_id {}".format(receiver, media_id))
|
||||
self.cache_dict[receiver] = ("voice", media_id)
|
||||
try:
|
||||
voice_file_path = reply.content
|
||||
with open(voice_file_path, 'rb') as f:
|
||||
# support: <2M, <60s, mp3/wma/wav/amr
|
||||
response = self.client.material.add("voice", f)
|
||||
logger.debug("[wechatmp] upload voice response: {}".format(response))
|
||||
# 根据文件大小估计一个微信自动审核的时间,审核结束前返回将会导致语音无法播放,这个估计有待验证
|
||||
f_size = os.fstat(f.fileno()).st_size
|
||||
time.sleep(1.0 + 2 * f_size / 1024 / 1024)
|
||||
# todo check media_id
|
||||
except WeChatClientException as e:
|
||||
logger.error("[wechatmp] upload voice failed: {}".format(e))
|
||||
return
|
||||
media_id = response["media_id"]
|
||||
logger.info("[wechatmp] voice uploaded, receiver {}, media_id {}".format(receiver, media_id))
|
||||
self.cache_dict[receiver] = ("voice", media_id)
|
||||
|
||||
elif reply.type == ReplyType.IMAGE_URL: # 从网络下载图片
|
||||
img_url = reply.content
|
||||
pic_res = requests.get(img_url, stream=True)
|
||||
@@ -91,33 +101,61 @@ class WechatMPChannel(ChatChannel):
|
||||
image_storage.write(block)
|
||||
image_storage.seek(0)
|
||||
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
|
||||
media_id = self.client.upload_permanent_media("image", (filename, image_storage, content_type))
|
||||
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 uploaded, receiver {}, media_id {}".format(receiver, media_id))
|
||||
self.cache_dict[receiver] = ("image", media_id)
|
||||
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
|
||||
filename = receiver + "-" + str(context['msg'].msg_id) + "." + image_type
|
||||
content_type = "image/" + image_type
|
||||
media_id = self.client.upload_permanent_media("image", (filename, image_storage, content_type))
|
||||
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 uploaded, receiver {}, media_id {}".format(receiver, media_id))
|
||||
self.cache_dict[receiver] = ("image", media_id)
|
||||
else:
|
||||
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)
|
||||
self.client.message.send_text(receiver, reply_text)
|
||||
logger.info("[wechatmp] Do send text to {}: {}".format(receiver, reply_text))
|
||||
elif reply.type == ReplyType.VOICE:
|
||||
voice_file_path = reply.content
|
||||
logger.debug("[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))
|
||||
# support: <2M, <60s, AMR\MP3
|
||||
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: # 从网络下载图片
|
||||
img_url = reply.content
|
||||
pic_res = requests.get(img_url, stream=True)
|
||||
@@ -126,20 +164,29 @@ class WechatMPChannel(ChatChannel):
|
||||
image_storage.write(block)
|
||||
image_storage.seek(0)
|
||||
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 = 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)
|
||||
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] Do send image to {}".format(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
|
||||
filename = receiver + "-" + str(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)
|
||||
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] Do send image to {}".format(receiver))
|
||||
return
|
||||
|
||||
|
||||
@@ -1,180 +1,41 @@
|
||||
import time
|
||||
import json
|
||||
import requests
|
||||
import threading
|
||||
from channel.wechatmp.common import *
|
||||
from wechatpy.client import WeChatClient
|
||||
from common.log import logger
|
||||
from config import conf
|
||||
from wechatpy.exceptions import APILimitedException
|
||||
|
||||
|
||||
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"),
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
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))
|
||||
self.fetch_access_token_lock = threading.Lock()
|
||||
|
||||
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")
|
||||
return self.post("clear_quota", data={"appid": self.appid})
|
||||
|
||||
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")
|
||||
return self.post("clear_quota/v2", params={"appid": self.appid, "appsecret": self.secret})
|
||||
|
||||
def fetch_access_token(self): # 重载父类方法,加锁避免多线程重复获取access_token
|
||||
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()
|
||||
|
||||
def _request(self, method, url_or_endpoint, **kwargs): # 重载父类方法,遇到API限流时,清除quota后重试
|
||||
try:
|
||||
return super()._request(method, url_or_endpoint, **kwargs)
|
||||
except APILimitedException as e:
|
||||
logger.error("[wechatmp] API quata has been used up. {}".format(e))
|
||||
response = self.clear_quota_v2()
|
||||
logger.debug("[wechatmp] API quata has been cleard, {}".format(response))
|
||||
return super()._request(method, url_or_endpoint, **kwargs)
|
||||
@@ -1,50 +1,65 @@
|
||||
# -*- coding: utf-8 -*-#
|
||||
# filename: receive.py
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from bridge.context import ContextType
|
||||
from channel.chat_message import ChatMessage
|
||||
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):
|
||||
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
|
||||
|
||||
# reply to other_user_id
|
||||
self.other_user_id = self.from_user_id
|
||||
if msg.type == "text":
|
||||
self.ctype = ContextType.TEXT
|
||||
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直接存临时目录路径
|
||||
|
||||
if self.msg_type == "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
|
||||
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
|
||||
|
||||
@@ -18,6 +18,7 @@ pysilk_mod>=1.6.0 # needed by send voice
|
||||
|
||||
# wechatmp
|
||||
web.py
|
||||
wechatpy
|
||||
|
||||
# chatgpt-tool-hub plugin
|
||||
|
||||
|
||||
@@ -33,6 +33,22 @@ def get_pcm_from_wav(wav_path):
|
||||
wav = wave.open(wav_path, "rb")
|
||||
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):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user