wxbot.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805
  1. #!/usr/bin/env python
  2. # coding: utf-8
  3. import pyqrcode
  4. import requests
  5. import json
  6. import xml.dom.minidom
  7. import urllib
  8. import time
  9. import re
  10. import random
  11. from requests.exceptions import *
  12. import os,subprocess,sys
  13. UNKONWN = 'unkonwn'
  14. SUCCESS = '200'
  15. SCANED = '201'
  16. TIMEOUT = '408'
  17. class WXBot:
  18. """WXBot, a framework to process WeChat messages"""
  19. def __init__(self):
  20. self.DEBUG = False
  21. self.uuid = ''
  22. self.base_uri = ''
  23. self.redirect_uri = ''
  24. self.uin = ''
  25. self.sid = ''
  26. self.skey = ''
  27. self.pass_ticket = ''
  28. self.device_id = 'e' + repr(random.random())[2:17]
  29. self.base_request = {}
  30. self.sync_key_str = ''
  31. self.sync_key = []
  32. self.user = {}
  33. self.account_info = {}
  34. self.member_list = [] # all kind of accounts: contacts, public accounts, groups, special accounts
  35. self.contact_list = [] # contact list
  36. self.public_list = [] # public account list
  37. self.group_list = [] # group chat list
  38. self.special_list = [] # special list account
  39. self.group_members = {} # members of all groups
  40. self.sync_host = ''
  41. self.session = requests.Session()
  42. self.session.headers.update({'User-Agent': 'Mozilla/5.0 (X11; Linux i686; U;) Gecko/20070322 Kazehakase/0.4.5'})
  43. self.conf = {'qr': 'png'}
  44. def get_contact(self):
  45. """Get information of all contacts of current account."""
  46. url = self.base_uri + '/webwxgetcontact?pass_ticket=%s&skey=%s&r=%s' \
  47. % (self.pass_ticket, self.skey, int(time.time()))
  48. r = self.session.post(url, data='{}')
  49. r.encoding = 'utf-8'
  50. if self.DEBUG:
  51. with open('contacts.json', 'w') as f:
  52. f.write(r.text.encode('utf-8'))
  53. dic = json.loads(r.text)
  54. self.member_list = dic['MemberList']
  55. special_users = ['newsapp', 'fmessage', 'filehelper', 'weibo', 'qqmail',
  56. 'fmessage', 'tmessage', 'qmessage', 'qqsync', 'floatbottle',
  57. 'lbsapp', 'shakeapp', 'medianote', 'qqfriend', 'readerapp',
  58. 'blogapp', 'facebookapp', 'masssendapp', 'meishiapp',
  59. 'feedsapp', 'voip', 'blogappweixin', 'weixin', 'brandsessionholder',
  60. 'weixinreminder', 'wxid_novlwrv3lqwv11', 'gh_22b87fa7cb3c',
  61. 'officialaccounts', 'notification_messages', 'wxid_novlwrv3lqwv11',
  62. 'gh_22b87fa7cb3c', 'wxitil', 'userexperience_alarm', 'notification_messages']
  63. self.contact_list = []
  64. self.public_list = []
  65. self.special_list = []
  66. self.group_list = []
  67. for contact in self.member_list:
  68. if contact['VerifyFlag'] & 8 != 0: # public account
  69. self.public_list.append(contact)
  70. self.account_info[contact['UserName']] = {'type': 'public', 'info': contact}
  71. elif contact['UserName'] in special_users: # special account
  72. self.special_list.append(contact)
  73. self.account_info[contact['UserName']] = {'type': 'special', 'info': contact}
  74. elif contact['UserName'].find('@@') != -1: # group
  75. self.group_list.append(contact)
  76. self.account_info[contact['UserName']] = {'type': 'group', 'info': contact}
  77. elif contact['UserName'] == self.user['UserName']: # self
  78. self.account_info[contact['UserName']] = {'type': 'self', 'info': contact}
  79. pass
  80. else:
  81. self.contact_list.append(contact)
  82. self.group_members = self.batch_get_group_members()
  83. for group in self.group_members:
  84. for member in self.group_members[group]:
  85. if member['UserName'] not in self.account_info:
  86. self.account_info[member['UserName']] = {'type': 'group_member', 'info': member, 'group': group}
  87. if self.DEBUG:
  88. with open('contact_list.json', 'w') as f:
  89. f.write(json.dumps(self.contact_list))
  90. with open('special_list.json', 'w') as f:
  91. f.write(json.dumps(self.special_list))
  92. with open('group_list.json', 'w') as f:
  93. f.write(json.dumps(self.group_list))
  94. with open('public_list.json', 'w') as f:
  95. f.write(json.dumps(self.public_list))
  96. with open('member_list.json', 'w') as f:
  97. f.write(json.dumps(self.member_list))
  98. with open('group_users.json', 'w') as f:
  99. f.write(json.dumps(self.group_members))
  100. with open('account_info.json', 'w') as f:
  101. f.write(json.dumps(self.account_info))
  102. return True
  103. def batch_get_group_members(self):
  104. """Get information of accounts in all groups at once."""
  105. url = self.base_uri + '/webwxbatchgetcontact?type=ex&r=%s&pass_ticket=%s' % (int(time.time()), self.pass_ticket)
  106. params = {
  107. 'BaseRequest': self.base_request,
  108. "Count": len(self.group_list),
  109. "List": [{"UserName": group['UserName'], "EncryChatRoomId": ""} for group in self.group_list]
  110. }
  111. r = self.session.post(url, data=json.dumps(params))
  112. r.encoding = 'utf-8'
  113. dic = json.loads(r.text)
  114. group_members = {}
  115. for group in dic['ContactList']:
  116. gid = group['UserName']
  117. members = group['MemberList']
  118. group_members[gid] = members
  119. return group_members
  120. def get_group_member_name(self, gid, uid):
  121. """
  122. Get name of a member in a group.
  123. :param gid: group id
  124. :param uid: group member id
  125. :return: names like {"display_name": "test_user", "nickname": "test", "remark_name": "for_test" }
  126. """
  127. if gid not in self.group_members:
  128. return None
  129. group = self.group_members[gid]
  130. for member in group:
  131. if member['UserName'] == uid:
  132. names = {}
  133. if 'RemarkName' in member:
  134. names['remark_name'] = member['RemarkName']
  135. if 'NickName' in member:
  136. names['nickname'] = member['NickName']
  137. if 'DisplayName' in member:
  138. names['display_name'] = member['DisplayName']
  139. return names
  140. return None
  141. def get_account_info(self, uid):
  142. if uid in self.account_info:
  143. return self.account_info[uid]
  144. else:
  145. return None
  146. def get_account_name(self, uid):
  147. info = self.get_account_info(uid)
  148. if info is None:
  149. return 'unknown'
  150. info = info['info']
  151. name = {}
  152. if 'RemarkName' in info and info['RemarkName']:
  153. name['remark_name'] = info['RemarkName']
  154. if 'NickName' in info and info['NickName']:
  155. name['nickname'] = info['NickName']
  156. if 'DisplayName' in info and info['DisplayName']:
  157. name['display_name'] = info['DisplayName']
  158. return name
  159. @staticmethod
  160. def get_prefer_name(name):
  161. if 'remark_name' in name:
  162. return name['remark_name']
  163. if 'display_name' in name:
  164. return name['display_name']
  165. if 'nickname' in name:
  166. return name['nickname']
  167. return 'unknown'
  168. def get_user_type(self, wx_user_id):
  169. """
  170. Get the relationship of a account and current user.
  171. :param wx_user_id:
  172. :return: The type of the account.
  173. """
  174. for account in self.contact_list:
  175. if wx_user_id == account['UserName']:
  176. return 'contact'
  177. for account in self.public_list:
  178. if wx_user_id == account['UserName']:
  179. return 'public'
  180. for account in self.special_list:
  181. if wx_user_id == account['UserName']:
  182. return 'special'
  183. for account in self.group_list:
  184. if wx_user_id == account['UserName']:
  185. return 'group'
  186. for group in self.group_members:
  187. for member in self.group_members[group]:
  188. if member['UserName'] == wx_user_id:
  189. return 'group_member'
  190. return 'unknown'
  191. def is_contact(self, uid):
  192. for account in self.contact_list:
  193. if uid == account['UserName']:
  194. return True
  195. return False
  196. def is_public(self, uid):
  197. for account in self.public_list:
  198. if uid == account['UserName']:
  199. return True
  200. return False
  201. def is_special(self, uid):
  202. for account in self.special_list:
  203. if uid == account['UserName']:
  204. return True
  205. return False
  206. def handle_msg_all(self, msg):
  207. """
  208. The function to process all WeChat messages, please override this function.
  209. msg:
  210. msg_id -> id of the received WeChat message
  211. msg_type_id -> the type of the message
  212. user -> the account that the message if sent from
  213. content -> content of the message
  214. :param msg: The received message.
  215. :return: None
  216. """
  217. pass
  218. def extract_msg_content(self, msg_type_id, msg):
  219. """
  220. content_type_id:
  221. 0 -> Text
  222. 1 -> Location
  223. 3 -> Image
  224. 4 -> Voice
  225. 5 -> Recommend
  226. 6 -> Animation
  227. 7 -> Share
  228. 8 -> Video
  229. 9 -> VideoCall
  230. 10 -> Redraw
  231. 11 -> Empty
  232. 99 -> Unknown
  233. :param msg_type_id: The type of the received message.
  234. :param msg: The received message.
  235. :return: The extracted content of the message.
  236. """
  237. mtype = msg['MsgType']
  238. content = msg['Content'].replace('&lt;', '<').replace('&gt;', '>')
  239. msg_id = msg['MsgId']
  240. msg_content = {}
  241. if msg_type_id == 0:
  242. return {'type': 11, 'data': ''}
  243. elif msg_type_id == 2: # File Helper
  244. return {'type': 0, 'data': content.replace('<br/>', '\n')}
  245. elif msg_type_id == 3: # Group
  246. sp = content.find('<br/>')
  247. uid = content[:sp]
  248. content = content[sp:]
  249. content = content.replace('<br/>', '')
  250. uid = uid[:-1]
  251. msg_content['user'] = {'id': uid, 'name': self.get_prefer_name(self.get_account_name(uid))}
  252. else: # Self, Contact, Special, Public, Unknown
  253. pass
  254. msg_prefix = (msg_content['user']['name'] + ':') if 'user' in msg_content else ''
  255. if mtype == 1:
  256. if content.find('http://weixin.qq.com/cgi-bin/redirectforward?args=') != -1:
  257. r = self.session.get(content)
  258. r.encoding = 'gbk'
  259. data = r.text
  260. pos = self.search_content('title', data, 'xml')
  261. msg_content['type'] = 1
  262. msg_content['data'] = pos
  263. msg_content['detail'] = data
  264. if self.DEBUG:
  265. print ' %s[Location] %s ' % (msg_prefix, pos)
  266. else:
  267. msg_content['type'] = 0
  268. msg_content['data'] = content.replace(u'\u2005', '')
  269. if self.DEBUG:
  270. print ' %s[Text] %s' % (msg_prefix, msg_content['data'])
  271. elif mtype == 3:
  272. msg_content['type'] = 3
  273. msg_content['data'] = self.get_msg_img_url(msg_id)
  274. if self.DEBUG:
  275. image = self.get_msg_img(msg_id)
  276. print ' %s[Image] %s' % (msg_prefix, image)
  277. elif mtype == 34:
  278. msg_content['type'] = 4
  279. msg_content['data'] = self.get_voice_url(msg_id)
  280. if self.DEBUG:
  281. voice = self.get_voice(msg_id)
  282. print ' %s[Voice] %s' % (msg_prefix, voice)
  283. elif mtype == 42:
  284. msg_content['type'] = 5
  285. info = msg['RecommendInfo']
  286. msg_content['data'] = {'nickname': info['NickName'],
  287. 'alias': info['Alias'],
  288. 'province': info['Province'],
  289. 'city': info['City'],
  290. 'gender': ['unknown', 'male', 'female'][info['Sex']]}
  291. if self.DEBUG:
  292. print ' %s[Recommend]' % msg_prefix
  293. print ' -----------------------------'
  294. print ' | NickName: %s' % info['NickName']
  295. print ' | Alias: %s' % info['Alias']
  296. print ' | Local: %s %s' % (info['Province'], info['City'])
  297. print ' | Gender: %s' % ['unknown', 'male', 'female'][info['Sex']]
  298. print ' -----------------------------'
  299. elif mtype == 47:
  300. msg_content['type'] = 6
  301. msg_content['data'] = self.search_content('cdnurl', content)
  302. if self.DEBUG:
  303. print ' %s[Animation] %s' % (msg_prefix, msg_content['data'])
  304. elif mtype == 49:
  305. msg_content['type'] = 7
  306. app_msg_type = ''
  307. if msg['AppMsgType'] == 3:
  308. app_msg_type = 'music'
  309. elif msg['AppMsgType'] == 5:
  310. app_msg_type = 'link'
  311. elif msg['AppMsgType'] == 7:
  312. app_msg_type = 'weibo'
  313. else:
  314. app_msg_type = 'unknown'
  315. msg_content['data'] = {'type': app_msg_type,
  316. 'title': msg['FileName'],
  317. 'desc': self.search_content('des', content, 'xml'),
  318. 'url': msg['Url'],
  319. 'from': self.search_content('appname', content, 'xml')}
  320. if self.DEBUG:
  321. print ' %s[Share] %s' % (msg_prefix, app_msg_type)
  322. print ' --------------------------'
  323. print ' | title: %s' % msg['FileName']
  324. print ' | desc: %s' % self.search_content('des', content, 'xml')
  325. print ' | link: %s' % msg['Url']
  326. print ' | from: %s' % self.search_content('appname', content, 'xml')
  327. print ' --------------------------'
  328. elif mtype == 62:
  329. msg_content['type'] = 8
  330. msg_content['data'] = content
  331. if self.DEBUG:
  332. print ' %s[Video] Please check on mobiles' % msg_prefix
  333. elif mtype == 53:
  334. msg_content['type'] = 9
  335. msg_content['data'] = content
  336. if self.DEBUG:
  337. print ' %s[Video Call]' % msg_prefix
  338. elif mtype == 10002:
  339. msg_content['type'] = 10
  340. msg_content['data'] = content
  341. if self.DEBUG:
  342. print ' %s[Redraw]' % msg_prefix
  343. elif mtype == 10000:
  344. msg_content['type'] = 12
  345. msg_content['data'] = msg['Content']
  346. if self.DEBUG:
  347. print ' [Red Packet]'
  348. else:
  349. msg_content['type'] = 99
  350. msg_content['data'] = content
  351. if self.DEBUG:
  352. print ' %s[Unknown]' % msg_prefix
  353. return msg_content
  354. def handle_msg(self, r):
  355. """
  356. The inner function that processes raw WeChat messages.
  357. msg_type_id:
  358. 0 -> Init
  359. 1 -> Self
  360. 2 -> FileHelper
  361. 3 -> Group
  362. 4 -> Contact
  363. 5 -> Public
  364. 6 -> Special
  365. 99 -> Unknown
  366. :param r: The raw data of the messages.
  367. :return: None
  368. """
  369. for msg in r['AddMsgList']:
  370. msg_type_id = 99
  371. user = {'id': msg['FromUserName'], 'name': 'unknown'}
  372. if msg['MsgType'] == 51: # init message
  373. msg_type_id = 0
  374. user['name'] = 'system'
  375. elif msg['FromUserName'] == self.user['UserName']: # Self
  376. msg_type_id = 1
  377. user['name'] = 'self'
  378. elif msg['ToUserName'] == 'filehelper': # File Helper
  379. msg_type_id = 2
  380. user['name'] = 'file_helper'
  381. elif msg['FromUserName'][:2] == '@@': # Group
  382. msg_type_id = 3
  383. user['name'] = self.get_prefer_name(self.get_account_name(user['id']))
  384. elif self.is_contact(msg['FromUserName']): # Contact
  385. msg_type_id = 4
  386. user['name'] = self.get_prefer_name(self.get_account_name(user['id']))
  387. elif self.is_public(msg['FromUserName']): # Public
  388. msg_type_id = 5
  389. user['name'] = self.get_prefer_name(self.get_account_name(user['id']))
  390. elif self.is_special(msg['FromUserName']): # Special
  391. msg_type_id = 6
  392. user['name'] = self.get_prefer_name(self.get_account_name(user['id']))
  393. else:
  394. msg_type_id = 99
  395. user['name'] = 'unknown'
  396. if self.DEBUG and msg_type_id != 0:
  397. print '[MSG] %s:' % user['name']
  398. content = self.extract_msg_content(msg_type_id, msg)
  399. message = {'msg_type_id': msg_type_id,
  400. 'msg_id': msg['MsgId'],
  401. 'content': content,
  402. 'to_user_id': msg['ToUserName'],
  403. 'user': user}
  404. self.handle_msg_all(message)
  405. def schedule(self):
  406. """
  407. The function to do schedule works.
  408. This function will be called a lot of times.
  409. Please override this if needed.
  410. :return: None
  411. """
  412. pass
  413. def proc_msg(self):
  414. self.test_sync_check()
  415. while True:
  416. check_time = time.time()
  417. [retcode, selector] = self.sync_check()
  418. if retcode == '1100': # logout from mobile
  419. break
  420. elif retcode == '1101': # login web WeChat from other devide
  421. break
  422. elif retcode == '0':
  423. if selector == '2': # new message
  424. r = self.sync()
  425. if r is not None:
  426. self.handle_msg(r)
  427. elif selector == '7': # Play WeChat on mobile
  428. r = self.sync()
  429. if r is not None:
  430. self.handle_msg(r)
  431. elif selector == '0': # nothing
  432. pass
  433. else:
  434. pass
  435. self.schedule()
  436. check_time = time.time() - check_time
  437. if check_time < 0.5:
  438. time.sleep(0.5 - check_time)
  439. def send_msg_by_uid(self, word, dst='filehelper'):
  440. url = self.base_uri + '/webwxsendmsg?pass_ticket=%s' % self.pass_ticket
  441. msg_id = str(int(time.time() * 1000)) + str(random.random())[:5].replace('.', '')
  442. if type(word) == 'str':
  443. word = word.decode('utf-8')
  444. params = {
  445. 'BaseRequest': self.base_request,
  446. 'Msg': {
  447. "Type": 1,
  448. "Content": word,
  449. "FromUserName": self.user['UserName'],
  450. "ToUserName": dst,
  451. "LocalID": msg_id,
  452. "ClientMsgId": msg_id
  453. }
  454. }
  455. headers = {'content-type': 'application/json; charset=UTF-8'}
  456. data = json.dumps(params, ensure_ascii=False).encode('utf8')
  457. try:
  458. r = self.session.post(url, data=data, headers=headers)
  459. except (ConnectionError, ReadTimeout):
  460. return False
  461. dic = r.json()
  462. return dic['BaseResponse']['Ret'] == 0
  463. def get_user_id(self, name):
  464. for contact in self.contact_list:
  465. if 'RemarkName' in contact and contact['RemarkName'] == name:
  466. return contact['UserName']
  467. elif 'NickName' in contact and contact['NickName'] == name:
  468. return contact['UserName']
  469. elif 'DisplayName' in contact and contact['DisplayName'] == name:
  470. return contact['UserName']
  471. return ''
  472. def send_msg(self, name, word, isfile=False):
  473. uid = self.get_user_id(name)
  474. if uid:
  475. if isfile:
  476. with open(word, 'r') as f:
  477. result = True
  478. for line in f.readlines():
  479. line = line.replace('\n', '')
  480. print '-> ' + name + ': ' + line
  481. if self.send_msg_by_uid(line, uid):
  482. pass
  483. else:
  484. result = False
  485. time.sleep(1)
  486. return result
  487. else:
  488. if self.send_msg_by_uid(word, uid):
  489. return True
  490. else:
  491. return False
  492. else:
  493. if self.DEBUG:
  494. print '[ERROR] This user does not exist .'
  495. return True
  496. @staticmethod
  497. def search_content(key, content, fmat='attr'):
  498. if fmat == 'attr':
  499. pm = re.search(key + '\s?=\s?"([^"<]+)"', content)
  500. if pm:
  501. return pm.group(1)
  502. elif fmat == 'xml':
  503. pm = re.search('<{0}>([^<]+)</{0}>'.format(key), content)
  504. if pm:
  505. return pm.group(1)
  506. return 'unknown'
  507. def run(self):
  508. self.get_uuid()
  509. self.gen_qr_code('qr.png')
  510. print '[INFO] Please use WeCaht to scan the QR code .'
  511. result = self.wait4login()
  512. if result != SUCCESS:
  513. print '[ERROR] Web WeChat login failed. failed code=%s'%(result, )
  514. return
  515. if self.login():
  516. print '[INFO] Web WeChat login succeed .'
  517. else:
  518. print '[ERROR] Web WeChat login failed.'
  519. return
  520. if self.init():
  521. print '[INFO] Web WeChat init succeed .'
  522. else:
  523. print '[INFO] Web WeChat init failed'
  524. return
  525. self.status_notify()
  526. self.get_contact()
  527. print '[INFO] Get %d contacts' % len(self.contact_list)
  528. print '[INFO] Start to process messages .'
  529. self.proc_msg()
  530. def get_uuid(self):
  531. url = 'https://login.weixin.qq.com/jslogin'
  532. params = {
  533. 'appid': 'wx782c26e4c19acffb',
  534. 'fun': 'new',
  535. 'lang': 'zh_CN',
  536. '_': int(time.time()) * 1000 + random.randint(1, 999),
  537. }
  538. r = self.session.get(url, params=params)
  539. r.encoding = 'utf-8'
  540. data = r.text
  541. regx = r'window.QRLogin.code = (\d+); window.QRLogin.uuid = "(\S+?)"'
  542. pm = re.search(regx, data)
  543. if pm:
  544. code = pm.group(1)
  545. self.uuid = pm.group(2)
  546. return code == '200'
  547. return False
  548. def gen_qr_code(self, qr_file_path):
  549. string = 'https://login.weixin.qq.com/l/' + self.uuid
  550. qr = pyqrcode.create(string)
  551. if self.conf['qr'] == 'png':
  552. qr.png(qr_file_path, scale=8)
  553. if sys.platform.find('darwin') >= 0:
  554. subprocess.call(['open', qr_file_path])
  555. elif sys.platform.find('linux') >= 0:
  556. subprocess.call(['xdg-open', qr_file_path])
  557. else:
  558. os.startfile(qr_file_path)
  559. elif self.conf['qr'] == 'tty':
  560. print(qr.terminal(quiet_zone=1))
  561. def do_request(self, url):
  562. r = self.session.get(url)
  563. r.encoding = 'utf-8'
  564. data = r.text
  565. param = re.search(r'window.code=(\d+);', data)
  566. code = param.group(1)
  567. return code, data
  568. def wait4login(self):
  569. '''
  570. http comet:
  571. tip=1, the request wait for user to scan the qr,
  572. 201: scaned
  573. 408: timeout
  574. tip=0, the request wait for user confirm,
  575. 200: confirmed
  576. '''
  577. LOGIN_TEMPLATE = 'https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login?tip=%s&uuid=%s&_=%s'
  578. tip = 1
  579. try_later_secs = 1
  580. MAX_RETRY_TIMES = 10
  581. code = UNKONWN
  582. retry_time = MAX_RETRY_TIMES
  583. while retry_time > 0:
  584. url = LOGIN_TEMPLATE % (tip, self.uuid, int(time.time()))
  585. code, data = self.do_request(url)
  586. if code == SCANED:
  587. print '[INFO] Please confirm to login .'
  588. tip = 0
  589. elif code == SUCCESS: #confirmed sucess
  590. param = re.search(r'window.redirect_uri="(\S+?)";', data)
  591. redirect_uri = param.group(1) + '&fun=new'
  592. self.redirect_uri = redirect_uri
  593. self.base_uri = redirect_uri[:redirect_uri.rfind('/')]
  594. return code
  595. elif code == TIMEOUT:
  596. print '[ERROR] WeChat login timeout. retry in %s secs later...'%(try_later_secs, )
  597. tip = 1 #need to reset tip, because the server will reset the peer connection
  598. retry_time -= 1
  599. time.sleep(try_later_secs)
  600. else:
  601. print ('[ERROR] WeChat login exception return_code=%s. retry in %s secs later...' %
  602. (code, try_later_secs))
  603. tip = 1
  604. retry_time -= 1
  605. time.sleep(try_later_secs)
  606. return code
  607. def login(self):
  608. if len(self.redirect_uri) < 4:
  609. print '[ERROR] Login failed due to network problem, please try again.'
  610. return False
  611. r = self.session.get(self.redirect_uri)
  612. r.encoding = 'utf-8'
  613. data = r.text
  614. doc = xml.dom.minidom.parseString(data)
  615. root = doc.documentElement
  616. for node in root.childNodes:
  617. if node.nodeName == 'skey':
  618. self.skey = node.childNodes[0].data
  619. elif node.nodeName == 'wxsid':
  620. self.sid = node.childNodes[0].data
  621. elif node.nodeName == 'wxuin':
  622. self.uin = node.childNodes[0].data
  623. elif node.nodeName == 'pass_ticket':
  624. self.pass_ticket = node.childNodes[0].data
  625. if '' in (self.skey, self.sid, self.uin, self.pass_ticket):
  626. return False
  627. self.base_request = {
  628. 'Uin': self.uin,
  629. 'Sid': self.sid,
  630. 'Skey': self.skey,
  631. 'DeviceID': self.device_id,
  632. }
  633. return True
  634. def init(self):
  635. url = self.base_uri + '/webwxinit?r=%i&lang=en_US&pass_ticket=%s' % (int(time.time()), self.pass_ticket)
  636. params = {
  637. 'BaseRequest': self.base_request
  638. }
  639. r = self.session.post(url, data=json.dumps(params))
  640. r.encoding = 'utf-8'
  641. dic = json.loads(r.text)
  642. self.sync_key = dic['SyncKey']
  643. self.user = dic['User']
  644. self.sync_key_str = '|'.join([str(keyVal['Key']) + '_' + str(keyVal['Val'])
  645. for keyVal in self.sync_key['List']])
  646. return dic['BaseResponse']['Ret'] == 0
  647. def status_notify(self):
  648. url = self.base_uri + '/webwxstatusnotify?lang=zh_CN&pass_ticket=%s' % self.pass_ticket
  649. self.base_request['Uin'] = int(self.base_request['Uin'])
  650. params = {
  651. 'BaseRequest': self.base_request,
  652. "Code": 3,
  653. "FromUserName": self.user['UserName'],
  654. "ToUserName": self.user['UserName'],
  655. "ClientMsgId": int(time.time())
  656. }
  657. r = self.session.post(url, data=json.dumps(params))
  658. r.encoding = 'utf-8'
  659. dic = json.loads(r.text)
  660. return dic['BaseResponse']['Ret'] == 0
  661. def test_sync_check(self):
  662. for host in ['webpush', 'webpush2']:
  663. self.sync_host = host
  664. retcode = self.sync_check()[0]
  665. if retcode == '0':
  666. return True
  667. return False
  668. def sync_check(self):
  669. params = {
  670. 'r': int(time.time()),
  671. 'sid': self.sid,
  672. 'uin': self.uin,
  673. 'skey': self.skey,
  674. 'deviceid': self.device_id,
  675. 'synckey': self.sync_key_str,
  676. '_': int(time.time()),
  677. }
  678. url = 'https://' + self.sync_host + '.weixin.qq.com/cgi-bin/mmwebwx-bin/synccheck?' + urllib.urlencode(params)
  679. try:
  680. r = self.session.get(url)
  681. except (ConnectionError, ReadTimeout):
  682. return [-1, -1]
  683. r.encoding = 'utf-8'
  684. data = r.text
  685. pm = re.search(r'window.synccheck=\{retcode:"(\d+)",selector:"(\d+)"\}', data)
  686. retcode = pm.group(1)
  687. selector = pm.group(2)
  688. return [retcode, selector]
  689. def sync(self):
  690. url = self.base_uri + '/webwxsync?sid=%s&skey=%s&lang=en_US&pass_ticket=%s' \
  691. % (self.sid, self.skey, self.pass_ticket)
  692. params = {
  693. 'BaseRequest': self.base_request,
  694. 'SyncKey': self.sync_key,
  695. 'rr': ~int(time.time())
  696. }
  697. try:
  698. r = self.session.post(url, data=json.dumps(params))
  699. except (ConnectionError, ReadTimeout):
  700. return None
  701. r.encoding = 'utf-8'
  702. dic = json.loads(r.text)
  703. if dic['BaseResponse']['Ret'] == 0:
  704. self.sync_key = dic['SyncKey']
  705. self.sync_key_str = '|'.join([str(keyVal['Key']) + '_' + str(keyVal['Val'])
  706. for keyVal in self.sync_key['List']])
  707. return dic
  708. def get_icon(self, uid):
  709. url = self.base_uri + '/webwxgeticon?username=%s&skey=%s' % (uid, self.skey)
  710. r = self.session.get(url)
  711. data = r.content
  712. fn = 'img_' + uid + '.jpg'
  713. with open(fn, 'wb') as f:
  714. f.write(data)
  715. return fn
  716. def get_head_img(self, uid):
  717. url = self.base_uri + '/webwxgetheadimg?username=%s&skey=%s' % (uid, self.skey)
  718. r = self.session.get(url)
  719. data = r.content
  720. fn = 'img_' + uid + '.jpg'
  721. with open(fn, 'wb') as f:
  722. f.write(data)
  723. return fn
  724. def get_msg_img_url(self, msgid):
  725. return self.base_uri + '/webwxgetmsgimg?MsgID=%s&skey=%s' % (msgid, self.skey)
  726. def get_msg_img(self, msgid):
  727. url = self.base_uri + '/webwxgetmsgimg?MsgID=%s&skey=%s' % (msgid, self.skey)
  728. r = self.session.get(url)
  729. data = r.content
  730. fn = 'img_' + msgid + '.jpg'
  731. with open(fn, 'wb') as f:
  732. f.write(data)
  733. return fn
  734. def get_voice_url(self, msgid):
  735. return self.base_uri + '/webwxgetvoice?msgid=%s&skey=%s' % (msgid, self.skey)
  736. def get_voice(self, msgid):
  737. url = self.base_uri + '/webwxgetvoice?msgid=%s&skey=%s' % (msgid, self.skey)
  738. r = self.session.get(url)
  739. data = r.content
  740. fn = 'voice_' + msgid + '.mp3'
  741. with open(fn, 'wb') as f:
  742. f.write(data)
  743. return fn