wxbot.py 35 KB

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