From 7de4842b826fbb81d7332617926001ed113b42fd Mon Sep 17 00:00:00 2001 From: raptorz Date: Fri, 13 Nov 2015 17:22:24 +0800 Subject: [PATCH 1/9] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=B8=BA=E5=85=BC?= =?UTF-8?q?=E5=AE=B9PY3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + wechat/crypt.py | 5 +++-- wechat/enterprise.py | 12 ++++++++---- wechat/models.py | 5 +++++ wechat/official.py | 13 ++++++++----- 5 files changed, 25 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index b7d8cc5..ffd0c23 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ dist *.egg-info *.pyc *.db +*.swp diff --git a/wechat/crypt.py b/wechat/crypt.py index 283598d..c5e6b18 100644 --- a/wechat/crypt.py +++ b/wechat/crypt.py @@ -10,8 +10,9 @@ import xml.etree.cElementTree as ET import sys import socket -reload(sys) -sys.setdefaultencoding('utf-8') +if sys.version<"3": + reload(sys) + sys.setdefaultencoding('utf-8') WXBizMsgCrypt_OK = 0 WXBizMsgCrypt_ValidateSignature_Error = -40001 diff --git a/wechat/enterprise.py b/wechat/enterprise.py index fbb2070..f41c601 100644 --- a/wechat/enterprise.py +++ b/wechat/enterprise.py @@ -9,6 +9,10 @@ WxVideoResponse, WxNewsResponse, APIError, WxEmptyResponse from .official import WxApplication as BaseApplication, WxBaseApi from .crypt import WXBizMsgCrypt +import sys + +uniencode = lambda s: (s.encode("utf-8") if isinstance(s, str) else s ) if sys.version>"3" else lambda s: s + __all__ = ['WxRequest', 'WxResponse', 'WxArticle', 'WxImage', 'WxVoice', 'WxVideo', 'WxLink', 'WxTextResponse', @@ -40,16 +44,16 @@ def process(self, params, xml=None, token=None, corp_id=None, msg_signature = params.get('msg_signature', '') echostr = params.get('echostr', '') - cpt = WXBizMsgCrypt(self.token, self.aes_key, self.corp_id) + cpt = WXBizMsgCrypt(uniencode(self.token), uniencode(self.aes_key), uniencode(self.corp_id)) - err, echo = cpt.VerifyURL(msg_signature, timestamp, nonce, echostr) + err, echo = cpt.VerifyURL(uniencode(msg_signature), uniencode(timestamp), uniencode(nonce), uniencode(echostr)) if err: return 'invalid request' if not xml: return echo - err, xml = cpt.DecryptMsg(xml, msg_signature, timestamp, nonce) + err, xml = cpt.DecryptMsg(uniencode(xml), uniencode(msg_signature), uniencode(timestamp), uniencode(nonce)) if err: return 'decrypt message error , code %s' % err @@ -65,7 +69,7 @@ def process(self, params, xml=None, token=None, corp_id=None, if not result: return '' - err, result = cpt.EncryptMsg(result, nonce) + err, result = cpt.EncryptMsg(uniencode(result), uniencode(nonce)) if err: return 'encrypt message error , code %s' % err return result diff --git a/wechat/models.py b/wechat/models.py index b55e0d7..034790a 100644 --- a/wechat/models.py +++ b/wechat/models.py @@ -3,6 +3,11 @@ from xml.dom import minidom import collections import time +import sys + +if sys.version>"3": + long = int + unicode = str def kv2element(key, value, doc): diff --git a/wechat/official.py b/wechat/official.py index 4c47b23..27d77e6 100644 --- a/wechat/official.py +++ b/wechat/official.py @@ -7,6 +7,9 @@ import shutil import os from .crypt import WXBizMsgCrypt +import sys + +uniencode = lambda s: (s.encode("utf-8") if isinstance(s, str) else s ) if sys.version>"3" else lambda s: s from .models import WxRequest, WxResponse from .models import WxMusic, WxArticle, WxImage, WxVoice, WxVideo, WxLink @@ -36,7 +39,7 @@ def is_valid_params(self, params): sign_ele = [self.token, timestamp, nonce] sign_ele.sort() - if(signature == sha1(''.join(sign_ele)).hexdigest()): + if(signature == sha1(uniencode(''.join(sign_ele))).hexdigest()): return True, echostr else: return None @@ -62,9 +65,9 @@ def process(self, params, xml=None, token=None, app_id=None, aes_key=None): timestamp = params.get('timestamp', '') nonce = params.get('nonce', '') if encrypt_type == 'aes': - cpt = WXBizMsgCrypt(self.token, - self.aes_key, self.app_id) - err, xml = cpt.DecryptMsg(xml, msg_signature, timestamp, nonce) + cpt = WXBizMsgCrypt(uniencode(self.token), + uniencode(self.aes_key), uniencode(self.app_id)) + err, xml = cpt.DecryptMsg(uniencode(xml), uniencode(msg_signature), uniencode(timestamp), uniencode(nonce)) if err: return 'decrypt message error, code : %s' % err else: @@ -83,7 +86,7 @@ def process(self, params, xml=None, token=None, app_id=None, aes_key=None): # 加密消息 if encrypt_type != '' and encrypt_type != 'raw': if encrypt_type == 'aes': - err, result = cpt.EncryptMsg(result, nonce) + err, result = cpt.EncryptMsg(uniencode(result), uniencode(nonce)) if err: return 'encrypt message error , code %s' % err else: From 3a38a753547418e2c60573e9416c67e322f1ef71 Mon Sep 17 00:00:00 2001 From: raptorz Date: Wed, 18 Nov 2015 14:48:49 +0800 Subject: [PATCH 2/9] =?UTF-8?q?=E5=A2=9E=E5=8A=A0token=E7=9A=84=E8=BF=87?= =?UTF-8?q?=E6=9C=9F=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wechat/official.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/wechat/official.py b/wechat/official.py index 27d77e6..4beee54 100644 --- a/wechat/official.py +++ b/wechat/official.py @@ -11,6 +11,8 @@ uniencode = lambda s: (s.encode("utf-8") if isinstance(s, str) else s ) if sys.version>"3" else lambda s: s +from datetime import datetime, timedelta + from .models import WxRequest, WxResponse from .models import WxMusic, WxArticle, WxImage, WxVoice, WxVideo, WxLink from .models import WxTextResponse, WxImageResponse, WxVoiceResponse,\ @@ -197,17 +199,19 @@ def __init__(self, appid, appsecret, api_entry=None): self.appid = appid self.appsecret = appsecret self._access_token = None + self._expires = datetime.now() + timedelta(seconds=-7200) self.api_entry = api_entry or self.API_PREFIX @property def access_token(self): - if not self._access_token: + if not self._access_token or self._expires and self._expires < datetime.now(): + self._expires = None token, err = self.get_access_token() if not err: self._access_token = token['access_token'] - return self._access_token + self._expires = datetime.now() + timedelta(seconds=token['expires_in']) else: - return None + self._access_token = None return self._access_token def set_access_token(self, token): From 72aaf33cb564caa9035f947fae0569737ca924bc Mon Sep 17 00:00:00 2001 From: raptorz Date: Fri, 20 Nov 2015 15:53:24 +0800 Subject: [PATCH 3/9] https verify must can be configured --- wechat/enterprise.py | 2 +- wechat/official.py | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/wechat/enterprise.py b/wechat/enterprise.py index f41c601..58540f8 100644 --- a/wechat/enterprise.py +++ b/wechat/enterprise.py @@ -118,7 +118,7 @@ def get_access_token(self, url=None, **kwargs): params.update(kwargs) rsp = requests.get(url or self.api_entry + 'cgi-bin/gettoken', params=params, - verify=False) + verify=WxBaseApi.VERIFY) return self._process_response(rsp) def departments(self): diff --git a/wechat/official.py b/wechat/official.py index 4beee54..c5b07d6 100644 --- a/wechat/official.py +++ b/wechat/official.py @@ -194,6 +194,7 @@ def post_process(self, rsp): class WxBaseApi(object): API_PREFIX = 'https://api.weixin.qq.com/cgi-bin/' + VERIFY = True def __init__(self, appid, appsecret, api_entry=None): self.appid = appid @@ -233,7 +234,7 @@ def _get(self, path, params=None): params = {} params['access_token'] = self.access_token rsp = requests.get(self.api_entry + path, params=params, - verify=False) + verify=WxBaseApi.VERIFY) return self._process_response(rsp) def _post(self, path, data, ctype='json'): @@ -245,7 +246,7 @@ def _post(self, path, data, ctype='json'): path += '?access_token=' + self.access_token if ctype == 'json': data = json.dumps(data, ensure_ascii=False).encode('utf-8') - rsp = requests.post(path, data=data, headers=headers, verify=False) + rsp = requests.post(path, data=data, headers=headers, verify=WxBaseApi.VERIFY) return self._process_response(rsp) def upload_media(self, mtype, file_path=None, file_content=None, @@ -268,7 +269,7 @@ def upload_media(self, mtype, file_path=None, file_content=None, f.close() media = open(tmp_path, 'rb') rsp = requests.post(path, files={'media': media}, - verify=False) + verify=WxBaseApi.VERIFY) media.close() os.remove(tmp_path) return self._process_response(rsp) @@ -277,7 +278,7 @@ def download_media(self, media_id, to_path, url='media/get'): rsp = requests.get(self.api_entry + url, params={'media_id': media_id, 'access_token': self.access_token}, - verify=False) + verify=WxBaseApi.VERIFY) if rsp.status_code == 200: save_file = open(to_path, 'wb') save_file.write(rsp.content) @@ -316,7 +317,7 @@ def get_access_token(self, url=None, **kwargs): if kwargs: params.update(kwargs) rsp = requests.get(url or self.api_entry + 'token', params=params, - verify=False) + verify=WxBaseApi.VERIFY) return self._process_response(rsp) def user_info(self, user_id, lang='zh_CN'): From d6d846c23e5f2fe718e1437dc62ccf1815b0b1a5 Mon Sep 17 00:00:00 2001 From: raptorz Date: Wed, 16 Dec 2015 14:13:14 +0800 Subject: [PATCH 4/9] =?UTF-8?q?=E5=A2=9E=E5=8A=A0token=E7=AE=A1=E7=90=86?= =?UTF-8?q?=EF=BC=8C=E6=94=AF=E6=8C=81=E4=BD=BF=E7=94=A8redis=E7=BB=B4?= =?UTF-8?q?=E6=8A=A4token=EF=BC=8C=E9=81=BF=E5=85=8D=E5=A4=9A=E4=B8=AA?= =?UTF-8?q?=E5=AE=9E=E4=BE=8B=E9=87=8D=E5=A4=8D=E7=94=B3=E8=AF=B7token?= =?UTF-8?q?=EF=BC=8C=E5=AF=BC=E8=87=B4=E8=B6=85=E5=87=BA=E5=BE=AE=E4=BF=A1?= =?UTF-8?q?=E9=99=90=E5=88=B6=E3=80=82=E5=8F=A6=EF=BC=8C=E6=BA=90=E7=A0=81?= =?UTF-8?q?=E5=B7=B2=E6=94=B9=E4=B8=BA=E7=AC=A6=E5=90=88PEP8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wechat/__init__.py | 2 +- wechat/crypt.py | 41 +++++++++++++++---- wechat/enterprise.py | 14 +++---- wechat/models.py | 4 +- wechat/official.py | 60 +++++++++++++++------------- wechat/token_manager.py | 87 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 161 insertions(+), 47 deletions(-) create mode 100644 wechat/token_manager.py diff --git a/wechat/__init__.py b/wechat/__init__.py index 3b56589..2845204 100644 --- a/wechat/__init__.py +++ b/wechat/__init__.py @@ -1,2 +1,2 @@ -#encoding=utf-8 +# encoding=utf-8 VERSION = "0.4.14" diff --git a/wechat/crypt.py b/wechat/crypt.py index c5e6b18..4cf9da6 100644 --- a/wechat/crypt.py +++ b/wechat/crypt.py @@ -1,4 +1,4 @@ -#encoding=utf-8 +# encoding=utf-8 import base64 import string @@ -8,11 +8,21 @@ import struct from Crypto.Cipher import AES import xml.etree.cElementTree as ET -import sys import socket -if sys.version<"3": +import sys + + +if sys.version < "3": reload(sys) sys.setdefaultencoding('utf-8') + PY3 = False + + def str2bytes(s): + return s +else: + def str2bytes(s): + return s.encode("utf-8") if isinstance(s, str) else s + WXBizMsgCrypt_OK = 0 WXBizMsgCrypt_ValidateSignature_Error = -40001 @@ -64,6 +74,13 @@ def getSHA1(self, token, timestamp, nonce, encrypt): except Exception: return WXBizMsgCrypt_ComputeSignature_Error, None + @staticmethod + def getSignature(token, timestamp, nonce): + sign_ele = [token, timestamp, nonce] + sign_ele.sort() + s = "".join(sign_ele) + return hashlib.sha1(str2bytes(s)).hexdigest() + class XMLParse: """提供提取消息格式中的密文及生成回复消息格式的接口""" @@ -83,7 +100,7 @@ def extract(self, xmltext): xml_tree = ET.fromstring(xmltext) encrypt = xml_tree.find("Encrypt") touser_name = xml_tree.find("ToUserName") - if touser_name != None: + if touser_name is not None: touser_name = touser_name.text return WXBizMsgCrypt_OK, encrypt.text, touser_name except Exception: @@ -140,7 +157,7 @@ class Prpcrypt(object): """提供接收和推送给公众平台消息的加解密接口""" def __init__(self, key): - #self.key = base64.b64decode(key+"=") + # self.key = base64.b64decode(key+"=") self.key = key # 设置加解密模式为AES的CBC模式 self.mode = AES.MODE_CBC @@ -179,8 +196,8 @@ def decrypt(self, text, appid): try: pad = ord(plain_text[-1]) # 去掉补位字符串 - #pkcs7 = PKCS7Encoder() - #plain_text = pkcs7.encode(plain_text) + # pkcs7 = PKCS7Encoder() + # plain_text = pkcs7.encode(plain_text) # 去除16位随机字符串 content = plain_text[16:-pad] xml_len = socket.ntohl(struct.unpack("I", content[:4])[0]) @@ -203,17 +220,21 @@ def get_random_str(self): class WXBizMsgCrypt(object): def __init__(self, sToken, sEncodingAESKey, sCorpId): + (sToken, sEncodingAESKey, sCorpId) = \ + map(str2bytes, (sToken, sEncodingAESKey, sCorpId)) try: self.key = base64.b64decode(sEncodingAESKey+"=") assert len(self.key) == 32 except: throw_exception("[error]: EncodingAESKey unvalid !", FormatException) - #return WXBizMsgCrypt_IllegalAesKey) + # return WXBizMsgCrypt_IllegalAesKey) self.m_sToken = sToken self.m_sCorpid = sCorpId def VerifyURL(self, sMsgSignature, sTimeStamp, sNonce, sEchoStr): + (sMsgSignature, sTimeStamp, sNonce, sEchoStr) = \ + map(str2bytes, (sMsgSignature, sTimeStamp, sNonce, sEchoStr)) sha1 = SHA1() ret, signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, sEchoStr) @@ -226,6 +247,8 @@ def VerifyURL(self, sMsgSignature, sTimeStamp, sNonce, sEchoStr): return ret, sReplyEchoStr def EncryptMsg(self, sReplyMsg, sNonce, timestamp=None): + (sReplyMsg, sNonce, timestamp) = \ + map(str2bytes, (sReplyMsg, sNonce, timestamp)) pc = Prpcrypt(self.key) ret, encrypt = pc.encrypt(sReplyMsg, self.m_sCorpid) if ret != 0: @@ -242,6 +265,8 @@ def EncryptMsg(self, sReplyMsg, sNonce, timestamp=None): return ret, xmlParse.generate(encrypt, signature, timestamp, sNonce) def DecryptMsg(self, sPostData, sMsgSignature, sTimeStamp, sNonce): + (sPostData, sMsgSignature, sTimeStamp, sNonce) = \ + map(str2bytes, (sPostData, sMsgSignature, sTimeStamp, sNonce)) xmlParse = XMLParse() ret, encrypt, touser_name = xmlParse.extract(sPostData) if ret != 0: diff --git a/wechat/enterprise.py b/wechat/enterprise.py index 58540f8..9652e3b 100644 --- a/wechat/enterprise.py +++ b/wechat/enterprise.py @@ -1,4 +1,4 @@ -#encoding=utf-8 +# encoding=utf-8 import requests import time @@ -11,8 +11,6 @@ from .crypt import WXBizMsgCrypt import sys -uniencode = lambda s: (s.encode("utf-8") if isinstance(s, str) else s ) if sys.version>"3" else lambda s: s - __all__ = ['WxRequest', 'WxResponse', 'WxArticle', 'WxImage', 'WxVoice', 'WxVideo', 'WxLink', 'WxTextResponse', @@ -44,16 +42,16 @@ def process(self, params, xml=None, token=None, corp_id=None, msg_signature = params.get('msg_signature', '') echostr = params.get('echostr', '') - cpt = WXBizMsgCrypt(uniencode(self.token), uniencode(self.aes_key), uniencode(self.corp_id)) + cpt = WXBizMsgCrypt(self.token, self.aes_key, self.corp_id) - err, echo = cpt.VerifyURL(uniencode(msg_signature), uniencode(timestamp), uniencode(nonce), uniencode(echostr)) + err, echo = cpt.VerifyURL(msg_signature, timestamp, nonce, echostr) if err: return 'invalid request' if not xml: return echo - err, xml = cpt.DecryptMsg(uniencode(xml), uniencode(msg_signature), uniencode(timestamp), uniencode(nonce)) + err, xml = cpt.DecryptMsg(xml, msg_signature, timestamp, nonce) if err: return 'decrypt message error , code %s' % err @@ -64,12 +62,12 @@ def process(self, params, xml=None, token=None, corp_id=None, self.pre_process() rsp = func(self.req) self.post_process() - result = rsp.as_xml().encode('UTF-8') + result = rsp.as_xml().encode('UTF-8') if not result: return '' - err, result = cpt.EncryptMsg(uniencode(result), uniencode(nonce)) + err, result = cpt.EncryptMsg(result, nonce) if err: return 'encrypt message error , code %s' % err return result diff --git a/wechat/models.py b/wechat/models.py index 034790a..b71eb3a 100644 --- a/wechat/models.py +++ b/wechat/models.py @@ -1,11 +1,11 @@ -#encoding=utf-8 +# encoding=utf-8 from xml.dom import minidom import collections import time import sys -if sys.version>"3": +if sys.version > "3": long = int unicode = str diff --git a/wechat/official.py b/wechat/official.py index c5b07d6..36848f6 100644 --- a/wechat/official.py +++ b/wechat/official.py @@ -1,16 +1,13 @@ # encoding=utf-8 -from hashlib import sha1 +from functools import wraps import requests import json import tempfile import shutil import os -from .crypt import WXBizMsgCrypt +from .crypt import WXBizMsgCrypt, SHA1 import sys - -uniencode = lambda s: (s.encode("utf-8") if isinstance(s, str) else s ) if sys.version>"3" else lambda s: s - from datetime import datetime, timedelta from .models import WxRequest, WxResponse @@ -18,6 +15,7 @@ from .models import WxTextResponse, WxImageResponse, WxVoiceResponse,\ WxVideoResponse, WxMusicResponse, WxNewsResponse, APIError, WxEmptyResponse + __all__ = ['WxRequest', 'WxResponse', 'WxMusic', 'WxArticle', 'WxImage', 'WxVoice', 'WxVideo', 'WxLink', 'WxTextResponse', 'WxImageResponse', 'WxVoiceResponse', 'WxVideoResponse', @@ -39,9 +37,7 @@ def is_valid_params(self, params): signature = params.get('signature', '') echostr = params.get('echostr', '') - sign_ele = [self.token, timestamp, nonce] - sign_ele.sort() - if(signature == sha1(uniencode(''.join(sign_ele))).hexdigest()): + if (signature == SHA1.getSignature(self.token, timestamp, nonce)): return True, echostr else: return None @@ -67,9 +63,8 @@ def process(self, params, xml=None, token=None, app_id=None, aes_key=None): timestamp = params.get('timestamp', '') nonce = params.get('nonce', '') if encrypt_type == 'aes': - cpt = WXBizMsgCrypt(uniencode(self.token), - uniencode(self.aes_key), uniencode(self.app_id)) - err, xml = cpt.DecryptMsg(uniencode(xml), uniencode(msg_signature), uniencode(timestamp), uniencode(nonce)) + cpt = WXBizMsgCrypt(self.token, self.aes_key, self.app_id) + err, xml = cpt.DecryptMsg(xml, msg_signature, timestamp, nonce) if err: return 'decrypt message error, code : %s' % err else: @@ -88,7 +83,7 @@ def process(self, params, xml=None, token=None, app_id=None, aes_key=None): # 加密消息 if encrypt_type != '' and encrypt_type != 'raw': if encrypt_type == 'aes': - err, result = cpt.EncryptMsg(uniencode(result), uniencode(nonce)) + err, result = cpt.EncryptMsg(result, nonce) if err: return 'encrypt message error , code %s' % err else: @@ -191,32 +186,31 @@ def post_process(self, rsp): pass +def retry_token(fn): + def wrapper(self, *args, **kwargs): + content, err = fn(self, *args, **kwargs) + if not content and err and err.code in [40001, 40014, 42001]: + self.token_manager.set_token(self.get_access_token()) + return fn(self, *args, **kwargs) + else: + return content, err + return wrapper + + class WxBaseApi(object): API_PREFIX = 'https://api.weixin.qq.com/cgi-bin/' VERIFY = True - def __init__(self, appid, appsecret, api_entry=None): + def __init__(self, appid, appsecret, token_manager, api_entry=None): self.appid = appid self.appsecret = appsecret - self._access_token = None - self._expires = datetime.now() + timedelta(seconds=-7200) + self.token_manager = token_manager self.api_entry = api_entry or self.API_PREFIX @property def access_token(self): - if not self._access_token or self._expires and self._expires < datetime.now(): - self._expires = None - token, err = self.get_access_token() - if not err: - self._access_token = token['access_token'] - self._expires = datetime.now() + timedelta(seconds=token['expires_in']) - else: - self._access_token = None - return self._access_token - - def set_access_token(self, token): - self._access_token = token + return self.token_manager.get_token(self.get_access_token) def _process_response(self, rsp): if rsp.status_code != 200: @@ -229,6 +223,7 @@ def _process_response(self, rsp): return None, APIError(content['errcode'], content['errmsg']) return content, None + @retry_token def _get(self, path, params=None): if not params: params = {} @@ -237,6 +232,7 @@ def _get(self, path, params=None): verify=WxBaseApi.VERIFY) return self._process_response(rsp) + @retry_token def _post(self, path, data, ctype='json'): headers = {'Content-type': 'application/json'} path = self.api_entry + path @@ -246,7 +242,8 @@ def _post(self, path, data, ctype='json'): path += '?access_token=' + self.access_token if ctype == 'json': data = json.dumps(data, ensure_ascii=False).encode('utf-8') - rsp = requests.post(path, data=data, headers=headers, verify=WxBaseApi.VERIFY) + rsp = requests.post(path, data=data, headers=headers, + verify=WxBaseApi.VERIFY) return self._process_response(rsp) def upload_media(self, mtype, file_path=None, file_content=None, @@ -391,6 +388,13 @@ def send_news(self, to_user, news): {'touser': to_user, 'msgtype': 'news', 'news': {'articles': news}}) + def send_template(self, to_user, template_id, data): + return self._post('message/template/send', + {'touser': to_user, 'template_id': template_id, + 'url': 'http://weixin.qq.com/download', + 'data': data} + ) + def create_group(self, name): return self._post('groups/create', {'group': {'name': name}}) diff --git a/wechat/token_manager.py b/wechat/token_manager.py new file mode 100644 index 0000000..a3f0045 --- /dev/null +++ b/wechat/token_manager.py @@ -0,0 +1,87 @@ +# encoding=utf-8 + +from datetime import datetime, timedelta +from time import sleep + +import redis + + +class TokenManager(object): + def get_token(self, fn_get_access_token): + token = self.token + expires = self.expires + if not token and not expires: + for i in xrang(12): + sleep(5) + if self.token: + break + elif not token or expires and expires < datetime.now(): + self.expires = None + token, err = fn_get_access_token() + if not err: + self.token = token['access_token'] + self.expires = datetime.now() + \ + timedelta(seconds=token['expires_in']) + else: + self.token = None + return self.token + + def set_token(self, token): + self.token = token + self.expires = datetime.now() + timedelta(seconds=7200) + + +class LocalTokenManager(TokenManager): + def __init__(self): + self._access_token = None + self._expires = datetime.now() + + @property + def token(self): + return self._access_token + + @token.setter + def token(self, token): + self._access_token = token + + @property + def expires(self): + return self._expires + + @expires.setter + def expires(self, expires): + self._expires = expires + + +class RedisTokenManager(TokenManager): + DATETIME_FMT = "%Y-%m-%d %H:%M:%S" + + def __init__(self, postfix="", **kwargs): + self.token_name = "_".join(["access_token", postfix]) + self.expires_name = "_".join(["access_token_expires", postfix]) + self.redis = redis.Redis(**kwargs) + if not self.expires: + self.expires = datetime.now() + + @property + def token(self): + token = self.redis.get(self.token_name) + return str(token, "utf-8") if token and isinstance( + token, bytes) else token + + @token.setter + def token(self, token): + self.redis.set(self.token_name, token) + + @property + def expires(self): + expires = self.redis.get(self.expires_name) + return datetime.strptime( + str(expires, "utf-8"), + RedisTokenManager.DATETIME_FMT) if expires else None + + @expires.setter + def expires(self, expires): + self.redis.set(self.expires_name, + expires.strftime(RedisTokenManager.DATETIME_FMT) + if expires else None) From a890eee777fea71c6664f14018992280b4214177 Mon Sep 17 00:00:00 2001 From: raptorz Date: Wed, 16 Dec 2015 17:41:22 +0800 Subject: [PATCH 5/9] =?UTF-8?q?=E5=8F=91=E9=80=81=E6=A8=A1=E6=9D=BF?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E5=8A=9F=E8=83=BD=E5=A2=9E=E5=8A=A0url?= =?UTF-8?q?=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wechat/official.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/wechat/official.py b/wechat/official.py index 36848f6..8ec5ca8 100644 --- a/wechat/official.py +++ b/wechat/official.py @@ -388,12 +388,10 @@ def send_news(self, to_user, news): {'touser': to_user, 'msgtype': 'news', 'news': {'articles': news}}) - def send_template(self, to_user, template_id, data): + def send_template(self, to_user, template_id, url, data): return self._post('message/template/send', {'touser': to_user, 'template_id': template_id, - 'url': 'http://weixin.qq.com/download', - 'data': data} - ) + 'url': url, 'data': data}) def create_group(self, name): return self._post('groups/create', From 4e04349738873a4a3c0c7a8995f67541f0698828 Mon Sep 17 00:00:00 2001 From: raptorz Date: Mon, 21 Dec 2015 15:33:49 +0800 Subject: [PATCH 6/9] bugfixed: retry token --- wechat/official.py | 8 ++++++-- wechat/token_manager.py | 19 +++++++++---------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/wechat/official.py b/wechat/official.py index 8ec5ca8..8c8eb87 100644 --- a/wechat/official.py +++ b/wechat/official.py @@ -127,9 +127,13 @@ def event_map(self): } def on_event(self, event): - func = self.event_map().get(event.Event, None) + func = self.event_map().get(event.Event, self.on_other_event) return func(event) + def on_other_event(self, event): + # Unhandled event + return WxEmptyResponse() + def on_subscribe(self, sub): return WxTextResponse(self.WELCOME_TXT, sub) @@ -190,7 +194,7 @@ def retry_token(fn): def wrapper(self, *args, **kwargs): content, err = fn(self, *args, **kwargs) if not content and err and err.code in [40001, 40014, 42001]: - self.token_manager.set_token(self.get_access_token()) + self.token_manager.refresh_token(self.get_access_token) return fn(self, *args, **kwargs) else: return content, err diff --git a/wechat/token_manager.py b/wechat/token_manager.py index a3f0045..9d3e87a 100644 --- a/wechat/token_manager.py +++ b/wechat/token_manager.py @@ -17,18 +17,17 @@ def get_token(self, fn_get_access_token): break elif not token or expires and expires < datetime.now(): self.expires = None - token, err = fn_get_access_token() - if not err: - self.token = token['access_token'] - self.expires = datetime.now() + \ - timedelta(seconds=token['expires_in']) - else: - self.token = None + self.refresh_token(fn_get_access_token) return self.token - def set_token(self, token): - self.token = token - self.expires = datetime.now() + timedelta(seconds=7200) + def refresh_token(self, fn_get_access_token): + token, err = fn_get_access_token() + if token and not err: + self.token = token['access_token'] + self.expires = datetime.now() + \ + timedelta(seconds=token['expires_in']) + else: + self.token = None class LocalTokenManager(TokenManager): From 8d20dec7462541e5f3ea4a59abadf44f6200d2a1 Mon Sep 17 00:00:00 2001 From: raptorz Date: Tue, 22 Dec 2015 10:47:09 +0800 Subject: [PATCH 7/9] =?UTF-8?q?=E5=8E=9Fevent=5Fmap=E5=8F=AA=E8=83=BD?= =?UTF-8?q?=E7=94=A8event=5Fhandlers=E8=BF=9B=E8=A1=8C=E6=9B=BF=E6=8D=A2?= =?UTF-8?q?=EF=BC=8C=E4=B8=8D=E5=A4=9F=E6=96=B9=E4=BE=BF=EF=BC=8C=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E5=8F=AF=E6=89=A9=E5=B1=95=E7=9A=84event=5Fhandlers?= =?UTF-8?q?=EF=BC=8C=E5=8F=96=E6=B6=88event=5Fmap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wechat/official.py | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/wechat/official.py b/wechat/official.py index 8c8eb87..eaf6965 100644 --- a/wechat/official.py +++ b/wechat/official.py @@ -31,6 +31,22 @@ class WxApplication(object): APP_ID = None ENCODING_AES_KEY = None + def __init__(self): + self.event_handlers = { + 'subscribe': self.on_subscribe, + 'unsubscribe': self.on_unsubscribe, + 'SCAN': self.on_scan, + 'LOCATION': self.on_location_update, + 'CLICK': self.on_click, + 'VIEW': self.on_view, + 'scancode_push': self.on_scancode_push, + 'scancode_waitmsg': self.on_scancode_waitmsg, + 'pic_sysphoto': self.on_pic_sysphoto, + 'pic_photo_or_album': self.on_pic_photo_or_album, + 'pic_weixin': self.on_pic_weixin, + 'location_select': self.on_location_select + } + def is_valid_params(self, params): timestamp = params.get('timestamp', '') nonce = params.get('nonce', '') @@ -108,26 +124,8 @@ def on_video(self, video): def on_location(self, loc): return WxTextResponse(self.UNSUPPORT_TXT, loc) - def event_map(self): - if getattr(self, 'event_handlers', None): - return self.event_handlers - return { - 'subscribe': self.on_subscribe, - 'unsubscribe': self.on_unsubscribe, - 'SCAN': self.on_scan, - 'LOCATION': self.on_location_update, - 'CLICK': self.on_click, - 'VIEW': self.on_view, - 'scancode_push': self.on_scancode_push, - 'scancode_waitmsg': self.on_scancode_waitmsg, - 'pic_sysphoto': self.on_pic_sysphoto, - 'pic_photo_or_album': self.on_pic_photo_or_album, - 'pic_weixin': self.on_pic_weixin, - 'location_select': self.on_location_select, - } - def on_event(self, event): - func = self.event_map().get(event.Event, self.on_other_event) + func = self.event_handlers.get(event.Event, self.on_other_event) return func(event) def on_other_event(self, event): From 81518291ccdba13e5f31eeb4acc9391e87776bd9 Mon Sep 17 00:00:00 2001 From: raptor Date: Thu, 3 Mar 2016 17:26:55 +0800 Subject: [PATCH 8/9] enterprise authorize url updated, token_manager updated --- wechat/enterprise.py | 14 +++++--------- wechat/token_manager.py | 26 +++++++++++--------------- 2 files changed, 16 insertions(+), 24 deletions(-) diff --git a/wechat/enterprise.py b/wechat/enterprise.py index 9652e3b..113d37a 100644 --- a/wechat/enterprise.py +++ b/wechat/enterprise.py @@ -337,16 +337,12 @@ def delete_menu(self, agentid): # OAuth2 def authorize_url(self, appid, redirect_uri, response_type='code', scope='snsapi_base', state=None): - # 变态的微信实现,参数的顺序也有讲究。。艹!这个实现太恶心,太恶心! - url = 'https://open.weixin.qq.com/connect/oauth2/authorize?' - rd_uri = urllib.urlencode({'redirect_uri': redirect_uri}) - url += 'appid=%s&' % appid - url += rd_uri - url += '&response_type=' + response_type - url += '&scope=' + scope + params = dict(appid=appid, redirect_uri=redirect_uri, response_type=response_type, scope=scope) if state: - url += '&state=' + state - return url + '#wechat_redirect' + params['state'] = state + url = '?'.join(['https://open.weixin.qq.com/connect/oauth2/authorize', urllib.urlencode(sorted(params.items()))]) + url = '#'.join([url, 'wechat_redirect']) + return url def get_user_info(self, agentid, code): return self._get('cgi-bin/user/getuserinfo', diff --git a/wechat/token_manager.py b/wechat/token_manager.py index 9d3e87a..fda27e0 100644 --- a/wechat/token_manager.py +++ b/wechat/token_manager.py @@ -1,10 +1,13 @@ # encoding=utf-8 -from datetime import datetime, timedelta -from time import sleep +from time import time, sleep import redis +import logging + +logger = logging.getLogger(__name__) + class TokenManager(object): def get_token(self, fn_get_access_token): @@ -15,7 +18,7 @@ def get_token(self, fn_get_access_token): sleep(5) if self.token: break - elif not token or expires and expires < datetime.now(): + elif not token or expires and expires < time(): self.expires = None self.refresh_token(fn_get_access_token) return self.token @@ -24,8 +27,7 @@ def refresh_token(self, fn_get_access_token): token, err = fn_get_access_token() if token and not err: self.token = token['access_token'] - self.expires = datetime.now() + \ - timedelta(seconds=token['expires_in']) + self.expires = time() + token['expires_in'] else: self.token = None @@ -33,7 +35,7 @@ def refresh_token(self, fn_get_access_token): class LocalTokenManager(TokenManager): def __init__(self): self._access_token = None - self._expires = datetime.now() + self._expires = time() @property def token(self): @@ -53,14 +55,12 @@ def expires(self, expires): class RedisTokenManager(TokenManager): - DATETIME_FMT = "%Y-%m-%d %H:%M:%S" - def __init__(self, postfix="", **kwargs): self.token_name = "_".join(["access_token", postfix]) self.expires_name = "_".join(["access_token_expires", postfix]) self.redis = redis.Redis(**kwargs) if not self.expires: - self.expires = datetime.now() + self.expires = time() @property def token(self): @@ -75,12 +75,8 @@ def token(self, token): @property def expires(self): expires = self.redis.get(self.expires_name) - return datetime.strptime( - str(expires, "utf-8"), - RedisTokenManager.DATETIME_FMT) if expires else None + return expires @expires.setter def expires(self, expires): - self.redis.set(self.expires_name, - expires.strftime(RedisTokenManager.DATETIME_FMT) - if expires else None) + self.redis.set(self.expires_name, expires) From db666faf653a6a89548cf16242f841fd99e0f58a Mon Sep 17 00:00:00 2001 From: raptorz Date: Mon, 14 Mar 2016 11:45:22 +0800 Subject: [PATCH 9/9] readme --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index b3149a0..9d6dca5 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,14 @@ +# 变更说明 + +作者:[@raptorz](https://github.com/raptorz) + +* 支持python3 +* 符合PEP8 +* 增加token管理,包括js_ticket +* 增加模板消息发送功能 +* 可选是否验证https证书(原版为不验证) +* event_map改为可扩展 + # 微信公众号Python-SDK 作者: [@jeff_kit](http://twitter.com/jeff_kit)