Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit a777231

Browse files
committed
feat: wechatmp channel support voice/image reply
1 parent 3420902 commit a777231

File tree

6 files changed

+164
-44
lines changed

6 files changed

+164
-44
lines changed

‎channel/chat_channel.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,14 +144,14 @@ def _compose_context(self, ctype: ContextType, content, **kwargs):
144144
context.type = ContextType.TEXT
145145
context.content = content.strip()
146146
if (
147-
"desire_rtype"notincontext
147+
context["desire_rtype"] ==None
148148
and conf().get("always_reply_voice")
149149
and ReplyType.VOICE not in self.NOT_SUPPORT_REPLYTYPE
150150
):
151151
context["desire_rtype"] = ReplyType.VOICE
152152
elif context.type == ContextType.VOICE:
153153
if (
154-
"desire_rtype"notincontext
154+
context["desire_rtype"] ==None
155155
and conf().get("voice_reply_voice")
156156
and ReplyType.VOICE not in self.NOT_SUPPORT_REPLYTYPE
157157
):

‎channel/wechatmp/active_reply.py‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from channel.wechatmp.wechatmp_message import parse_xml
66
from channel.wechatmp.passive_reply_message import TextMsg
77
from bridge.context import *
8+
from bridge.reply import ReplyType
89
from channel.wechatmp.common import *
910
from channel.wechatmp.wechatmp_channel import WechatMPChannel
1011
from common.log import logger
@@ -29,7 +30,7 @@ def POST(self):
2930
# or wechatmp_msg.msg_type == "image"
3031
):
3132
from_user = wechatmp_msg.from_user_id
32-
message = wechatmp_msg.content.decode("utf-8")
33+
message = wechatmp_msg.content
3334
message_id = wechatmp_msg.msg_id
3435

3536
logger.info(
@@ -41,8 +42,9 @@ def POST(self):
4142
message,
4243
)
4344
)
45+
rtype = ReplyType.VOICE if wechatmp_msg.msg_type == "voice" else None
4446
context = channel._compose_context(
45-
ContextType.TEXT, message, isgroup=False, msg=wechatmp_msg
47+
ContextType.TEXT, message, isgroup=False, desire_rtype=rtype, msg=wechatmp_msg
4648
)
4749
if context:
4850
# set private openai_api_key

‎channel/wechatmp/passive_reply.py‎

Lines changed: 39 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import time
2+
import asyncio
23

34
import web
45

56
from channel.wechatmp.wechatmp_message import parse_xml
6-
from channel.wechatmp.passive_reply_message import TextMsg
7+
from channel.wechatmp.passive_reply_message import TextMsg, VoiceMsg, ImageMsg
78
from bridge.context import *
9+
from bridge.reply import ReplyType
810
from channel.wechatmp.common import *
911
from channel.wechatmp.wechatmp_channel import WechatMPChannel
1012
from common.log import logger
@@ -26,7 +28,7 @@ def POST(self):
2628
if wechatmp_msg.msg_type == "text" or wechatmp_msg.msg_type == "voice":
2729
from_user = wechatmp_msg.from_user_id
2830
to_user = wechatmp_msg.to_user_id
29-
message = wechatmp_msg.content.decode("utf-8")
31+
message = wechatmp_msg.content
3032
message_id = wechatmp_msg.msg_id
3133

3234
supported = True
@@ -41,8 +43,9 @@ def POST(self):
4143
and message_id not in channel.request_cnt # insert the godcmd
4244
):
4345
# The first query begin
46+
rtype = ReplyType.VOICE if wechatmp_msg.msg_type == "voice" else None
4447
context = channel._compose_context(
45-
ContextType.TEXT, message, isgroup=False, msg=wechatmp_msg
48+
ContextType.TEXT, message, isgroup=False, desire_rtype=rtype, msg=wechatmp_msg
4649
)
4750
logger.debug(
4851
"[wechatmp] context: {} {}".format(context, wechatmp_msg)
@@ -115,10 +118,10 @@ def POST(self):
115118
else: # request_cnt == 3:
116119
# return timeout message
117120
reply_text = "【正在思考中,回复任意文字尝试获取回复】"
118-
# replyPost = reply.TextMsg(from_user, to_user, reply_text).send()
119-
# return replyPost
121+
replyPost = TextMsg(from_user, to_user, reply_text).send()
122+
return replyPost
120123

121-
# reply or reply_text is ready
124+
# reply is ready
122125
channel.request_cnt.pop(message_id)
123126

124127
# no return because of bandwords or other reasons
@@ -128,14 +131,13 @@ def POST(self):
128131
):
129132
return "success"
130133

131-
# reply is ready
132-
if from_user in channel.cache_dict:
133-
# Only one message thread can access to the cached data
134-
try:
135-
content = channel.cache_dict.pop(from_user)
136-
except KeyError:
137-
return "success"
134+
# Only one request can access to the cached data
135+
try:
136+
(reply_type, content) = channel.cache_dict.pop(from_user)
137+
except KeyError:
138+
return "success"
138139

140+
if (reply_type == "text"):
139141
if len(content.encode("utf8")) <= MAX_UTF8_LEN:
140142
reply_text = content
141143
else:
@@ -146,19 +148,31 @@ def POST(self):
146148
max_split=1,
147149
)
148150
reply_text = splits[0] + continue_text
149-
channel.cache_dict[from_user] = splits[1]
150-
151-
logger.info(
152-
"[wechatmp] Request {} do send to {} {}: {}\n{}".format(
153-
request_cnt,
154-
from_user,
155-
message_id,
156-
message,
157-
reply_text,
151+
channel.cache_dict[from_user] = ("text", splits[1])
152+
153+
logger.info(
154+
"[wechatmp] Request {} do send to {} {}: {}\n{}".format(
155+
request_cnt,
156+
from_user,
157+
message_id,
158+
message,
159+
reply_text,
160+
)
158161
)
159-
)
160-
replyPost = TextMsg(from_user, to_user, reply_text).send()
161-
return replyPost
162+
replyPost = TextMsg(from_user, to_user, reply_text).send()
163+
return replyPost
164+
165+
elif (reply_type == "voice"):
166+
media_id = content
167+
asyncio.run_coroutine_threadsafe(channel.delete_media(media_id), channel.delete_media_loop)
168+
replyPost = VoiceMsg(from_user, to_user, media_id).send()
169+
return replyPost
170+
171+
elif (reply_type == "image"):
172+
media_id = content
173+
asyncio.run_coroutine_threadsafe(channel.delete_media(media_id), channel.delete_media_loop)
174+
replyPost = ImageMsg(from_user, to_user, media_id).send()
175+
return replyPost
162176

163177
elif wechatmp_msg.msg_type == "event":
164178
logger.info(

‎channel/wechatmp/wechatmp_channel.py‎

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# -*- coding: utf-8 -*-
22
import io
3+
import os
4+
import time
35
import imghdr
46
import requests
57
from bridge.context import *
@@ -11,6 +13,9 @@
1113
from common.singleton import singleton
1214
from config import conf
1315

16+
import asyncio
17+
from threading import Thread
18+
1419
import web
1520
# If using SSL, uncomment the following lines, and modify the certificate path.
1621
# from cheroot.server import HTTPServer
@@ -25,19 +30,20 @@ class WechatMPChannel(ChatChannel):
2530
def __init__(self, passive_reply=True):
2631
super().__init__()
2732
self.passive_reply = passive_reply
28-
self.flag = 0
29-
33+
self.NOT_SUPPORT_REPLYTYPE = []
34+
self.client=WechatMPClient()
3035
if self.passive_reply:
31-
self.NOT_SUPPORT_REPLYTYPE = [ReplyType.IMAGE, ReplyType.VOICE]
3236
# Cache the reply to the user's first message
3337
self.cache_dict = dict()
3438
# Record whether the current message is being processed
3539
self.running = set()
3640
# Count the request from wechat official server by message_id
3741
self.request_cnt = dict()
38-
else:
39-
self.NOT_SUPPORT_REPLYTYPE = []
40-
self.client = WechatMPClient()
42+
# The permanent media need to be deleted to avoid media number limit
43+
self.delete_media_loop = asyncio.new_event_loop()
44+
t = Thread(target=self.start_loop, args=(self.delete_media_loop,))
45+
t.setDaemon(True)
46+
t.start()
4147

4248

4349
def startup(self):
@@ -49,18 +55,63 @@ def startup(self):
4955
port = conf().get("wechatmp_port", 8080)
5056
web.httpserver.runsimple(app.wsgifunc(), ("0.0.0.0", port))
5157

58+
def start_loop(self, loop):
59+
asyncio.set_event_loop(loop)
60+
loop.run_forever()
61+
62+
async def delete_media(self, media_id):
63+
logger.info("[wechatmp] media {} will be deleted in 10s".format(media_id))
64+
await asyncio.sleep(10)
65+
self.client.delete_permanent_media(media_id)
66+
logger.info("[wechatmp] media {} has been deleted".format(media_id))
5267

5368
def send(self, reply: Reply, context: Context):
5469
receiver = context["receiver"]
5570
if self.passive_reply:
56-
logger.info("[wechatmp] reply to {} cached:\n{}".format(receiver, reply))
57-
self.cache_dict[receiver] = reply.content
71+
if reply.type == ReplyType.TEXT or reply.type == ReplyType.INFO or reply.type == ReplyType.ERROR:
72+
reply_text = reply.content
73+
logger.info("[wechatmp] reply to {} cached:\n{}".format(receiver, reply_text))
74+
self.cache_dict[receiver] = ("text", reply_text)
75+
elif reply.type == ReplyType.VOICE:
76+
voice_file_path = reply.content
77+
logger.info("[wechatmp] voice file path {}".format(voice_file_path))
78+
with open(voice_file_path, 'rb') as f:
79+
filename = receiver + "-" + context["msg"].msg_id + ".mp3"
80+
media_id = self.client.upload_permanent_media("voice", (filename, f, "audio/mpeg"))
81+
# 根据文件大小估计一个微信自动审核的时间,审核结束前返回将会导致语音无法播放,这个估计有待验证
82+
f_size = os.fstat(f.fileno()).st_size
83+
print(f_size)
84+
time.sleep(1.0 + 2 * f_size / 1024 / 1024)
85+
logger.info("[wechatmp] voice reply to {} uploaded: {}".format(receiver, media_id))
86+
self.cache_dict[receiver] = ("voice", media_id)
87+
elif reply.type == ReplyType.IMAGE_URL: # 从网络下载图片
88+
img_url = reply.content
89+
pic_res = requests.get(img_url, stream=True)
90+
print(pic_res.headers)
91+
image_storage = io.BytesIO()
92+
for block in pic_res.iter_content(1024):
93+
image_storage.write(block)
94+
image_storage.seek(0)
95+
image_type = imghdr.what(image_storage)
96+
filename = receiver + "-" + context["msg"].msg_id + "." + image_type
97+
content_type = "image/" + image_type
98+
media_id = self.client.upload_permanent_media("image", (filename, image_storage, content_type))
99+
logger.info("[wechatmp] image reply to {} uploaded: {}".format(receiver, media_id))
100+
self.cache_dict[receiver] = ("image", media_id)
101+
elif reply.type == ReplyType.IMAGE: # 从文件读取图片
102+
image_storage = reply.content
103+
image_storage.seek(0)
104+
image_type = imghdr.what(image_storage)
105+
filename = receiver + "-" + context["msg"].msg_id + "." + image_type
106+
content_type = "image/" + image_type
107+
media_id = self.client.upload_permanent_media("image", (filename, image_storage, content_type))
108+
logger.info("[wechatmp] image reply to {} uploaded: {}".format(receiver, media_id))
109+
self.cache_dict[receiver] = ("image", media_id)
58110
else:
59111
if reply.type == ReplyType.TEXT or reply.type == ReplyType.INFO or reply.type == ReplyType.ERROR:
60112
reply_text = reply.content
61113
self.client.send_text(receiver, reply_text)
62114
logger.info("[wechatmp] Do send to {}: {}".format(receiver, reply_text))
63-
64115
elif reply.type == ReplyType.VOICE:
65116
voice_file_path = reply.content
66117
logger.info("[wechatmp] voice file path {}".format(voice_file_path))
@@ -69,7 +120,6 @@ def send(self, reply: Reply, context: Context):
69120
media_id = self.client.upload_media("voice", (filename, f, "audio/mpeg"))
70121
self.client.send_voice(receiver, media_id)
71122
logger.info("[wechatmp] Do send voice to {}".format(receiver))
72-
73123
elif reply.type == ReplyType.IMAGE_URL: # 从网络下载图片
74124
img_url = reply.content
75125
pic_res = requests.get(img_url, stream=True)
@@ -85,7 +135,6 @@ def send(self, reply: Reply, context: Context):
85135
media_id = self.client.upload_media("image", (filename, image_storage, content_type))
86136
self.client.send_image(receiver, media_id)
87137
logger.info("[wechatmp] sendImage url={}, receiver={}".format(img_url, receiver))
88-
89138
elif reply.type == ReplyType.IMAGE: # 从文件读取图片
90139
image_storage = reply.content
91140
image_storage.seek(0)
@@ -95,7 +144,6 @@ def send(self, reply: Reply, context: Context):
95144
media_id = self.client.upload_media("image", (filename, image_storage, content_type))
96145
self.client.send_image(receiver, media_id)
97146
logger.info("[wechatmp] sendImage, receiver={}".format(receiver))
98-
99147
return
100148

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

‎channel/wechatmp/wechatmp_client.py‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ def wechatmp_request(self, method, url, **kwargs):
2323
r.encoding = "utf-8"
2424
ret = r.json()
2525
if "errcode" in ret and ret["errcode"] != 0:
26+
if ret["errcode"] == 45009:
27+
self.clear_quota_v2()
2628
raise WeChatAPIException("{}".format(ret))
2729
return ret
2830

@@ -123,3 +125,54 @@ def upload_media(self, media_type, media_file):
123125
files=files
124126
)
125127
return ret["media_id"]
128+
129+
130+
def upload_permanent_media(self, media_type, media_file):
131+
url="https://api.weixin.qq.com/cgi-bin/material/add_material"
132+
params={
133+
"access_token": self.get_access_token(),
134+
"type": media_type
135+
}
136+
files={"media": media_file}
137+
logger.info("[wechatmp] media {} uploaded".format(media_file))
138+
ret = self.wechatmp_request(
139+
method="post",
140+
url=url,
141+
params=params,
142+
files=files
143+
)
144+
return ret["media_id"]
145+
146+
147+
def delete_permanent_media(self, media_id):
148+
url="https://api.weixin.qq.com/cgi-bin/material/del_material"
149+
params={
150+
"access_token": self.get_access_token()
151+
}
152+
logger.info("[wechatmp] media {} deleted".format(media_id))
153+
self.wechatmp_request(
154+
method="post",
155+
url=url,
156+
params=params,
157+
data={"media_id": media_id}
158+
)
159+
160+
def clear_quota(self):
161+
url="https://api.weixin.qq.com/cgi-bin/clear_quota"
162+
params = {
163+
"access_token": self.get_access_token()
164+
}
165+
self.wechatmp_request(
166+
method="post",
167+
url=url,
168+
params=params,
169+
data={"appid": self.app_id}
170+
)
171+
172+
def clear_quota_v2(self):
173+
url="https://api.weixin.qq.com/cgi-bin/clear_quota/v2"
174+
self.wechatmp_request(
175+
method="post",
176+
url=url,
177+
data={"appid": self.app_id, "appsecret": self.app_secret}
178+
)

‎channel/wechatmp/wechatmp_message.py‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,15 @@ def __init__(self, xmlData):
3232

3333
if self.msg_type == "text":
3434
self.ctype = ContextType.TEXT
35-
self.content = xmlData.find("Content").text.encode("utf-8")
35+
self.content = xmlData.find("Content").text
3636
elif self.msg_type == "voice":
3737
self.ctype = ContextType.TEXT
38-
self.content = xmlData.find("Recognition").text.encode("utf-8") # 接收语音识别结果
38+
self.content = xmlData.find("Recognition").text # 接收语音识别结果
39+
# other voice_to_text method not implemented yet
40+
if self.content == None:
41+
self.content = "你好"
3942
elif self.msg_type == "image":
40-
# not implemented
43+
# not implemented yet
4144
self.pic_url = xmlData.find("PicUrl").text
4245
self.media_id = xmlData.find("MediaId").text
4346
elif self.msg_type == "event":

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /