Files
test/sg_wechat_enterprise/we_api/component.py
2024-07-10 15:58:47 +08:00

432 lines
14 KiB
Python

# -*- 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)