wxbot.py 19 KB

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