msg.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. # -*- coding: utf-8 -*-
  2. from __future__ import unicode_literals
  3. import time
  4. import hashlib
  5. from datetime import datetime
  6. from .base import WeixinError
  7. try:
  8. from flask import request, Response
  9. except ImportError:
  10. request, Response = None, None
  11. try:
  12. from django.http import HttpResponse, HttpResponseForbidden, HttpResponseNotAllowed
  13. except Exception:
  14. HttpResponse, HttpResponseForbidden, HttpResponseNotAllowed = None, None, None
  15. try:
  16. from lxml import etree
  17. except ImportError:
  18. from xml.etree import cElementTree as etree
  19. except ImportError:
  20. from xml.etree import ElementTree as etree
  21. __all__ = ("WeixinMsgError", "WeixinMsg")
  22. class WeixinMsgError(WeixinError):
  23. def __init__(self, msg):
  24. super(WeixinMsgError, self).__init__(msg)
  25. class WeixinMsg(object):
  26. def __init__(self, token, sender=None, expires_in=0):
  27. self.token = token
  28. self.sender = sender
  29. self.expires_in = expires_in
  30. self._registry = dict()
  31. def validate(self, signature, timestamp, nonce):
  32. if not self.token:
  33. raise WeixinMsgError("weixin token is missing")
  34. if self.expires_in:
  35. try:
  36. timestamp = int(timestamp)
  37. except ValueError:
  38. return False
  39. delta = time.time() - timestamp
  40. if delta < 0 or delta > self.expires_in:
  41. return False
  42. values = [self.token, str(timestamp), str(nonce)]
  43. s = ''.join(sorted(values))
  44. hsh = hashlib.sha1(s.encode("utf-8")).hexdigest()
  45. return signature == hsh
  46. def parse(self, content):
  47. raw = {}
  48. root = etree.fromstring(content)
  49. for child in root:
  50. raw[child.tag] = child.text
  51. formatted = self.format(raw)
  52. msg_type = formatted['type']
  53. msg_parser = getattr(self, 'parse_{0}'.format(msg_type), None)
  54. if callable(msg_parser):
  55. parsed = msg_parser(raw)
  56. else:
  57. parsed = self.parse_invalid_type(raw)
  58. formatted.update(parsed)
  59. return formatted
  60. def format(self, kwargs):
  61. timestamp = int(kwargs['CreateTime'])
  62. return {
  63. 'id': kwargs.get('MsgId'),
  64. 'timestamp': timestamp,
  65. 'receiver': kwargs['ToUserName'],
  66. 'sender': kwargs['FromUserName'],
  67. 'type': kwargs['MsgType'],
  68. 'time': datetime.fromtimestamp(timestamp),
  69. }
  70. def parse_text(self, raw):
  71. return {'content': raw['Content']}
  72. def parse_image(self, raw):
  73. return {'picurl': raw['PicUrl']}
  74. def parse_location(self, raw):
  75. return {
  76. 'location_x': raw['Location_X'],
  77. 'location_y': raw['Location_Y'],
  78. 'scale': int(raw.get('Scale', 0)),
  79. 'label': raw['Label'],
  80. }
  81. def parse_link(self, raw):
  82. return {
  83. 'title': raw['Title'],
  84. 'description': raw['Description'],
  85. 'url': raw['url'],
  86. }
  87. def parse_voice(self, raw):
  88. return {
  89. 'media_id': raw['MediaId'],
  90. 'format': raw['Format'],
  91. 'recognition': raw['Recognition'],
  92. }
  93. def parse_video(self, raw):
  94. return {
  95. 'media_id': raw['MediaId'],
  96. 'thumb_media_id': raw['ThumbMediaId'],
  97. }
  98. def parse_shortvideo(self, raw):
  99. return {
  100. 'media_id': raw['MediaId'],
  101. 'thumb_media_id': raw['ThumbMediaId'],
  102. }
  103. def parse_event(self, raw):
  104. return {
  105. 'event': raw.get('Event'),
  106. 'event_key': raw.get('EventKey'),
  107. 'ticket': raw.get('Ticket'),
  108. 'latitude': raw.get('Latitude'),
  109. 'longitude': raw.get('Longitude'),
  110. 'precision': raw.get('Precision'),
  111. 'status': raw.get('status')
  112. }
  113. def parse_invalid_type(self, raw):
  114. return {}
  115. def reply(self, username=None, type='text', sender=None, **kwargs):
  116. if not username:
  117. raise RuntimeError("username is missing")
  118. sender = sender or self.sender
  119. if not sender:
  120. raise RuntimeError('WEIXIN_SENDER or sender argument is missing')
  121. if type == 'text':
  122. content = kwargs.get('content', '')
  123. return text_reply(username, sender, content)
  124. if type == 'music':
  125. values = {}
  126. for k in ('title', 'description', 'music_url', 'hq_music_url'):
  127. values[k] = kwargs[k]
  128. return music_reply(username, sender, **values)
  129. if type == 'news':
  130. items = kwargs['articles']
  131. return news_reply(username, sender, *items)
  132. if type == 'customer_service':
  133. service_account = kwargs['service_account']
  134. return transfer_customer_service_reply(username, sender,
  135. service_account)
  136. if type == 'image':
  137. media_id = kwargs.get['media_id']
  138. return image_reply(username, sender, media_id)
  139. if type == 'voice':
  140. media_id = kwargs.get['media_id']
  141. return voice_reply(username, sender, media_id)
  142. if type == 'video':
  143. values = {}
  144. for k in ('media_id', 'title', 'description'):
  145. values[k] = kwargs[k]
  146. return video_reply(username, sender, **values)
  147. def register(self, type, key=None, func=None):
  148. if func:
  149. key = '*' if not key else key
  150. self._registry.setdefault(type, dict())[key] = func
  151. return func
  152. return self.__call__(type, key)
  153. def __call__(self, type, key):
  154. def wrapper(func):
  155. self.register(type, key, func)
  156. return func
  157. return wrapper
  158. @property
  159. def all(self):
  160. return self.register('*')
  161. def text(self, key='*'):
  162. return self.register('text', key)
  163. def __getattr__(self, key):
  164. key = key.lower()
  165. if key in ['image', 'video', 'voice', 'shortvideo', 'location', 'link', 'event']:
  166. return self.register(key)
  167. if key in ['subscribe', 'unsubscribe', 'location', 'click', 'view', 'scan', \
  168. 'scancode_push', 'scancode_waitmsg', 'pic_sysphoto', \
  169. 'pic_photo_or_album', 'pic_weixin', 'location_select', \
  170. 'qualification_verify_success', 'qualification_verify_fail', 'naming_verify_success', \
  171. 'naming_verify_fail', 'annual_renew', 'verify_expired', \
  172. 'card_pass_check', 'user_get_card', 'user_del_card', 'user_consume_card', \
  173. 'user_pay_from_pay_cell', 'user_view_card', 'user_enter_session_from_card', \
  174. 'card_sku_remind']:
  175. return self.register('event', key)
  176. raise AttributeError('invalid attribute "' + key + '"')
  177. def django_view_func(self):
  178. def run(request):
  179. if HttpResponse is None:
  180. raise RuntimeError('django_view_func need Django be installed')
  181. signature = request.GET.get('signature')
  182. timestamp = request.GET.get('timestamp')
  183. nonce = request.GET.get('nonce')
  184. if not self.validate(signature, timestamp, nonce):
  185. return HttpResponseForbidden('signature failed')
  186. if request.method == 'GET':
  187. echostr = request.args.get('echostr', '')
  188. return HttpResponse(echostr)
  189. elif request.method == "POST":
  190. try:
  191. ret = self.parse(request.body)
  192. except ValueError:
  193. return HttpResponseForbidden('invalid')
  194. func = None
  195. type = ret['type']
  196. _registry = self._registry.get(type, dict())
  197. if type == 'text':
  198. if ret['content'] in _registry:
  199. func = _registry[ret['content']]
  200. elif type == 'event':
  201. if ret['event'].lower() in _registry:
  202. func = _registry[ret['event'].lower()]
  203. if func is None and '*' in _registry:
  204. func = _registry['*']
  205. if func is None and '*' in self._registry:
  206. func = self._registry.get('*', dict()).get('*')
  207. text = ''
  208. if func is None:
  209. text = 'failed'
  210. if callable(func):
  211. text = func(**ret)
  212. content = ''
  213. if isinstance(text, basestring):
  214. if text:
  215. content = self.reply(
  216. username=ret['sender'],
  217. sender=ret['receiver'],
  218. content=text,
  219. )
  220. elif isinstance(text, dict):
  221. text.setdefault('username', ret['sender'])
  222. text.setdefault('sender', ret['receiver'])
  223. content = self.reply(**text)
  224. return HttpResponse(content, content_type='text/xml; charset=utf-8')
  225. return HttpResponseNotAllowed(['GET', 'POST'])
  226. return run
  227. def view_func(self):
  228. if request is None:
  229. raise RuntimeError('view_func need Flask be installed')
  230. signature = request.args.get('signature')
  231. timestamp = request.args.get('timestamp')
  232. nonce = request.args.get('nonce')
  233. if not self.validate(signature, timestamp, nonce):
  234. return 'signature failed', 400
  235. if request.method == 'GET':
  236. echostr = request.args.get('echostr', '')
  237. return echostr
  238. try:
  239. ret = self.parse(request.data)
  240. except ValueError:
  241. return 'invalid', 400
  242. func = None
  243. type = ret['type']
  244. _registry = self._registry.get(type, dict())
  245. if type == 'text':
  246. if ret['content'] in _registry:
  247. func = _registry[ret['content']]
  248. elif type == 'event':
  249. if ret['event'].lower() in _registry:
  250. func = _registry[ret['event'].lower()]
  251. if func is None and '*' in _registry:
  252. func = _registry['*']
  253. if func is None and '*' in self._registry:
  254. func = self._registry.get('*', dict()).get('*')
  255. text = ''
  256. if func is None:
  257. text = 'failed'
  258. if callable(func):
  259. text = func(**ret)
  260. content = ''
  261. if isinstance(text, basestring):
  262. if text:
  263. content = self.reply(
  264. username=ret['sender'],
  265. sender=ret['receiver'],
  266. content=text,
  267. )
  268. elif isinstance(text, dict):
  269. text.setdefault('username', ret['sender'])
  270. text.setdefault('sender', ret['receiver'])
  271. content = self.reply(**text)
  272. return Response(content, content_type='text/xml; charset=utf-8')
  273. view_func.methods = ['GET', 'POST']
  274. def text_reply(username, sender, content):
  275. shared = _shared_reply(username, sender, 'text')
  276. template = '<xml>%s<Content><![CDATA[%s]]></Content></xml>'
  277. return template % (shared, content)
  278. def music_reply(username, sender, **kwargs):
  279. kwargs['shared'] = _shared_reply(username, sender, 'music')
  280. template = (
  281. '<xml>'
  282. '%(shared)s'
  283. '<Music>'
  284. '<Title><![CDATA[%(title)s]]></Title>'
  285. '<Description><![CDATA[%(description)s]]></Description>'
  286. '<MusicUrl><![CDATA[%(music_url)s]]></MusicUrl>'
  287. '<HQMusicUrl><![CDATA[%(hq_music_url)s]]></HQMusicUrl>'
  288. '</Music>'
  289. '</xml>'
  290. )
  291. return template % kwargs
  292. def news_reply(username, sender, *items):
  293. item_template = (
  294. '<item>'
  295. '<Title><![CDATA[%(title)s]]></Title>'
  296. '<Description><![CDATA[%(description)s]]></Description>'
  297. '<PicUrl><![CDATA[%(picurl)s]]></PicUrl>'
  298. '<Url><![CDATA[%(url)s]]></Url>'
  299. '</item>'
  300. )
  301. articles = [item_template % o for o in items]
  302. template = (
  303. '<xml>'
  304. '%(shared)s'
  305. '<ArticleCount>%(count)d</ArticleCount>'
  306. '<Articles>%(articles)s</Articles>'
  307. '</xml>'
  308. )
  309. dct = {
  310. 'shared': _shared_reply(username, sender, 'news'),
  311. 'count': len(items),
  312. 'articles': ''.join(articles)
  313. }
  314. return template % dct
  315. def transfer_customer_service_reply(username, sender, service_account):
  316. template = (
  317. '<xml>%(shared)s'
  318. '%(transfer_info)s</xml>')
  319. transfer_info = ''
  320. if service_account:
  321. transfer_info = (
  322. '<TransInfo>'
  323. '<KfAccount>![CDATA[%s]]</KfAccount>'
  324. '</TransInfo>') % service_account
  325. dct = {
  326. 'shared': _shared_reply(username, sender,
  327. type='transfer_customer_service'),
  328. 'transfer_info': transfer_info
  329. }
  330. return template % dct
  331. def image_reply(username, sender, media_id):
  332. shared = _shared_reply(username, sender, 'image')
  333. template = '<xml>%s<Image><MediaId><![CDATA[%s]]></MediaId></Image></xml>'
  334. return template % (shared, media_id)
  335. def voice_reply(username, sender, media_id):
  336. shared = _shared_reply(username, sender, 'voice')
  337. template = '<xml>%s<Voice><MediaId><![CDATA[%s]]></MediaId></Voice></xml>'
  338. return template % (shared, media_id)
  339. def video_reply(username, sender, **kwargs):
  340. kwargs['shared'] = _shared_reply(username, sender, 'video')
  341. template = (
  342. '<xml>'
  343. '%(shared)s'
  344. '<Video>'
  345. '<MediaId><![CDATA[%(media_id)s]]></MediaId>'
  346. '<Title><![CDATA[%(title)s]]></Title>'
  347. '<Description><![CDATA[%(description)s]]></Description>'
  348. '</Video>'
  349. '</xml>'
  350. )
  351. return template % kwargs
  352. def _shared_reply(username, sender, type):
  353. dct = {
  354. 'username': username,
  355. 'sender': sender,
  356. 'type': type,
  357. 'timestamp': int(time.time()),
  358. }
  359. template = (
  360. '<ToUserName><![CDATA[%(username)s]]></ToUserName>'
  361. '<FromUserName><![CDATA[%(sender)s]]></FromUserName>'
  362. '<CreateTime>%(timestamp)d</CreateTime>'
  363. '<MsgType><![CDATA[%(type)s]]></MsgType>'
  364. )
  365. return template % dct