# -*- coding: utf-8 -*- from __future__ import unicode_literals import time import hashlib from datetime import datetime from .base import WeixinError try: from flask import request, Response except ImportError: request, Response = None, None try: from django.http import HttpResponse, HttpResponseForbidden, HttpResponseNotAllowed except Exception: HttpResponse, HttpResponseForbidden, HttpResponseNotAllowed = None, None, None try: from lxml import etree except ImportError: from xml.etree import cElementTree as etree except ImportError: from xml.etree import ElementTree as etree __all__ = ("WeixinMsgError", "WeixinMsg") class WeixinMsgError(WeixinError): def __init__(self, msg): super(WeixinMsgError, self).__init__(msg) class WeixinMsg(object): def __init__(self, token, sender=None, expires_in=0): self.token = token self.sender = sender self.expires_in = expires_in self._registry = dict() def validate(self, signature, timestamp, nonce): if not self.token: raise WeixinMsgError("weixin token is missing") if self.expires_in: try: timestamp = int(timestamp) except ValueError: return False delta = time.time() - timestamp if delta < 0 or delta > self.expires_in: return False values = [self.token, str(timestamp), str(nonce)] s = ''.join(sorted(values)) hsh = hashlib.sha1(s.encode("utf-8")).hexdigest() return signature == hsh def parse(self, content): raw = {} root = etree.fromstring(content) for child in root: raw[child.tag] = child.text formatted = self.format(raw) msg_type = formatted['type'] msg_parser = getattr(self, 'parse_{0}'.format(msg_type), None) if callable(msg_parser): parsed = msg_parser(raw) else: parsed = self.parse_invalid_type(raw) formatted.update(parsed) return formatted def format(self, kwargs): timestamp = int(kwargs['CreateTime']) return { 'id': kwargs.get('MsgId'), 'timestamp': timestamp, 'receiver': kwargs['ToUserName'], 'sender': kwargs['FromUserName'], 'type': kwargs['MsgType'], 'time': datetime.fromtimestamp(timestamp), } def parse_text(self, raw): return {'content': raw['Content']} def parse_image(self, raw): return {'picurl': raw['PicUrl']} def parse_location(self, raw): return { 'location_x': raw['Location_X'], 'location_y': raw['Location_Y'], 'scale': int(raw.get('Scale', 0)), 'label': raw['Label'], } def parse_link(self, raw): return { 'title': raw['Title'], 'description': raw['Description'], 'url': raw['url'], } def parse_voice(self, raw): return { 'media_id': raw['MediaId'], 'format': raw['Format'], 'recognition': raw['Recognition'], } def parse_video(self, raw): return { 'media_id': raw['MediaId'], 'thumb_media_id': raw['ThumbMediaId'], } def parse_shortvideo(self, raw): return { 'media_id': raw['MediaId'], 'thumb_media_id': raw['ThumbMediaId'], } def parse_event(self, raw): return { 'event': raw.get('Event'), 'event_key': raw.get('EventKey'), 'ticket': raw.get('Ticket'), 'latitude': raw.get('Latitude'), 'longitude': raw.get('Longitude'), 'precision': raw.get('Precision'), 'status': raw.get('status') } def parse_invalid_type(self, raw): return {} def reply(self, username=None, type='text', sender=None, **kwargs): if not username: raise RuntimeError("username is missing") sender = sender or self.sender if not sender: raise RuntimeError('WEIXIN_SENDER or sender argument is missing') if type == 'text': content = kwargs.get('content', '') return text_reply(username, sender, content) if type == 'music': values = {} for k in ('title', 'description', 'music_url', 'hq_music_url'): values[k] = kwargs[k] return music_reply(username, sender, **values) if type == 'news': items = kwargs['articles'] return news_reply(username, sender, *items) if type == 'customer_service': service_account = kwargs['service_account'] return transfer_customer_service_reply(username, sender, service_account) if type == 'image': media_id = kwargs.get['media_id'] return image_reply(username, sender, media_id) if type == 'voice': media_id = kwargs.get['media_id'] return voice_reply(username, sender, media_id) if type == 'video': values = {} for k in ('media_id', 'title', 'description'): values[k] = kwargs[k] return video_reply(username, sender, **values) def register(self, type, key=None, func=None): if func: key = '*' if not key else key self._registry.setdefault(type, dict())[key] = func return func return self.__call__(type, key) def __call__(self, type, key): def wrapper(func): self.register(type, key, func) return func return wrapper @property def all(self): return self.register('*') def text(self, key='*'): return self.register('text', key) def __getattr__(self, key): key = key.lower() if key in ['image', 'video', 'voice', 'shortvideo', 'location', 'link', 'event']: return self.register(key) if key in ['subscribe', 'unsubscribe', 'location', 'click', 'view', 'scan', \ 'scancode_push', 'scancode_waitmsg', 'pic_sysphoto', \ 'pic_photo_or_album', 'pic_weixin', 'location_select', \ 'qualification_verify_success', 'qualification_verify_fail', 'naming_verify_success', \ 'naming_verify_fail', 'annual_renew', 'verify_expired', \ 'card_pass_check', 'user_get_card', 'user_del_card', 'user_consume_card', \ 'user_pay_from_pay_cell', 'user_view_card', 'user_enter_session_from_card', \ 'card_sku_remind']: return self.register('event', key) raise AttributeError('invalid attribute "' + key + '"') def django_view_func(self): def run(request): if HttpResponse is None: raise RuntimeError('django_view_func need Django be installed') signature = request.GET.get('signature') timestamp = request.GET.get('timestamp') nonce = request.GET.get('nonce') if not self.validate(signature, timestamp, nonce): return HttpResponseForbidden('signature failed') if request.method == 'GET': echostr = request.args.get('echostr', '') return HttpResponse(echostr) elif request.method == "POST": try: ret = self.parse(request.body) except ValueError: return HttpResponseForbidden('invalid') func = None type = ret['type'] _registry = self._registry.get(type, dict()) if type == 'text': if ret['content'] in _registry: func = _registry[ret['content']] elif type == 'event': if ret['event'].lower() in _registry: func = _registry[ret['event'].lower()] if func is None and '*' in _registry: func = _registry['*'] if func is None and '*' in self._registry: func = self._registry.get('*', dict()).get('*') text = '' if func is None: text = 'failed' if callable(func): text = func(**ret) content = '' if isinstance(text, basestring): if text: content = self.reply( username=ret['sender'], sender=ret['receiver'], content=text, ) elif isinstance(text, dict): text.setdefault('username', ret['sender']) text.setdefault('sender', ret['receiver']) content = self.reply(**text) return HttpResponse(content, content_type='text/xml; charset=utf-8') return HttpResponseNotAllowed(['GET', 'POST']) return run def view_func(self): if request is None: raise RuntimeError('view_func need Flask be installed') signature = request.args.get('signature') timestamp = request.args.get('timestamp') nonce = request.args.get('nonce') if not self.validate(signature, timestamp, nonce): return 'signature failed', 400 if request.method == 'GET': echostr = request.args.get('echostr', '') return echostr try: ret = self.parse(request.data) except ValueError: return 'invalid', 400 func = None type = ret['type'] _registry = self._registry.get(type, dict()) if type == 'text': if ret['content'] in _registry: func = _registry[ret['content']] elif type == 'event': if ret['event'].lower() in _registry: func = _registry[ret['event'].lower()] if func is None and '*' in _registry: func = _registry['*'] if func is None and '*' in self._registry: func = self._registry.get('*', dict()).get('*') text = '' if func is None: text = 'failed' if callable(func): text = func(**ret) content = '' if isinstance(text, basestring): if text: content = self.reply( username=ret['sender'], sender=ret['receiver'], content=text, ) elif isinstance(text, dict): text.setdefault('username', ret['sender']) text.setdefault('sender', ret['receiver']) content = self.reply(**text) return Response(content, content_type='text/xml; charset=utf-8') view_func.methods = ['GET', 'POST'] def text_reply(username, sender, content): shared = _shared_reply(username, sender, 'text') template = '%s' return template % (shared, content) def music_reply(username, sender, **kwargs): kwargs['shared'] = _shared_reply(username, sender, 'music') template = ( '' '%(shared)s' '' '<![CDATA[%(title)s]]>' '' '' '' '' '' ) return template % kwargs def news_reply(username, sender, *items): item_template = ( '' '<![CDATA[%(title)s]]>' '' '' '' '' ) articles = [item_template % o for o in items] template = ( '' '%(shared)s' '%(count)d' '%(articles)s' '' ) dct = { 'shared': _shared_reply(username, sender, 'news'), 'count': len(items), 'articles': ''.join(articles) } return template % dct def transfer_customer_service_reply(username, sender, service_account): template = ( '%(shared)s' '%(transfer_info)s') transfer_info = '' if service_account: transfer_info = ( '' '![CDATA[%s]]' '') % service_account dct = { 'shared': _shared_reply(username, sender, type='transfer_customer_service'), 'transfer_info': transfer_info } return template % dct def image_reply(username, sender, media_id): shared = _shared_reply(username, sender, 'image') template = '%s' return template % (shared, media_id) def voice_reply(username, sender, media_id): shared = _shared_reply(username, sender, 'voice') template = '%s' return template % (shared, media_id) def video_reply(username, sender, **kwargs): kwargs['shared'] = _shared_reply(username, sender, 'video') template = ( '' '%(shared)s' '' '' ) return template % kwargs def _shared_reply(username, sender, type): dct = { 'username': username, 'sender': sender, 'type': type, 'timestamp': int(time.time()), } template = ( '' '' '%(timestamp)d' '' ) return template % dct