Source code for wechatpy.component

# -*- 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
        self._http = requests.Session()

    @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 = self._http.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 = self._http.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
        )


[docs]class WeChatComponent(BaseWeChatComponent):
[docs] def create_preauthcode(self): """ 获取预授权码 """ return self.post( '/component/api_create_preauthcode', data={ 'component_appid': self.component_appid } )
[docs] 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 } )
[docs] 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 } )
[docs] 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, } )
[docs] 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 } )
[docs] 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 } )
[docs] 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 )
[docs] 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 )
[docs] 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)
[docs] 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)