wxbot.py 35 KB

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