official.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. # encoding=utf-8
  2. from hashlib import sha1
  3. import requests
  4. import json
  5. import tempfile
  6. import shutil
  7. import os
  8. from .crypt import WXBizMsgCrypt
  9. from .models import WxRequest, WxResponse
  10. from .models import WxMusic, WxArticle, WxImage, WxVoice, WxVideo, WxLink
  11. from .models import WxTextResponse, WxImageResponse, WxVoiceResponse,\
  12. WxVideoResponse, WxMusicResponse, WxNewsResponse, APIError, WxEmptyResponse
  13. __all__ = ['WxRequest', 'WxResponse', 'WxMusic', 'WxArticle', 'WxImage',
  14. 'WxVoice', 'WxVideo', 'WxLink', 'WxTextResponse',
  15. 'WxImageResponse', 'WxVoiceResponse', 'WxVideoResponse',
  16. 'WxMusicResponse', 'WxNewsResponse', 'WxApplication',
  17. 'WxEmptyResponse', 'WxApi', 'APIError']
  18. class WxApplication(object):
  19. UNSUPPORT_TXT = u'暂不支持此类型消息'
  20. WELCOME_TXT = u'你好!感谢您的关注!'
  21. SECRET_TOKEN = None
  22. APP_ID = None
  23. ENCODING_AES_KEY = None
  24. def is_valid_params(self, params):
  25. timestamp = params.get('timestamp', '')
  26. nonce = params.get('nonce', '')
  27. signature = params.get('signature', '')
  28. echostr = params.get('echostr', '')
  29. sign_ele = [self.token, timestamp, nonce]
  30. sign_ele.sort()
  31. if(signature == sha1(''.join(sign_ele)).hexdigest()):
  32. return True, echostr
  33. else:
  34. return None
  35. def process(self, params, xml=None, token=None, app_id=None, aes_key=None):
  36. self.token = token if token else self.SECRET_TOKEN
  37. self.app_id = app_id if app_id else self.APP_ID
  38. self.aes_key = aes_key if aes_key else self.ENCODING_AES_KEY
  39. assert self.token is not None
  40. ret = self.is_valid_params(params)
  41. if not ret:
  42. return 'invalid request'
  43. if not xml:
  44. # 微信开发者设置的调用测试
  45. return ret[1]
  46. # 解密消息
  47. encrypt_type = params.get('encrypt_type', '')
  48. if encrypt_type != '' and encrypt_type != 'raw':
  49. msg_signature = params.get('msg_signature', '')
  50. timestamp = params.get('timestamp', '')
  51. nonce = params.get('nonce', '')
  52. if encrypt_type == 'aes':
  53. cpt = WXBizMsgCrypt(self.token,
  54. self.aes_key, self.app_id)
  55. err, xml = cpt.DecryptMsg(xml, msg_signature, timestamp, nonce)
  56. if err:
  57. return 'decrypt message error, code : %s' % err
  58. else:
  59. return 'unsupport encrypty type %s' % encrypt_type
  60. req = WxRequest(xml)
  61. self.wxreq = req
  62. func = self.handler_map().get(req.MsgType, None)
  63. if not func:
  64. return WxTextResponse(self.UNSUPPORT_TXT, req)
  65. self.pre_process()
  66. rsp = func(req)
  67. self.post_process(rsp)
  68. result = rsp.as_xml().encode('UTF-8')
  69. # 加密消息
  70. if encrypt_type != '' and encrypt_type != 'raw':
  71. if encrypt_type == 'aes':
  72. err, result = cpt.EncryptMsg(result, nonce)
  73. if err:
  74. return 'encrypt message error , code %s' % err
  75. else:
  76. return 'unsupport encrypty type %s' % encrypt_type
  77. return result
  78. def on_text(self, text):
  79. return WxTextResponse(self.UNSUPPORT_TXT, text)
  80. def on_link(self, link):
  81. return WxTextResponse(self.UNSUPPORT_TXT, link)
  82. def on_image(self, image):
  83. return WxTextResponse(self.UNSUPPORT_TXT, image)
  84. def on_voice(self, voice):
  85. return WxTextResponse(self.UNSUPPORT_TXT, voice)
  86. def on_video(self, video):
  87. return WxTextResponse(self.UNSUPPORT_TXT, video)
  88. def on_location(self, loc):
  89. return WxTextResponse(self.UNSUPPORT_TXT, loc)
  90. def event_map(self):
  91. if getattr(self, 'event_handlers', None):
  92. return self.event_handlers
  93. return {
  94. 'subscribe': self.on_subscribe,
  95. 'unsubscribe': self.on_unsubscribe,
  96. 'SCAN': self.on_scan,
  97. 'LOCATION': self.on_location_update,
  98. 'CLICK': self.on_click,
  99. 'VIEW': self.on_view,
  100. 'scancode_push': self.on_scancode_push,
  101. 'scancode_waitmsg': self.on_scancode_waitmsg,
  102. 'pic_sysphoto': self.on_pic_sysphoto,
  103. 'pic_photo_or_album': self.on_pic_photo_or_album,
  104. 'pic_weixin': self.on_pic_weixin,
  105. 'location_select': self.on_location_select,
  106. }
  107. def on_event(self, event):
  108. func = self.event_map().get(event.Event, None)
  109. return func(event)
  110. def on_subscribe(self, sub):
  111. return WxTextResponse(self.WELCOME_TXT, sub)
  112. def on_unsubscribe(self, unsub):
  113. return WxEmptyResponse()
  114. def on_click(self, click):
  115. return WxEmptyResponse()
  116. def on_scan(self, scan):
  117. return WxEmptyResponse()
  118. def on_location_update(self, location):
  119. return WxEmptyResponse()
  120. def on_view(self, view):
  121. return WxEmptyResponse()
  122. def on_scancode_push(self, event):
  123. return WxEmptyResponse()
  124. def on_scancode_waitmsg(self, event):
  125. return WxEmptyResponse()
  126. def on_pic_sysphoto(self, event):
  127. return WxEmptyResponse()
  128. def on_pic_photo_or_album(self, event):
  129. return WxEmptyResponse()
  130. def on_pic_weixin(self, event):
  131. return WxEmptyResponse()
  132. def on_location_select(self, event):
  133. return WxEmptyResponse()
  134. def handler_map(self):
  135. if getattr(self, 'handlers', None):
  136. return self.handlers
  137. return {
  138. 'text': self.on_text,
  139. 'link': self.on_link,
  140. 'image': self.on_image,
  141. 'voice': self.on_voice,
  142. 'video': self.on_video,
  143. 'location': self.on_location,
  144. 'event': self.on_event,
  145. }
  146. def pre_process(self):
  147. pass
  148. def post_process(self, rsp):
  149. pass
  150. class WxBaseApi(object):
  151. API_PREFIX = 'https://api.weixin.qq.com/cgi-bin/'
  152. def __init__(self, appid, appsecret, api_entry=None):
  153. self.appid = appid
  154. self.appsecret = appsecret
  155. self._access_token = None
  156. self.api_entry = api_entry or self.API_PREFIX
  157. @property
  158. def access_token(self):
  159. if not self._access_token:
  160. token, err = self.get_access_token()
  161. if not err:
  162. self._access_token = token['access_token']
  163. return self._access_token
  164. else:
  165. return None
  166. return self._access_token
  167. def set_access_token(self, token):
  168. self._access_token = token
  169. def _process_response(self, rsp):
  170. if rsp.status_code != 200:
  171. return None, APIError(rsp.status_code, 'http error')
  172. try:
  173. content = rsp.json()
  174. except:
  175. return None, APIError(99999, 'invalid rsp')
  176. if 'errcode' in content and content['errcode'] != 0:
  177. return None, APIError(content['errcode'], content['errmsg'])
  178. return content, None
  179. def _get(self, path, params=None):
  180. if not params:
  181. params = {}
  182. params['access_token'] = self.access_token
  183. rsp = requests.get(self.api_entry + path, params=params,
  184. verify=False)
  185. return self._process_response(rsp)
  186. def _post(self, path, data, ctype='json'):
  187. headers = {'Content-type': 'application/json'}
  188. path = self.api_entry + path
  189. if '?' in path:
  190. path += '&access_token=' + self.access_token
  191. else:
  192. path += '?access_token=' + self.access_token
  193. if ctype == 'json':
  194. data = json.dumps(data, ensure_ascii=False).encode('utf-8')
  195. rsp = requests.post(path, data=data, headers=headers, verify=False)
  196. return self._process_response(rsp)
  197. def upload_media(self, mtype, file_path=None, file_content=None,
  198. url='media/upload', suffies=None):
  199. path = self.api_entry + url + '?access_token=' \
  200. + self.access_token + '&type=' + mtype
  201. suffies = suffies or {'image': '.jpg', 'voice': '.mp3',
  202. 'video': 'mp4', 'thumb': 'jpg'}
  203. suffix = None
  204. if mtype in suffies:
  205. suffix = suffies[mtype]
  206. if file_path:
  207. fd, tmp_path = tempfile.mkstemp(suffix=suffix)
  208. shutil.copy(file_path, tmp_path)
  209. os.close(fd)
  210. elif file_content:
  211. fd, tmp_path = tempfile.mkstemp(suffix=suffix)
  212. f = os.fdopen(fd, 'wb')
  213. f.write(file_content)
  214. f.close()
  215. media = open(tmp_path, 'rb')
  216. rsp = requests.post(path, files={'media': media},
  217. verify=False)
  218. media.close()
  219. os.remove(tmp_path)
  220. return self._process_response(rsp)
  221. def download_media(self, media_id, to_path, url='media/get'):
  222. rsp = requests.get(self.api_entry + url,
  223. params={'media_id': media_id,
  224. 'access_token': self.access_token},
  225. verify=False)
  226. if rsp.status_code == 200:
  227. save_file = open(to_path, 'wb')
  228. save_file.write(rsp.content)
  229. save_file.close()
  230. return {'errcode': 0}, None
  231. else:
  232. return None, APIError(rsp.status_code, 'http error')
  233. def _get_media_id(self, obj, resource, content_type):
  234. if not obj.get(resource + '_id'):
  235. rsp, err = None, None
  236. if obj.get(resource + '_content'):
  237. rsp, err = self.upload_media(
  238. content_type,
  239. file_content=obj.get(resource + '_content'))
  240. if err:
  241. return None
  242. elif obj.get(resource + '_url'):
  243. rs = requests.get(obj.get(resource + '_url'))
  244. rsp, err = self.upload_media(
  245. content_type,
  246. file_content=rs.content)
  247. if err:
  248. return None
  249. else:
  250. return None
  251. return rsp['media_id']
  252. return obj.get(resource + '_id')
  253. class WxApi(WxBaseApi):
  254. def get_access_token(self, url=None, **kwargs):
  255. params = {'grant_type': 'client_credential', 'appid': self.appid,
  256. 'secret': self.appsecret}
  257. if kwargs:
  258. params.update(kwargs)
  259. rsp = requests.get(url or self.api_entry + 'token', params=params,
  260. verify=False)
  261. return self._process_response(rsp)
  262. def user_info(self, user_id, lang='zh_CN'):
  263. return self._get('user/info', {'openid': user_id, 'lang': lang})
  264. def followers(self, next_id=''):
  265. return self._get('user/get', {'next_openid': next_id})
  266. def send_message(self, to_user, msg_type, content):
  267. func = {'text': self.send_text,
  268. 'image': self.send_image,
  269. 'voice': self.send_voice,
  270. 'video': self.send_video,
  271. 'music': self.send_music,
  272. 'news': self.send_news}.get(msg_type, None)
  273. if func:
  274. return func(to_user, content)
  275. return None, None
  276. def send_text(self, to_user, content):
  277. return self._post('message/custom/send',
  278. {'touser': to_user, 'msgtype': 'text',
  279. 'text': {'content': content}})
  280. def send_image(self, to_user, media_id=None, media_url=None):
  281. if media_id and media_id.startswith('http'):
  282. media_url = media_id
  283. media_id = None
  284. mid = self._get_media_id(
  285. {'media_id': media_id, 'media_url': media_url},
  286. 'media', 'image')
  287. return self._post('message/custom/send',
  288. {'touser': to_user, 'msgtype': 'image',
  289. 'image': {'media_id': mid}})
  290. def send_voice(self, to_user, media_id=None, media_url=None):
  291. if media_id and media_id.startswith('http'):
  292. media_url = media_id
  293. media_id = None
  294. mid = self._get_media_id(
  295. {'media_id': media_id, 'media_url': media_url},
  296. 'media', 'voice')
  297. return self._post('message/custom/send',
  298. {'touser': to_user, 'msgtype': 'voice',
  299. 'voice': {'media_id': mid}})
  300. def send_music(self, to_user, music):
  301. music['thumb_media_id'] = self._get_media_id(music,
  302. 'thumb_media',
  303. 'image')
  304. if not music.get('thumb_media_id'):
  305. return None, APIError(41006, 'missing media_id')
  306. return self._post('message/custom/send',
  307. {'touser': to_user, 'msgtype': 'music',
  308. 'music': music})
  309. def send_video(self, to_user, video):
  310. video['media_id'] = self._get_media_id(video, 'media', 'video')
  311. video['thumb_media_id'] = self._get_media_id(video,
  312. 'thumb_media', 'image')
  313. if 'media_id' not in video or 'thumb_media_id' not in video:
  314. return None, APIError(41006, 'missing media_id')
  315. return self._post('message/custom/send',
  316. {'touser': to_user, 'msgtype': 'video',
  317. 'video': video})
  318. def send_news(self, to_user, news):
  319. if isinstance(news, dict):
  320. news = [news]
  321. return self._post('message/custom/send',
  322. {'touser': to_user, 'msgtype': 'news',
  323. 'news': {'articles': news}})
  324. def create_group(self, name):
  325. return self._post('groups/create',
  326. {'group': {'name': name}})
  327. def groups(self):
  328. return self._get('groups/get')
  329. def update_group(self, group_id, name):
  330. return self._post('groups/update',
  331. {'group': {'id': group_id, 'name': name}})
  332. def group_of_user(self, user_id):
  333. return self._get('groups/getid', {'openid': user_id})
  334. def move_user_to_group(self, user_id, group_id):
  335. return self._post('groups/members/update',
  336. {'openid': user_id, 'to_groupid': group_id})
  337. def create_menu(self, menus):
  338. return self._post('menu/create', menus)
  339. def get_menu(self):
  340. return self._get('menu/get')
  341. def delete_menu(self):
  342. return self._get('menu/delete')
  343. def customservice_records(self, starttime, endtime, openid=None,
  344. pagesize=100, pageindex=1):
  345. return self._get('customservice/getrecord',
  346. {'starttime': starttime,
  347. 'endtime': endtime,
  348. 'openid': openid,
  349. 'pagesize': pagesize,
  350. 'pageindex': pageindex})