wxbot.py 22 KB

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