wx.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. from __future__ import unicode_literals
  2. import time
  3. import json
  4. import requests
  5. import redis
  6. import string
  7. import random
  8. import hashlib
  9. __all__ = ("wx_login", "WeixinMP", )
  10. try:
  11. unicode = unicode
  12. except NameError:
  13. # python 3
  14. basestring = (str, bytes)
  15. else:
  16. # python 2
  17. bytes = str
  18. class WeixinError(Exception):
  19. def __init__(self, msg):
  20. super(WeixinError, self).__init__(msg)
  21. class Map(dict):
  22. """
  23. 提供字典的dot访问模式
  24. Example:
  25. m = Map({'first_name': 'Eduardo'}, last_name='Pool', age=24, sports=['Soccer'])
  26. """
  27. def __init__(self, *args, **kwargs):
  28. super(Map, self).__init__(*args, **kwargs)
  29. for arg in args:
  30. if isinstance(arg, dict):
  31. for k, v in arg.items():
  32. if isinstance(v, dict):
  33. v = Map(v)
  34. self[k] = v
  35. if kwargs:
  36. for k, v in kwargs.items():
  37. if isinstance(v, dict):
  38. v = Map(v)
  39. self[k] = v
  40. def __getattr__(self, attr):
  41. return self[attr]
  42. def __setattr__(self, key, value):
  43. self.__setitem__(key, value)
  44. def __getitem__(self, key):
  45. if key not in self.__dict__:
  46. super(Map, self).__setitem__(key, {})
  47. self.__dict__.update({key: Map()})
  48. return self.__dict__[key]
  49. def __setitem__(self, key, value):
  50. super(Map, self).__setitem__(key, value)
  51. self.__dict__.update({key: value})
  52. def __delattr__(self, item):
  53. self.__delitem__(item)
  54. def __delitem__(self, key):
  55. super(Map, self).__delitem__(key)
  56. del self.__dict__[key]
  57. class WeixinLoginError(WeixinError):
  58. def __init__(self, msg):
  59. super(WeixinLoginError, self).__init__(msg)
  60. class WeixinMP(object):
  61. api_uri = "https://api.weixin.qq.com/cgi-bin"
  62. def __init__(self, app_id=None, app_secret=None, redis_url=None):
  63. self.app_id = app_id
  64. self.app_secret = app_secret
  65. self.session = requests.Session()
  66. if redis_url is not None:
  67. self.redis_store = redis.StrictRedis.from_url(
  68. redis_url, decode_responses=True)
  69. else:
  70. self.redis_store = None
  71. def init_app(self, app):
  72. """
  73. flask config
  74. """
  75. app.config.setdefault("WXMP_REDIS_URL", None)
  76. app.config.setdefault("WXMP_APP_ID", None)
  77. app.config.setdefault("WXMP_APP_SECRET", None)
  78. self.app_id = app.config.get("WXMP_APP_ID")
  79. self.app_secret = app.config.get("WXMP_APP_SECRET")
  80. redis_url = app.config.get("WXMP_REDIS_URL")
  81. assert self.app_id is not None, "APP_ID IS NULL"
  82. assert self.app_secret is not None, "APP_SECRET IS NULL"
  83. # assert redis_url is not None, "WXMP_REDIS_URL IS NULL"
  84. if redis_url:
  85. self.redis_store = redis.StrictRedis.from_url(
  86. redis_url, decode_responses=True)
  87. def fetch(self, method, url, params=None, data=None, headers=None):
  88. req = requests.Request(method, url, params=params,
  89. data=data, headers=headers)
  90. prepped = req.prepare()
  91. resp = self.session.send(prepped, timeout=20)
  92. data = Map(resp.json())
  93. # if data.errcode:
  94. # msg = "%(errcode)d %(errmsg)s" % data
  95. # raise WeixinError(msg)
  96. return data
  97. def get(self, path, params=None, token=True):
  98. url = "{0}{1}".format(self.api_uri, path)
  99. params = {} if not params else params
  100. token and params.setdefault("access_token", self.access_token)
  101. return self.fetch("GET", url, params)
  102. def post(self, path, params={}, isjson=True):
  103. url = "{0}{1}".format(self.api_uri, path)
  104. if isjson:
  105. resp = requests.post(url, data=json.dumps(params))
  106. else:
  107. resp = requests.post(url, data=params)
  108. data = Map(resp.json())
  109. return data
  110. def gen_token(self):
  111. params = dict()
  112. params.setdefault("grant_type", "client_credential")
  113. params.setdefault("appid", self.app_id)
  114. params.setdefault("secret", self.app_secret)
  115. data = self.get("/token", params, False)
  116. print("-" * 20, data, params)
  117. return data.access_token
  118. def get_user_info(self, access_token, user_id="hehe", lang="zh_CN"):
  119. """
  120. 获取用户基本信息。
  121. :param user_id: 用户 ID 。 就是你收到的 `Message` 的 source
  122. :param lang: 返回国家地区语言版本,zh_CN 简体,zh_TW 繁体,en 英语
  123. :return: 返回的 JSON 数据包
  124. """
  125. params = dict()
  126. params.setdefault("openid", user_id)
  127. params.setdefault("access_token", access_token)
  128. params.setdefault("lang", lang)
  129. data = self.get("/user/info", params, False)
  130. print(data, "get_user_info")
  131. return data
  132. def check_token(self, access_token):
  133. data = self.get_user_info(access_token)
  134. if data.errcode in (40001, 42001):
  135. return False
  136. return True
  137. @property
  138. def access_token(self):
  139. """
  140. 获取服务端凭证
  141. """
  142. if self.redis_store:
  143. ac_key = "access_token:%s" % self.app_id
  144. access_token = self.redis_store.get(ac_key)
  145. if not access_token:
  146. access_token = self.gen_token()
  147. self.redis_store.setex(ac_key, 2 * 60 * 60, access_token)
  148. return access_token
  149. else:
  150. if self.check_token(access_token):
  151. return access_token
  152. return self.gen_token()
  153. def gen_ticket(self):
  154. params = dict()
  155. params.setdefault("type", "jsapi")
  156. data = self.get("/ticket/getticket", params, True)
  157. return data.ticket
  158. @property
  159. def jsapi_ticket(self):
  160. """
  161. 获取jsapi ticket
  162. """
  163. if self.redis_store:
  164. ticket_key = "jsapi_ticket:%s" % self.app_id
  165. ticket = self.redis_store.get(ticket_key)
  166. if not ticket:
  167. ticket = self.gen_ticket()
  168. self.redis_store.setex(ticket_key, 2 * 60 * 60, ticket)
  169. return ticket
  170. else:
  171. return self.gen_ticket()
  172. @property
  173. def nonce_str(self):
  174. char = string.ascii_letters + string.digits
  175. return "".join(random.choice(char) for _ in range(32))
  176. def jsapi_sign(self, **kwargs):
  177. """
  178. 生成签名给js使用
  179. """
  180. timestamp = str(int(time.time()))
  181. nonce_str = self.nonce_str
  182. kwargs.setdefault("jsapi_ticket", self.jsapi_ticket)
  183. kwargs.setdefault("timestamp", timestamp)
  184. kwargs.setdefault("noncestr", nonce_str)
  185. raw = [(str(k), str(kwargs[k])) for k in sorted(kwargs.keys())]
  186. s = "&".join("=".join(kv) for kv in raw if kv[1])
  187. print(s)
  188. sign = hashlib.sha1(s.encode("utf-8")).hexdigest().lower()
  189. return Map(sign=sign, timestamp=timestamp, noncestr=nonce_str)
  190. def send_sp_template_msg(self, openid, temp_id, data,
  191. page="", miniprogram_state=""):
  192. """
  193. 发送小程序模板消息
  194. """
  195. url_path = "/message/subscribe/send?access_token={}".format(
  196. self.access_token)
  197. _dict = dict(touser=openid, template_id=temp_id,
  198. page=page, miniprogram_state=miniprogram_state,
  199. data=data)
  200. print(url_path, _dict)
  201. return self.post(url_path, _dict)
  202. class WeixinLogin(object):
  203. def __init__(self, app=None, prefix=""):
  204. if app:
  205. self.app_id = app.config.get(prefix + "WXMP_APP_ID")
  206. # print(self.app_id)
  207. self.app_secret = app.config.get(prefix + "WXMP_APP_SECRET")
  208. else:
  209. self.app_id = ""
  210. self.app_secret = ""
  211. def init_app(self, app, prefix=""):
  212. self.app_id = app.config.get(prefix + "WXMP_APP_ID")
  213. self.app_secret = app.config.get(prefix + "WXMP_APP_SECRET")
  214. def _get(self, url, params):
  215. resp = requests.get(url, params=params)
  216. data = Map(json.loads(resp.content.decode("utf-8")))
  217. # if data.errcode:
  218. # msg = "%(errcode)d %(errmsg)s" % data
  219. # raise WeixinLoginError(msg)
  220. return data
  221. def authorize(self, redirect_uri, scope="snsapi_base", state=None):
  222. """
  223. 生成微信认证地址并且跳转
  224. :param redirect_uri: 跳转地址
  225. :param scope: 微信认证方式,有`snsapi_base`跟`snsapi_userinfo`两种
  226. :param state: 认证成功后会原样带上此字段
  227. """
  228. assert scope in ["snsapi_base", "snsapi_userinfo", "snsapi_login"]
  229. if scope == "snsapi_login":
  230. url = "https://open.weixin.qq.com/connect/qrconnect"
  231. else:
  232. url = "https://open.weixin.qq.com/connect/oauth2/authorize"
  233. data = dict()
  234. data.setdefault("appid", self.app_id)
  235. data.setdefault("redirect_uri", redirect_uri)
  236. data.setdefault("response_type", "code")
  237. data.setdefault("scope", scope)
  238. if state:
  239. data.setdefault("state", state)
  240. print(data, "data")
  241. data = [(k, data[k]) for k in sorted(data.keys()) if data[k]]
  242. s = "&".join("=".join(kv) for kv in data if kv[1])
  243. print(s)
  244. return "{0}?{1}#wechat_redirect".format(url, s)
  245. def access_token(self, code):
  246. """
  247. 获取令牌
  248. """
  249. url = "https://api.weixin.qq.com/sns/oauth2/access_token"
  250. args = dict()
  251. args.setdefault("appid", self.app_id)
  252. args.setdefault("secret", self.app_secret)
  253. args.setdefault("code", code)
  254. args.setdefault("grant_type", "authorization_code")
  255. return self._get(url, args)
  256. def auth(self, access_token, openid):
  257. """
  258. 检验授权凭证
  259. :param access_token: 授权凭证
  260. :param openid: 唯一id
  261. """
  262. url = "https://api.weixin.qq.com/sns/auth"
  263. args = dict()
  264. args.setdefault("access_token", access_token)
  265. args.setdefault("openid", openid)
  266. return self._get(url, args)
  267. def refresh_token(self, refresh_token):
  268. """
  269. 重新获取access_token
  270. :param refresh_token: 刷新令牌
  271. """
  272. url = "https://api.weixin.qq.com/sns/oauth2/refresh_token"
  273. args = dict()
  274. args.setdefault("appid", self.app_id)
  275. args.setdefault("grant_type", "refresh_token")
  276. args.setdefault("refresh_token", refresh_token)
  277. return self._get(url, args)
  278. def userinfo(self, access_token, openid):
  279. """
  280. 获取用户信息
  281. :param access_token: 令牌
  282. :param openid: 用户id,每个应用内唯一
  283. """
  284. url = "https://api.weixin.qq.com/sns/userinfo"
  285. args = dict()
  286. args.setdefault("access_token", access_token)
  287. args.setdefault("openid", openid)
  288. args.setdefault("lang", "zh_CN")
  289. return self._get(url, args)
  290. def user_info(self, access_token, openid):
  291. """
  292. 获取用户信息
  293. 兼容老版本0.3.0,与WeixinMP的user_info冲突
  294. """
  295. return self.userinfo(access_token, openid)
  296. def get_sp_unionid(self, code):
  297. url = "https://api.weixin.qq.com/sns/jscode2session"
  298. args = dict(appid=self.app_id, secret=self.app_secret,
  299. js_code=code, grant_type="authorization_code")
  300. return self._get(url, args)
  301. wx_login = WeixinLogin()