wxbot.py 34 KB

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