|
1
|
|
|
# -*- coding: utf-8 -*- |
|
2
|
10 |
|
""" |
|
3
|
|
|
wechatpy.replies |
|
4
|
|
|
~~~~~~~~~~~~~~~~~~ |
|
5
|
|
|
This module defines all kinds of replies you can send to WeChat |
|
6
|
|
|
|
|
7
|
|
|
:copyright: (c) 2014 by messense. |
|
8
|
|
|
:license: MIT, see LICENSE for more details. |
|
9
|
|
|
""" |
|
10
|
10 |
|
from __future__ import absolute_import, unicode_literals |
|
11
|
10 |
|
import time |
|
12
|
10 |
|
import six |
|
13
|
10 |
|
import xmltodict |
|
14
|
|
|
|
|
15
|
10 |
|
from wechatpy.fields import ( |
|
16
|
|
|
StringField, |
|
17
|
|
|
IntegerField, |
|
18
|
|
|
ImageField, |
|
19
|
|
|
VoiceField, |
|
20
|
|
|
VideoField, |
|
21
|
|
|
MusicField, |
|
22
|
|
|
ArticlesField, |
|
23
|
|
|
Base64EncodeField, |
|
24
|
|
|
HardwareField, |
|
25
|
|
|
) |
|
26
|
10 |
|
from wechatpy.messages import BaseMessage, MessageMetaClass |
|
27
|
10 |
|
from wechatpy.utils import to_text, to_binary |
|
28
|
|
|
|
|
29
|
|
|
|
|
30
|
10 |
|
REPLY_TYPES = {} |
|
31
|
|
|
|
|
32
|
|
|
|
|
33
|
10 |
|
def register_reply(reply_type): |
|
34
|
10 |
|
def register(cls): |
|
35
|
10 |
|
REPLY_TYPES[reply_type] = cls |
|
36
|
10 |
|
return cls |
|
37
|
10 |
|
return register |
|
38
|
|
|
|
|
39
|
|
|
|
|
40
|
10 |
|
class BaseReply(six.with_metaclass(MessageMetaClass)): |
|
41
|
|
|
"""Base class for all replies""" |
|
42
|
10 |
|
source = StringField('FromUserName') |
|
43
|
10 |
|
target = StringField('ToUserName') |
|
44
|
10 |
|
time = IntegerField('CreateTime', time.time()) |
|
|
|
|
|
|
45
|
10 |
|
type = 'unknown' |
|
46
|
|
|
|
|
47
|
10 |
|
def __init__(self, **kwargs): |
|
48
|
10 |
|
self._data = {} |
|
49
|
10 |
|
message = kwargs.pop('message', None) |
|
50
|
10 |
|
if message and isinstance(message, BaseMessage): |
|
51
|
10 |
|
if 'source' not in kwargs: |
|
52
|
10 |
|
kwargs['source'] = message.target |
|
53
|
10 |
|
if 'target' not in kwargs: |
|
54
|
10 |
|
kwargs['target'] = message.source |
|
55
|
10 |
|
if hasattr(message, 'agent') and 'agent' not in kwargs: |
|
56
|
10 |
|
kwargs['agent'] = message.agent |
|
57
|
10 |
|
if 'time' not in kwargs: |
|
58
|
10 |
|
kwargs['time'] = time.time() |
|
59
|
10 |
|
for name, value in kwargs.items(): |
|
60
|
10 |
|
field = self._fields.get(name) |
|
61
|
10 |
|
if field: |
|
62
|
10 |
|
self._data[field.name] = value |
|
63
|
|
|
else: |
|
64
|
10 |
|
setattr(self, name, value) |
|
65
|
|
|
|
|
66
|
10 |
|
def render(self): |
|
67
|
|
|
"""Render reply from Python object to XML string""" |
|
68
|
10 |
|
tpl = '<xml>\n{data}\n</xml>' |
|
69
|
10 |
|
nodes = [] |
|
70
|
10 |
|
msg_type = '<MsgType><![CDATA[{msg_type}]]></MsgType>'.format( |
|
71
|
|
|
msg_type=self.type |
|
72
|
|
|
) |
|
73
|
10 |
|
nodes.append(msg_type) |
|
74
|
10 |
|
for name, field in self._fields.items(): |
|
75
|
10 |
|
value = getattr(self, name, field.default) |
|
76
|
10 |
|
node_xml = field.to_xml(value) |
|
77
|
10 |
|
nodes.append(node_xml) |
|
78
|
10 |
|
data = '\n'.join(nodes) |
|
79
|
10 |
|
return tpl.format(data=data) |
|
80
|
|
|
|
|
81
|
10 |
|
def __str__(self): |
|
82
|
|
|
if six.PY2: |
|
83
|
|
|
return to_binary(self.render()) |
|
84
|
|
|
else: |
|
85
|
|
|
return to_text(self.render()) |
|
86
|
|
|
|
|
87
|
|
|
|
|
88
|
10 |
|
@register_reply('empty') |
|
89
|
10 |
|
class EmptyReply(BaseReply): |
|
90
|
|
|
""" |
|
91
|
|
|
回复空串 |
|
92
|
|
|
|
|
93
|
|
|
微信服务器不会对此作任何处理,并且不会发起重试 |
|
94
|
|
|
""" |
|
95
|
10 |
|
def __init__(self): |
|
96
|
10 |
|
pass |
|
97
|
|
|
|
|
98
|
10 |
|
def render(self): |
|
99
|
10 |
|
return '' |
|
100
|
|
|
|
|
101
|
|
|
|
|
102
|
10 |
|
@register_reply('text') |
|
103
|
10 |
|
class TextReply(BaseReply): |
|
104
|
|
|
""" |
|
105
|
|
|
文本回复 |
|
106
|
|
|
详情请参阅 http://mp.weixin.qq.com/wiki/9/2c15b20a16019ae613d413e30cac8ea1.html |
|
107
|
|
|
""" |
|
108
|
10 |
|
type = 'text' |
|
109
|
10 |
|
content = StringField('Content') |
|
110
|
|
|
|
|
111
|
|
|
|
|
112
|
10 |
|
@register_reply('image') |
|
113
|
10 |
|
class ImageReply(BaseReply): |
|
114
|
|
|
""" |
|
115
|
|
|
图片回复 |
|
116
|
|
|
详情请参阅 |
|
117
|
|
|
http://mp.weixin.qq.com/wiki/9/2c15b20a16019ae613d413e30cac8ea1.html |
|
118
|
|
|
""" |
|
119
|
10 |
|
type = 'image' |
|
120
|
10 |
|
image = ImageField('Image') |
|
121
|
|
|
|
|
122
|
10 |
|
@property |
|
123
|
|
|
def media_id(self): |
|
124
|
10 |
|
return self.image |
|
125
|
|
|
|
|
126
|
10 |
|
@media_id.setter |
|
127
|
|
|
def media_id(self, value): |
|
128
|
10 |
|
self.image = value |
|
129
|
|
|
|
|
130
|
|
|
|
|
131
|
10 |
|
@register_reply('voice') |
|
132
|
10 |
|
class VoiceReply(BaseReply): |
|
133
|
|
|
""" |
|
134
|
|
|
语音回复 |
|
135
|
|
|
详情请参阅 |
|
136
|
|
|
http://mp.weixin.qq.com/wiki/9/2c15b20a16019ae613d413e30cac8ea1.html |
|
137
|
|
|
""" |
|
138
|
10 |
|
type = 'voice' |
|
139
|
10 |
|
voice = VoiceField('Voice') |
|
140
|
|
|
|
|
141
|
10 |
|
@property |
|
142
|
|
|
def media_id(self): |
|
143
|
10 |
|
return self.voice |
|
144
|
|
|
|
|
145
|
10 |
|
@media_id.setter |
|
146
|
|
|
def media_id(self, value): |
|
147
|
10 |
|
self.voice = value |
|
148
|
|
|
|
|
149
|
|
|
|
|
150
|
10 |
|
@register_reply('video') |
|
151
|
10 |
|
class VideoReply(BaseReply): |
|
152
|
|
|
""" |
|
153
|
|
|
视频回复 |
|
154
|
|
|
详情请参阅 |
|
155
|
|
|
http://mp.weixin.qq.com/wiki/9/2c15b20a16019ae613d413e30cac8ea1.html |
|
156
|
|
|
""" |
|
157
|
10 |
|
type = 'video' |
|
158
|
10 |
|
video = VideoField('Video', {}) |
|
159
|
|
|
|
|
160
|
10 |
|
@property |
|
161
|
|
|
def media_id(self): |
|
162
|
10 |
|
return self.video.get('media_id') |
|
163
|
|
|
|
|
164
|
10 |
|
@media_id.setter |
|
165
|
|
|
def media_id(self, value): |
|
166
|
10 |
|
video = self.video |
|
167
|
10 |
|
video['media_id'] = value |
|
168
|
10 |
|
self.video = video |
|
169
|
|
|
|
|
170
|
10 |
|
@property |
|
171
|
|
|
def title(self): |
|
172
|
10 |
|
return self.video.get('title') |
|
173
|
|
|
|
|
174
|
10 |
|
@title.setter |
|
175
|
|
|
def title(self, value): |
|
176
|
10 |
|
video = self.video |
|
177
|
10 |
|
video['title'] = value |
|
178
|
10 |
|
self.video = video |
|
179
|
|
|
|
|
180
|
10 |
|
@property |
|
181
|
|
|
def description(self): |
|
182
|
|
|
return self.video.get('description') |
|
183
|
|
|
|
|
184
|
10 |
|
@description.setter |
|
185
|
|
|
def description(self, value): |
|
186
|
10 |
|
video = self.video |
|
187
|
10 |
|
video['description'] = value |
|
188
|
10 |
|
self.video = video |
|
189
|
|
|
|
|
190
|
|
|
|
|
191
|
10 |
|
@register_reply('music') |
|
192
|
10 |
|
class MusicReply(BaseReply): |
|
193
|
|
|
""" |
|
194
|
|
|
音乐回复 |
|
195
|
|
|
详情请参阅 |
|
196
|
|
|
http://mp.weixin.qq.com/wiki/9/2c15b20a16019ae613d413e30cac8ea1.html |
|
197
|
|
|
""" |
|
198
|
10 |
|
type = 'music' |
|
199
|
10 |
|
music = MusicField('Music', {}) |
|
200
|
|
|
|
|
201
|
10 |
|
@property |
|
202
|
|
|
def thumb_media_id(self): |
|
203
|
10 |
|
return self.music.get('thumb_media_id') |
|
204
|
|
|
|
|
205
|
10 |
|
@thumb_media_id.setter |
|
206
|
|
|
def thumb_media_id(self, value): |
|
207
|
10 |
|
music = self.music |
|
208
|
10 |
|
music['thumb_media_id'] = value |
|
209
|
10 |
|
self.music = music |
|
210
|
|
|
|
|
211
|
10 |
|
@property |
|
212
|
|
|
def title(self): |
|
213
|
10 |
|
return self.music.get('title') |
|
214
|
|
|
|
|
215
|
10 |
|
@title.setter |
|
216
|
|
|
def title(self, value): |
|
217
|
10 |
|
music = self.music |
|
218
|
10 |
|
music['title'] = value |
|
219
|
10 |
|
self.music = music |
|
220
|
|
|
|
|
221
|
10 |
|
@property |
|
222
|
|
|
def description(self): |
|
223
|
10 |
|
return self.music.get('description') |
|
224
|
|
|
|
|
225
|
10 |
|
@description.setter |
|
226
|
|
|
def description(self, value): |
|
227
|
10 |
|
music = self.music |
|
228
|
10 |
|
music['description'] = value |
|
229
|
10 |
|
self.music = music |
|
230
|
|
|
|
|
231
|
10 |
|
@property |
|
232
|
|
|
def music_url(self): |
|
233
|
10 |
|
return self.music.get('music_url') |
|
234
|
|
|
|
|
235
|
10 |
|
@music_url.setter |
|
236
|
|
|
def music_url(self, value): |
|
237
|
10 |
|
music = self.music |
|
238
|
10 |
|
music['music_url'] = value |
|
239
|
10 |
|
self.music = music |
|
240
|
|
|
|
|
241
|
10 |
|
@property |
|
242
|
|
|
def hq_music_url(self): |
|
243
|
10 |
|
return self.music.get('hq_music_url') |
|
244
|
|
|
|
|
245
|
10 |
|
@hq_music_url.setter |
|
246
|
|
|
def hq_music_url(self, value): |
|
247
|
10 |
|
music = self.music |
|
248
|
10 |
|
music['hq_music_url'] = value |
|
249
|
10 |
|
self.music = music |
|
250
|
|
|
|
|
251
|
|
|
|
|
252
|
10 |
|
@register_reply('news') |
|
253
|
10 |
|
class ArticlesReply(BaseReply): |
|
254
|
|
|
""" |
|
255
|
|
|
图文回复 |
|
256
|
|
|
详情请参阅 |
|
257
|
|
|
http://mp.weixin.qq.com/wiki/9/2c15b20a16019ae613d413e30cac8ea1.html |
|
258
|
|
|
""" |
|
259
|
10 |
|
type = 'news' |
|
260
|
10 |
|
articles = ArticlesField('Articles', []) |
|
261
|
|
|
|
|
262
|
10 |
|
def add_article(self, article): |
|
263
|
10 |
|
if len(self.articles) == 10: |
|
264
|
|
|
raise AttributeError("Can't add more than 10 articles" |
|
265
|
|
|
" in an ArticlesReply") |
|
266
|
10 |
|
articles = self.articles |
|
267
|
10 |
|
articles.append(article) |
|
268
|
10 |
|
self.articles = articles |
|
269
|
|
|
|
|
270
|
|
|
|
|
271
|
10 |
|
@register_reply('transfer_customer_service') |
|
272
|
10 |
|
class TransferCustomerServiceReply(BaseReply): |
|
273
|
|
|
""" |
|
274
|
|
|
将消息转发到多客服 |
|
275
|
|
|
详情请参阅 |
|
276
|
|
|
http://mp.weixin.qq.com/wiki/5/ae230189c9bd07a6b221f48619aeef35.html |
|
277
|
|
|
""" |
|
278
|
10 |
|
type = 'transfer_customer_service' |
|
279
|
|
|
|
|
280
|
|
|
|
|
281
|
10 |
|
@register_reply('device_text') |
|
282
|
10 |
|
class DeviceTextReply(BaseReply): |
|
283
|
10 |
|
type = 'device_text' |
|
284
|
10 |
|
device_type = StringField('DeviceType') |
|
285
|
10 |
|
device_id = StringField('DeviceID') |
|
286
|
10 |
|
session_id = StringField('SessionID') |
|
287
|
10 |
|
content = Base64EncodeField('Content') |
|
288
|
|
|
|
|
289
|
|
|
|
|
290
|
10 |
|
@register_reply('device_event') |
|
291
|
10 |
|
class DeviceEventReply(BaseReply): |
|
292
|
10 |
|
type = 'device_event' |
|
293
|
10 |
|
event = StringField('Event') |
|
294
|
10 |
|
device_type = StringField('DeviceType') |
|
295
|
10 |
|
device_id = StringField('DeviceID') |
|
296
|
10 |
|
session_id = StringField('SessionID') |
|
297
|
10 |
|
content = Base64EncodeField('Content') |
|
298
|
|
|
|
|
299
|
|
|
|
|
300
|
10 |
|
@register_reply('device_status') |
|
301
|
10 |
|
class DeviceStatusReply(BaseReply): |
|
302
|
10 |
|
type = 'device_status' |
|
303
|
10 |
|
device_type = StringField('DeviceType') |
|
304
|
10 |
|
device_id = StringField('DeviceID') |
|
305
|
10 |
|
status = IntegerField('DeviceStatus') |
|
306
|
|
|
|
|
307
|
|
|
|
|
308
|
10 |
|
@register_reply('hardware') |
|
309
|
10 |
|
class HardwareReply(BaseReply): |
|
310
|
10 |
|
type = 'hardware' |
|
311
|
10 |
|
func_flag = IntegerField('FuncFlag', 0) |
|
312
|
10 |
|
hardware = HardwareField('HardWare') |
|
313
|
|
|
|
|
314
|
|
|
|
|
315
|
10 |
View Code Duplication |
def create_reply(reply, message=None, render=False): |
|
|
|
|
|
|
316
|
|
|
""" |
|
317
|
|
|
Create a reply quickly |
|
318
|
|
|
""" |
|
319
|
10 |
|
r = None |
|
320
|
10 |
|
if not reply: |
|
321
|
10 |
|
r = EmptyReply() |
|
322
|
10 |
|
elif isinstance(reply, BaseReply): |
|
323
|
10 |
|
r = reply |
|
324
|
10 |
|
if message: |
|
325
|
|
|
r.source = message.target |
|
326
|
|
|
r.target = message.source |
|
327
|
10 |
|
elif isinstance(reply, six.string_types): |
|
328
|
10 |
|
r = TextReply( |
|
329
|
|
|
message=message, |
|
330
|
|
|
content=reply |
|
331
|
|
|
) |
|
332
|
10 |
|
elif isinstance(reply, (tuple, list)): |
|
333
|
10 |
|
if len(reply) > 10: |
|
334
|
10 |
|
raise AttributeError("Can't add more than 10 articles" |
|
335
|
|
|
" in an ArticlesReply") |
|
336
|
10 |
|
r = ArticlesReply( |
|
337
|
|
|
message=message, |
|
338
|
|
|
articles=reply |
|
339
|
|
|
) |
|
340
|
10 |
|
if r and render: |
|
341
|
10 |
|
return r.render() |
|
342
|
10 |
|
return r |
|
343
|
|
|
|
|
344
|
|
|
|
|
345
|
10 |
|
def deserialize_reply(xml, update_time=False): |
|
346
|
|
|
""" |
|
347
|
|
|
反序列化被动回复 |
|
348
|
|
|
:param xml: 待反序列化的xml |
|
349
|
|
|
:param update_time: 是否用当前时间替换xml中的时间 |
|
350
|
|
|
:raises ValueError: 不能辨识的reply xml |
|
351
|
|
|
:rtype: wechatpy.replies.BaseReply |
|
352
|
|
|
""" |
|
353
|
10 |
|
if not xml: |
|
354
|
10 |
|
return EmptyReply() |
|
355
|
|
|
|
|
356
|
10 |
|
try: |
|
357
|
10 |
|
reply_dict = xmltodict.parse(xml)["xml"] |
|
358
|
10 |
|
msg_type = reply_dict["MsgType"] |
|
359
|
|
|
except (xmltodict.expat.ExpatError, KeyError): |
|
360
|
|
|
raise ValueError("bad reply xml") |
|
361
|
10 |
|
if msg_type not in REPLY_TYPES: |
|
362
|
|
|
raise ValueError("unknown reply type") |
|
363
|
|
|
|
|
364
|
10 |
|
cls = REPLY_TYPES[msg_type] |
|
365
|
10 |
|
kwargs = dict() |
|
366
|
10 |
|
for attr, field in cls._fields.items(): |
|
367
|
10 |
|
if field.name in reply_dict: |
|
368
|
10 |
|
str_value = reply_dict[field.name] |
|
369
|
10 |
|
kwargs[attr] = field.from_xml(str_value) |
|
370
|
|
|
|
|
371
|
10 |
|
if update_time: |
|
372
|
|
|
kwargs["time"] = time.time() |
|
373
|
|
|
|
|
374
|
|
|
return cls(**kwargs) |
|
375
|
|
|
|