1
|
|
|
# -*- coding: utf-8 -*- |
2
|
10 |
|
""" |
3
|
|
|
wechatpy.component |
4
|
|
|
~~~~~~~~~~~~~~~ |
5
|
|
|
|
6
|
|
|
This module provides client library for WeChat Open Platform |
7
|
|
|
|
8
|
|
|
:copyright: (c) 2015 by hunter007. |
9
|
|
|
:license: MIT, see LICENSE for more details. |
10
|
|
|
""" |
11
|
10 |
|
from __future__ import absolute_import, unicode_literals |
12
|
|
|
|
13
|
10 |
|
import logging |
14
|
10 |
|
import time |
15
|
10 |
|
import warnings |
16
|
|
|
|
17
|
10 |
|
import requests |
18
|
10 |
|
import six |
19
|
10 |
|
import xmltodict |
20
|
10 |
|
from six.moves.urllib.parse import quote |
21
|
|
|
|
22
|
10 |
|
from wechatpy.client import WeChatComponentClient |
23
|
10 |
|
from wechatpy.constants import WeChatErrorCode |
24
|
10 |
|
from wechatpy.crypto import WeChatCrypto |
25
|
10 |
|
from wechatpy.exceptions import APILimitedException, WeChatClientException, WeChatOAuthException, \ |
26
|
|
|
WeChatComponentOAuthException |
27
|
10 |
|
from wechatpy.fields import DateTimeField, StringField |
28
|
10 |
|
from wechatpy.messages import MessageMetaClass |
29
|
10 |
|
from wechatpy.session.memorystorage import MemoryStorage |
30
|
10 |
|
from wechatpy.utils import get_querystring, json, to_binary, to_text, ObjectDict |
31
|
|
|
|
32
|
10 |
|
logger = logging.getLogger(__name__) |
33
|
|
|
|
34
|
10 |
|
COMPONENT_MESSAGE_TYPES = {} |
35
|
|
|
|
36
|
|
|
|
37
|
10 |
|
def register_component_message(msg_type): |
38
|
10 |
|
def register(cls): |
39
|
10 |
|
COMPONENT_MESSAGE_TYPES[msg_type] = cls |
40
|
10 |
|
return cls |
41
|
10 |
|
return register |
42
|
|
|
|
43
|
|
|
|
44
|
10 |
|
class BaseComponentMessage(six.with_metaclass(MessageMetaClass)): |
45
|
|
|
"""Base class for all component messages and events""" |
46
|
10 |
|
type = 'unknown' |
47
|
10 |
|
appid = StringField('AppId') |
48
|
10 |
|
create_time = DateTimeField('CreateTime') |
49
|
|
|
|
50
|
10 |
|
def __init__(self, message): |
51
|
|
|
self._data = message |
52
|
|
|
|
53
|
|
|
def __repr__(self): |
54
|
|
|
_repr = "{klass}({msg})".format( |
55
|
|
|
klass=self.__class__.__name__, |
56
|
|
|
msg=repr(self._data) |
57
|
|
|
) |
58
|
|
|
if six.PY2: |
59
|
|
|
return to_binary(_repr) |
60
|
|
|
else: |
61
|
|
|
return to_text(_repr) |
62
|
|
|
|
63
|
|
|
|
64
|
10 |
|
@register_component_message('component_verify_ticket') |
65
|
10 |
|
class ComponentVerifyTicketMessage(BaseComponentMessage): |
66
|
|
|
""" |
67
|
|
|
component_verify_ticket协议 |
68
|
|
|
""" |
69
|
10 |
|
type = 'component_verify_ticket' |
70
|
10 |
|
verify_ticket = StringField('ComponentVerifyTicket') |
71
|
|
|
|
72
|
|
|
|
73
|
10 |
|
@register_component_message('unauthorized') |
74
|
10 |
|
class ComponentUnauthorizedMessage(BaseComponentMessage): |
75
|
|
|
""" |
76
|
|
|
取消授权通知 |
77
|
|
|
""" |
78
|
10 |
|
type = 'unauthorized' |
79
|
10 |
|
authorizer_appid = StringField('AuthorizerAppid') |
80
|
|
|
|
81
|
|
|
|
82
|
10 |
|
@register_component_message('authorized') |
83
|
10 |
|
class ComponentAuthorizedMessage(BaseComponentMessage): |
84
|
|
|
""" |
85
|
|
|
新增授权通知 |
86
|
|
|
""" |
87
|
10 |
|
type = 'authorized' |
88
|
10 |
|
authorizer_appid = StringField('AuthorizerAppid') |
89
|
10 |
|
authorization_code = StringField('AuthorizationCode') |
90
|
10 |
|
authorization_code_expired_time = StringField('AuthorizationCodeExpiredTime') |
91
|
10 |
|
pre_auth_code = StringField('PreAuthCode') |
92
|
|
|
|
93
|
|
|
|
94
|
10 |
|
@register_component_message('updateauthorized') |
95
|
10 |
|
class ComponentUpdateauthorizedMessage(BaseComponentMessage): |
96
|
|
|
""" |
97
|
|
|
更新授权通知 |
98
|
|
|
""" |
99
|
10 |
|
type = 'updateauthorized' |
100
|
10 |
|
authorizer_appid = StringField('AuthorizerAppid') |
101
|
10 |
|
authorization_code = StringField('AuthorizationCode') |
102
|
10 |
|
authorization_code_expired_time = StringField('AuthorizationCodeExpiredTime') |
103
|
10 |
|
pre_auth_code = StringField('PreAuthCode') |
104
|
|
|
|
105
|
|
|
|
106
|
10 |
|
class ComponentUnknownMessage(BaseComponentMessage): |
107
|
|
|
""" |
108
|
|
|
未知通知 |
109
|
|
|
""" |
110
|
10 |
|
type = 'unknown' |
111
|
|
|
|
112
|
|
|
|
113
|
10 |
|
class BaseWeChatComponent(object): |
114
|
10 |
|
API_BASE_URL = 'https://api.weixin.qq.com/cgi-bin' |
115
|
|
|
|
116
|
10 |
|
def __init__(self, |
117
|
|
|
component_appid, |
118
|
|
|
component_appsecret, |
119
|
|
|
component_token, |
120
|
|
|
encoding_aes_key, |
121
|
|
|
session=None, |
122
|
|
|
auto_retry=True): |
123
|
|
|
""" |
124
|
|
|
:param component_appid: 第三方平台appid |
125
|
|
|
:param component_appsecret: 第三方平台appsecret |
126
|
|
|
:param component_token: 公众号消息校验Token |
127
|
|
|
:param encoding_aes_key: 公众号消息加解密Key |
128
|
|
|
""" |
129
|
10 |
|
self._http = requests.Session() |
130
|
10 |
|
self.component_appid = component_appid |
131
|
10 |
|
self.component_appsecret = component_appsecret |
132
|
10 |
|
self.expires_at = None |
133
|
10 |
|
self.crypto = WeChatCrypto( |
134
|
|
|
component_token, encoding_aes_key, component_appid) |
135
|
10 |
|
self.session = session or MemoryStorage() |
136
|
10 |
|
self.auto_retry = auto_retry |
137
|
|
|
|
138
|
10 |
View Code Duplication |
if isinstance(session, six.string_types): |
|
|
|
|
139
|
|
|
from shove import Shove |
140
|
|
|
from wechatpy.session.shovestorage import ShoveStorage |
141
|
|
|
|
142
|
|
|
querystring = get_querystring(session) |
143
|
|
|
prefix = querystring.get('prefix', ['wechatpy'])[0] |
144
|
|
|
|
145
|
|
|
shove = Shove(session) |
146
|
|
|
storage = ShoveStorage(shove, prefix) |
147
|
|
|
self.session = storage |
148
|
|
|
|
149
|
10 |
|
@property |
150
|
|
|
def component_verify_ticket(self): |
151
|
10 |
|
return self.session.get('component_verify_ticket') |
152
|
|
|
|
153
|
10 |
|
def _request(self, method, url_or_endpoint, **kwargs): |
154
|
10 |
|
if not url_or_endpoint.startswith(('http://', 'https://')): |
155
|
10 |
|
api_base_url = kwargs.pop('api_base_url', self.API_BASE_URL) |
156
|
10 |
|
url = '{base}{endpoint}'.format( |
157
|
|
|
base=api_base_url, |
158
|
|
|
endpoint=url_or_endpoint |
159
|
|
|
) |
160
|
|
|
else: |
161
|
|
|
url = url_or_endpoint |
162
|
|
|
|
163
|
10 |
|
if 'params' not in kwargs: |
164
|
10 |
|
kwargs['params'] = {} |
165
|
10 |
|
if isinstance(kwargs['params'], dict) and \ |
166
|
|
|
'component_access_token' not in kwargs['params']: |
167
|
10 |
|
kwargs['params'][ |
168
|
|
|
'component_access_token'] = self.access_token |
169
|
10 |
|
if isinstance(kwargs['data'], dict): |
170
|
10 |
|
kwargs['data'] = json.dumps(kwargs['data']) |
171
|
|
|
|
172
|
10 |
|
res = self._http.request( |
173
|
|
|
method=method, |
174
|
|
|
url=url, |
175
|
|
|
**kwargs |
176
|
|
|
) |
177
|
10 |
|
try: |
178
|
10 |
|
res.raise_for_status() |
179
|
|
|
except requests.RequestException as reqe: |
180
|
|
|
raise WeChatClientException( |
181
|
|
|
errcode=None, |
182
|
|
|
errmsg=None, |
183
|
|
|
client=self, |
184
|
|
|
request=reqe.request, |
185
|
|
|
response=reqe.response |
186
|
|
|
) |
187
|
|
|
|
188
|
10 |
|
return self._handle_result(res, method, url, **kwargs) |
189
|
|
|
|
190
|
10 |
View Code Duplication |
def _handle_result(self, res, method=None, url=None, **kwargs): |
|
|
|
|
191
|
10 |
|
result = json.loads(res.content.decode('utf-8', 'ignore'), strict=False) |
192
|
10 |
|
if 'errcode' in result: |
193
|
10 |
|
result['errcode'] = int(result['errcode']) |
194
|
|
|
|
195
|
10 |
|
if 'errcode' in result and result['errcode'] != 0: |
196
|
|
|
errcode = result['errcode'] |
197
|
|
|
errmsg = result.get('errmsg', errcode) |
198
|
|
|
if self.auto_retry and errcode in ( |
199
|
|
|
WeChatErrorCode.INVALID_CREDENTIAL.value, |
200
|
|
|
WeChatErrorCode.INVALID_ACCESS_TOKEN.value, |
201
|
|
|
WeChatErrorCode.EXPIRED_ACCESS_TOKEN.value): |
202
|
|
|
logger.info('Component access token expired, fetch a new one and retry request') |
203
|
|
|
self.fetch_access_token() |
204
|
|
|
kwargs['params']['component_access_token'] = self.session.get( |
205
|
|
|
'component_access_token' |
206
|
|
|
) |
207
|
|
|
return self._request( |
208
|
|
|
method=method, |
209
|
|
|
url_or_endpoint=url, |
210
|
|
|
**kwargs |
211
|
|
|
) |
212
|
|
|
elif errcode == WeChatErrorCode.OUT_OF_API_FREQ_LIMIT.value: |
213
|
|
|
# api freq out of limit |
214
|
|
|
raise APILimitedException( |
215
|
|
|
errcode, |
216
|
|
|
errmsg, |
217
|
|
|
client=self, |
218
|
|
|
request=res.request, |
219
|
|
|
response=res |
220
|
|
|
) |
221
|
|
|
else: |
222
|
|
|
raise WeChatClientException( |
223
|
|
|
errcode, |
224
|
|
|
errmsg, |
225
|
|
|
client=self, |
226
|
|
|
request=res.request, |
227
|
|
|
response=res |
228
|
|
|
) |
229
|
10 |
|
return result |
230
|
|
|
|
231
|
10 |
|
def fetch_access_token(self): |
232
|
|
|
""" |
233
|
|
|
获取 component_access_token |
234
|
|
|
详情请参考 https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list\ |
235
|
|
|
&t=resource/res_list&verify=1&id=open1419318587&token=&lang=zh_CN |
236
|
|
|
|
237
|
|
|
:return: 返回的 JSON 数据包 |
238
|
|
|
""" |
239
|
10 |
|
url = '{0}{1}'.format( |
240
|
|
|
self.API_BASE_URL, |
241
|
|
|
'/component/api_component_token' |
242
|
|
|
) |
243
|
10 |
|
return self._fetch_access_token( |
244
|
|
|
url=url, |
245
|
|
|
data=json.dumps({ |
246
|
|
|
'component_appid': self.component_appid, |
247
|
|
|
'component_appsecret': self.component_appsecret, |
248
|
|
|
'component_verify_ticket': self.component_verify_ticket |
249
|
|
|
}) |
250
|
|
|
) |
251
|
|
|
|
252
|
10 |
View Code Duplication |
def _fetch_access_token(self, url, data): |
|
|
|
|
253
|
|
|
""" The real fetch access token """ |
254
|
10 |
|
logger.info('Fetching component access token') |
255
|
10 |
|
res = self._http.post( |
256
|
|
|
url=url, |
257
|
|
|
data=data |
258
|
|
|
) |
259
|
10 |
|
try: |
260
|
10 |
|
res.raise_for_status() |
261
|
|
|
except requests.RequestException as reqe: |
262
|
|
|
raise WeChatClientException( |
263
|
|
|
errcode=None, |
264
|
|
|
errmsg=None, |
265
|
|
|
client=self, |
266
|
|
|
request=reqe.request, |
267
|
|
|
response=reqe.response |
268
|
|
|
) |
269
|
10 |
|
result = res.json() |
270
|
10 |
|
if 'errcode' in result and result['errcode'] != 0: |
271
|
|
|
raise WeChatClientException( |
272
|
|
|
result['errcode'], |
273
|
|
|
result['errmsg'], |
274
|
|
|
client=self, |
275
|
|
|
request=res.request, |
276
|
|
|
response=res |
277
|
|
|
) |
278
|
|
|
|
279
|
10 |
|
expires_in = 7200 |
280
|
10 |
|
if 'expires_in' in result: |
281
|
10 |
|
expires_in = result['expires_in'] |
282
|
10 |
|
self.session.set( |
283
|
|
|
'component_access_token', |
284
|
|
|
result['component_access_token'], |
285
|
|
|
expires_in |
286
|
|
|
) |
287
|
10 |
|
self.expires_at = int(time.time()) + expires_in |
288
|
10 |
|
return result |
289
|
|
|
|
290
|
10 |
|
@property |
291
|
|
|
def access_token(self): |
292
|
|
|
""" WeChat component access token """ |
293
|
10 |
|
access_token = self.session.get('component_access_token') |
294
|
10 |
|
if access_token: |
295
|
10 |
|
if not self.expires_at: |
296
|
|
|
# user provided access_token, just return it |
297
|
|
|
return access_token |
298
|
|
|
|
299
|
10 |
|
timestamp = time.time() |
300
|
10 |
|
if self.expires_at - timestamp > 60: |
301
|
10 |
|
return access_token |
302
|
|
|
|
303
|
10 |
|
self.fetch_access_token() |
304
|
10 |
|
return self.session.get('component_access_token') |
305
|
|
|
|
306
|
10 |
|
def get(self, url, **kwargs): |
307
|
|
|
return self._request( |
308
|
|
|
method='get', |
309
|
|
|
url_or_endpoint=url, |
310
|
|
|
**kwargs |
311
|
|
|
) |
312
|
|
|
|
313
|
10 |
|
def post(self, url, **kwargs): |
314
|
10 |
|
return self._request( |
315
|
|
|
method='post', |
316
|
|
|
url_or_endpoint=url, |
317
|
|
|
**kwargs |
318
|
|
|
) |
319
|
|
|
|
320
|
|
|
|
321
|
10 |
|
class WeChatComponent(BaseWeChatComponent): |
322
|
|
|
|
323
|
10 |
|
PRE_AUTH_URL = 'https://mp.weixin.qq.com/cgi-bin/componentloginpage' |
324
|
|
|
|
325
|
10 |
|
def get_pre_auth_url(self, redirect_uri): |
326
|
|
|
redirect_uri = quote(redirect_uri, safe=b'') |
327
|
|
|
return "{0}?component_appid={1}&pre_auth_code={2}&redirect_uri={3}".format( |
328
|
|
|
self.PRE_AUTH_URL, self.component_appid, self.create_preauthcode()['pre_auth_code'], redirect_uri |
329
|
|
|
) |
330
|
|
|
|
331
|
10 |
|
def get_pre_auth_url_m(self, redirect_uri): |
332
|
|
|
""" |
333
|
|
|
快速获取pre auth url,可以直接微信中发送该链接,直接授权 |
334
|
|
|
""" |
335
|
|
|
url = "https://mp.weixin.qq.com/safe/bindcomponent?action=bindcomponent&auth_type=3&no_scan=1&" |
336
|
|
|
redirect_uri = quote(redirect_uri, safe='') |
337
|
|
|
return "{0}component_appid={1}&pre_auth_code={2}&redirect_uri={3}".format( |
338
|
|
|
url, self.component_appid, self.create_preauthcode()['pre_auth_code'], redirect_uri |
339
|
|
|
) |
340
|
|
|
|
341
|
10 |
|
def create_preauthcode(self): |
342
|
|
|
""" |
343
|
|
|
获取预授权码 |
344
|
|
|
""" |
345
|
10 |
|
return self.post( |
346
|
|
|
'/component/api_create_preauthcode', |
347
|
|
|
data={ |
348
|
|
|
'component_appid': self.component_appid |
349
|
|
|
} |
350
|
|
|
) |
351
|
|
|
|
352
|
10 |
|
def _query_auth(self, authorization_code): |
353
|
|
|
""" |
354
|
|
|
使用授权码换取公众号的授权信息 |
355
|
|
|
|
356
|
|
|
:params authorization_code: 授权code,会在授权成功时返回给第三方平台,详见第三方平台授权流程说明 |
357
|
|
|
""" |
358
|
10 |
|
return self.post( |
359
|
|
|
'/component/api_query_auth', |
360
|
|
|
data={ |
361
|
|
|
'component_appid': self.component_appid, |
362
|
|
|
'authorization_code': authorization_code |
363
|
|
|
} |
364
|
|
|
) |
365
|
|
|
|
366
|
10 |
|
def query_auth(self, authorization_code): |
367
|
|
|
""" |
368
|
|
|
使用授权码换取公众号的授权信息,同时储存token信息 |
369
|
|
|
|
370
|
|
|
:params authorization_code: 授权code,会在授权成功时返回给第三方平台,详见第三方平台授权流程说明 |
371
|
|
|
""" |
372
|
10 |
|
result = self._query_auth(authorization_code) |
373
|
|
|
|
374
|
10 |
|
assert result is not None \ |
375
|
|
|
and 'authorization_info' in result \ |
376
|
|
|
and 'authorizer_appid' in result['authorization_info'] |
377
|
|
|
|
378
|
10 |
|
authorizer_appid = result['authorization_info']['authorizer_appid'] |
379
|
10 |
|
if 'authorizer_access_token' in result['authorization_info'] \ |
380
|
|
|
and result['authorization_info']['authorizer_access_token']: |
381
|
10 |
|
access_token = result['authorization_info']['authorizer_access_token'] |
382
|
10 |
|
access_token_key = '{0}_access_token'.format(authorizer_appid) |
383
|
10 |
|
expires_in = 7200 |
384
|
10 |
|
if 'expires_in' in result['authorization_info']: |
385
|
10 |
|
expires_in = result['authorization_info']['expires_in'] |
386
|
10 |
|
self.session.set(access_token_key, access_token, expires_in) |
387
|
10 |
|
if 'authorizer_refresh_token' in result['authorization_info'] \ |
388
|
|
|
and result['authorization_info']['authorizer_refresh_token']: |
389
|
10 |
|
refresh_token = result['authorization_info']['authorizer_refresh_token'] |
390
|
10 |
|
refresh_token_key = '{0}_refresh_token'.format(authorizer_appid) |
391
|
10 |
|
self.session.set(refresh_token_key, refresh_token) # refresh_token 需要永久储存,不建议使用内存储存,否则每次重启服务需要重新扫码授权 |
392
|
10 |
|
return result |
393
|
|
|
|
394
|
10 |
|
def refresh_authorizer_token( |
395
|
|
|
self, authorizer_appid, authorizer_refresh_token): |
396
|
|
|
""" |
397
|
|
|
获取(刷新)授权公众号的令牌 |
398
|
|
|
|
399
|
|
|
:params authorizer_appid: 授权方appid |
400
|
|
|
:params authorizer_refresh_token: 授权方的刷新令牌 |
401
|
|
|
""" |
402
|
10 |
|
return self.post( |
403
|
|
|
'/component/api_authorizer_token', |
404
|
|
|
data={ |
405
|
|
|
'component_appid': self.component_appid, |
406
|
|
|
'authorizer_appid': authorizer_appid, |
407
|
|
|
'authorizer_refresh_token': authorizer_refresh_token |
408
|
|
|
} |
409
|
|
|
) |
410
|
|
|
|
411
|
10 |
|
def get_authorizer_info(self, authorizer_appid): |
412
|
|
|
""" |
413
|
|
|
获取授权方的账户信息 |
414
|
|
|
|
415
|
|
|
:params authorizer_appid: 授权方appid |
416
|
|
|
""" |
417
|
10 |
|
return self.post( |
418
|
|
|
'/component/api_get_authorizer_info', |
419
|
|
|
data={ |
420
|
|
|
'component_appid': self.component_appid, |
421
|
|
|
'authorizer_appid': authorizer_appid, |
422
|
|
|
} |
423
|
|
|
) |
424
|
|
|
|
425
|
10 |
|
def get_authorizer_list(self, offset=0, count=500): |
426
|
|
|
""" |
427
|
|
|
拉取所有已授权的帐号信息 |
428
|
|
|
|
429
|
|
|
:params offset: 偏移位置/起始位置 |
430
|
|
|
:params count: 拉取数量 |
431
|
|
|
""" |
432
|
|
|
return self.post( |
433
|
|
|
'/component/api_get_authorizer_list', |
434
|
|
|
data={ |
435
|
|
|
'component_appid': self.component_appid, |
436
|
|
|
'offset': offset, |
437
|
|
|
'count': count, |
438
|
|
|
} |
439
|
|
|
) |
440
|
|
|
|
441
|
10 |
|
def get_authorizer_option(self, authorizer_appid, option_name): |
442
|
|
|
""" |
443
|
|
|
获取授权方的选项设置信息 |
444
|
|
|
|
445
|
|
|
:params authorizer_appid: 授权公众号appid |
446
|
|
|
:params option_name: 选项名称 |
447
|
|
|
""" |
448
|
10 |
|
return self.post( |
449
|
|
|
'/component/api_get_authorizer_option', |
450
|
|
|
data={ |
451
|
|
|
'component_appid': self.component_appid, |
452
|
|
|
'authorizer_appid': authorizer_appid, |
453
|
|
|
'option_name': option_name |
454
|
|
|
} |
455
|
|
|
) |
456
|
|
|
|
457
|
10 |
|
def set_authorizer_option( |
458
|
|
|
self, authorizer_appid, option_name, option_value): |
459
|
|
|
""" |
460
|
|
|
设置授权方的选项信息 |
461
|
|
|
|
462
|
|
|
:params authorizer_appid: 授权公众号appid |
463
|
|
|
:params option_name: 选项名称 |
464
|
|
|
:params option_value: 设置的选项值 |
465
|
|
|
""" |
466
|
10 |
|
return self.post( |
467
|
|
|
'/component/api_set_authorizer_option', |
468
|
|
|
data={ |
469
|
|
|
'component_appid': self.component_appid, |
470
|
|
|
'authorizer_appid': authorizer_appid, |
471
|
|
|
'option_name': option_name, |
472
|
|
|
'option_value': option_value |
473
|
|
|
} |
474
|
|
|
) |
475
|
|
|
|
476
|
10 |
|
def get_client_by_authorization_code(self, authorization_code): |
477
|
|
|
""" |
478
|
|
|
通过授权码直接获取 Client 对象 |
479
|
|
|
|
480
|
|
|
:params authorization_code: 授权code,会在授权成功时返回给第三方平台,详见第三方平台授权流程说明 |
481
|
|
|
""" |
482
|
|
|
warnings.warn('`get_client_by_authorization_code` method of `WeChatComponent` is deprecated,' |
483
|
|
|
'Use `parse_message` parse message and ' |
484
|
|
|
'Use `get_client_by_appid` instead', |
485
|
|
|
DeprecationWarning, stacklevel=2) |
486
|
|
|
result = self.query_auth(authorization_code) |
487
|
|
|
access_token = result['authorization_info']['authorizer_access_token'] |
488
|
|
|
refresh_token = result['authorization_info']['authorizer_refresh_token'] # NOQA |
489
|
|
|
authorizer_appid = result['authorization_info']['authorizer_appid'] # noqa |
490
|
|
|
return WeChatComponentClient( |
491
|
|
|
authorizer_appid, self, access_token, refresh_token, |
492
|
|
|
session=self.session |
493
|
|
|
) |
494
|
|
|
|
495
|
10 |
|
def get_client_by_appid(self, authorizer_appid): |
496
|
|
|
""" |
497
|
|
|
通过 authorizer_appid 获取 Client 对象 |
498
|
|
|
|
499
|
|
|
:params authorizer_appid: 授权公众号appid |
500
|
|
|
""" |
501
|
|
|
access_token_key = '{0}_access_token'.format(authorizer_appid) |
502
|
|
|
refresh_token_key = '{0}_refresh_token'.format(authorizer_appid) |
503
|
|
|
access_token = self.session.get(access_token_key) |
504
|
|
|
refresh_token = self.session.get(refresh_token_key) |
505
|
|
|
assert refresh_token |
506
|
|
|
|
507
|
|
|
if not access_token: |
508
|
|
|
ret = self.refresh_authorizer_token( |
509
|
|
|
authorizer_appid, |
510
|
|
|
refresh_token |
511
|
|
|
) |
512
|
|
|
access_token = ret['authorizer_access_token'] |
513
|
|
|
refresh_token = ret['authorizer_refresh_token'] |
514
|
|
|
access_token_key = '{0}_access_token'.format(authorizer_appid) |
515
|
|
|
expires_in = 7200 |
516
|
|
|
if 'expires_in' in ret: |
517
|
|
|
expires_in = ret['expires_in'] |
518
|
|
|
self.session.set(access_token_key, access_token, expires_in) |
519
|
|
|
|
520
|
|
|
return WeChatComponentClient( |
521
|
|
|
authorizer_appid, |
522
|
|
|
self, |
523
|
|
|
session=self.session |
524
|
|
|
) |
525
|
|
|
|
526
|
10 |
|
def parse_message(self, msg, msg_signature, timestamp, nonce): |
527
|
|
|
""" |
528
|
|
|
处理 wechat server 推送消息 |
529
|
|
|
|
530
|
|
|
:params msg: 加密内容 |
531
|
|
|
:params msg_signature: 消息签名 |
532
|
|
|
:params timestamp: 时间戳 |
533
|
|
|
:params nonce: 随机数 |
534
|
|
|
""" |
535
|
|
|
content = self.crypto.decrypt_message(msg, msg_signature, timestamp, nonce) |
536
|
|
|
message = xmltodict.parse(to_text(content))['xml'] |
537
|
|
|
message_type = message['InfoType'].lower() |
538
|
|
|
message_class = COMPONENT_MESSAGE_TYPES.get(message_type, ComponentUnknownMessage) |
539
|
|
|
msg = message_class(message) |
540
|
|
|
if msg.type == 'component_verify_ticket': |
541
|
|
|
self.session.set(msg.type, msg.verify_ticket) |
542
|
|
|
elif msg.type in ('authorized', 'updateauthorized'): |
543
|
|
|
msg.query_auth_result = self.query_auth(msg.authorization_code) |
544
|
|
|
return msg |
545
|
|
|
|
546
|
10 |
|
def cache_component_verify_ticket(self, msg, signature, timestamp, nonce): |
547
|
|
|
""" |
548
|
|
|
处理 wechat server 推送的 component_verify_ticket消息 |
549
|
|
|
|
550
|
|
|
:params msg: 加密内容 |
551
|
|
|
:params signature: 消息签名 |
552
|
|
|
:params timestamp: 时间戳 |
553
|
|
|
:params nonce: 随机数 |
554
|
|
|
""" |
555
|
|
|
warnings.warn('`cache_component_verify_ticket` method of `WeChatComponent` is deprecated,' |
556
|
|
|
'Use `parse_message` instead', |
557
|
|
|
DeprecationWarning, stacklevel=2) |
558
|
|
|
content = self.crypto.decrypt_message(msg, signature, timestamp, nonce) |
559
|
|
|
message = xmltodict.parse(to_text(content))['xml'] |
560
|
|
|
o = ComponentVerifyTicketMessage(message) |
561
|
|
|
self.session.set(o.type, o.verify_ticket) |
562
|
|
|
|
563
|
10 |
|
def get_unauthorized(self, msg, signature, timestamp, nonce): |
564
|
|
|
""" |
565
|
|
|
处理取消授权通知 |
566
|
|
|
|
567
|
|
|
:params msg: 加密内容 |
568
|
|
|
:params signature: 消息签名 |
569
|
|
|
:params timestamp: 时间戳 |
570
|
|
|
:params nonce: 随机数 |
571
|
|
|
""" |
572
|
|
|
warnings.warn('`get_unauthorized` method of `WeChatComponent` is deprecated,' |
573
|
|
|
'Use `parse_message` instead', |
574
|
|
|
DeprecationWarning, stacklevel=2) |
575
|
|
|
content = self.crypto.decrypt_message(msg, signature, timestamp, nonce) |
576
|
|
|
message = xmltodict.parse(to_text(content))['xml'] |
577
|
|
|
return ComponentUnauthorizedMessage(message) |
578
|
|
|
|
579
|
10 |
|
def get_component_oauth(self, authorizer_appid): |
580
|
|
|
""" |
581
|
|
|
代公众号 OAuth 网页授权 |
582
|
|
|
|
583
|
|
|
:params authorizer_appid: 授权公众号appid |
584
|
|
|
""" |
585
|
|
|
return ComponentOAuth(authorizer_appid, component=self) |
586
|
|
|
|
587
|
|
|
|
588
|
10 |
|
class ComponentOAuth(object): |
589
|
|
|
""" 微信开放平台 代公众号 OAuth 网页授权 |
590
|
|
|
|
591
|
|
|
详情请参考 |
592
|
|
|
https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419318590 |
593
|
|
|
""" |
594
|
10 |
|
API_BASE_URL = 'https://api.weixin.qq.com/' |
595
|
10 |
|
OAUTH_BASE_URL = 'https://open.weixin.qq.com/connect/' |
596
|
|
|
|
597
|
10 |
|
def __init__(self, app_id, component_appid=None, component_access_token=None, |
598
|
|
|
redirect_uri=None, scope='snsapi_base', state='', component=None): |
599
|
|
|
""" |
600
|
|
|
|
601
|
|
|
:param app_id: 微信公众号 app_id |
602
|
|
|
:param component: WeChatComponent |
603
|
|
|
""" |
604
|
10 |
|
self._http = requests.Session() |
605
|
10 |
|
self.app_id = app_id |
606
|
10 |
|
self.component = component |
607
|
10 |
|
if self.component is None: |
608
|
10 |
|
warnings.warn('cannot found `component` param of `ComponentOAuth` `__init__` method,' |
609
|
|
|
'Use `WeChatComponent.get_component_oauth` instead', |
610
|
|
|
DeprecationWarning, stacklevel=2) |
611
|
|
|
|
612
|
10 |
|
self.component = ObjectDict({'component_appid': component_appid, 'access_token': component_access_token}) |
613
|
10 |
|
if redirect_uri is not None: |
614
|
10 |
|
warnings.warn('found `redirect_uri` param of `ComponentOAuth` `__init__` method,' |
615
|
|
|
'Use `ComponentOAuth.get_authorize_url` instead', |
616
|
|
|
DeprecationWarning, stacklevel=2) |
617
|
10 |
|
self.authorize_url = self.get_authorize_url(redirect_uri, scope, state) |
618
|
|
|
|
619
|
10 |
|
def get_authorize_url(self, redirect_uri, scope='snsapi_base', state=''): |
620
|
|
|
""" |
621
|
|
|
|
622
|
|
|
:param redirect_uri: 重定向地址,需要urlencode,这里填写的应是服务开发方的回调地址 |
623
|
|
|
:param scope: 可选,微信公众号 OAuth2 scope,默认为 ``snsapi_base`` |
624
|
|
|
:param state: 可选,重定向后会带上state参数,开发者可以填写任意参数值,最多128字节 |
625
|
|
|
""" |
626
|
10 |
|
redirect_uri = quote(redirect_uri, safe=b'') |
627
|
10 |
|
url_list = [ |
628
|
|
|
self.OAUTH_BASE_URL, |
629
|
|
|
'oauth2/authorize?appid=', |
630
|
|
|
self.app_id, |
631
|
|
|
'&redirect_uri=', |
632
|
|
|
redirect_uri, |
633
|
|
|
'&response_type=code&scope=', |
634
|
|
|
scope, |
635
|
|
|
] |
636
|
10 |
|
if state: |
637
|
|
|
url_list.extend(['&state=', state]) |
638
|
10 |
|
url_list.extend([ |
639
|
|
|
'&component_appid=', |
640
|
|
|
self.component.component_appid, |
641
|
|
|
]) |
642
|
10 |
|
url_list.append('#wechat_redirect') |
643
|
10 |
|
return ''.join(url_list) |
644
|
|
|
|
645
|
10 |
View Code Duplication |
def fetch_access_token(self, code): |
|
|
|
|
646
|
|
|
"""获取 access_token |
647
|
|
|
|
648
|
|
|
:param code: 授权完成跳转回来后 URL 中的 code 参数 |
649
|
|
|
:return: JSON 数据包 |
650
|
|
|
""" |
651
|
10 |
|
res = self._get( |
652
|
|
|
'sns/oauth2/component/access_token', |
653
|
|
|
params={ |
654
|
|
|
'appid': self.app_id, |
655
|
|
|
'component_appid': self.component.component_appid, |
656
|
|
|
'component_access_token': self.component.access_token, |
657
|
|
|
'code': code, |
658
|
|
|
'grant_type': 'authorization_code', |
659
|
|
|
} |
660
|
|
|
) |
661
|
10 |
|
self.access_token = res['access_token'] |
662
|
10 |
|
self.open_id = res['openid'] |
663
|
10 |
|
self.refresh_token = res['refresh_token'] |
664
|
10 |
|
self.expires_in = res['expires_in'] |
665
|
10 |
|
self.scope = res['scope'] |
666
|
10 |
|
return res |
667
|
|
|
|
668
|
10 |
View Code Duplication |
def refresh_access_token(self, refresh_token): |
|
|
|
|
669
|
|
|
"""刷新 access token |
670
|
|
|
|
671
|
|
|
:param refresh_token: OAuth2 refresh token |
672
|
|
|
:return: JSON 数据包 |
673
|
|
|
""" |
674
|
10 |
|
res = self._get( |
675
|
|
|
'sns/oauth2/component/refresh_token', |
676
|
|
|
params={ |
677
|
|
|
'appid': self.app_id, |
678
|
|
|
'grant_type': 'refresh_token', |
679
|
|
|
'refresh_token': refresh_token, |
680
|
|
|
'component_appid': self.component.component_appid, |
681
|
|
|
'component_access_token': self.component.access_token, |
682
|
|
|
} |
683
|
|
|
) |
684
|
10 |
|
self.access_token = res['access_token'] |
685
|
10 |
|
self.open_id = res['openid'] |
686
|
10 |
|
self.refresh_token = res['refresh_token'] |
687
|
10 |
|
self.expires_in = res['expires_in'] |
688
|
10 |
|
self.scope = res['scope'] |
689
|
10 |
|
return res |
690
|
|
|
|
691
|
10 |
|
def get_user_info(self, openid=None, access_token=None, lang='zh_CN'): |
692
|
|
|
""" 获取用户基本信息(需授权作用域为snsapi_userinfo) |
693
|
|
|
|
694
|
|
|
如果网页授权作用域为snsapi_userinfo,则此时开发者可以通过access_token和openid拉取用户信息了。 |
695
|
|
|
|
696
|
|
|
:param openid: 可选,微信 openid,默认获取当前授权用户信息 |
697
|
|
|
:param access_token: 可选,access_token,默认使用当前授权用户的 access_token |
698
|
|
|
:param lang: 可选,语言偏好, 默认为 ``zh_CN`` |
699
|
|
|
:return: JSON 数据包 |
700
|
|
|
""" |
701
|
10 |
|
openid = openid or self.open_id |
702
|
10 |
|
access_token = access_token or self.access_token |
703
|
10 |
|
return self._get( |
704
|
|
|
'sns/userinfo', |
705
|
|
|
params={ |
706
|
|
|
'access_token': access_token, |
707
|
|
|
'openid': openid, |
708
|
|
|
'lang': lang |
709
|
|
|
} |
710
|
|
|
) |
711
|
|
|
|
712
|
10 |
|
def _request(self, method, url_or_endpoint, **kwargs): |
713
|
10 |
|
if not url_or_endpoint.startswith(('http://', 'https://')): |
714
|
10 |
|
url = '{base}{endpoint}'.format( |
715
|
|
|
base=self.API_BASE_URL, |
716
|
|
|
endpoint=url_or_endpoint |
717
|
|
|
) |
718
|
|
|
else: |
719
|
|
|
url = url_or_endpoint |
720
|
|
|
|
721
|
10 |
|
if isinstance(kwargs.get('data', ''), dict): |
722
|
|
|
body = json.dumps(kwargs['data'], ensure_ascii=False) |
723
|
|
|
body = body.encode('utf-8') |
724
|
|
|
kwargs['data'] = body |
725
|
|
|
|
726
|
10 |
|
res = self._http.request( |
727
|
|
|
method=method, |
728
|
|
|
url=url, |
729
|
|
|
**kwargs |
730
|
|
|
) |
731
|
10 |
|
try: |
732
|
10 |
|
res.raise_for_status() |
733
|
10 |
|
except requests.RequestException as reqe: |
734
|
10 |
|
raise WeChatOAuthException( |
735
|
|
|
errcode=None, |
736
|
|
|
errmsg=None, |
737
|
|
|
client=self, |
738
|
|
|
request=reqe.request, |
739
|
|
|
response=reqe.response |
740
|
|
|
) |
741
|
|
|
|
742
|
10 |
|
return self._handle_result(res, method=method, url=url, **kwargs) |
743
|
|
|
|
744
|
10 |
View Code Duplication |
def _handle_result(self, res, method=None, url=None, **kwargs): |
|
|
|
|
745
|
10 |
|
result = json.loads(res.content.decode('utf-8', 'ignore'), strict=False) |
746
|
10 |
|
if 'errcode' in result: |
747
|
|
|
result['errcode'] = int(result['errcode']) |
748
|
|
|
|
749
|
10 |
|
if 'errcode' in result and result['errcode'] != 0: |
750
|
|
|
errcode = result['errcode'] |
751
|
|
|
errmsg = result.get('errmsg', errcode) |
752
|
|
|
if self.component.auto_retry and errcode in ( |
753
|
|
|
WeChatErrorCode.INVALID_CREDENTIAL.value, |
754
|
|
|
WeChatErrorCode.INVALID_ACCESS_TOKEN.value, |
755
|
|
|
WeChatErrorCode.EXPIRED_ACCESS_TOKEN.value): |
756
|
|
|
logger.info('Component access token expired, fetch a new one and retry request') |
757
|
|
|
self.component.fetch_access_token() |
758
|
|
|
kwargs['params']['component_access_token'] = self.component.access_token |
759
|
|
|
return self._request( |
760
|
|
|
method=method, |
761
|
|
|
url_or_endpoint=url, |
762
|
|
|
**kwargs |
763
|
|
|
) |
764
|
|
|
elif errcode == WeChatErrorCode.OUT_OF_API_FREQ_LIMIT.value: |
765
|
|
|
# api freq out of limit |
766
|
|
|
raise APILimitedException( |
767
|
|
|
errcode, |
768
|
|
|
errmsg, |
769
|
|
|
client=self, |
770
|
|
|
request=res.request, |
771
|
|
|
response=res |
772
|
|
|
) |
773
|
|
|
else: |
774
|
|
|
raise WeChatComponentOAuthException( |
775
|
|
|
errcode, |
776
|
|
|
errmsg, |
777
|
|
|
client=self, |
778
|
|
|
request=res.request, |
779
|
|
|
response=res |
780
|
|
|
) |
781
|
10 |
|
return result |
782
|
|
|
|
783
|
10 |
|
def _get(self, url, **kwargs): |
784
|
10 |
|
return self._request( |
785
|
|
|
method='get', |
786
|
|
|
url_or_endpoint=url, |
787
|
|
|
**kwargs |
788
|
|
|
) |
789
|
|
|
|