replies.py 9.3 KB

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