wxbot.py 22 KB

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