wxbot.py 19 KB

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