From 63f94525f072f08e42fd938c29e90c8b57743171 Mon Sep 17 00:00:00 2001 From: Joker Qyou Date: Fri, 8 Apr 2016 22:27:12 +0800 Subject: [PATCH] Fix PEP8 complaints: use all spaces for indent --- weixin.py | 1992 ++++++++++++++++++++++++++++------------------------- 1 file changed, 1046 insertions(+), 946 deletions(-) diff --git a/weixin.py b/weixin.py index 919b57e..a6a2148 100755 --- a/weixin.py +++ b/weixin.py @@ -1,946 +1,1046 @@ -#!/usr/bin/env python -# coding: utf-8 -import qrcode -import urllib, urllib2 -import cookielib -import requests -import xml.dom.minidom -import json -import time, re, sys, os, random -import multiprocessing -import platform -import logging -from collections import defaultdict -from urlparse import urlparse -from lxml import html - -# for media upload -import os -import mimetypes -from requests_toolbelt.multipart.encoder import MultipartEncoder - -def catchKeyboardInterrupt(fn): - def wrapper(*args): - try: - return fn(*args) - except KeyboardInterrupt: - print '\n[*] 强制退出程序' - logging.debug('[*] 强制退出程序') - return wrapper - -def _decode_list(data): - rv = [] - for item in data: - if isinstance(item, unicode): - item = item.encode('utf-8') - elif isinstance(item, list): - item = _decode_list(item) - elif isinstance(item, dict): - item = _decode_dict(item) - rv.append(item) - return rv - -def _decode_dict(data): - rv = {} - for key, value in data.iteritems(): - if isinstance(key, unicode): - key = key.encode('utf-8') - if isinstance(value, unicode): - value = value.encode('utf-8') - elif isinstance(value, list): - value = _decode_list(value) - elif isinstance(value, dict): - value = _decode_dict(value) - rv[key] = value - return rv - -class WebWeixin(object): - def __str__(self): - description = \ - "=========================\n" + \ - "[#] Web Weixin\n" + \ - "[#] Debug Mode: " + str(self.DEBUG) + "\n" + \ - "[#] Uuid: " + self.uuid + "\n" + \ - "[#] Uin: " + str(self.uin) + "\n" + \ - "[#] Sid: " + self.sid + "\n" + \ - "[#] Skey: " + self.skey + "\n" + \ - "[#] DeviceId: " + self.deviceId + "\n" + \ - "[#] PassTicket: " + self.pass_ticket + "\n" + \ - "=========================" - return description - - def __init__(self): - self.DEBUG = False - self.uuid = '' - self.base_uri = '' - self.redirect_uri= '' - self.uin = '' - self.sid = '' - self.skey = '' - self.pass_ticket = '' - self.deviceId = 'e' + repr(random.random())[2:17] - self.BaseRequest = {} - self.synckey = '' - self.SyncKey = [] - self.User = [] - self.MemberList = [] - self.ContactList = [] # 好友 - self.GroupList = [] # 群 - self.GroupMemeberList = [] # 群友 - self.PublicUsersList = [] # 公众号/服务号 - self.SpecialUsersList = [] # 特殊账号 - self.autoReplyMode = False - self.syncHost = '' - self.user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.109 Safari/537.36' - self.interactive = False - self.autoOpen = False - self.saveFolder = os.path.join(os.getcwd(), 'saved') - self.saveSubFolders = {'webwxgeticon': 'icons', 'webwxgetheadimg': 'headimgs', 'webwxgetmsgimg': 'msgimgs', 'webwxgetvideo': 'videos', 'webwxgetvoice': 'voices', '_showQRCodeImg': 'qrcodes'} - self.appid = 'wx782c26e4c19acffb' - self.lang = 'zh_CN' - self.lastCheckTs = time.time() - self.memberCount = 0 - self.SpecialUsers = ['newsapp', 'fmessage', 'filehelper', 'weibo', 'qqmail', 'fmessage', 'tmessage', 'qmessage', 'qqsync', 'floatbottle', 'lbsapp', 'shakeapp', 'medianote', 'qqfriend', 'readerapp', 'blogapp', 'facebookapp', 'masssendapp', 'meishiapp', 'feedsapp', 'voip', 'blogappweixin', 'weixin', 'brandsessionholder', 'weixinreminder', 'wxid_novlwrv3lqwv11', 'gh_22b87fa7cb3c', 'officialaccounts', 'notification_messages', 'wxid_novlwrv3lqwv11', 'gh_22b87fa7cb3c', 'wxitil', 'userexperience_alarm', 'notification_messages'] - self.TimeOut = 20 # 同步最短时间间隔(单位:秒) - self.media_count = -1 - - self.cookie = cookielib.CookieJar() - opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(self.cookie)) - opener.addheaders = [('User-agent', self.user_agent)] - urllib2.install_opener(opener) - - def loadConfig(self, config): - if config['DEBUG']: self.DEBUG = config['DEBUG'] - if config['autoReplyMode']: self.autoReplyMode = config['autoReplyMode'] - if config['user_agent']: self.user_agent = config['user_agent'] - if config['interactive']: self.interactive = config['interactive'] - if config['autoOpen']: self.autoOpen = config['autoOpen'] - - def getUUID(self): - url = 'https://login.weixin.qq.com/jslogin' - params = { - 'appid': self.appid, - 'fun': 'new', - 'lang': self.lang, - '_': int(time.time()), - } - data = self._post(url, params, False) - regx = r'window.QRLogin.code = (\d+); window.QRLogin.uuid = "(\S+?)"' - pm = re.search(regx, data) - if pm: - code = pm.group(1) - self.uuid = pm.group(2) - return code == '200' - return False - - def genQRCode(self): - if sys.platform.startswith('win'): - self._showQRCodeImg() - else: - self._str2qr('https://login.weixin.qq.com/l/' + self.uuid) - - def _showQRCodeImg(self): - url = 'https://login.weixin.qq.com/qrcode/' + self.uuid - params = { - 't' : 'webwx', - '_' : int(time.time()) - } - - data = self._post(url, params, False) - QRCODE_PATH = self._saveFile('qrcode.jpg', data, '_showQRCodeImg') - os.startfile(QRCODE_PATH) - - def waitForLogin(self, tip = 1): - time.sleep(tip) - url = 'https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login?tip=%s&uuid=%s&_=%s' % (tip, self.uuid, int(time.time())) - data = self._get(url) - pm = re.search(r'window.code=(\d+);', data) - code = pm.group(1) - - if code == '201': return True - elif code == '200': - pm = re.search(r'window.redirect_uri="(\S+?)";', data) - r_uri = pm.group(1) + '&fun=new' - self.redirect_uri = r_uri - self.base_uri = r_uri[:r_uri.rfind('/')] - return True - elif code == '408': - self._echo('[登陆超时] \n') - else: - self._echo('[登陆异常] \n') - return False - - def login(self): - data = self._get(self.redirect_uri) - doc = xml.dom.minidom.parseString(data) - root = doc.documentElement - - for node in root.childNodes: - if node.nodeName == 'skey': - self.skey = node.childNodes[0].data - elif node.nodeName == 'wxsid': - self.sid = node.childNodes[0].data - elif node.nodeName == 'wxuin': - self.uin = node.childNodes[0].data - elif node.nodeName == 'pass_ticket': - self.pass_ticket = node.childNodes[0].data - - if '' in (self.skey, self.sid, self.uin, self.pass_ticket): - return False - - self.BaseRequest = { - 'Uin': int(self.uin), - 'Sid': self.sid, - 'Skey': self.skey, - 'DeviceID': self.deviceId, - } - return True - - def webwxinit(self): - url = self.base_uri + '/webwxinit?pass_ticket=%s&skey=%s&r=%s' % (self.pass_ticket, self.skey, int(time.time())) - params = { - 'BaseRequest': self.BaseRequest - } - dic = self._post(url, params) - self.SyncKey = dic['SyncKey'] - self.User = dic['User'] - # synckey for synccheck - self.synckey = '|'.join([ str(keyVal['Key']) + '_' + str(keyVal['Val']) for keyVal in self.SyncKey['List'] ]) - - return dic['BaseResponse']['Ret'] == 0 - - def webwxstatusnotify(self): - url = self.base_uri + '/webwxstatusnotify?lang=zh_CN&pass_ticket=%s' % (self.pass_ticket) - params = { - 'BaseRequest': self.BaseRequest, - "Code": 3, - "FromUserName": self.User['UserName'], - "ToUserName": self.User['UserName'], - "ClientMsgId": int(time.time()) - } - dic = self._post(url, params) - - return dic['BaseResponse']['Ret'] == 0 - - def webwxgetcontact(self): - SpecialUsers = self.SpecialUsers - print self.base_uri - url = self.base_uri + '/webwxgetcontact?pass_ticket=%s&skey=%s&r=%s' % (self.pass_ticket, self.skey, int(time.time())) - dic = self._post(url, {}) - - self.MemberCount = dic['MemberCount'] - self.MemberList = dic['MemberList'] - ContactList = self.MemberList[:] - GroupList = self.GroupList[:] - PublicUsersList = self.PublicUsersList[:] - SpecialUsersList = self.SpecialUsersList[:] - - for i in xrange(len(ContactList) - 1, -1, -1): - Contact = ContactList[i] - if Contact['VerifyFlag'] & 8 != 0: # 公众号/服务号 - ContactList.remove(Contact) - self.PublicUsersList.append(Contact) - elif Contact['UserName'] in SpecialUsers: # 特殊账号 - ContactList.remove(Contact) - self.SpecialUsersList.append(Contact) - elif Contact['UserName'].find('@@') != -1: # 群聊 - ContactList.remove(Contact) - self.GroupList.append(Contact) - elif Contact['UserName'] == self.User['UserName']: # 自己 - ContactList.remove(Contact) - self.ContactList = ContactList - - return True - - def webwxbatchgetcontact(self): - url = self.base_uri + '/webwxbatchgetcontact?type=ex&r=%s&pass_ticket=%s' % (int(time.time()), self.pass_ticket) - params = { - 'BaseRequest': self.BaseRequest, - "Count": len(self.GroupList), - "List": [ {"UserName": g['UserName'], "EncryChatRoomId":""} for g in self.GroupList ] - } - dic = self._post(url, params) - - # blabla ... - ContactList = dic['ContactList'] - ContactCount = dic['Count'] - self.GroupList = ContactList - - for i in xrange(len(ContactList) - 1, -1, -1): - Contact = ContactList[i] - MemberList = Contact['MemberList'] - for member in MemberList: - self.GroupMemeberList.append(member) - return True - - def getNameById(self, id): - url = self.base_uri + '/webwxbatchgetcontact?type=ex&r=%s&pass_ticket=%s' % (int(time.time()), self.pass_ticket) - params = { - 'BaseRequest': self.BaseRequest, - "Count": 1, - "List": [ {"UserName": id, "EncryChatRoomId":""} ] - } - dic = self._post(url, params) - - # blabla ... - return dic['ContactList'] - - def testsynccheck(self): - SyncHost = [ - 'webpush.weixin.qq.com', - 'webpush2.weixin.qq.com', - 'webpush.wechat.com', - 'webpush1.wechat.com', - 'webpush2.wechat.com', - 'webpush1.wechatapp.com', - # 'webpush.wechatapp.com' - ] - for host in SyncHost: - self.syncHost = host - [retcode, selector] = self.synccheck() - if retcode == '0': return True - return False - - def synccheck(self): - params = { - 'r': int(time.time()), - 'sid': self.sid, - 'uin': self.uin, - 'skey': self.skey, - 'deviceid': self.deviceId, - 'synckey': self.synckey, - '_': int(time.time()), - } - url = 'https://' + self.syncHost + '/cgi-bin/mmwebwx-bin/synccheck?' + urllib.urlencode(params) - data = self._get(url) - pm = re.search(r'window.synccheck={retcode:"(\d+)",selector:"(\d+)"}', data) - retcode = pm.group(1) - selector = pm.group(2) - return [retcode, selector] - - def webwxsync(self): - url = self.base_uri + '/webwxsync?sid=%s&skey=%s&pass_ticket=%s' % (self.sid, self.skey, self.pass_ticket) - params = { - 'BaseRequest': self.BaseRequest, - 'SyncKey': self.SyncKey, - 'rr': ~int(time.time()) - } - dic = self._post(url, params) - if self.DEBUG: - print json.dumps(dic, indent=4) - logging.debug(json.dumps(dic, indent=4)) - - if dic['BaseResponse']['Ret'] == 0: - self.SyncKey = dic['SyncKey'] - self.synckey = '|'.join([ str(keyVal['Key']) + '_' + str(keyVal['Val']) for keyVal in self.SyncKey['List'] ]) - return dic - - def webwxsendmsg(self, word, to = 'filehelper'): - url = self.base_uri + '/webwxsendmsg?pass_ticket=%s' % (self.pass_ticket) - clientMsgId = str(int(time.time()*1000)) + str(random.random())[:5].replace('.','') - params = { - 'BaseRequest': self.BaseRequest, - 'Msg': { - "Type": 1, - "Content": self._transcoding(word), - "FromUserName": self.User['UserName'], - "ToUserName": to, - "LocalID": clientMsgId, - "ClientMsgId": clientMsgId - } - } - headers = {'content-type': 'application/json; charset=UTF-8'} - data = json.dumps(params, ensure_ascii=False).encode('utf8') - r = requests.post(url, data = data, headers = headers) - dic = r.json() - return dic['BaseResponse']['Ret'] == 0 - - def webwxuploadmedia(self, image_name): - url = 'https://file2.wx.qq.com/cgi-bin/mmwebwx-bin/webwxuploadmedia?f=json' - # 计数器 - self.media_count = self.media_count + 1 - # 文件名 - file_name = image_name - # MIME格式 - # mime_type = application/pdf, image/jpeg, image/png, etc. - mime_type = mimetypes.guess_type(image_name,strict=False)[0] - # 微信识别的文档格式,微信服务器应该只支持两种类型的格式。pic和doc - # pic格式,直接显示。doc格式则显示为文件。 - media_type = 'pic' if mime_type.split('/')[0] == 'image' else 'doc' - # 上一次修改日期 - lastModifieDate = 'Thu Mar 17 2016 00:55:10 GMT+0800 (CST)' - # 文件大小 - file_size = os.path.getsize(file_name) - # PassTicket - pass_ticket = self.pass_ticket - # clientMediaId - client_media_id = str(int(time.time()*1000)) + str(random.random())[:5].replace('.','') - # webwx_data_ticket - webwx_data_ticket = '' - for item in self.cookie: - if item.name == 'webwx_data_ticket': - webwx_data_ticket = item.value - break; - if (webwx_data_ticket == ''): - return "None Fuck Cookie" - - uploadmediarequest = json.dumps({ - "BaseRequest": self.BaseRequest, - "ClientMediaId": client_media_id, - "TotalLen": file_size, - "StartPos": 0, - "DataLen": file_size, - "MediaType": 4 - }, ensure_ascii=False).encode('utf8') - - multipart_encoder = MultipartEncoder( - fields = { - 'id': 'WU_FILE_' + str(self.media_count), - 'name': file_name, - 'type': mime_type, - 'lastModifieDate': lastModifieDate, - 'size': str(file_size), - 'mediatype': media_type, - 'uploadmediarequest': uploadmediarequest, - 'webwx_data_ticket': webwx_data_ticket, - 'pass_ticket': pass_ticket, - 'filename': (file_name, open(file_name, 'rb'), mime_type.split('/')[1]) - }, - boundary = '-----------------------------1575017231431605357584454111' - ) - - headers = { - 'Host': 'file2.wx.qq.com', - 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:42.0) Gecko/20100101 Firefox/42.0', - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', - 'Accept-Language': 'en-US,en;q=0.5', - 'Accept-Encoding': 'gzip, deflate', - 'Referer': 'https://wx2.qq.com/', - 'Content-Type': multipart_encoder.content_type, - 'Origin': 'https://wx2.qq.com', - 'Connection': 'keep-alive', - 'Pragma': 'no-cache', - 'Cache-Control': 'no-cache' - } - - r = requests.post(url, data = multipart_encoder, headers = headers) - response_json = r.json() - if response_json['BaseResponse']['Ret'] == 0: - return response_json - return None - - def webwxsendmsgimg(self, user_id, media_id): - url = 'https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsgimg?fun=async&f=json&pass_ticket=%s' % self.pass_ticket - clientMsgId = str(int(time.time()*1000)) + str(random.random())[:5].replace('.','') - data_json = { - "BaseRequest": self.BaseRequest, - "Msg": { - "Type": 3, - "MediaId": media_id, - "FromUserName": self.User['UserName'], - "ToUserName": user_id, - "LocalID": clientMsgId, - "ClientMsgId": clientMsgId - } - } - headers = {'content-type': 'application/json; charset=UTF-8'} - data = json.dumps(data_json, ensure_ascii=False).encode('utf8') - r = requests.post(url, data = data, headers = headers) - dic = r.json() - return dic['BaseResponse']['Ret'] == 0 - - def _saveFile(self, filename, data, api=None): - fn = filename - if self.saveSubFolders[api]: - dirName = os.path.join(self.saveFolder, self.saveSubFolders[api]) - if not os.path.exists(dirName): - os.makedirs(dirName) - fn = os.path.join(dirName, filename) - logging.debug('Saved file: %s' % fn) - with open(fn, 'wb') as f: f.write(data); f.close() - return fn - - def webwxgeticon(self, id): - url = self.base_uri + '/webwxgeticon?username=%s&skey=%s' % (id, self.skey) - data = self._get(url) - fn = 'img_'+id+'.jpg' - return self._saveFile(fn, data, 'webwxgeticon') - - def webwxgetheadimg(self, id): - url = self.base_uri + '/webwxgetheadimg?username=%s&skey=%s' % (id, self.skey) - data = self._get(url) - fn = 'img_'+id+'.jpg' - return self._saveFile(fn, data, 'webwxgetheadimg') - - def webwxgetmsgimg(self, msgid): - url = self.base_uri + '/webwxgetmsgimg?MsgID=%s&skey=%s' % (msgid, self.skey) - data = self._get(url) - fn = 'img_'+msgid+'.jpg' - return self._saveFile(fn, data, 'webwxgetmsgimg') - - # Not work now for weixin haven't support this API - def webwxgetvideo(self, msgid): - url = self.base_uri + '/webwxgetvideo?msgid=%s&skey=%s' % (msgid, self.skey) - data = self._get(url, api='webwxgetvideo') - fn = 'video_'+msgid+'.mp4' - return self._saveFile(fn, data, 'webwxgetvideo') - - def webwxgetvoice(self, msgid): - url = self.base_uri + '/webwxgetvoice?msgid=%s&skey=%s' % (msgid, self.skey) - data = self._get(url) - fn = 'voice_'+msgid+'.mp3' - return self._saveFile(fn, data, 'webwxgetvoice') - - def getGroupName(self, id): - name = '未知群' - for member in self.GroupList: - if member['UserName'] == id: - name = member['NickName'] - if name == '未知群': - # 现有群里面查不到 - GroupList = self.getNameById(id) - for group in GroupList: - self.GroupList.append(group) - if group['UserName'] == id: - name = group['NickName'] - MemberList = group['MemberList'] - for member in MemberList: - self.GroupMemeberList.append(member) - return name - - def getUserRemarkName(self, id): - name = '未知群' if id[:2] == '@@' else '陌生人' - if id == self.User['UserName']: return self.User['NickName'] # 自己 - - if id[:2] == '@@': - # 群 - name = self.getGroupName(id) - else: - # 特殊账号 - for member in self.SpecialUsersList: - if member['UserName'] == id: - name = member['RemarkName'] if member['RemarkName'] else member['NickName'] - - # 公众号或服务号 - for member in self.PublicUsersList: - if member['UserName'] == id: - name = member['RemarkName'] if member['RemarkName'] else member['NickName'] - - # 直接联系人 - for member in self.ContactList: - if member['UserName'] == id: - name = member['RemarkName'] if member['RemarkName'] else member['NickName'] - # 群友 - for member in self.GroupMemeberList: - if member['UserName'] == id: - name = member['DisplayName'] if member['DisplayName'] else member['NickName'] - - if name == '未知群' or name == '陌生人': logging.debug(id) - return name - - def getUSerID(self, name): - for member in self.MemberList: - if name == member['RemarkName'] or name == member['NickName']: - return member['UserName'] - return None - - def _showMsg(self, message): - - srcName = None - dstName = None - groupName = None - content = None - - msg = message - logging.debug(msg) - - if msg['raw_msg']: - srcName = self.getUserRemarkName(msg['raw_msg']['FromUserName']) - dstName = self.getUserRemarkName(msg['raw_msg']['ToUserName']) - content = msg['raw_msg']['Content'].replace('<','<').replace('>','>') - message_id = msg['raw_msg']['MsgId'] - - if content.find('http://weixin.qq.com/cgi-bin/redirectforward?args=') != -1: - # 地理位置消息 - data = self._get(content).decode('gbk').encode('utf-8') - pos = self._searchContent('title', data, 'xml') - tree = html.fromstring(self._get(content)) - url = tree.xpath('//html/body/div/img')[0].attrib['src'] - - for item in urlparse(url).query.split('&'): - if item.split('=')[0] == 'center': loc = item.split('=')[-1:] - - content = '%s 发送了一个 位置消息 - 我在 [%s](%s) @ %s]' % (srcName, pos, url, loc) - - if msg['raw_msg']['ToUserName'] == 'filehelper': - # 文件传输助手 - dstName = '文件传输助手' - - if msg['raw_msg']['FromUserName'][:2] == '@@': - # 接收到来自群的消息 - if re.search(":
", content, re.IGNORECASE): - [people, content] = content.split(':
') - groupName = srcName - srcName = self.getUserRemarkName(people) - dstName = 'GROUP' - else: - groupName = srcName - srcName = 'SYSTEM' - elif msg['raw_msg']['ToUserName'][:2] == '@@': - # 自己发给群的消息 - groupName = dstName - dstName = 'GROUP' - - # 收到了红包 - if content == '收到红包,请在手机上查看': msg['message'] = content - - # 指定了消息内容 - if 'message' in msg.keys(): content = msg['message'] - - - if groupName != None: - print '%s |%s| %s -> %s: %s' % (message_id, groupName.strip(), srcName.strip(), dstName.strip(), content.replace('
','\n')) - logging.info('%s |%s| %s -> %s: %s' % (message_id, groupName.strip(), srcName.strip(), dstName.strip(), content.replace('
','\n'))) - else: - print '%s %s -> %s: %s' % (message_id, srcName.strip(), dstName.strip(), content.replace('
','\n')) - logging.info('%s %s -> %s: %s' % (message_id, srcName.strip(), dstName.strip(), content.replace('
','\n'))) - - def handleMsg(self, r): - for msg in r['AddMsgList']: - print '[*] 你有新的消息,请注意查收' - logging.debug('[*] 你有新的消息,请注意查收') - - if self.DEBUG: - fn = 'msg' + str(int(random.random() * 1000)) + '.json' - with open(fn, 'w') as f: f.write(json.dumps(msg)) - print '[*] 该消息已储存到文件: ' + fn - logging.debug('[*] 该消息已储存到文件: %s' % (fn)) - - msgType = msg['MsgType'] - name = self.getUserRemarkName(msg['FromUserName']) - content = msg['Content'].replace('<','<').replace('>','>') - msgid = msg['MsgId'] - - if msgType == 1: - raw_msg = { 'raw_msg': msg } - self._showMsg(raw_msg) - if self.autoReplyMode: - ans = self._xiaodoubi(content)+'\n[微信机器人自动回复]' - if self.webwxsendmsg(ans, msg['FromUserName']): - print '自动回复: '+ans - logging.info('自动回复: '+ans) - else: - print '自动回复失败' - logging.info('自动回复失败') - elif msgType == 3: - image = self.webwxgetmsgimg(msgid) - raw_msg = { 'raw_msg': msg, 'message': '%s 发送了一张图片: %s' % (name, image) } - self._showMsg(raw_msg) - self._safe_open(image) - elif msgType == 34: - voice = self.webwxgetvoice(msgid) - raw_msg = { 'raw_msg': msg, 'message': '%s 发了一段语音: %s' % (name, voice) } - self._showMsg(raw_msg) - self._safe_open(voice) - elif msgType == 42: - info = msg['RecommendInfo'] - print '%s 发送了一张名片:' % name - print '=========================' - print '= 昵称: %s' % info['NickName'] - print '= 微信号: %s' % info['Alias'] - print '= 地区: %s %s' % (info['Province'], info['City']) - print '= 性别: %s' % ['未知', '男', '女'][info['Sex']] - print '=========================' - raw_msg = { 'raw_msg': msg, 'message': '%s 发送了一张名片: %s' % (name.strip(), json.dumps(info)) } - self._showMsg(raw_msg) - elif msgType == 47: - url = self._searchContent('cdnurl', content) - raw_msg = { 'raw_msg': msg, 'message': '%s 发了一个动画表情,点击下面链接查看: %s' % (name, url) } - self._showMsg(raw_msg) - self._safe_open(url) - elif msgType == 49: - appMsgType = defaultdict(lambda : "") - appMsgType.update({5:'链接', 3:'音乐', 7:'微博'}) - print '%s 分享了一个%s:' % (name, appMsgType[msg['AppMsgType']]) - print '=========================' - print '= 标题: %s' % msg['FileName'] - print '= 描述: %s' % self._searchContent('des', content, 'xml') - print '= 链接: %s' % msg['Url'] - print '= 来自: %s' % self._searchContent('appname', content, 'xml') - print '=========================' - card = { - 'title': msg['FileName'], - 'description': self._searchContent('des', content, 'xml'), - 'url': msg['Url'], - 'appname': self._searchContent('appname', content, 'xml') - } - raw_msg = { 'raw_msg': msg, 'message': '%s 分享了一个%s: %s' % (name, appMsgType[msg['AppMsgType']], json.dumps(card)) } - self._showMsg(raw_msg) - elif msgType == 51: - raw_msg = { 'raw_msg': msg, 'message': '[*] 成功获取联系人信息' } - self._showMsg(raw_msg) - elif msgType == 62: - video = self.webwxgetvideo(msgid) - raw_msg = { 'raw_msg': msg, 'message': '%s 发了一段小视频: %s' % (name, video) } - self._showMsg(raw_msg) - self._safe_open(video) - elif msgType == 10002: - raw_msg = { 'raw_msg': msg, 'message': '%s 撤回了一条消息' % name } - self._showMsg(raw_msg) - else: - logging.debug('[*] 该消息类型为: %d,可能是表情,图片, 链接或红包: %s' % (msg['MsgType'], json.dumps(msg))) - raw_msg = { 'raw_msg': msg, 'message': '[*] 该消息类型为: %d,可能是表情,图片, 链接或红包' % msg['MsgType'] } - self._showMsg(raw_msg) - - def listenMsgMode(self): - print '[*] 进入消息监听模式 ... 成功' - logging.debug('[*] 进入消息监听模式 ... 成功') - self._run('[*] 进行同步线路测试 ... ', self.testsynccheck) - playWeChat = 0 - redEnvelope = 0 - while True: - self.lastCheckTs = time.time() - [retcode, selector] = self.synccheck() - if self.DEBUG: print 'retcode: %s, selector: %s' % (retcode, selector) - logging.debug('retcode: %s, selector: %s' % (retcode, selector)) - if retcode == '1100': - print '[*] 你在手机上登出了微信,债见' - logging.debug('[*] 你在手机上登出了微信,债见') - break - if retcode == '1101': - print '[*] 你在其他地方登录了 WEB 版微信,债见' - logging.debug('[*] 你在其他地方登录了 WEB 版微信,债见') - break - elif retcode == '0': - if selector == '2': - r = self.webwxsync() - if r is not None: self.handleMsg(r) - elif selector == '6': - # TODO - redEnvelope += 1 - print '[*] 收到疑似红包消息 %d 次' % redEnvelope - logging.debug('[*] 收到疑似红包消息 %d 次' % redEnvelope) - elif selector == '7': - playWeChat += 1 - print '[*] 你在手机上玩微信被我发现了 %d 次' % playWeChat - logging.debug('[*] 你在手机上玩微信被我发现了 %d 次' % playWeChat) - r = self.webwxsync() - elif selector == '0': - time.sleep(1) - if (time.time() - self.lastCheckTs) <= 20: time.sleep(time.time() - self.lastCheckTs) - - def sendMsg(self, name, word, isfile = False): - id = self.getUSerID(name) - if id: - if isfile: - with open(word, 'r') as f: - for line in f.readlines(): - line = line.replace('\n','') - self._echo('-> '+name+': '+line) - if self.webwxsendmsg(line, id): - print ' [成功]' - else: - print ' [失败]' - time.sleep(1) - else: - if self.webwxsendmsg(word, id): - print '[*] 消息发送成功' - logging.debug('[*] 消息发送成功') - else: - print '[*] 消息发送失败' - logging.debug('[*] 消息发送失败') - else: - print '[*] 此用户不存在' - logging.debug('[*] 此用户不存在') - - def sendMsgToAll(self, word): - for contact in self.ContactList: - name = contact['RemarkName'] if contact['RemarkName'] else contact['NickName'] - id = contact['UserName'] - self._echo('-> '+name+': '+word) - if self.webwxsendmsg(word, id): - print ' [成功]' - else: - print ' [失败]' - time.sleep(1) - - def sendImg(self, name, file_name): - response = self.webwxuploadmedia(file_name) - media_id = "" - if response is not None: - media_id = response['MediaId'] - user_id = self.getUSerID(name) - response = self.webwxsendmsgimg(user_id, media_id) - - @catchKeyboardInterrupt - def start(self): - self._echo('[*] 微信网页版 ... 开动'); print; logging.debug('[*] 微信网页版 ... 开动') - while True: - self._run('[*] 正在获取 uuid ... ', self.getUUID) - self._echo('[*] 正在获取二维码 ... 成功'); print; logging.debug('[*] 微信网页版 ... 开动'); self.genQRCode() - print '[*] 请使用微信扫描二维码以登录 ... ' - if not self.waitForLogin(): - continue - print '[*] 请在手机上点击确认以登录 ... ' - if not self.waitForLogin(0): - continue - break - - self._run('[*] 正在登录 ... ', self.login) - self._run('[*] 微信初始化 ... ', self.webwxinit) - self._run('[*] 开启状态通知 ... ', self.webwxstatusnotify) - self._run('[*] 获取联系人 ... ', self.webwxgetcontact) - self._echo('[*] 应有 %s 个联系人,读取到联系人 %d 个' % (self.MemberCount, len(self.MemberList))); print - self._echo('[*] 共有 %d 个群 | %d 个直接联系人 | %d 个特殊账号 | %d 公众号或服务号' % (len(self.GroupList), len(self.ContactList), len(self.SpecialUsersList), len(self.PublicUsersList) )); print - self._run('[*] 获取群 ... ', self.webwxbatchgetcontact) - logging.debug('[*] 微信网页版 ... 开动') - if self.DEBUG: print self - logging.debug(self) - - if self.interactive and raw_input('[*] 是否开启自动回复模式(y/n): ') == 'y': - self.autoReplyMode = True - print '[*] 自动回复模式 ... 开启' - logging.debug('[*] 自动回复模式 ... 开启') - else: - print '[*] 自动回复模式 ... 关闭' - logging.debug('[*] 自动回复模式 ... 关闭') - - listenProcess = multiprocessing.Process(target=self.listenMsgMode) - listenProcess.start() - - while True: - text = raw_input('') - if text == 'quit': - listenProcess.terminate() - print('[*] 退出微信') - logging.debug('[*] 退出微信') - exit() - elif text[:2] == '->': - [name, word] = text[2:].split(':') - if name == 'all': self.sendMsgToAll(word) - else: self.sendMsg(name, word) - elif text[:3] == 'm->': - [name, file] = text[3:].split(':') - self.sendMsg(name, file, True) - elif text[:3] == 'f->': - print '发送文件' - logging.debug('发送文件') - elif text[:3] == 'i->': - print '发送图片' - [name, file_name] = text[3:].split(':') - self.sendImg(name, file_name) - logging.debug('发送图片') - - def _safe_open(self, path): - if self.autoOpen: - if platform.system() == "Linux": - os.system("xdg-open %s &" % path) - else: - os.system('open %s &' % path) - - def _run(self, str, func, *args): - self._echo(str) - if func(*args): print '成功'; logging.debug('%s... 成功' % (str)) - else: print('失败\n[*] 退出程序'); logging.debug('%s... 失败' % (str)); logging.debug('[*] 退出程序'); exit() - - def _echo(self, str): - sys.stdout.write(str) - sys.stdout.flush() - - def _printQR(self, mat): - for i in mat: - BLACK = '\033[40m \033[0m' - WHITE = '\033[47m \033[0m' - print ''.join([BLACK if j else WHITE for j in i]) - - def _str2qr(self, str): - qr = qrcode.QRCode() - qr.border = 1 - qr.add_data(str) - mat = qr.get_matrix() - self._printQR(mat) # qr.print_tty() or qr.print_ascii() - - def _transcoding(self, data): - if not data: return data - result = None - if type(data) == unicode: - result = data - elif type(data) == str: - result = data.decode('utf-8') - return result - - def _get(self, url, api=None): - request = urllib2.Request(url = url) - request.add_header('Referer', 'https://wx.qq.com/') - if api == 'webwxgetvoice': request.add_header('Range', 'bytes=0-') - if api == 'webwxgetvideo': request.add_header('Range', 'bytes=0-') - response = urllib2.urlopen(request) - data = response.read() - logging.debug(url) - return data - - def _post(self, url, params, jsonfmt = True): - if jsonfmt: - request = urllib2.Request(url = url, data = json.dumps(params)) - request.add_header('ContentType', 'application/json; charset=UTF-8') - else: - request = urllib2.Request(url = url, data = urllib.urlencode(params)) - response = urllib2.urlopen(request) - data = response.read() - if jsonfmt: return json.loads(data, object_hook=_decode_dict) - return data - - def _xiaodoubi(self, word): - url = 'http://www.xiaodoubi.com/bot/chat.php' - try: - r = requests.post(url, data = {'chat': word}) - return r.content - except: - return "让我一个人静静 T_T..." - - def _simsimi(self, word): - key = '' - url = 'http://sandbox.api.simsimi.com/request.p?key=%s&lc=ch&ft=0.0&text=%s' % (key, word) - r = requests.get(url) - ans = r.json() - if ans['result'] == '100': return ans['response'] - else: return '你在说什么,风太大听不清列' - - def _searchContent(self, key, content, fmat = 'attr'): - if fmat == 'attr': - pm = re.search(key+'\s?=\s?"([^"<]+)"', content) - if pm: return pm.group(1) - elif fmat == 'xml': - pm = re.search('<{0}>([^<]+)'.format(key),content) - if not pm: pm = re.search('<{0}><\!\[CDATA\[(.*?)\]\]>'.format(key),content) - if pm: return pm.group(1) - return '未知' - -class UnicodeStreamFilter: - def __init__(self, target): - self.target = target - self.encoding = 'utf-8' - self.errors = 'replace' - self.encode_to = self.target.encoding - - def write(self, s): - if type(s) == str: - s = s.decode('utf-8') - s = s.encode(self.encode_to, self.errors).decode(self.encode_to) - self.target.write(s) - - def flush(self): - self.target.flush() - -if sys.stdout.encoding == 'cp936': - sys.stdout = UnicodeStreamFilter(sys.stdout) - - -if __name__ == '__main__': - - logger = logging.getLogger(__name__) - import coloredlogs - coloredlogs.install(level='DEBUG') - - webwx = WebWeixin() - webwx.start() +#!/usr/bin/env python +# coding: utf-8 +import qrcode +import urllib +import urllib2 +import cookielib +import requests +import xml.dom.minidom +import json +import time +import re +import sys +import os +import random +import multiprocessing +import platform +import logging +from collections import defaultdict +from urlparse import urlparse +from lxml import html + +# for media upload +import mimetypes +from requests_toolbelt.multipart.encoder import MultipartEncoder + + +def catchKeyboardInterrupt(fn): + def wrapper(*args): + try: + return fn(*args) + except KeyboardInterrupt: + print '\n[*] 强制退出程序' + logging.debug('[*] 强制退出程序') + return wrapper + + +def _decode_list(data): + rv = [] + for item in data: + if isinstance(item, unicode): + item = item.encode('utf-8') + elif isinstance(item, list): + item = _decode_list(item) + elif isinstance(item, dict): + item = _decode_dict(item) + rv.append(item) + return rv + + +def _decode_dict(data): + rv = {} + for key, value in data.iteritems(): + if isinstance(key, unicode): + key = key.encode('utf-8') + if isinstance(value, unicode): + value = value.encode('utf-8') + elif isinstance(value, list): + value = _decode_list(value) + elif isinstance(value, dict): + value = _decode_dict(value) + rv[key] = value + return rv + + +class WebWeixin(object): + + def __str__(self): + description = \ + "=========================\n" + \ + "[#] Web Weixin\n" + \ + "[#] Debug Mode: " + str(self.DEBUG) + "\n" + \ + "[#] Uuid: " + self.uuid + "\n" + \ + "[#] Uin: " + str(self.uin) + "\n" + \ + "[#] Sid: " + self.sid + "\n" + \ + "[#] Skey: " + self.skey + "\n" + \ + "[#] DeviceId: " + self.deviceId + "\n" + \ + "[#] PassTicket: " + self.pass_ticket + "\n" + \ + "=========================" + return description + + def __init__(self): + self.DEBUG = False + self.uuid = '' + self.base_uri = '' + self.redirect_uri = '' + self.uin = '' + self.sid = '' + self.skey = '' + self.pass_ticket = '' + self.deviceId = 'e' + repr(random.random())[2:17] + self.BaseRequest = {} + self.synckey = '' + self.SyncKey = [] + self.User = [] + self.MemberList = [] + self.ContactList = [] # 好友 + self.GroupList = [] # 群 + self.GroupMemeberList = [] # 群友 + self.PublicUsersList = [] # 公众号/服务号 + self.SpecialUsersList = [] # 特殊账号 + self.autoReplyMode = False + self.syncHost = '' + self.user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.109 Safari/537.36' + self.interactive = False + self.autoOpen = False + self.saveFolder = os.path.join(os.getcwd(), 'saved') + self.saveSubFolders = {'webwxgeticon': 'icons', 'webwxgetheadimg': 'headimgs', 'webwxgetmsgimg': 'msgimgs', + 'webwxgetvideo': 'videos', 'webwxgetvoice': 'voices', '_showQRCodeImg': 'qrcodes'} + self.appid = 'wx782c26e4c19acffb' + self.lang = 'zh_CN' + self.lastCheckTs = time.time() + self.memberCount = 0 + self.SpecialUsers = ['newsapp', 'fmessage', 'filehelper', 'weibo', 'qqmail', 'fmessage', 'tmessage', 'qmessage', 'qqsync', 'floatbottle', 'lbsapp', 'shakeapp', 'medianote', 'qqfriend', 'readerapp', 'blogapp', 'facebookapp', 'masssendapp', 'meishiapp', 'feedsapp', + 'voip', 'blogappweixin', 'weixin', 'brandsessionholder', 'weixinreminder', 'wxid_novlwrv3lqwv11', 'gh_22b87fa7cb3c', 'officialaccounts', 'notification_messages', 'wxid_novlwrv3lqwv11', 'gh_22b87fa7cb3c', 'wxitil', 'userexperience_alarm', 'notification_messages'] + self.TimeOut = 20 # 同步最短时间间隔(单位:秒) + self.media_count = -1 + + self.cookie = cookielib.CookieJar() + opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(self.cookie)) + opener.addheaders = [('User-agent', self.user_agent)] + urllib2.install_opener(opener) + + def loadConfig(self, config): + if config['DEBUG']: + self.DEBUG = config['DEBUG'] + if config['autoReplyMode']: + self.autoReplyMode = config['autoReplyMode'] + if config['user_agent']: + self.user_agent = config['user_agent'] + if config['interactive']: + self.interactive = config['interactive'] + if config['autoOpen']: + self.autoOpen = config['autoOpen'] + + def getUUID(self): + url = 'https://login.weixin.qq.com/jslogin' + params = { + 'appid': self.appid, + 'fun': 'new', + 'lang': self.lang, + '_': int(time.time()), + } + data = self._post(url, params, False) + regx = r'window.QRLogin.code = (\d+); window.QRLogin.uuid = "(\S+?)"' + pm = re.search(regx, data) + if pm: + code = pm.group(1) + self.uuid = pm.group(2) + return code == '200' + return False + + def genQRCode(self): + if sys.platform.startswith('win'): + self._showQRCodeImg() + else: + self._str2qr('https://login.weixin.qq.com/l/' + self.uuid) + + def _showQRCodeImg(self): + url = 'https://login.weixin.qq.com/qrcode/' + self.uuid + params = { + 't': 'webwx', + '_': int(time.time()) + } + + data = self._post(url, params, False) + QRCODE_PATH = self._saveFile('qrcode.jpg', data, '_showQRCodeImg') + os.startfile(QRCODE_PATH) + + def waitForLogin(self, tip=1): + time.sleep(tip) + url = 'https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login?tip=%s&uuid=%s&_=%s' % ( + tip, self.uuid, int(time.time())) + data = self._get(url) + pm = re.search(r'window.code=(\d+);', data) + code = pm.group(1) + + if code == '201': + return True + elif code == '200': + pm = re.search(r'window.redirect_uri="(\S+?)";', data) + r_uri = pm.group(1) + '&fun=new' + self.redirect_uri = r_uri + self.base_uri = r_uri[:r_uri.rfind('/')] + return True + elif code == '408': + self._echo('[登陆超时] \n') + else: + self._echo('[登陆异常] \n') + return False + + def login(self): + data = self._get(self.redirect_uri) + doc = xml.dom.minidom.parseString(data) + root = doc.documentElement + + for node in root.childNodes: + if node.nodeName == 'skey': + self.skey = node.childNodes[0].data + elif node.nodeName == 'wxsid': + self.sid = node.childNodes[0].data + elif node.nodeName == 'wxuin': + self.uin = node.childNodes[0].data + elif node.nodeName == 'pass_ticket': + self.pass_ticket = node.childNodes[0].data + + if '' in (self.skey, self.sid, self.uin, self.pass_ticket): + return False + + self.BaseRequest = { + 'Uin': int(self.uin), + 'Sid': self.sid, + 'Skey': self.skey, + 'DeviceID': self.deviceId, + } + return True + + def webwxinit(self): + url = self.base_uri + '/webwxinit?pass_ticket=%s&skey=%s&r=%s' % ( + self.pass_ticket, self.skey, int(time.time())) + params = { + 'BaseRequest': self.BaseRequest + } + dic = self._post(url, params) + self.SyncKey = dic['SyncKey'] + self.User = dic['User'] + # synckey for synccheck + self.synckey = '|'.join( + [str(keyVal['Key']) + '_' + str(keyVal['Val']) for keyVal in self.SyncKey['List']]) + + return dic['BaseResponse']['Ret'] == 0 + + def webwxstatusnotify(self): + url = self.base_uri + \ + '/webwxstatusnotify?lang=zh_CN&pass_ticket=%s' % (self.pass_ticket) + params = { + 'BaseRequest': self.BaseRequest, + "Code": 3, + "FromUserName": self.User['UserName'], + "ToUserName": self.User['UserName'], + "ClientMsgId": int(time.time()) + } + dic = self._post(url, params) + + return dic['BaseResponse']['Ret'] == 0 + + def webwxgetcontact(self): + SpecialUsers = self.SpecialUsers + print self.base_uri + url = self.base_uri + '/webwxgetcontact?pass_ticket=%s&skey=%s&r=%s' % ( + self.pass_ticket, self.skey, int(time.time())) + dic = self._post(url, {}) + + self.MemberCount = dic['MemberCount'] + self.MemberList = dic['MemberList'] + ContactList = self.MemberList[:] + GroupList = self.GroupList[:] + PublicUsersList = self.PublicUsersList[:] + SpecialUsersList = self.SpecialUsersList[:] + + for i in xrange(len(ContactList) - 1, -1, -1): + Contact = ContactList[i] + if Contact['VerifyFlag'] & 8 != 0: # 公众号/服务号 + ContactList.remove(Contact) + self.PublicUsersList.append(Contact) + elif Contact['UserName'] in SpecialUsers: # 特殊账号 + ContactList.remove(Contact) + self.SpecialUsersList.append(Contact) + elif Contact['UserName'].find('@@') != -1: # 群聊 + ContactList.remove(Contact) + self.GroupList.append(Contact) + elif Contact['UserName'] == self.User['UserName']: # 自己 + ContactList.remove(Contact) + self.ContactList = ContactList + + return True + + def webwxbatchgetcontact(self): + url = self.base_uri + \ + '/webwxbatchgetcontact?type=ex&r=%s&pass_ticket=%s' % ( + int(time.time()), self.pass_ticket) + params = { + 'BaseRequest': self.BaseRequest, + "Count": len(self.GroupList), + "List": [{"UserName": g['UserName'], "EncryChatRoomId":""} for g in self.GroupList] + } + dic = self._post(url, params) + + # blabla ... + ContactList = dic['ContactList'] + ContactCount = dic['Count'] + self.GroupList = ContactList + + for i in xrange(len(ContactList) - 1, -1, -1): + Contact = ContactList[i] + MemberList = Contact['MemberList'] + for member in MemberList: + self.GroupMemeberList.append(member) + return True + + def getNameById(self, id): + url = self.base_uri + \ + '/webwxbatchgetcontact?type=ex&r=%s&pass_ticket=%s' % ( + int(time.time()), self.pass_ticket) + params = { + 'BaseRequest': self.BaseRequest, + "Count": 1, + "List": [{"UserName": id, "EncryChatRoomId": ""}] + } + dic = self._post(url, params) + + # blabla ... + return dic['ContactList'] + + def testsynccheck(self): + SyncHost = [ + 'webpush.weixin.qq.com', + 'webpush2.weixin.qq.com', + 'webpush.wechat.com', + 'webpush1.wechat.com', + 'webpush2.wechat.com', + 'webpush1.wechatapp.com', + # 'webpush.wechatapp.com' + ] + for host in SyncHost: + self.syncHost = host + [retcode, selector] = self.synccheck() + if retcode == '0': + return True + return False + + def synccheck(self): + params = { + 'r': int(time.time()), + 'sid': self.sid, + 'uin': self.uin, + 'skey': self.skey, + 'deviceid': self.deviceId, + 'synckey': self.synckey, + '_': int(time.time()), + } + url = 'https://' + self.syncHost + \ + '/cgi-bin/mmwebwx-bin/synccheck?' + urllib.urlencode(params) + data = self._get(url) + pm = re.search( + r'window.synccheck={retcode:"(\d+)",selector:"(\d+)"}', data) + retcode = pm.group(1) + selector = pm.group(2) + return [retcode, selector] + + def webwxsync(self): + url = self.base_uri + \ + '/webwxsync?sid=%s&skey=%s&pass_ticket=%s' % ( + self.sid, self.skey, self.pass_ticket) + params = { + 'BaseRequest': self.BaseRequest, + 'SyncKey': self.SyncKey, + 'rr': ~int(time.time()) + } + dic = self._post(url, params) + if self.DEBUG: + print json.dumps(dic, indent=4) + logging.debug(json.dumps(dic, indent=4)) + + if dic['BaseResponse']['Ret'] == 0: + self.SyncKey = dic['SyncKey'] + self.synckey = '|'.join( + [str(keyVal['Key']) + '_' + str(keyVal['Val']) for keyVal in self.SyncKey['List']]) + return dic + + def webwxsendmsg(self, word, to='filehelper'): + url = self.base_uri + \ + '/webwxsendmsg?pass_ticket=%s' % (self.pass_ticket) + clientMsgId = str(int(time.time() * 1000)) + \ + str(random.random())[:5].replace('.', '') + params = { + 'BaseRequest': self.BaseRequest, + 'Msg': { + "Type": 1, + "Content": self._transcoding(word), + "FromUserName": self.User['UserName'], + "ToUserName": to, + "LocalID": clientMsgId, + "ClientMsgId": clientMsgId + } + } + headers = {'content-type': 'application/json; charset=UTF-8'} + data = json.dumps(params, ensure_ascii=False).encode('utf8') + r = requests.post(url, data=data, headers=headers) + dic = r.json() + return dic['BaseResponse']['Ret'] == 0 + + def webwxuploadmedia(self, image_name): + url = 'https://file2.wx.qq.com/cgi-bin/mmwebwx-bin/webwxuploadmedia?f=json' + # 计数器 + self.media_count = self.media_count + 1 + # 文件名 + file_name = image_name + # MIME格式 + # mime_type = application/pdf, image/jpeg, image/png, etc. + mime_type = mimetypes.guess_type(image_name, strict=False)[0] + # 微信识别的文档格式,微信服务器应该只支持两种类型的格式。pic和doc + # pic格式,直接显示。doc格式则显示为文件。 + media_type = 'pic' if mime_type.split('/')[0] == 'image' else 'doc' + # 上一次修改日期 + lastModifieDate = 'Thu Mar 17 2016 00:55:10 GMT+0800 (CST)' + # 文件大小 + file_size = os.path.getsize(file_name) + # PassTicket + pass_ticket = self.pass_ticket + # clientMediaId + client_media_id = str(int(time.time() * 1000)) + \ + str(random.random())[:5].replace('.', '') + # webwx_data_ticket + webwx_data_ticket = '' + for item in self.cookie: + if item.name == 'webwx_data_ticket': + webwx_data_ticket = item.value + break + if (webwx_data_ticket == ''): + return "None Fuck Cookie" + + uploadmediarequest = json.dumps({ + "BaseRequest": self.BaseRequest, + "ClientMediaId": client_media_id, + "TotalLen": file_size, + "StartPos": 0, + "DataLen": file_size, + "MediaType": 4 + }, ensure_ascii=False).encode('utf8') + + multipart_encoder = MultipartEncoder( + fields={ + 'id': 'WU_FILE_' + str(self.media_count), + 'name': file_name, + 'type': mime_type, + 'lastModifieDate': lastModifieDate, + 'size': str(file_size), + 'mediatype': media_type, + 'uploadmediarequest': uploadmediarequest, + 'webwx_data_ticket': webwx_data_ticket, + 'pass_ticket': pass_ticket, + 'filename': (file_name, open(file_name, 'rb'), mime_type.split('/')[1]) + }, + boundary='-----------------------------1575017231431605357584454111' + ) + + headers = { + 'Host': 'file2.wx.qq.com', + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:42.0) Gecko/20100101 Firefox/42.0', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.5', + 'Accept-Encoding': 'gzip, deflate', + 'Referer': 'https://wx2.qq.com/', + 'Content-Type': multipart_encoder.content_type, + 'Origin': 'https://wx2.qq.com', + 'Connection': 'keep-alive', + 'Pragma': 'no-cache', + 'Cache-Control': 'no-cache' + } + + r = requests.post(url, data=multipart_encoder, headers=headers) + response_json = r.json() + if response_json['BaseResponse']['Ret'] == 0: + return response_json + return None + + def webwxsendmsgimg(self, user_id, media_id): + url = 'https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsgimg?fun=async&f=json&pass_ticket=%s' % self.pass_ticket + clientMsgId = str(int(time.time() * 1000)) + \ + str(random.random())[:5].replace('.', '') + data_json = { + "BaseRequest": self.BaseRequest, + "Msg": { + "Type": 3, + "MediaId": media_id, + "FromUserName": self.User['UserName'], + "ToUserName": user_id, + "LocalID": clientMsgId, + "ClientMsgId": clientMsgId + } + } + headers = {'content-type': 'application/json; charset=UTF-8'} + data = json.dumps(data_json, ensure_ascii=False).encode('utf8') + r = requests.post(url, data=data, headers=headers) + dic = r.json() + return dic['BaseResponse']['Ret'] == 0 + + def _saveFile(self, filename, data, api=None): + fn = filename + if self.saveSubFolders[api]: + dirName = os.path.join(self.saveFolder, self.saveSubFolders[api]) + if not os.path.exists(dirName): + os.makedirs(dirName) + fn = os.path.join(dirName, filename) + logging.debug('Saved file: %s' % fn) + with open(fn, 'wb') as f: + f.write(data) + f.close() + return fn + + def webwxgeticon(self, id): + url = self.base_uri + \ + '/webwxgeticon?username=%s&skey=%s' % (id, self.skey) + data = self._get(url) + fn = 'img_' + id + '.jpg' + return self._saveFile(fn, data, 'webwxgeticon') + + def webwxgetheadimg(self, id): + url = self.base_uri + \ + '/webwxgetheadimg?username=%s&skey=%s' % (id, self.skey) + data = self._get(url) + fn = 'img_' + id + '.jpg' + return self._saveFile(fn, data, 'webwxgetheadimg') + + def webwxgetmsgimg(self, msgid): + url = self.base_uri + \ + '/webwxgetmsgimg?MsgID=%s&skey=%s' % (msgid, self.skey) + data = self._get(url) + fn = 'img_' + msgid + '.jpg' + return self._saveFile(fn, data, 'webwxgetmsgimg') + + # Not work now for weixin haven't support this API + def webwxgetvideo(self, msgid): + url = self.base_uri + \ + '/webwxgetvideo?msgid=%s&skey=%s' % (msgid, self.skey) + data = self._get(url, api='webwxgetvideo') + fn = 'video_' + msgid + '.mp4' + return self._saveFile(fn, data, 'webwxgetvideo') + + def webwxgetvoice(self, msgid): + url = self.base_uri + \ + '/webwxgetvoice?msgid=%s&skey=%s' % (msgid, self.skey) + data = self._get(url) + fn = 'voice_' + msgid + '.mp3' + return self._saveFile(fn, data, 'webwxgetvoice') + + def getGroupName(self, id): + name = '未知群' + for member in self.GroupList: + if member['UserName'] == id: + name = member['NickName'] + if name == '未知群': + # 现有群里面查不到 + GroupList = self.getNameById(id) + for group in GroupList: + self.GroupList.append(group) + if group['UserName'] == id: + name = group['NickName'] + MemberList = group['MemberList'] + for member in MemberList: + self.GroupMemeberList.append(member) + return name + + def getUserRemarkName(self, id): + name = '未知群' if id[:2] == '@@' else '陌生人' + if id == self.User['UserName']: + return self.User['NickName'] # 自己 + + if id[:2] == '@@': + # 群 + name = self.getGroupName(id) + else: + # 特殊账号 + for member in self.SpecialUsersList: + if member['UserName'] == id: + name = member['RemarkName'] if member[ + 'RemarkName'] else member['NickName'] + + # 公众号或服务号 + for member in self.PublicUsersList: + if member['UserName'] == id: + name = member['RemarkName'] if member[ + 'RemarkName'] else member['NickName'] + + # 直接联系人 + for member in self.ContactList: + if member['UserName'] == id: + name = member['RemarkName'] if member[ + 'RemarkName'] else member['NickName'] + # 群友 + for member in self.GroupMemeberList: + if member['UserName'] == id: + name = member['DisplayName'] if member[ + 'DisplayName'] else member['NickName'] + + if name == '未知群' or name == '陌生人': + logging.debug(id) + return name + + def getUSerID(self, name): + for member in self.MemberList: + if name == member['RemarkName'] or name == member['NickName']: + return member['UserName'] + return None + + def _showMsg(self, message): + + srcName = None + dstName = None + groupName = None + content = None + + msg = message + logging.debug(msg) + + if msg['raw_msg']: + srcName = self.getUserRemarkName(msg['raw_msg']['FromUserName']) + dstName = self.getUserRemarkName(msg['raw_msg']['ToUserName']) + content = msg['raw_msg']['Content'].replace( + '<', '<').replace('>', '>') + message_id = msg['raw_msg']['MsgId'] + + if content.find('http://weixin.qq.com/cgi-bin/redirectforward?args=') != -1: + # 地理位置消息 + data = self._get(content).decode('gbk').encode('utf-8') + pos = self._searchContent('title', data, 'xml') + tree = html.fromstring(self._get(content)) + url = tree.xpath('//html/body/div/img')[0].attrib['src'] + + for item in urlparse(url).query.split('&'): + if item.split('=')[0] == 'center': + loc = item.split('=')[-1:] + + content = '%s 发送了一个 位置消息 - 我在 [%s](%s) @ %s]' % ( + srcName, pos, url, loc) + + if msg['raw_msg']['ToUserName'] == 'filehelper': + # 文件传输助手 + dstName = '文件传输助手' + + if msg['raw_msg']['FromUserName'][:2] == '@@': + # 接收到来自群的消息 + if re.search(":
", content, re.IGNORECASE): + [people, content] = content.split(':
') + groupName = srcName + srcName = self.getUserRemarkName(people) + dstName = 'GROUP' + else: + groupName = srcName + srcName = 'SYSTEM' + elif msg['raw_msg']['ToUserName'][:2] == '@@': + # 自己发给群的消息 + groupName = dstName + dstName = 'GROUP' + + # 收到了红包 + if content == '收到红包,请在手机上查看': + msg['message'] = content + + # 指定了消息内容 + if 'message' in msg.keys(): + content = msg['message'] + + if groupName != None: + print '%s |%s| %s -> %s: %s' % (message_id, groupName.strip(), srcName.strip(), dstName.strip(), content.replace('
', '\n')) + logging.info('%s |%s| %s -> %s: %s' % (message_id, groupName.strip(), + srcName.strip(), dstName.strip(), content.replace('
', '\n'))) + else: + print '%s %s -> %s: %s' % (message_id, srcName.strip(), dstName.strip(), content.replace('
', '\n')) + logging.info('%s %s -> %s: %s' % (message_id, srcName.strip(), + dstName.strip(), content.replace('
', '\n'))) + + def handleMsg(self, r): + for msg in r['AddMsgList']: + print '[*] 你有新的消息,请注意查收' + logging.debug('[*] 你有新的消息,请注意查收') + + if self.DEBUG: + fn = 'msg' + str(int(random.random() * 1000)) + '.json' + with open(fn, 'w') as f: + f.write(json.dumps(msg)) + print '[*] 该消息已储存到文件: ' + fn + logging.debug('[*] 该消息已储存到文件: %s' % (fn)) + + msgType = msg['MsgType'] + name = self.getUserRemarkName(msg['FromUserName']) + content = msg['Content'].replace('<', '<').replace('>', '>') + msgid = msg['MsgId'] + + if msgType == 1: + raw_msg = {'raw_msg': msg} + self._showMsg(raw_msg) + if self.autoReplyMode: + ans = self._xiaodoubi(content) + '\n[微信机器人自动回复]' + if self.webwxsendmsg(ans, msg['FromUserName']): + print '自动回复: ' + ans + logging.info('自动回复: ' + ans) + else: + print '自动回复失败' + logging.info('自动回复失败') + elif msgType == 3: + image = self.webwxgetmsgimg(msgid) + raw_msg = {'raw_msg': msg, + 'message': '%s 发送了一张图片: %s' % (name, image)} + self._showMsg(raw_msg) + self._safe_open(image) + elif msgType == 34: + voice = self.webwxgetvoice(msgid) + raw_msg = {'raw_msg': msg, + 'message': '%s 发了一段语音: %s' % (name, voice)} + self._showMsg(raw_msg) + self._safe_open(voice) + elif msgType == 42: + info = msg['RecommendInfo'] + print '%s 发送了一张名片:' % name + print '=========================' + print '= 昵称: %s' % info['NickName'] + print '= 微信号: %s' % info['Alias'] + print '= 地区: %s %s' % (info['Province'], info['City']) + print '= 性别: %s' % ['未知', '男', '女'][info['Sex']] + print '=========================' + raw_msg = {'raw_msg': msg, 'message': '%s 发送了一张名片: %s' % ( + name.strip(), json.dumps(info))} + self._showMsg(raw_msg) + elif msgType == 47: + url = self._searchContent('cdnurl', content) + raw_msg = {'raw_msg': msg, + 'message': '%s 发了一个动画表情,点击下面链接查看: %s' % (name, url)} + self._showMsg(raw_msg) + self._safe_open(url) + elif msgType == 49: + appMsgType = defaultdict(lambda: "") + appMsgType.update({5: '链接', 3: '音乐', 7: '微博'}) + print '%s 分享了一个%s:' % (name, appMsgType[msg['AppMsgType']]) + print '=========================' + print '= 标题: %s' % msg['FileName'] + print '= 描述: %s' % self._searchContent('des', content, 'xml') + print '= 链接: %s' % msg['Url'] + print '= 来自: %s' % self._searchContent('appname', content, 'xml') + print '=========================' + card = { + 'title': msg['FileName'], + 'description': self._searchContent('des', content, 'xml'), + 'url': msg['Url'], + 'appname': self._searchContent('appname', content, 'xml') + } + raw_msg = {'raw_msg': msg, 'message': '%s 分享了一个%s: %s' % ( + name, appMsgType[msg['AppMsgType']], json.dumps(card))} + self._showMsg(raw_msg) + elif msgType == 51: + raw_msg = {'raw_msg': msg, 'message': '[*] 成功获取联系人信息'} + self._showMsg(raw_msg) + elif msgType == 62: + video = self.webwxgetvideo(msgid) + raw_msg = {'raw_msg': msg, + 'message': '%s 发了一段小视频: %s' % (name, video)} + self._showMsg(raw_msg) + self._safe_open(video) + elif msgType == 10002: + raw_msg = {'raw_msg': msg, 'message': '%s 撤回了一条消息' % name} + self._showMsg(raw_msg) + else: + logging.debug('[*] 该消息类型为: %d,可能是表情,图片, 链接或红包: %s' % + (msg['MsgType'], json.dumps(msg))) + raw_msg = { + 'raw_msg': msg, 'message': '[*] 该消息类型为: %d,可能是表情,图片, 链接或红包' % msg['MsgType']} + self._showMsg(raw_msg) + + def listenMsgMode(self): + print '[*] 进入消息监听模式 ... 成功' + logging.debug('[*] 进入消息监听模式 ... 成功') + self._run('[*] 进行同步线路测试 ... ', self.testsynccheck) + playWeChat = 0 + redEnvelope = 0 + while True: + self.lastCheckTs = time.time() + [retcode, selector] = self.synccheck() + if self.DEBUG: + print 'retcode: %s, selector: %s' % (retcode, selector) + logging.debug('retcode: %s, selector: %s' % (retcode, selector)) + if retcode == '1100': + print '[*] 你在手机上登出了微信,债见' + logging.debug('[*] 你在手机上登出了微信,债见') + break + if retcode == '1101': + print '[*] 你在其他地方登录了 WEB 版微信,债见' + logging.debug('[*] 你在其他地方登录了 WEB 版微信,债见') + break + elif retcode == '0': + if selector == '2': + r = self.webwxsync() + if r is not None: + self.handleMsg(r) + elif selector == '6': + # TODO + redEnvelope += 1 + print '[*] 收到疑似红包消息 %d 次' % redEnvelope + logging.debug('[*] 收到疑似红包消息 %d 次' % redEnvelope) + elif selector == '7': + playWeChat += 1 + print '[*] 你在手机上玩微信被我发现了 %d 次' % playWeChat + logging.debug('[*] 你在手机上玩微信被我发现了 %d 次' % playWeChat) + r = self.webwxsync() + elif selector == '0': + time.sleep(1) + if (time.time() - self.lastCheckTs) <= 20: + time.sleep(time.time() - self.lastCheckTs) + + def sendMsg(self, name, word, isfile=False): + id = self.getUSerID(name) + if id: + if isfile: + with open(word, 'r') as f: + for line in f.readlines(): + line = line.replace('\n', '') + self._echo('-> ' + name + ': ' + line) + if self.webwxsendmsg(line, id): + print ' [成功]' + else: + print ' [失败]' + time.sleep(1) + else: + if self.webwxsendmsg(word, id): + print '[*] 消息发送成功' + logging.debug('[*] 消息发送成功') + else: + print '[*] 消息发送失败' + logging.debug('[*] 消息发送失败') + else: + print '[*] 此用户不存在' + logging.debug('[*] 此用户不存在') + + def sendMsgToAll(self, word): + for contact in self.ContactList: + name = contact['RemarkName'] if contact[ + 'RemarkName'] else contact['NickName'] + id = contact['UserName'] + self._echo('-> ' + name + ': ' + word) + if self.webwxsendmsg(word, id): + print ' [成功]' + else: + print ' [失败]' + time.sleep(1) + + def sendImg(self, name, file_name): + response = self.webwxuploadmedia(file_name) + media_id = "" + if response is not None: + media_id = response['MediaId'] + user_id = self.getUSerID(name) + response = self.webwxsendmsgimg(user_id, media_id) + + @catchKeyboardInterrupt + def start(self): + self._echo('[*] 微信网页版 ... 开动') + print + logging.debug('[*] 微信网页版 ... 开动') + while True: + self._run('[*] 正在获取 uuid ... ', self.getUUID) + self._echo('[*] 正在获取二维码 ... 成功') + print + logging.debug('[*] 微信网页版 ... 开动') + self.genQRCode() + print '[*] 请使用微信扫描二维码以登录 ... ' + if not self.waitForLogin(): + continue + print '[*] 请在手机上点击确认以登录 ... ' + if not self.waitForLogin(0): + continue + break + + self._run('[*] 正在登录 ... ', self.login) + self._run('[*] 微信初始化 ... ', self.webwxinit) + self._run('[*] 开启状态通知 ... ', self.webwxstatusnotify) + self._run('[*] 获取联系人 ... ', self.webwxgetcontact) + self._echo('[*] 应有 %s 个联系人,读取到联系人 %d 个' % + (self.MemberCount, len(self.MemberList))) + print + self._echo('[*] 共有 %d 个群 | %d 个直接联系人 | %d 个特殊账号 | %d 公众号或服务号' % (len(self.GroupList), + len(self.ContactList), len(self.SpecialUsersList), len(self.PublicUsersList))) + print + self._run('[*] 获取群 ... ', self.webwxbatchgetcontact) + logging.debug('[*] 微信网页版 ... 开动') + if self.DEBUG: + print self + logging.debug(self) + + if self.interactive and raw_input('[*] 是否开启自动回复模式(y/n): ') == 'y': + self.autoReplyMode = True + print '[*] 自动回复模式 ... 开启' + logging.debug('[*] 自动回复模式 ... 开启') + else: + print '[*] 自动回复模式 ... 关闭' + logging.debug('[*] 自动回复模式 ... 关闭') + + listenProcess = multiprocessing.Process(target=self.listenMsgMode) + listenProcess.start() + + while True: + text = raw_input('') + if text == 'quit': + listenProcess.terminate() + print('[*] 退出微信') + logging.debug('[*] 退出微信') + exit() + elif text[:2] == '->': + [name, word] = text[2:].split(':') + if name == 'all': + self.sendMsgToAll(word) + else: + self.sendMsg(name, word) + elif text[:3] == 'm->': + [name, file] = text[3:].split(':') + self.sendMsg(name, file, True) + elif text[:3] == 'f->': + print '发送文件' + logging.debug('发送文件') + elif text[:3] == 'i->': + print '发送图片' + [name, file_name] = text[3:].split(':') + self.sendImg(name, file_name) + logging.debug('发送图片') + + def _safe_open(self, path): + if self.autoOpen: + if platform.system() == "Linux": + os.system("xdg-open %s &" % path) + else: + os.system('open %s &' % path) + + def _run(self, str, func, *args): + self._echo(str) + if func(*args): + print '成功' + logging.debug('%s... 成功' % (str)) + else: + print('失败\n[*] 退出程序') + logging.debug('%s... 失败' % (str)) + logging.debug('[*] 退出程序') + exit() + + def _echo(self, str): + sys.stdout.write(str) + sys.stdout.flush() + + def _printQR(self, mat): + for i in mat: + BLACK = '\033[40m \033[0m' + WHITE = '\033[47m \033[0m' + print ''.join([BLACK if j else WHITE for j in i]) + + def _str2qr(self, str): + qr = qrcode.QRCode() + qr.border = 1 + qr.add_data(str) + mat = qr.get_matrix() + self._printQR(mat) # qr.print_tty() or qr.print_ascii() + + def _transcoding(self, data): + if not data: + return data + result = None + if type(data) == unicode: + result = data + elif type(data) == str: + result = data.decode('utf-8') + return result + + def _get(self, url, api=None): + request = urllib2.Request(url=url) + request.add_header('Referer', 'https://wx.qq.com/') + if api == 'webwxgetvoice': + request.add_header('Range', 'bytes=0-') + if api == 'webwxgetvideo': + request.add_header('Range', 'bytes=0-') + response = urllib2.urlopen(request) + data = response.read() + logging.debug(url) + return data + + def _post(self, url, params, jsonfmt=True): + if jsonfmt: + request = urllib2.Request(url=url, data=json.dumps(params)) + request.add_header( + 'ContentType', 'application/json; charset=UTF-8') + else: + request = urllib2.Request(url=url, data=urllib.urlencode(params)) + response = urllib2.urlopen(request) + data = response.read() + if jsonfmt: + return json.loads(data, object_hook=_decode_dict) + return data + + def _xiaodoubi(self, word): + url = 'http://www.xiaodoubi.com/bot/chat.php' + try: + r = requests.post(url, data={'chat': word}) + return r.content + except: + return "让我一个人静静 T_T..." + + def _simsimi(self, word): + key = '' + url = 'http://sandbox.api.simsimi.com/request.p?key=%s&lc=ch&ft=0.0&text=%s' % ( + key, word) + r = requests.get(url) + ans = r.json() + if ans['result'] == '100': + return ans['response'] + else: + return '你在说什么,风太大听不清列' + + def _searchContent(self, key, content, fmat='attr'): + if fmat == 'attr': + pm = re.search(key + '\s?=\s?"([^"<]+)"', content) + if pm: + return pm.group(1) + elif fmat == 'xml': + pm = re.search('<{0}>([^<]+)'.format(key), content) + if not pm: + pm = re.search( + '<{0}><\!\[CDATA\[(.*?)\]\]>'.format(key), content) + if pm: + return pm.group(1) + return '未知' + + +class UnicodeStreamFilter: + + def __init__(self, target): + self.target = target + self.encoding = 'utf-8' + self.errors = 'replace' + self.encode_to = self.target.encoding + + def write(self, s): + if type(s) == str: + s = s.decode('utf-8') + s = s.encode(self.encode_to, self.errors).decode(self.encode_to) + self.target.write(s) + + def flush(self): + self.target.flush() + +if sys.stdout.encoding == 'cp936': + sys.stdout = UnicodeStreamFilter(sys.stdout) + + +if __name__ == '__main__': + + logger = logging.getLogger(__name__) + import coloredlogs + coloredlogs.install(level='DEBUG') + + webwx = WebWeixin() + webwx.start()