wxbot.py 20 KB

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