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 ConnectionError, ReadTimeout
  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 WeChat 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 faile .'
  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