wxbot.py 19 KB

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