wxbot.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  1. #!/usr/bin/env python
  2. # coding: utf-8
  3. from collections import defaultdict
  4. import pyqrcode
  5. import requests
  6. import json
  7. import xml.dom.minidom
  8. import multiprocessing
  9. import urllib
  10. import time, re, sys, os, random
  11. def utf82gbk(string):
  12. return string.decode('utf8').encode('gbk')
  13. def make_unicode(data):
  14. if not data:
  15. return data
  16. result = None
  17. if type(data) == unicode:
  18. result = data
  19. elif type(data) == str:
  20. result = data.decode('utf-8')
  21. return result
  22. class WXBot:
  23. def __init__(self):
  24. self.DEBUG = False
  25. self.uuid = ''
  26. self.base_uri = ''
  27. self.redirect_uri= ''
  28. self.uin = ''
  29. self.sid = ''
  30. self.skey = ''
  31. self.pass_ticket = ''
  32. self.device_id = 'e' + repr(random.random())[2:17]
  33. self.base_request = {}
  34. self.sync_key_str = ''
  35. self.sync_key = []
  36. self.user = []
  37. self.member_list = []
  38. self.contact_list = [] # contact list
  39. self.public_list = [] # public account list
  40. self.group_list = [] # group chat list
  41. self.special_list = [] # special list account
  42. self.sync_host = ''
  43. self.session = requests.Session()
  44. self.session.headers.update({'User-Agent': 'Mozilla/5.0 (X11; Linux i686; U;) Gecko/20070322 Kazehakase/0.4.5'})
  45. self.conf = {'qr': 'png',}
  46. def get_uuid(self):
  47. url = 'https://login.weixin.qq.com/jslogin'
  48. params = {
  49. 'appid': 'wx782c26e4c19acffb',
  50. 'fun': 'new',
  51. 'lang': 'zh_CN',
  52. '_': int(time.time())*1000 + random.randint(1,999),
  53. }
  54. r = self.session.get(url, params=params)
  55. r.encoding = 'utf-8'
  56. data = r.text
  57. regx = r'window.QRLogin.code = (\d+); window.QRLogin.uuid = "(\S+?)"'
  58. pm = re.search(regx, data)
  59. if pm:
  60. code = pm.group(1)
  61. self.uuid = pm.group(2)
  62. return code == '200'
  63. return False
  64. def gen_qr_code(self, qr_file_path):
  65. string = 'https://login.weixin.qq.com/l/' + self.uuid
  66. qr = pyqrcode.create(string)
  67. if self.conf['qr'] == 'png':
  68. qr.png(qr_file_path)
  69. elif self.conf['qr'] == 'tty':
  70. print 'Not support tty'
  71. pass
  72. #qr.print_tty()
  73. def wait4login(self, tip):
  74. time.sleep(tip)
  75. url = 'https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login?tip=%s&uuid=%s&_=%s' % (tip, self.uuid, int(time.time()))
  76. r = self.session.get(url)
  77. r.encoding = 'utf-8'
  78. data = r.text
  79. param = re.search(r'window.code=(\d+);', data)
  80. code = param.group(1)
  81. if code == '201':
  82. return True
  83. elif code == '200':
  84. param = re.search(r'window.redirect_uri="(\S+?)";', data)
  85. redirect_uri = param.group(1) + '&fun=new'
  86. self.redirect_uri = redirect_uri
  87. self.base_uri = redirect_uri[:redirect_uri.rfind('/')]
  88. return True
  89. elif code == '408':
  90. print '[ERROR] WeChat login timeout .'
  91. else:
  92. print '[ERROR] WeChat login exception .'
  93. return False
  94. def login(self):
  95. r = self.session.get(self.redirect_uri)
  96. r.encoding = 'utf-8'
  97. data = r.text
  98. doc = xml.dom.minidom.parseString(data)
  99. root = doc.documentElement
  100. for node in root.childNodes:
  101. if node.nodeName == 'skey':
  102. self.skey = node.childNodes[0].data
  103. elif node.nodeName == 'wxsid':
  104. self.sid = node.childNodes[0].data
  105. elif node.nodeName == 'wxuin':
  106. self.uin = node.childNodes[0].data
  107. elif node.nodeName == 'pass_ticket':
  108. self.pass_ticket = node.childNodes[0].data
  109. if '' in (self.skey, self.sid, self.uin, self.pass_ticket):
  110. return False
  111. self.base_request = {
  112. 'Uin': self.uin,
  113. 'Sid': self.sid,
  114. 'Skey': self.skey,
  115. 'DeviceID': self.device_id,
  116. }
  117. return True
  118. def init(self):
  119. url = self.base_uri + '/webwxinit?r=%i&lang=en_US&pass_ticket=%s' % (int(time.time()), self.pass_ticket)
  120. params = {
  121. 'BaseRequest': self.base_request
  122. }
  123. r = self.session.post(url, data=json.dumps(params))
  124. r.encoding = 'utf-8'
  125. dic = json.loads(r.text)
  126. self.sync_key = dic['SyncKey']
  127. self.user = dic['User']
  128. self.sync_key_str = '|'.join([ str(keyVal['Key']) + '_' + str(keyVal['Val']) for keyVal in self.sync_key['List'] ])
  129. return dic['BaseResponse']['Ret'] == 0
  130. def status_notify(self):
  131. url = self.base_uri + '/webwxstatusnotify?lang=zh_CN&pass_ticket=%s' % (self.pass_ticket)
  132. self.base_request['Uin'] = int(self.base_request['Uin'])
  133. params = {
  134. 'BaseRequest': self.base_request,
  135. "Code": 3,
  136. "FromUserName": self.user['UserName'],
  137. "ToUserName": self.user['UserName'],
  138. "ClientMsgId": int(time.time())
  139. }
  140. r = self.session.post(url, data=json.dumps(params))
  141. r.encoding = 'utf-8'
  142. dic = json.loads(r.text)
  143. return dic['BaseResponse']['Ret'] == 0
  144. def get_contact(self):
  145. url = self.base_uri + '/webwxgetcontact?pass_ticket=%s&skey=%s&r=%s' % (self.pass_ticket, self.skey, int(time.time()))
  146. r = self.session.post(url, data='{}')
  147. r.encoding = 'utf-8'
  148. if self.DEBUG:
  149. with open('contacts.json', 'w') as f:
  150. f.write(r.text.encode('utf-8'))
  151. dic = json.loads(r.text)
  152. self.member_list = dic['MemberList']
  153. SpecialUsers = ['newsapp','fmessage','filehelper','weibo','qqmail','fmessage','tmessage','qmessage','qqsync','floatbottle','lbsapp','shakeapp','medianote',
  154. 'qqfriend','readerapp','blogapp','facebookapp','masssendapp','meishiapp','feedsapp','voip','blogappweixin','weixin','brandsessionholder','weixinreminder','wxid_novlwrv3lqwv11',
  155. 'gh_22b87fa7cb3c','officialaccounts','notification_messages','wxid_novlwrv3lqwv11','gh_22b87fa7cb3c','wxitil','userexperience_alarm','notification_messages']
  156. self.contact_list = []
  157. self.public_list = []
  158. self.special_list = []
  159. self.group_list = []
  160. for contact in self.member_list:
  161. if contact['VerifyFlag'] & 8 != 0: # public account
  162. self.public_list.append(contact)
  163. elif contact['UserName'] in SpecialUsers: # special account
  164. self.special_list.append(contact)
  165. elif contact['UserName'].find('@@') != -1: # group
  166. self.group_list.append(contact)
  167. elif contact['UserName'] == self.user['UserName']: # self
  168. pass
  169. else:
  170. self.contact_list.append(contact)
  171. if self.DEBUG:
  172. with open('contact_list.json', 'w') as f:
  173. f.write(json.dumps(self.contact_list))
  174. with open('special_list.json', 'w') as f:
  175. f.write(json.dumps(self.special_list))
  176. with open('group_list.json', 'w') as f:
  177. f.write(json.dumps(self.group_list))
  178. with open('public_list.json', 'w') as f:
  179. f.write(json.dumps(self.public_list))
  180. return True
  181. def batch_get_contact(self):
  182. url = self.base_uri + '/webwxbatchgetcontact?type=ex&r=%s&pass_ticket=%s' % (int(time.time()), self.pass_ticket)
  183. params = {
  184. 'BaseRequest': self.base_request,
  185. "Count": len(self.group_list),
  186. "List": [ {"UserName": g['UserName'], "EncryChatRoomId":""} for g in self.group_list ]
  187. }
  188. r = self.session.post(url, data=params)
  189. r.encoding = 'utf-8'
  190. dic = json.loads(r.text)
  191. return True
  192. def test_sync_check(self):
  193. for host in ['webpush', 'webpush2']:
  194. self.sync_host = host
  195. [retcode, selector] = self.sync_check()
  196. if retcode == '0':
  197. return True
  198. return False
  199. def sync_check(self):
  200. params = {
  201. 'r': int(time.time()),
  202. 'sid': self.sid,
  203. 'uin': self.uin,
  204. 'skey': self.skey,
  205. 'deviceid': self.device_id,
  206. 'synckey': self.sync_key_str,
  207. '_': int(time.time()),
  208. }
  209. url = 'https://' + self.sync_host + '.weixin.qq.com/cgi-bin/mmwebwx-bin/synccheck?' + urllib.urlencode(params)
  210. r = self.session.get(url)
  211. r.encoding = 'utf-8'
  212. data = r.text
  213. pm = re.search(r'window.synccheck={retcode:"(\d+)",selector:"(\d+)"}', data)
  214. retcode = pm.group(1)
  215. selector = pm.group(2)
  216. return [retcode, selector]
  217. def sync(self):
  218. url = self.base_uri + '/webwxsync?sid=%s&skey=%s&lang=en_US&pass_ticket=%s' % (self.sid, self.skey, self.pass_ticket)
  219. params = {
  220. 'BaseRequest': self.base_request,
  221. 'SyncKey': self.sync_key,
  222. 'rr': ~int(time.time())
  223. }
  224. r = self.session.post(url, data=json.dumps(params))
  225. r.encoding = 'utf-8'
  226. dic = json.loads(r.text)
  227. if dic['BaseResponse']['Ret'] == 0:
  228. self.sync_key = dic['SyncKey']
  229. self.sync_key_str = '|'.join([ str(keyVal['Key']) + '_' + str(keyVal['Val']) for keyVal in self.sync_key['List'] ])
  230. return dic
  231. def get_icon(self, id):
  232. url = self.base_uri + '/webwxgeticon?username=%s&skey=%s' % (id, self.skey)
  233. r = self.session.get(url)
  234. data = r.content
  235. fn = 'img_'+id+'.jpg'
  236. with open(fn, 'wb') as f:
  237. f.write(data)
  238. return fn
  239. def get_head_img(self, id):
  240. url = self.base_uri + '/webwxgetheadimg?username=%s&skey=%s' % (id, self.skey)
  241. r = self.session.get(url)
  242. data = r.content
  243. fn = 'img_'+id+'.jpg'
  244. with open(fn, 'wb') as f:
  245. f.write(data)
  246. return fn
  247. def get_msg_img_url(self, msgid):
  248. return self.base_uri + '/webwxgetmsgimg?MsgID=%s&skey=%s' % (msgid, self.skey)
  249. def get_msg_img(self, msgid):
  250. url = self.base_uri + '/webwxgetmsgimg?MsgID=%s&skey=%s' % (msgid, self.skey)
  251. r = self.session.get(url)
  252. data = r.content
  253. fn = 'img_'+msgid+'.jpg'
  254. with open(fn, 'wb') as f:
  255. f.write(data)
  256. return fn
  257. def get_voice_url(self, msgid):
  258. return self.base_uri + '/webwxgetvoice?msgid=%s&skey=%s' % (msgid, self.skey)
  259. def get_voice(self, msgid):
  260. url = self.base_uri + '/webwxgetvoice?msgid=%s&skey=%s' % (msgid, self.skey)
  261. r = self.session.get(url)
  262. data = r.content
  263. fn = 'voice_'+msgid+'.mp3'
  264. with open(fn, 'wb') as f:
  265. f.write(data)
  266. return fn
  267. #Get the NickName or RemarkName of an user by user id
  268. def get_user_remark_name(self, uid):
  269. name = 'unknown group' if uid[:2] == '@@' else 'stranger'
  270. for member in self.member_list:
  271. if member['UserName'] == uid:
  272. name = member['RemarkName'] if member['RemarkName'] else member['NickName']
  273. return name
  274. #Get user id of an user
  275. def get_user_id(self, name):
  276. for member in self.member_list:
  277. if name == member['RemarkName'] or name == member['NickName'] or name == member['UserName']:
  278. return member['UserName']
  279. return None
  280. def get_user_type(self, wx_user_id):
  281. for account in self.contact_list:
  282. if wx_user_id == account['UserName']:
  283. return 'contact'
  284. for account in self.public_list:
  285. if wx_user_id == account['UserName']:
  286. return 'public'
  287. for account in self.special_list:
  288. if wx_user_id == account['UserName']:
  289. return 'special'
  290. for account in self.group_list:
  291. if wx_user_id == account['UserName']:
  292. return 'group'
  293. return 'unknown'
  294. '''
  295. msg:
  296. user_type
  297. msg_id
  298. msg_type_id
  299. user_id
  300. user_name
  301. content
  302. '''
  303. def handle_msg_all(self, msg):
  304. pass
  305. '''
  306. msg_type_id:
  307. 1 -> Location
  308. 2 -> FileHelper
  309. 3 -> Self
  310. 4 -> Group
  311. 5 -> User Text Message
  312. 6 -> Image
  313. 7 -> Voice
  314. 8 -> Recommend
  315. 9 -> Animation
  316. 10 -> Share
  317. 11 -> Video
  318. 12 -> Video Call
  319. 13 -> Redraw
  320. 14 -> Init Message
  321. 99 -> Unknown
  322. '''
  323. def handle_msg(self, r):
  324. for msg in r['AddMsgList']:
  325. mtype = msg['MsgType']
  326. wx_user_id = msg['FromUserName']
  327. user_type = self.get_user_type(wx_user_id)
  328. name = self.get_user_remark_name(wx_user_id)
  329. content = msg['Content'].replace('&lt;','<').replace('&gt;','>')
  330. msg_id = msg['MsgId']
  331. msg_type_id = 99
  332. if mtype == 51: #init message
  333. msg_type_id = 14
  334. elif mtype == 1:
  335. if content.find('http://weixin.qq.com/cgi-bin/redirectforward?args=') != -1:
  336. r = self.session.get(content)
  337. r.encoding = 'gbk'
  338. data = r.text
  339. pos = self.search_content('title', data, 'xml')
  340. msg_type_id = 1
  341. content = {'location': pos, 'xml': data}
  342. if self.DEBUG:
  343. print '[Location] %s : I am at %s ' % (name, pos)
  344. elif msg['ToUserName'] == 'filehelper':
  345. msg_type_id = 2
  346. content = content.replace('<br/>','\n')
  347. if self.DEBUG:
  348. print '[File] %s : %s' % (name, )
  349. elif msg['FromUserName'] == self.user['UserName']: #self
  350. msg_type_id = 3
  351. elif msg['FromUserName'][:2] == '@@':
  352. [people, content] = content.split(':<br/>')
  353. group = self.get_user_remark_name(msg['FromUserName'])
  354. name = self.get_user_remark_name(people)
  355. msg_type_id = 4
  356. content = {'group_id': msg['FromUserName'], 'group_name': group, 'user': people, 'user_name': name, 'msg': content}
  357. if self.DEBUG:
  358. print '[Group] |%s| %s: %s' % (group, name, content.replace('<br/>','\n'))
  359. else:
  360. msg_type_id = 5
  361. if self.DEBUG:
  362. print '[Text] ', name, ' : ', content
  363. elif mtype == 3:
  364. msg_type_id = 6
  365. content = self.get_msg_img_url(msg_id)
  366. if self.DEBUG:
  367. image = self.get_msg_img(msg_id)
  368. print '[Image] %s : %s' % (name, image)
  369. elif mtype == 34:
  370. msg_type_id = 7
  371. content = self.get_voice_url(msg_id)
  372. if self.DEBUG:
  373. voice = self.get_voice(msg_id)
  374. print '[Voice] %s : %s' % (name, voice)
  375. elif mtype == 42:
  376. msg_type_id = 8
  377. info = msg['RecommendInfo']
  378. content = {}
  379. content['nickname'] = info['NickName']
  380. content['alias'] = info['Alias']
  381. content['province'] = info['Province']
  382. content['city'] = info['City']
  383. content['gender'] = ['unknown', 'male', 'female'][info['Sex']]
  384. if self.DEBUG:
  385. print '[Recommend] %s : ' % name
  386. print '========================='
  387. print '= NickName: %s' % info['NickName']
  388. print '= Alias: %s' % info['Alias']
  389. print '= Local: %s %s' % (info['Province'], info['City'])
  390. print '= Gender: %s' % ['unknown', 'male', 'female'][info['Sex']]
  391. print '========================='
  392. elif mtype == 47:
  393. msg_type_id = 9
  394. url = self.search_content('cdnurl', content)
  395. content = url
  396. if self.DEBUG:
  397. print '[Animation] %s : %s' % (name, url)
  398. elif mtype == 49:
  399. msg_type_id = 10
  400. appMsgType = defaultdict(lambda : "")
  401. appMsgType.update({5:'link', 3:'music', 7:'weibo'})
  402. content = {'type': appMsgType[msg['AppMsgType']], 'title': msg['FileName'], 'desc': self.search_content('des', content, 'xml'), 'url': msg['Url'], 'from': self.search_content('appname', content, 'xml')}
  403. if self.DEBUG:
  404. print '[Share] %s : %s' % (name, appMsgType[msg['AppMsgType']])
  405. print '========================='
  406. print '= title: %s' % msg['FileName']
  407. print '= desc: %s' % self.search_content('des', content, 'xml')
  408. print '= link: %s' % msg['Url']
  409. print '= from: %s' % self.search_content('appname', content, 'xml')
  410. print '========================='
  411. elif mtype == 62:
  412. msg_type_id = 11
  413. if self.DEBUG:
  414. print '[Video] ', name, ' sent you a video, please check on mobiles'
  415. elif mtype == 53:
  416. msg_type_id = 12
  417. if self.DEBUG:
  418. print '[Video Call] ', name, ' call you'
  419. elif mtype == 10002:
  420. msg_type_id = 13
  421. if self.DEBUG:
  422. print '[Redraw] ', name, ' redraw back a message'
  423. else:
  424. msg_type_id = 99
  425. if self.DEBUG:
  426. print '[Unknown] : %s' % str(mtype)
  427. print msg
  428. message = {'user_type': user_type, 'msg_id':msg_id, 'msg_type_id': msg_type_id, 'content': content, 'user_id': msg['FromUserName'], 'user_name': name}
  429. self.handle_msg_all(message)
  430. def schedule(self):
  431. pass
  432. def proc_msg(self):
  433. self.test_sync_check()
  434. while True:
  435. [retcode, selector] = self.sync_check()
  436. if retcode == '1100': # User have login on mobile
  437. pass
  438. elif retcode == '0':
  439. if selector == '2':
  440. r = self.sync()
  441. if r is not None:
  442. self.handle_msg(r)
  443. elif selector == '7': # Play WeChat on mobile
  444. r = self.sync()
  445. if r is not None:
  446. self.handle_msg(r)
  447. elif selector == '0':
  448. time.sleep(1)
  449. self.schedule()
  450. def send_msg_by_uid(self, word, dst = 'filehelper'):
  451. url = self.base_uri + '/webwxsendmsg?pass_ticket=%s' % (self.pass_ticket)
  452. msg_id = str(int(time.time()*1000)) + str(random.random())[:5].replace('.','')
  453. params = {
  454. 'BaseRequest': self.base_request,
  455. 'Msg': {
  456. "Type": 1,
  457. "Content": make_unicode(word),
  458. "FromUserName": self.user['UserName'],
  459. "ToUserName": dst,
  460. "LocalID": msg_id,
  461. "ClientMsgId": msg_id
  462. }
  463. }
  464. headers = {'content-type': 'application/json; charset=UTF-8'}
  465. data = json.dumps(params, ensure_ascii=False).encode('utf8')
  466. r = self.session.post(url, data = data, headers = headers)
  467. dic = r.json()
  468. return dic['BaseResponse']['Ret'] == 0
  469. def send_msg(self, name, word, isfile = False):
  470. uid = self.get_user_id(name)
  471. if uid:
  472. if isfile:
  473. with open(word, 'r') as f:
  474. result = True
  475. for line in f.readlines():
  476. line = line.replace('\n','')
  477. print '-> '+name+': '+line
  478. if self.send_msg_by_uid(line, uid):
  479. pass
  480. else:
  481. result = False
  482. time.sleep(1)
  483. return result
  484. else:
  485. if self.send_msg_by_uid(word, uid):
  486. return True
  487. else:
  488. return False
  489. else:
  490. if self.DEBUG:
  491. print '[ERROR] This user does not exist .'
  492. return True
  493. def search_content(self, key, content, fmat = 'attr'):
  494. if fmat == 'attr':
  495. pm = re.search(key+'\s?=\s?"([^"<]+)"', content)
  496. if pm: return pm.group(1)
  497. elif fmat == 'xml':
  498. pm=re.search('<{0}>([^<]+)</{0}>'.format(key),content)
  499. if pm: return pm.group(1)
  500. return 'unknown'
  501. def run(self):
  502. self.get_uuid()
  503. self.gen_qr_code('qr.png')
  504. print '[INFO] Please use WeCaht to scan the QR code .'
  505. self.wait4login(1)
  506. print '[INFO] Please confirm to login .'
  507. self.wait4login(0)
  508. if self.login():
  509. print '[INFO] Web WeChat login succeed .'
  510. else:
  511. print '[ERROR] Web WeChat login failed .'
  512. return
  513. if self.init():
  514. print '[INFO] Web WeChat init succeed .'
  515. else:
  516. print '[INFO] Web WeChat init failed'
  517. return
  518. self.status_notify()
  519. self.get_contact()
  520. print '[INFO] Get %d contacts' % len(self.contact_list)
  521. print '[INFO] Start to process messages .'
  522. self.proc_msg()