# coding=utf-8 import hashlib import random import time import string import requests import logging import json from urllib.parse import urlencode from datetime import timedelta, datetime from django.conf import settings from django.utils import timezone from talos.cache.base import wechat_cache as db from talos.logger import info_logger, exception_logger datetime_format = '%Y-%m-%dT%H:%M:%S.%fZ' class WxTkApiErr(Exception): def __init__(self, desc, res): self.desc = desc self.res = res def __str__(self): return self.desc class WxTkApi(object): API_DOMAIN = 'https://api.weixin.qq.com' def __init__(self, app_id=None, app_secret=None): if app_id: self.app_id = app_id else: self.app_id = settings.WX_APP_ID if app_secret: self.app_secret = app_secret else: self.app_secret = settings.WX_APP_SECRET def get_access_token(self): cached_access_token = db.get('wechat:access_token') if cached_access_token is None: return self._get_wx_token() try: expired_time = db.get('wechat:access_token:expired_time') expired_time = datetime.strptime(expired_time, datetime_format) if datetime.now() < expired_time: return cached_access_token else: return self._get_wx_token() except Exception as e: logging.error(u'get_access_token 报错 %s', e.message) return self.get_access_token() def _get_wx_token(self): url = WxTkApi.API_DOMAIN + ('/cgi-bin/token') payload = { 'grant_type': 'client_credential', 'appid': self.app_id, 'secret': self.app_secret, } try: r = requests.get(url, params=payload, timeout=4) res = r.json() except requests.exceptions.Timeout: raise WxTkApiErr('timeout', None) except Exception: raise WxTkApiErr('unknown', None) if 'access_token' not in res: raise WxTkApiErr('error', r.text) now = timezone.now() expired_time = now + timedelta(seconds=res['expires_in'] - 60) expired_time = expired_time.strftime(datetime_format) db.set('wechat:access_token', res['access_token']) db.set('wechat:access_token:expired_time', expired_time) return res['access_token'] def get_jsapi_ticket(self, access_token): cached_jsapi_ticket = db.get('wechat:jsapi_ticket') if cached_jsapi_ticket is None: return self._get_jsapi_ticket(access_token) expired_time = db.get('wechat:jsapi_ticket:expired_time') expired_time = datetime.strptime(expired_time, datetime_format) if datetime.utcnow() < expired_time: return cached_jsapi_ticket else: return self._get_jsapi_ticket(access_token) def _get_jsapi_ticket(self, access_token): url = WxTkApi.API_DOMAIN + ('/cgi-bin/ticket/getticket') payload = { 'access_token': access_token, 'type': 'jsapi', } try: r = requests.get(url, params=payload, timeout=4) res = r.json() except requests.exceptions.Timeout: raise WxTkApiErr('timeout', None) except Exception as e: logging.error(u'_get_jsapi_ticket 报错 %s', e.message) raise WxTkApiErr('unknown', None) if 'ticket' not in res: raise WxTkApiErr('error', r.text) now = timezone.now() expired_time = now + timedelta(seconds=res['expires_in'] - 60) expired_time = expired_time.strftime(datetime_format) db.set('wechat:jsapi_ticket', res['ticket']) db.set('wechat:jsapi_ticket:expired_time', expired_time) return res['ticket'] def get_jsapi_sign(self, jsapi_ticket, url): nonce_str = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(15)) timestamp = int(time.time()) ret = { 'nonceStr': nonce_str, 'jsapi_ticket': jsapi_ticket, 'timestamp': timestamp, 'url': url, } str1 = '&'.join(['%s=%s' % (key.lower(), ret[key]) for key in sorted(ret)]) sign = hashlib.sha1(str1).hexdigest() ret['signature'] = sign del ret['jsapi_ticket'] return ret def get_media(self, access_token, media_id): url = 'http://file.api.weixin.qq.com/cgi-bin/media/get' payload = { 'access_token': access_token, 'media_id': media_id, } try: r = requests.get(url, params=payload, timeout=4) return r.content.decode("utf-8") except requests.exceptions.Timeout: raise WxTkApiErr('timeout', None) except Exception: raise WxTkApiErr('unknown', None) def get_material_list(self, access_token, material_type="news", offset=0, count=10): url = 'https://api.weixin.qq.com/cgi-bin/material/batchget_material' payload = { 'access_token': access_token, } data = { 'type': material_type, 'offset': offset, 'count': count, } try: r = requests.post(url, params=payload, data=json.dumps(data), timeout=4) return r.content.decode("utf-8") except requests.exceptions.Timeout: raise WxTkApiErr('timeout', None) except Exception: raise WxTkApiErr('unknown', None) def get_material(self, access_token, media_id): url = 'https://api.weixin.qq.com/cgi-bin/material/get_material' payload = { 'access_token': access_token, } data = { 'media_id': media_id } try: r = requests.post(url, params=payload, data=json.dumps(data), timeout=4) return r.content.decode("utf-8") except requests.exceptions.Timeout: raise WxTkApiErr('timeout', None) except Exception: raise WxTkApiErr('unknown', None) def get_material_count(self, access_token): url = 'https://api.weixin.qq.com/cgi-bin/material/get_materialcount' payload = { 'access_token': access_token, } try: r = requests.post(url, params=payload, timeout=4) return r.content.decode("utf-8") except requests.exceptions.Timeout: raise WxTkApiErr('timeout', None) except Exception: raise WxTkApiErr('unknown', None) def get_auth_url(self, redirect_url, scope='snsapi_base', state='STATE'): auth_url = 'https://open.weixin.qq.com/connect/oauth2/authorize?' + urlencode([ # here comes a bug for weixin, which needs urls params strictly to be in the following order # so we should use tuples instead of dict ('appid', self.app_id), ('redirect_uri', redirect_url), # key is URI, not URL ('response_type', 'code'), ('scope', scope), ('state', state), ]) + '#wechat_redirect' info_logger.info(u'wexin_auth:%s', auth_url) return auth_url def get_accesstoken_by_refresh(self, refresh_token): url = 'https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=%s&grant_type=refresh_token&refresh_token=%s' url = url % (self.app_id, refresh_token) try: r = requests.get(url) res = r.json() except Exception as e: exception_logger.error(e) raise WxTkApiErr('error', r.text) if 'access_token' not in res: raise WxTkApiErr('error', r.text) return res def get_sns_access_token(self, code): ''' CORRECT: { "access_token":"ACCESS_TOKEN", "expires_in":7200, "refresh_token":"REFRESH_TOKEN", "openid":"OPENID", "scope":"SCOPE" } ''' url = WxTkApi.API_DOMAIN + '/sns/oauth2/access_token?' + 'appid=' + self.app_id + '&secret=' + self.app_secret + '&code=' + code + '&grant_type=authorization_code' try: r = requests.get(url, timeout=4) info_logger.info("%s%s" % (code, r.content)) res = r.json() except requests.exceptions.Timeout: raise WxTkApiErr('timeout', None) except Exception: raise WxTkApiErr('unknown', None) if 'access_token' not in res: exception_logger.error(res) raise WxTkApiErr('error', r.text) return res def get_sns_userinfo(self, access_token, openid): ''' CORRECT: { "openid":"OPENID", "nickname":"NICKNAME", "sex":1, "province":"PROVINCE", "city":"CITY", "country":"COUNTRY", "headimgurl": "http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/0", "privilege":[ "PRIVILEGE1", "PRIVILEGE2" ], "unionid": " o6_bmasdasdsad6_2sgVt7hMZOPfL" } ''' url = WxTkApi.API_DOMAIN + '/sns/userinfo' payload = { 'access_token': access_token, 'openid': openid, 'lang': 'zh_CN', } try: r = requests.get(url, params=payload, timeout=4) res = r.json() except requests.exceptions.Timeout: raise WxTkApiErr('timeout', None) except Exception: raise WxTkApiErr('unknown', None) if not 'openid' in res: logging.error(u'get_sns_userinfo 报错 %s, access_token:%s, openid:%s', r.text, access_token, openid) raise WxTkApiErr('error', r.text) return res def get_user_info(self, access_token, openid): ''' { "subscribe": 1, # 1 表示用户关注过该公众号 0表示未关注 "openid": "o6_bmjrPTlm6_2sgVt7hMZOPfL2M", "nickname": "Band", "sex": 1, "language": "zh_CN", "city": "广州", "province": "广东", "country": "中国", "headimgurl": "http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/0", "subscribe_time": 1382694957, "unionid": " o6_bmasdasdsad6_2sgVt7hMZOPfL", "remark": "", "groupid": 0 } ''' url = WxTkApi.API_DOMAIN + '/cgi-bin/user/info' payload = { 'access_token': access_token, 'openid': openid, 'lang': 'zh_CN', } try: r = requests.get(url, params=payload, timeout=4) res = r.json() except requests.exceptions.Timeout: raise WxTkApiErr('timeout', None) except Exception: raise WxTkApiErr('unknown', None) return res