# -*- coding: utf-8 -*- """ wechatpy.component ~~~~~~~~~~~~~~~ This module provides client library for WeChat Open Platform :copyright: (c) 2015 by hunter007. :license: MIT, see LICENSE for more details. """ from __future__ import absolute_import, unicode_literals import time import logging import six import requests import xmltodict from wechatpy.utils import to_text, to_binary, get_querystring, json from wechatpy.fields import StringField, DateTimeField from wechatpy.messages import MessageMetaClass from wechatpy.session.memorystorage import MemoryStorage from wechatpy.exceptions import WeChatClientException, APILimitedException from wechatpy.crypto import WeChatCrypto from wechatpy.client import WeChatComponentClient logger = logging.getLogger(__name__) class BaseComponentMessage(six.with_metaclass(MessageMetaClass)): """Base class for all component messages and events""" type = 'unknown' appid = StringField('AppId') create_time = DateTimeField('CreateTime') def __init__(self, message): self._data = message def __repr__(self): _repr = "{klass}({msg})".format( klass=self.__class__.__name__, msg=repr(self._data) ) if six.PY2: return to_binary(_repr) else: return to_text(_repr) class ComponentVerifyTicketMessage(BaseComponentMessage): """ component_verify_ticket协议 """ type = 'component_verify_ticket' verify_ticket = StringField('ComponentVerifyTicket') class ComponentUnauthorizedMessage(BaseComponentMessage): """ 取消授权通知 """ type = 'unauthorized' authorizer_appid = StringField('AuthorizerAppid') class BaseWeChatComponent(object): API_BASE_URL = 'https://api.weixin.qq.com/cgi-bin' def __init__(self, component_appid, component_appsecret, component_token, encoding_aes_key, session=None): """ :param component_appid: 第三方平台appid :param component_appsecret: 第三方平台appsecret :param component_token: 公众号消息校验Token :param encoding_aes_key: 公众号消息加解密Key """ self.component_appid = component_appid self.component_appsecret = component_appsecret self.expires_at = None self.crypto = WeChatCrypto( component_token, encoding_aes_key, component_appid) self.session = session or MemoryStorage() if isinstance(session, six.string_types): from shove import Shove from wechatpy.session.shovestorage import ShoveStorage querystring = get_querystring(session) prefix = querystring.get('prefix', ['wechatpy'])[0] shove = Shove(session) storage = ShoveStorage(shove, prefix) self.session = storage @property def component_verify_ticket(self): return self.session.get('component_verify_ticket') def _request(self, method, url_or_endpoint, **kwargs): if not url_or_endpoint.startswith(('http://', 'https://')): api_base_url = kwargs.pop('api_base_url', self.API_BASE_URL) url = '{base}{endpoint}'.format( base=api_base_url, endpoint=url_or_endpoint ) else: url = url_or_endpoint if 'params' not in kwargs: kwargs['params'] = {} if isinstance(kwargs['params'], dict) and \ 'component_access_token' not in kwargs['params']: kwargs['params'][ 'component_access_token'] = self.access_token if isinstance(kwargs['data'], dict): kwargs['data'] = json.dumps(kwargs['data']) res = requests.request( method=method, url=url, **kwargs ) try: res.raise_for_status() except requests.RequestException as reqe: raise WeChatClientException( errcode=None, errmsg=None, client=self, request=reqe.request, response=reqe.response ) return self._handle_result(res, method, url, **kwargs) def _handle_result(self, res, method=None, url=None, **kwargs): result = json.loads(res.content.decode('utf-8', 'ignore'), strict=False) if 'errcode' in result: result['errcode'] = int(result['errcode']) if 'errcode' in result and result['errcode'] != 0: errcode = result['errcode'] errmsg = result['errmsg'] if errcode == 42001: logger.info('Component access token expired, fetch a new one and retry request') self.fetch_component_access_token() kwargs['params']['component_access_token'] = self.session.get( 'component_access_token' ) return self._request( method=method, url_or_endpoint=url, **kwargs ) elif errcode == 45009: # api freq out of limit raise APILimitedException( errcode, errmsg, client=self, request=res.request, response=res ) else: raise WeChatClientException( errcode, errmsg, client=self, request=res.request, response=res ) return result def fetch_access_token(self): """ 获取 component_access_token 详情请参考 https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list\ &t=resource/res_list&verify=1&id=open1419318587&token=&lang=zh_CN :return: 返回的 JSON 数据包 """ url = '{0}{1}'.format( self.API_BASE_URL, '/component/api_component_token' ) return self._fetch_access_token( url=url, data=json.dumps({ 'component_appid': self.component_appid, 'component_appsecret': self.component_appsecret, 'component_verify_ticket': self.component_verify_ticket }) ) def _fetch_access_token(self, url, data): """ The real fetch access token """ logger.info('Fetching component access token') res = requests.post( url=url, data=data ) try: res.raise_for_status() except requests.RequestException as reqe: raise WeChatClientException( errcode=None, errmsg=None, client=self, request=reqe.request, response=reqe.response ) result = res.json() if 'errcode' in result and result['errcode'] != 0: raise WeChatClientException( result['errcode'], result['errmsg'], client=self, request=res.request, response=res ) expires_in = 7200 if 'expires_in' in result: expires_in = result['expires_in'] self.session.set( 'component_access_token', result['component_access_token'], expires_in ) self.expires_at = int(time.time()) + expires_in return result @property def access_token(self): """ WeChat component access token """ access_token = self.session.get('component_access_token') if access_token: if not self.expires_at: # user provided access_token, just return it return access_token timestamp = time.time() if self.expires_at - timestamp > 60: return access_token self.fetch_access_token() return self.session.get('component_access_token') def get(self, url, **kwargs): return self._request( method='get', url_or_endpoint=url, **kwargs ) def post(self, url, **kwargs): return self._request( method='post', url_or_endpoint=url, **kwargs ) class WeChatComponent(BaseWeChatComponent): def create_preauthcode(self): """ 获取预授权码 """ return self.post( '/component/api_create_preauthcode', data={ 'component_appid': self.component_appid } ) def query_auth(self, authorization_code): """ 使用授权码换取公众号的授权信息 :params authorization_code: 授权code,会在授权成功时返回给第三方平台,详见第三方平台授权流程说明 """ return self.post( '/component/api_query_auth', data={ 'component_appid': self.component_appid, 'authorization_code': authorization_code } ) def refresh_authorizer_token( self, authorizer_appid, authorizer_refresh_token): """ 获取(刷新)授权公众号的令牌 :params authorizer_appid: 授权方appid :params authorizer_refresh_token: 授权方的刷新令牌 """ return self.post( '/component/api_authorizer_token', data={ 'component_appid': self.component_appid, 'authorizer_appid': authorizer_appid, 'authorizer_refresh_token': authorizer_refresh_token } ) def get_authorizer_info(self, authorizer_appid): """ 获取授权方的账户信息 :params authorizer_appid: 授权方appid """ return self.post( '/component/api_get_authorizer_info', data={ 'component_appid': self.component_appid, 'authorizer_appid': authorizer_appid, } ) def get_authorizer_option(self, authorizer_appid, option_name): """ 获取授权方的选项设置信息 :params authorizer_appid: 授权公众号appid :params option_name: 选项名称 """ return self.post( '/component/api_get_authorizer_option', data={ 'component_appid': self.component_appid, 'authorizer_appid': authorizer_appid, 'option_name': option_name } ) def set_authorizer_option( self, authorizer_appid, option_name, option_value): """ 设置授权方的选项信息 :params authorizer_appid: 授权公众号appid :params option_name: 选项名称 :params option_value: 设置的选项值 """ return self.post( '/component/api_set_authorizer_option', data={ 'component_appid': self.component_appid, 'authorizer_appid': authorizer_appid, 'option_name': option_name, 'option_value': option_value } ) def get_client_by_authorization_code(self, authorization_code): """ 通过授权码直接获取 Client 对象 :params authorization_code: 授权code,会在授权成功时返回给第三方平台,详见第三方平台授权流程说明 """ result = self.query_auth(authorization_code) access_token = result['authorization_info']['authorizer_access_token'] refresh_token = result['authorization_info']['authorizer_refresh_token'] # NOQA authorizer_appid = result['authorization_info']['authorizer_appid'] # noqa return WeChatComponentClient( authorizer_appid, self, access_token, refresh_token, session=self.session ) def get_client_by_appid(self, authorizer_appid): """ 通过 authorizer_appid 获取 Client 对象 :params authorizer_appid: 授权公众号appid """ access_token_key = '{0}_access_token'.format(authorizer_appid) refresh_token_key = '{0}_refresh_token'.format(authorizer_appid) access_token = self.session.get(access_token_key) refresh_token = self.session.get(refresh_token_key) if not access_token: ret = self.refresh_authorizer_token( authorizer_appid, refresh_token ) access_token = ret['authorizer_access_token'] refresh_token = ret['authorizer_refresh_token'] return WeChatComponentClient( authorizer_appid, self, access_token, refresh_token, session=self.session ) def cache_component_verify_ticket(self, msg, signature, timestamp, nonce): """ 处理 wechat server 推送的 component_verify_ticket消息 :params msg: 加密内容 :params signature: 消息签名 :params timestamp: 时间戳 :params nonce: 随机数 """ content = self.crypto.decrypt_message(msg, signature, timestamp, nonce) message = xmltodict.parse(to_text(content))['xml'] o = ComponentVerifyTicketMessage(message) self.session.set(o.type, o.verify_ticket, 600) def get_unauthorized(self, msg, signature, timestamp, nonce): """ 处理取消授权通知 :params msg: 加密内容 :params signature: 消息签名 :params timestamp: 时间戳 :params nonce: 随机数 """ content = self.crypto.decrypt_message(msg, signature, timestamp, nonce) message = xmltodict.parse(to_text(content))['xml'] return ComponentUnauthorizedMessage(message)