wxbot.py 36 KB

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