├── .gitignore ├── LICENSE ├── README.md ├── README.rst ├── WxRobot ├── __init__.py ├── message.py ├── reply.py ├── webwxapi.py └── wxrobot.py ├── myrobot.py ├── requirement.txt ├── screenshot ├── 1.png ├── 2.png ├── 3.png └── 4.png └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.jpg 3 | *.pyc 4 | *.mp3 5 | *.mp4 6 | dist/ 7 | wxrobot.egg-info/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2016 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WxRobot ![python](https://img.shields.io/badge/python-3.4-ff69b4.svg) 2 | 面向个人账户的微信机器人框架 3 | 4 | 文档在此: 5 | 6 | https://sharpdeep.gitbooks.io/wxrobot-document/content/ 7 | 8 | ##Usage 9 | 10 | ###pip 安装 11 | ``` 12 | pip install wxrobot 13 | ``` 14 | 15 | ###setup.py 安装 16 | 下载项目到本地; 17 | 然后安装: 18 | ``` 19 | python setup.py install 20 | ``` 21 | 22 | 接口使用方法参考`myrobot.py`,详细接口请查看文档。 23 | 24 | ##Credit 25 | 26 | I used 27 | 28 | [WeixinBot](https://github.com/Urinx/WeixinBot) by [Urinx](https://github.com/Urinx) 29 | 30 | [WeRobot](https://github.com/whtsky/WeRoBot) by [whtsky](https://github.com/whtsky) 31 | 32 | [wechat-deleted-friends](https://github.com/0x5e/wechat-deleted-friends) by [0x5e](https://github.com/0x5e) 33 | 34 | 35 | ##Todo 36 | - [x] 添加更多类型消息接口 37 | - [x] 自动回复 38 | - [x] 小黄鸡或图灵机器人自动回复 39 | - [x] 主动发送消息 40 | - [ ] 发送图片 41 | - [ ] 用微信遥控电脑 42 | - [x] 完善文档 43 | 44 | ##反馈 45 | 46 | 有问题或者意见可以到[Issues](https://github.com/sharpdeep/WxRobot/issues)中提出 47 | 48 | 49 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | WxRobot |python| 2 | ================ 3 | 4 | 面向个人账户的微信机器人框架 5 | 6 | 文档在此: 7 | 8 | https://sharpdeep.gitbooks.io/wxrobot-document/content/ 9 | 10 | Usage 11 | ----- 12 | 13 | pip 安装 14 | ~~~~~~~~ 15 | 16 | :: 17 | 18 | pip install wxrobot 19 | 20 | setup.py 安装 21 | ~~~~~~~~~~~~~ 22 | 23 | 下载项目到本地; 然后安装: 24 | 25 | :: 26 | 27 | python setup.py install 28 | 29 | 接口使用方法参考\ ``myrobot.py``\ ,详细接口请查看文档。 30 | 31 | Credit 32 | ------ 33 | 34 | I used 35 | 36 | `WeixinBot `__ by 37 | `Urinx `__ 38 | 39 | `WeRobot `__ by 40 | `whtsky `__ 41 | 42 | `wechat-deleted-friends `__ 43 | by `0x5e `__ 44 | 45 | Todo 46 | ---- 47 | 48 | - [x] 添加更多类型消息接口 49 | - [x] 自动回复 50 | - [x] 小黄鸡或图灵机器人自动回复 51 | - [x] 主动发送消息 52 | - [ ] 发送图片 53 | - [ ] 用微信遥控电脑 54 | - [x] 完善文档 55 | 56 | 反馈 57 | ---- 58 | 59 | 有问题或者意见可以到\ `Issues `__\ 中提出 60 | 61 | .. |python| image:: https://img.shields.io/badge/python-3.4-ff69b4.svg 62 | -------------------------------------------------------------------------------- /WxRobot/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | @author: sharpdeep 6 | @file:__init__.py 7 | @time: 2016-02-15 14:27 8 | """ 9 | 10 | -------------------------------------------------------------------------------- /WxRobot/message.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | @author: sharpdeep 6 | @file:message.py 7 | @time: 2016-02-13 15:53 8 | """ 9 | 10 | MESSAGE_TYPES = {} 11 | 12 | def handle_for_type(type): 13 | def register(cls): 14 | MESSAGE_TYPES[type] = cls 15 | cls.type = type 16 | return cls 17 | return register 18 | 19 | class WeChatMessage(object): 20 | def __init__(self,message): 21 | self.fromUserName = message.pop('FromUserName',None) 22 | self.toUserName = message.pop('ToUserName',None) 23 | self.fromUserId = message.pop('FromUserId',None) 24 | self.toUserId = message.pop('ToUserId',None) 25 | self.msgId = message.pop('MsgId',0) 26 | self.createTime = message.pop('CreateTime',0) 27 | self.content = message.pop('Content','') 28 | self.__dict__.update(message) 29 | self.isBatch = False 30 | if self.fromUserId[:2] == '@@': 31 | self.isBatch = True 32 | self.fromMemberId = message.pop('FromMemberId',None) 33 | self.fromMemberName = message.pop('FromMemberName',None) 34 | 35 | @handle_for_type('text') 36 | class TextMessage(WeChatMessage): 37 | def __init__(self,api,message): 38 | super(TextMessage,self).__init__(message) 39 | 40 | @handle_for_type('location') 41 | class LocationMessage(WeChatMessage): 42 | def __init__(self,api,message): 43 | super(LocationMessage,self).__init__(message) 44 | 45 | 46 | @handle_for_type('image') 47 | class ImageMessage(WeChatMessage): 48 | def __init__(self,api,message): 49 | self.image = api.webwxgetmsgimg(message['MsgId']) 50 | super(ImageMessage,self).__init__(message) 51 | 52 | 53 | @handle_for_type('voice') 54 | class VoiceMessage(WeChatMessage): 55 | def __init__(self,api,message): 56 | self.voice = api.webwxgetmsgvoice(message['MsgId']) 57 | super(VoiceMessage,self).__init__(message) 58 | 59 | @handle_for_type('video') 60 | class VideoMessage(WeChatMessage): 61 | def __init__(self,api,message): 62 | self.video = api.webwxgetmsgvideo(message['MsgId']) 63 | super(VideoMessage,self).__init__(message) 64 | 65 | @handle_for_type('sharelocation') 66 | class ShareLocationMessage(WeChatMessage): 67 | def __init__(self,api,message): 68 | super(ShareLocationMessage,self).__init__(message) 69 | 70 | @handle_for_type('recommend') 71 | class RecommendMessage(WeChatMessage): 72 | def __init__(self,api,message): 73 | info = message['RecommendInfo'] 74 | self.recommend = '=============================\n' + \ 75 | '昵称 : %s \n'%info['NickName'] + \ 76 | '微信号: %s \n'%info['Alias'] + \ 77 | '地区 : %s %s \n'%(info['Province'], info['City']) + \ 78 | '性别 : %s \n'%['未知','男','女'][info['Sex']] + \ 79 | '=============================\n' 80 | super(RecommendMessage,self).__init__(message) 81 | 82 | @handle_for_type('revoke') 83 | class RevokeMessage(WeChatMessage): 84 | def __init__(self,api,message): 85 | self.revokeMsgId = api._searchContent('msgid',message['Content'],fmat='xml') 86 | super(RevokeMessage,self).__init__(message) 87 | 88 | 89 | @handle_for_type('initmsg') 90 | class InitMessage(WeChatMessage): 91 | def __init__(self,api,message): 92 | super(InitMessage,self).__init__(message) 93 | 94 | class UnKnownMessage(WeChatMessage): 95 | def __init__(self,api,message): 96 | self.type = 'unkonwn' 97 | super(UnKnownMessage,self).__init__(message) 98 | -------------------------------------------------------------------------------- /WxRobot/reply.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | @author: sharpdeep 6 | @file:reply.py 7 | @time: 2016-02-17 21:47 8 | """ 9 | REPLY_TYPES = {} 10 | 11 | def handle_for_reply_type(type): 12 | def register(cls): 13 | cls.type = type 14 | REPLY_TYPES[type] = cls 15 | return cls 16 | return register 17 | 18 | class WeChatReply(object): 19 | def __init__(self): 20 | pass 21 | 22 | @handle_for_reply_type('text') 23 | class TextReply(WeChatReply): 24 | def __init__(self,reply): 25 | self.content = reply 26 | super(TextReply,self).__init__() 27 | 28 | @handle_for_reply_type('unknown') 29 | class UnkonwnReply(WeChatReply): 30 | def __init__(self): 31 | super(UnkonwnReply,self).__init__() 32 | 33 | 34 | -------------------------------------------------------------------------------- /WxRobot/webwxapi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | @author: sharpdeep 6 | @file:webwxapi.py 7 | @time: 2016-02-13 15:47 8 | """ 9 | import inspect 10 | import time 11 | import re 12 | import json 13 | import random,sys,os 14 | import subprocess 15 | import qrcode 16 | from bs4 import BeautifulSoup 17 | from urllib import request, parse 18 | from http.cookiejar import CookieJar 19 | from WxRobot.message import MESSAGE_TYPES, UnKnownMessage 20 | from WxRobot.reply import WeChatReply,TextReply 21 | 22 | QRCODE_PATH = os.path.join(os.getcwd(), 'qrcode.jpg') 23 | 24 | class WebWxAPI(object): 25 | message_types_dict = { 26 | 1: 'text', # 文本消息 27 | 3: 'image', # 图片消息 28 | 34: 'voice', # 语音消息 29 | 42: 'recommend', #名片 30 | 48: 'sharelocation', # 位置共享 31 | 51: 'initmsg', # 微信初始化消息 32 | 62: 'video', # 小视频 33 | 10002: 'revoke', #撤回消息 34 | } 35 | 36 | message_types = message_types_dict.values() 37 | 38 | def __str__(self): 39 | description = \ 40 | "=========================\n" + \ 41 | "[#] Web Weixin\n" + \ 42 | "[#] Debug Mode: " + str(self.DEBUG) + "\n" + \ 43 | "[#] Uuid: " + self.uuid + "\n" + \ 44 | "[#] Uin: " + str(self.uin) + "\n" + \ 45 | "[#] Sid: " + self.sid + "\n" + \ 46 | "[#] Skey: " + self.skey + "\n" + \ 47 | "[#] DeviceId: " + self.deviceId + "\n" + \ 48 | "[#] PassTicket: " + self.pass_ticket + "\n" + \ 49 | "=========================" 50 | return description 51 | 52 | def __init__(self): 53 | self.DEBUG = False 54 | self.appid = 'wx782c26e4c19acffb' 55 | self.uuid = '' 56 | self.base_uri = '' 57 | self.redirect_uri = '' 58 | self.uin = '' 59 | self.sid = '' 60 | self.skey = '' 61 | self.pass_ticket = '' 62 | self.deviceId = 'e' + repr(random.random())[2:17] 63 | self.BaseRequest = {} 64 | self.synckey = '' 65 | self.SyncKey = [] 66 | self.User = [] 67 | self.MemberList = [] 68 | self.ContactList = [] 69 | self.GroupList = [] 70 | self.autoReplyMode = False 71 | self.syncHost = '' 72 | 73 | self._handlers = dict((k, []) for k in self.message_types) 74 | self._handlers['location'] = [] 75 | self._handlers['all'] = [] 76 | 77 | self._filters = dict() 78 | 79 | opener = request.build_opener(request.HTTPCookieProcessor(CookieJar())) 80 | opener.addheaders = [('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'), 81 | ('Referer','https://wx2.qq.com/')] 82 | request.install_opener(opener) 83 | 84 | def getUUID(self): 85 | url = 'https://login.weixin.qq.com/jslogin' 86 | params = { 87 | 'appid': self.appid, 88 | 'fun': 'new', 89 | 'lang': 'zh_CN', 90 | '_': int(time.time()), 91 | } 92 | data = self._post(url, params, False) 93 | regx = r'window.QRLogin.code = (\d+); window.QRLogin.uuid = "(\S+?)"' 94 | result = re.search(regx, data) 95 | if result: 96 | code = result.group(1) 97 | self.uuid = result.group(2) 98 | return code == '200' 99 | return False 100 | 101 | def genQRCode(self): 102 | if sys.platform.find('win') >= 0: 103 | self._genQRCodeImg() 104 | self._safe_open(QRCODE_PATH) 105 | else: 106 | mat = self._str2QRMat('https://login.weixin.qq.com/l/' + self.uuid) 107 | self._printQR(mat) 108 | 109 | def waitForLogin(self, tip=1): 110 | data = self._get('https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login?tip=%s&uuid=%s&_=%s' % ( 111 | tip, self.uuid, int(time.time()))) 112 | result = re.search(r'window.code=(\d+);', data) 113 | code = result.group(1) 114 | 115 | if code == '201': # 扫描成功 116 | return True 117 | elif code == '200': # 登陆成功 118 | result = re.search(r'window.redirect_uri="(\S+?)";', data) 119 | r_uri = result.group(1) + '&fun=new' 120 | self.redirect_uri = r_uri 121 | self.base_uri = r_uri[:r_uri.rfind('/')] 122 | return True 123 | elif code == '408': # 登陆超时 124 | return False, '登陆超时' 125 | else: # 登陆异常 126 | return False, '登陆异常' 127 | 128 | def login(self): 129 | data = self._get(self.redirect_uri) 130 | soup = BeautifulSoup(data, "html.parser") 131 | 132 | self.skey = soup.skey.text 133 | self.sid = soup.wxsid.text 134 | self.uin = soup.wxuin.text 135 | self.pass_ticket = soup.pass_ticket.text 136 | 137 | if '' in (self.skey, self.sid, self.uin, self.pass_ticket): 138 | return False 139 | 140 | self.BaseRequest = { 141 | 'Uin': int(self.uin), 142 | 'Sid': self.sid, 143 | 'Skey': self.skey, 144 | 'DeviceID': self.deviceId, 145 | } 146 | return True 147 | 148 | def webwxinit(self): 149 | url = self.base_uri + '/webwxinit?pass_ticket=%s&skey=%s&r=%s' % (self.pass_ticket, self.skey, int(time.time())) 150 | params = { 151 | 'BaseRequest': self.BaseRequest 152 | } 153 | dic = self._post(url, params) 154 | self.SyncKey = dic['SyncKey'] 155 | self.User = dic['User'] 156 | # synckey for synccheck 157 | self.synckey = '|'.join([str(keyVal['Key']) + '_' + str(keyVal['Val']) for keyVal in self.SyncKey['List']]) 158 | return dic['BaseResponse']['Ret'] == 0 159 | 160 | def webwxstatusnotify(self): 161 | url = self.base_uri + '/webwxstatusnotify?lang=zh_CN&pass_ticket=%s' % (self.pass_ticket) 162 | params = { 163 | 'BaseRequest': self.BaseRequest, 164 | "Code": 3, 165 | "FromUserName": self.User['UserName'], 166 | "ToUserName": self.User['UserName'], 167 | "ClientMsgId": int(time.time()) 168 | } 169 | dic = self._post(url, params) 170 | 171 | return dic['BaseResponse']['Ret'] == 0 172 | 173 | def webwxgetcontact(self): 174 | url = self.base_uri + '/webwxgetcontact?pass_ticket=%s&skey=%s&r=%s' % ( 175 | self.pass_ticket, self.skey, int(time.time())) 176 | dic = self._post(url, {}) 177 | self.MemberList = dic['MemberList'] 178 | 179 | ContactList = self.MemberList[:] 180 | 181 | for i in range(len(ContactList) - 1, -1, -1): 182 | Contact = ContactList[i] 183 | if Contact['VerifyFlag'] & 8 != 0: # 公众号/服务号 184 | ContactList.remove(Contact) 185 | elif Contact['UserName'] in specialUsers: # 特殊账号 186 | ContactList.remove(Contact) 187 | elif Contact['UserName'].find('@@') != -1: # 群聊 188 | self.GroupList.append(Contact) 189 | ContactList.remove(Contact) 190 | elif Contact['UserName'] == self.User['UserName']: # 自己 191 | ContactList.remove(Contact) 192 | self.ContactList = ContactList 193 | 194 | return True 195 | 196 | def webwxgetbatchcontact(self,groupid): 197 | if groupid[:2] != '@@': 198 | return None 199 | url = self.base_uri + '/webwxbatchgetcontact?type=ex&r=%s&pass_ticket=%s' % (int(time.time()), self.pass_ticket) 200 | params = { 201 | 'BaseRequest': self.BaseRequest, 202 | "Count": len(self.GroupList), 203 | "List": [{"UserName": groupid, "EncryChatRoomId": ""}] 204 | } 205 | dic = self._post(url, params) 206 | group = dic['ContactList'][0] 207 | # 群联系人(todo) 208 | return group 209 | 210 | def webwxsendtextmsg(self,text,to = 'filehelper'): 211 | url = self.base_uri + '/webwxsendmsg?pass_ticket=%s' % (self.pass_ticket) 212 | clientMsgId = str(int(time.time()*1000)) + str(random.random())[:5].replace('.','') 213 | params = { 214 | 'BaseRequest': self.BaseRequest, 215 | 'Msg': { 216 | "Type": 1, 217 | "Content": text, 218 | "FromUserName": self.User['UserName'], 219 | "ToUserName": to, 220 | "LocalID": clientMsgId, 221 | "ClientMsgId": clientMsgId 222 | } 223 | } 224 | # headers = {'content-type': 'application/json; charset=UTF-8'} 225 | # data = json.dumps(params, ensure_ascii=False).encode('utf8') 226 | # r = requests.post(url, data = data, headers = headers) 227 | # dic = r.json() 228 | # return dic['BaseResponse']['Ret'] == 0 229 | dic = self._post(url,params) 230 | return dic['BaseResponse']['Ret'] == 0 231 | 232 | def webwxgeticon(self,id): 233 | url = self.base_uri + '/webwxgeticon?username=%s&skey=%s' % (id, self.skey) 234 | data = self._get(url,byte_ret=True) 235 | icon_path = os.path.join(os.getcwd(),'icon_'+id+'.jpg') 236 | with open(icon_path,'wb') as f: 237 | f.write(data) 238 | return icon_path 239 | 240 | def webwxgetheading(self,id): 241 | url = self.base_uri + '/webwxgetheadimg?username=%s&skey=%s' % (id, self.skey) 242 | data = self._get(url,byte_ret=True) 243 | head_path = os.path.join(os.getcwd(),'head_'+id+'.jpg') 244 | with open(head_path,'wb') as f: 245 | f.write(data) 246 | return head_path 247 | 248 | def webwxgetmsgimg(self,msgid): 249 | url = self.base_uri + '/webwxgetmsgimg?MsgID=%s&skey=%s' % (msgid, self.skey) 250 | data = self._get(url,byte_ret=True) 251 | return self._save_file(data,'msgimg_' + msgid + '.jpg') 252 | 253 | 254 | def webwxgetmsgvideo(self,msgid): 255 | url = self.base_uri + '/webwxgetvideo?msgid=%s&skey=%s' % (msgid, self.skey) 256 | data = self._get(url,byte_ret=True) 257 | return self._save_file(data,'msgvideo_'+msgid+'.mp4') 258 | 259 | 260 | def webwxgetmsgvoice(self,msgid): 261 | url = self.base_uri + '/webwxgetvoice?msgid=%s&skey=%s' % (msgid, self.skey) 262 | data = self._get(url,byte_ret=True) 263 | return self._save_file(data,'msgvoice_'+msgid+'.mp3') 264 | 265 | 266 | def testsynccheck(self): 267 | syncHost = [ 268 | 'webpush.weixin.qq.com', 269 | 'webpush1.weixin.qq.com', 270 | 'webpush2.weixin.qq.com', 271 | 'webpush.wechat.com', 272 | 'webpush1.wechat.com', 273 | 'webpush2.wechat.com', 274 | 'webpush1.wechatapp.com', 275 | ] 276 | for host in syncHost: 277 | self.syncHost = host 278 | [retcode, selector] = self.synccheck() 279 | if self.DEBUG: 280 | print('[*] test',host,'->',retcode) 281 | if retcode == '0': return True 282 | return False 283 | 284 | def synccheck(self): 285 | params = { 286 | 'r': int(time.time()), 287 | 'sid': self.sid, 288 | 'uin': self.uin, 289 | 'skey': self.skey, 290 | 'deviceid': self.deviceId, 291 | 'synckey': self.synckey, 292 | '_': int(time.time()), 293 | } 294 | url = 'https://' + self.syncHost + '/cgi-bin/mmwebwx-bin/synccheck?' + parse.urlencode(params) 295 | data = self._get(url) 296 | pm = re.search(r'window.synccheck={retcode:"(\d+)",selector:"(\d+)"}', data) 297 | retcode = pm.group(1) 298 | selector = pm.group(2) 299 | return [retcode, selector] 300 | 301 | def webwxsync(self): 302 | url = self.base_uri + '/webwxsync?sid=%s&skey=%s&pass_ticket=%s' % (self.sid, self.skey, self.pass_ticket) 303 | params = { 304 | 'BaseRequest': self.BaseRequest, 305 | 'SyncKey': self.SyncKey, 306 | 'rr': ~int(time.time()) 307 | } 308 | dic = self._post(url, params) 309 | if self.DEBUG: 310 | print(json.dumps(dic, indent=4)) 311 | 312 | if dic['BaseResponse']['Ret'] == 0: 313 | self.SyncKey = dic['SyncKey'] 314 | self.synckey = '|'.join([str(keyVal['Key']) + '_' + str(keyVal['Val']) for keyVal in self.SyncKey['List']]) 315 | return dic 316 | 317 | def sendTextMsg(self,name,text,isfile = False): 318 | id = self.getUserId(name) 319 | if id: 320 | if self.webwxsendtextmsg(text,id): 321 | return True,None 322 | else: 323 | return False,'api调用失败' 324 | else: 325 | return False,'用户不存在' 326 | 327 | 328 | 329 | 330 | def listenMsgLoop(self, onExit, onMsgReceive, onPhoneInteract,onIdle,onSyncError): 331 | # 测试同步线路 332 | if not self.testsynccheck(): 333 | onSyncError() 334 | while True: 335 | [retcode, selector] = self.synccheck() 336 | if retcode == '1100': 337 | onExit() 338 | break 339 | elif retcode == '0': 340 | if selector == '2': 341 | onMsgReceive() 342 | syncRet = self.webwxsync() 343 | if syncRet is not None: 344 | self.handleMsg(syncRet) 345 | elif selector == '7': 346 | onPhoneInteract() 347 | elif selector == '0': 348 | onIdle() 349 | time.sleep(1) 350 | 351 | def handleMsg(self, syncret): 352 | for msg in syncret['AddMsgList']: 353 | message = self._process_message(msg) 354 | handlers = self.get_handler(message.type) 355 | for handler, args_count in handlers: 356 | #filte message 357 | filters = self.get_filter(handler) 358 | is_match = self._filte(message,*filters) 359 | if not is_match: 360 | continue 361 | args = [message, ][:args_count] 362 | reply = handler(*args) 363 | if reply: 364 | self._process_reply(reply,message) 365 | 366 | 367 | def getUserRemarkName(self, id): 368 | name = '未知群' if id[:2] == '@@' else '陌生人' 369 | if id in specialUsers: 370 | return specialUsersDict[id] 371 | for member in self.MemberList: 372 | if self.User['UserName'] == id: # 自己 373 | return '我' 374 | if member['UserName'] == id: 375 | name = member['RemarkName'] if member['RemarkName'] else member['NickName'] 376 | return name 377 | if id[:2] == '@@': #没加入通讯录的群 378 | newGroup = self.webwxgetbatchcontact(id) 379 | if not newGroup['RemarkName'] and not newGroup['NickName']: 380 | return '未命名群' 381 | self.GroupList.append(newGroup) 382 | name = newGroup['RemarkName'] if newGroup['RemarkName'] else newGroup['NickName'] 383 | return name 384 | return name 385 | 386 | def getUserId(self,name): 387 | for member in self.MemberList: 388 | if name == member['RemarkName'] or name == member['NickName']: 389 | return member['UserName'] 390 | return None 391 | 392 | def createChatroom(self,userNames): 393 | memberList = [{'UserName':username} for username in userNames] 394 | 395 | url = self.base_uri + '/webwxcreatechatroom?pass_ticket=%s&r=%s' % (self.pass_ticket,int(time.time())) 396 | params = { 397 | 'BaseRequest':self.BaseRequest, 398 | 'MemberCount':len(memberList), 399 | 'MemberList':memberList, 400 | 'Topic':'', 401 | } 402 | 403 | dic = self._post(url = url, params = params) 404 | 405 | state = True if dic['BaseResponse']['Ret'] == 0 else False 406 | errMsg = dic['BaseResponse']['ErrMsg'] 407 | chatRoomName = dic['ChatRoomName'] 408 | memberList = dic['MemberList'] 409 | deletedList = [] 410 | blockedList = [] 411 | for member in memberList: 412 | if member['MemberStatus'] == 4: #被对方删除了 413 | deletedList.append(member['UserName']) 414 | elif member['MemberStatus'] == 3: #被加入黑名单 415 | blockedList.append(member['UserName']) 416 | return state,errMsg,chatRoomName,deletedList,blockedList 417 | 418 | def addChatroomMember(self,chatRoomName,userNames): 419 | url = self.base_uri + '/webwxupdatechatroom?fun=addmember&pass_ticket=%s' % (self.pass_ticket) 420 | params = { 421 | 'BaseRequest': self.BaseRequest, 422 | 'ChatRoomName': chatRoomName, 423 | 'AddMemberList': ','.join(userNames), 424 | } 425 | 426 | dic = self._post(url,params) 427 | 428 | state = True if dic['BaseResponse']['Ret'] == 0 else False 429 | errMsg = dic['BaseResponse']['ErrMsg'] 430 | memberList = dic['MemberList'] 431 | deletedList = [] 432 | blockedList = [] 433 | for member in memberList: 434 | if member['MemberStatus'] == 4: #被对方删除了 435 | deletedList.append(member['UserName']) 436 | elif member['MemberStatus'] == 3: #被加入黑名单 437 | blockedList.append(member['UserName']) 438 | return state,errMsg,deletedList,blockedList 439 | 440 | def delChatroomMember(self,chatRoomName,userNames): 441 | url = self.base_uri + '/webwxupdatechatroom?fun=delmember&pass_ticket=%s' % (self.pass_ticket) 442 | params = { 443 | 'BaseRequest': self.BaseRequest, 444 | 'ChatRoomName': chatRoomName, 445 | 'DelMemberList': ','.join(userNames), 446 | } 447 | dic = self._post(url,params) 448 | 449 | return dic['BaseResponse']['Ret'] == 0 450 | 451 | def getBatchMemberRemarkName(self,groupid,memberid): 452 | name = '陌生人' 453 | for group in self.GroupList: 454 | if group['UserName'] == groupid: 455 | for member in group['MemberList']: 456 | if member['UserName'] == memberid: 457 | name = member['DisplayName'] if member['DisplayName'] else member['NickName'] 458 | return name 459 | new_group = self.webwxgetbatchcontact(groupid) 460 | if new_group: 461 | for member in new_group['MemberList']: 462 | if member['UserName'] == memberid: 463 | name = member['DisplayName'] if member['DisplayName'] else member['NickName'] 464 | return name 465 | return name 466 | 467 | def _process_message(self, message): 468 | message['type'] = self.message_types_dict.get(message.pop('MsgType'), None) 469 | message['FromUserId'] = message.get('FromUserName',None) 470 | message['FromUserName'] = self.getUserRemarkName(message.pop('FromUserName')) 471 | message['ToUserId'] = message.get('ToUserName',None) 472 | message['ToUserName'] = self.getUserRemarkName(message.pop('ToUserName')) 473 | message['Content'] = message.pop('Content').replace('
', '\n').replace('<', '<').replace('>', '>') 474 | 475 | if message['FromUserId'][:2] == '@@': #群消息 476 | fromMemberId = message['Content'].split(':')[0] 477 | message['FromMemberId'] = fromMemberId 478 | message['FromMemberName'] = self.getBatchMemberRemarkName(message['FromUserId'],fromMemberId) 479 | message['Content'] = ''.join(message['Content'].split(':')[1:]) 480 | 481 | if message['type'] == 'text' and message['Content'].find( #位置消息 482 | 'http://weixin.qq.com/cgi-bin/redirectforward?args=') != -1: 483 | message['type'] = 'location' 484 | data = self._get(message['Content'],encoding='gbk') 485 | location = self._searchContent('title',data,fmat='xml') 486 | message['location'] = location 487 | 488 | 489 | message_type = MESSAGE_TYPES.get(message['type'], UnKnownMessage) 490 | return message_type(self,message) 491 | 492 | def _process_reply(self,reply,message): 493 | if isinstance(reply,tuple): 494 | for r in reply: 495 | self._process_reply(r,message) 496 | elif isinstance(reply,str): 497 | self.sendTextMsg(message.fromUserName,reply) 498 | elif isinstance(reply,WeChatReply): 499 | if isinstance(reply,TextReply): #文本回复 500 | self.sendTextMsg(message.fromUserName,reply.content) 501 | else: 502 | raise TypeError('your reply is a %s,reply should be str or WechatReply instance'%type(reply)) 503 | 504 | def _str2QRMat(self, str): 505 | qr = qrcode.QRCode() 506 | qr.border = 1 507 | qr.add_data(str) 508 | mat = qr.get_matrix() 509 | return mat 510 | 511 | def _genQRCodeImg(self): 512 | url = 'https://login.weixin.qq.com/qrcode/' + self.uuid 513 | params = { 514 | 't': 'webwx', 515 | '_': int(time.time()), 516 | } 517 | 518 | req = request.Request(url=url, data=parse.urlencode(params).encode('utf-8')) 519 | response = request.urlopen(req) 520 | data = response.read() 521 | 522 | with open(QRCODE_PATH,'wb') as f: 523 | f.write(data) 524 | 525 | def _printQR(self, mat): 526 | BLACK = '\033[40m \033[0m' 527 | WHITE = '\033[47m \033[0m' 528 | for row in mat: 529 | print(''.join([BLACK if item else WHITE for item in row])) 530 | 531 | def _get(self, url,encoding='utf-8',byte_ret = False): 532 | req = request.Request(url) 533 | response = request.urlopen(req) 534 | if byte_ret: 535 | return response.read() 536 | data = response.read().decode(encoding) 537 | return data 538 | 539 | def _post(self, url, params, jsonfmt=True,encoding='utf-8',byte_ret = False): 540 | if jsonfmt: 541 | req = request.Request(url=url, data=json.dumps(params,ensure_ascii=False).encode('utf-8')) 542 | req.add_header('ContentType', 'application/json; charset=UTF-8') 543 | else: 544 | req = request.Request(url=url, data=parse.urlencode(params).encode('utf-8')) 545 | 546 | response = request.urlopen(req) 547 | if byte_ret: 548 | return response.read() 549 | data = response.read().decode(encoding) 550 | return json.loads(data) if jsonfmt else data 551 | 552 | def _filte(self,message,*filters): 553 | is_match = True 554 | if len(filters) == 0: 555 | return is_match 556 | 557 | for filter in filters: 558 | if not is_match: 559 | break 560 | if filter[0] and not message.fromUserName in filter[0]: #filte fromUserName 561 | is_match = False 562 | if filter[1] and not message.toUserName in filter[1]: #filte toUserName 563 | is_match = False 564 | if filter[2] and not self._filte_content(message,*filter[2]): 565 | is_match = False 566 | is_match = not is_match if filter[3] else is_match 567 | 568 | 569 | return is_match 570 | 571 | def _filte_content(self,message,*args): 572 | if len(args) > 1: 573 | for x in args: 574 | if self._filte_content(message,x): 575 | return True 576 | return False 577 | else: 578 | target_content = args[0] 579 | if isinstance(target_content,str): 580 | return target_content == message.content 581 | elif hasattr(target_content, "match") and callable(target_content.match): #正则匹配 582 | return target_content.match(message.content) 583 | else: 584 | raise TypeError("%s is not a valid target_content" % target_content) 585 | 586 | def allMsg(self,func): 587 | self.add_handler(func) 588 | return func 589 | 590 | def textMsg(self, func): 591 | self.add_handler(func, type='text') 592 | return func 593 | 594 | def imageMsg(self, func): 595 | self.add_handler(func, type='image') 596 | return func 597 | 598 | def videoMsg(self, func): 599 | self.add_handler(func, type='video') 600 | return func 601 | 602 | def voiceMsg(self, func): 603 | self.add_handler(func, type='voice') 604 | return func 605 | 606 | def sharelocation(self, func): 607 | self.add_handler(func, type='sharelocation') 608 | return func 609 | 610 | def location(self, func): 611 | self.add_handler(func, type='location') 612 | return func 613 | 614 | def recommend(self,func): 615 | self.add_handler(func,type='recommend') 616 | return func 617 | 618 | def revoke(self,func): 619 | self.add_handler(func,type='revoke') 620 | return func 621 | 622 | def initMsg(self, func): 623 | self.add_handler(func, type='initmsg') 624 | return func 625 | 626 | def textFilter(self,*args,beside = False): 627 | def wrapper(func): 628 | self.add_filter(func,content=args,beside=beside) 629 | return func 630 | return wrapper 631 | 632 | def sourceFilter(self,*fromUserNames,beside = False): 633 | def wrapper(func): 634 | self.add_filter(func,fromUserNames=fromUserNames,beside=beside) 635 | return func 636 | return wrapper 637 | 638 | def targetFilter(self,*toUserNames,beside = False): 639 | def wrapper(func): 640 | self.add_filter(func,toUserNames=toUserNames,beside=beside) 641 | return func 642 | return wrapper 643 | 644 | def filter(self,*args,beside = False): 645 | args_is_list = False 646 | if len(args) > 1: 647 | args_is_list = True 648 | elif len(args) == 0: 649 | raise ValueError('filter should have 1 argments at least') 650 | else: 651 | target_content = args[0] 652 | if isinstance(target_content,str): 653 | def _compareContent(message): 654 | compareResult = (target_content == message.content) 655 | return compareResult if not beside else not compareResult 656 | elif hasattr(target_content, "match") and callable(target_content.match): #正则匹配 657 | def _compareContent(message): 658 | compareResult = target_content.match(message.content) 659 | return compareResult if not beside else not compareResult 660 | else: 661 | raise TypeError("%s is not a valid target_content" % target_content) 662 | 663 | def wrapper(f): 664 | if args_is_list: 665 | for x in args: 666 | self.filter(x)(f) 667 | return f 668 | 669 | @self.textMsg 670 | def _f(message): 671 | if _compareContent(message): 672 | return f(message) 673 | return f 674 | return wrapper 675 | 676 | 677 | def add_handler(self, func, type='all'): 678 | if not callable(func): 679 | raise ValueError("{} is not callable".format(func)) 680 | self._handlers[type].append((func, len(inspect.getargspec(func).args))) 681 | 682 | def add_filter(self,func,fromUserNames = None,toUserNames = None,content = None,beside = False): 683 | 684 | fromUserNames = None if isinstance(fromUserNames,tuple) and len(fromUserNames) == 0 else fromUserNames 685 | toUserNames = None if isinstance(toUserNames,tuple) and len(toUserNames) == 0 else toUserNames 686 | content = None if isinstance(content,tuple) and len(content) == 0 else content 687 | 688 | if not self._filters.get(func): 689 | self._filters[func] = [] 690 | 691 | self._filters[func].append((fromUserNames,toUserNames,content,beside)) 692 | 693 | def get_handler(self, type): 694 | return self._handlers.get(type, []) + self._handlers['all'] 695 | 696 | def get_filter(self,func): 697 | return self._filters.get(func,[]) 698 | 699 | def _searchContent(self, key, content, fmat='attr'): 700 | if fmat == 'attr': 701 | pm = re.search(key + '\s?=\s?"([^"<]+)"', content) 702 | if pm: return pm.group(1) 703 | elif fmat == 'xml': 704 | pm = re.search('<{0}>([^<]+)'.format(key), content) 705 | if pm: return pm.group(1) 706 | return '未知' 707 | 708 | def _save_file(self,data,file_name): 709 | file_type = file_name[:file_name.find('_')] 710 | if file_type == 'msgimg': 711 | path = self._mkdir(os.path.join(os.getcwd(),'images')) 712 | elif file_type == 'msgvoice': 713 | path = self._mkdir(os.path.join(os.getcwd(),'voices')) 714 | elif file_type == 'msgvideo': 715 | path = self._mkdir(os.path.join(os.getcwd(),'videos')) 716 | elif file_type == 'icon': 717 | path = self._mkdir(os.path.join(os.getcwd(),'icons')) 718 | else: 719 | path = self._mkdir(os.path.join(os.getcwd(),'tmp')) 720 | path = os.path.join(path,file_name) 721 | with open(path,'wb') as f: 722 | f.write(data) 723 | return path 724 | 725 | 726 | 727 | def _mkdir(self,path): 728 | if not os.path.exists(path): 729 | self._mkdir(os.path.split(path)[0]) 730 | os.mkdir(path) 731 | elif not os.path.isdir(path): 732 | return False 733 | return path 734 | 735 | def _safe_open(self,file_path): 736 | try: 737 | if sys.platform.find('darwin') >= 0: 738 | subprocess.call(['open',file_path]) 739 | elif sys.platform.find('linux') >= 0: 740 | subprocess.call(['xdg-open',file_path]) 741 | else: 742 | os.startfile(file_path) 743 | return True 744 | except: 745 | return False 746 | 747 | specialUsers = ['newsapp', 'fmessage', 'filehelper', 'weibo', 'qqmail', 'fmessage', 'tmessage', 'qmessage', 748 | 'qqsync', 'floatbottle', 'lbsapp', 'shakeapp', 'medianote', 'qqfriend', 'readerapp', 'blogapp', 749 | 'facebookapp', 'masssendapp', 'meishiapp', 'feedsapp', 'voip', 'blogappweixin', 'weixin', 750 | 'brandsessionholder', 'weixinreminder', 'wxid_novlwrv3lqwv11', 'gh_22b87fa7cb3c', 751 | 'officialaccounts', 'notification_messages', 'wxid_novlwrv3lqwv11', 'gh_22b87fa7cb3c', 'wxitil', 752 | 'userexperience_alarm', 'notification_messages'] 753 | 754 | specialUsersDict = { 755 | 'blogapp': '微博阅读', 756 | 'blogappweixin': '微博阅读', 757 | 'brandsessionholder': 'brandsessionholder', 758 | 'facebookapp': 'Facebook', 759 | 'feedsapp': '朋友圈', 760 | 'filehelper': '文件传输助手', 761 | 'floatbottle': '漂流瓶', 762 | 'fmessage': '朋友圈推荐消息', 763 | 'gh_22b87fa7cb3c': 'gh_22b87fa7cb3c', 764 | 'lbsapp': 'lbsapp', 765 | 'masssendapp': '群发助手', 766 | 'medianote': '语音记事本', 767 | 'meishiapp': 'meishiapp', 768 | 'newsapp': '腾讯新闻', 769 | 'notification_messages': 'notification_messages', 770 | 'officialaccounts': 'officialaccounts', 771 | 'qmessage': 'QQ离线助手', 772 | 'qqfriend': 'qqfriend', 773 | 'qqmail': 'QQ邮箱', 774 | 'qqsync': '通讯录同步助手', 775 | 'readerapp': 'readerapp', 776 | 'shakeapp': '摇一摇', 777 | 'tmessage': 'tmessage', 778 | 'userexperience_alarm': '用户体验报警', 779 | 'voip': 'voip', 780 | 'weibo': 'weibo', 781 | 'weixin': '微信', 782 | 'weixinreminder': 'weixinreminder', 783 | 'wxid_novlwrv3lqwv11': 'wxid_novlwrv3lqwv11', 784 | 'wxitil': '微信小管家' 785 | } -------------------------------------------------------------------------------- /WxRobot/wxrobot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | __author__ = 'sharpdeep' 4 | 5 | import multiprocessing 6 | import sys,os 7 | import functools 8 | import math 9 | import time 10 | import inspect 11 | import ssl 12 | import requests 13 | import subprocess 14 | 15 | from WxRobot.webwxapi import WebWxAPI 16 | from WxRobot.reply import * 17 | 18 | MAX_GROUP_NUM = 35 #每组人数 19 | INTERFACE_CALL_INTERVAL = 60 #接口调用时间间隔 20 | 21 | # windows下编码问题修复 22 | # http://blog.csdn.net/heyuxuanzee/article/details/8442718 23 | 24 | 25 | class UnicodeStreamFilter: 26 | 27 | def __init__(self, target): 28 | self.target = target 29 | self.encoding = 'utf-8' 30 | self.errors = 'replace' 31 | self.encode_to = self.target.encoding 32 | 33 | def write(self, s): 34 | s.encode(self.encode_to,self.errors).decode(self.encode_to) 35 | self.target.write(s) 36 | 37 | def flush(self): 38 | self.target.flush() 39 | 40 | if sys.stdout.encoding == 'cp936': 41 | sys.stdout = UnicodeStreamFilter(sys.stdout) 42 | 43 | def catchKeyboardInterrupt(fun): 44 | @functools.wraps(fun) 45 | def wrapper(*args): 46 | try: 47 | return fun(*args) 48 | except KeyboardInterrupt: 49 | print('\n[*] 强制退出程序') 50 | 51 | return wrapper 52 | 53 | class WxRobot(object): 54 | 55 | def __init__(self,api): 56 | self.DEBUG = False 57 | self.api = api 58 | self.commands = dict() 59 | self.addCommand('quit',self._logout,'退出微信') 60 | self.addCommand('help',self._print_help_msg,'显示可用命令') 61 | self.addCommand('delfriend',self._deleted_friends_detected,'清理好友') 62 | 63 | @catchKeyboardInterrupt 64 | def start(self): 65 | ssl._create_default_https_context = ssl._create_unverified_context 66 | print('[*] WxRobot ... start') 67 | self._run('[*] 获取uuid ... ', self.api.getUUID) 68 | print('[*] 生成二维码 ... 成功'); 69 | self.api.genQRCode() 70 | self._run('[*] 扫描二维码登陆 ... ', self.api.waitForLogin) 71 | self._run('[*] 在手机上确认登陆 ... ', self.api.waitForLogin, 0) 72 | self._run('[*] 正在登陆 ... ', self.api.login) 73 | self._run('[*] 初始化微信 ... ', self.api.webwxinit) 74 | self._run('[*] 开启状态通知 ... ', self.api.webwxstatusnotify) 75 | self._run('[*] 获取联系人 ... ', self.api.webwxgetcontact) 76 | print('[*] 共有%d位联系人' % len(self.api.ContactList)) 77 | if self.api.DEBUG: 78 | print(self.api) 79 | 80 | self._print_help_msg() 81 | 82 | self.listenLoop = multiprocessing.Process(target=self.api.listenMsgLoop, 83 | args=(self._onPhoneExit, self._onMsgReceive, self._onPhoneInteract,self._onIdle,self._onSyncError)) 84 | self.listenLoop.start() 85 | while True: 86 | commandline = input('').lower() 87 | if len(commandline.strip()) == 0: 88 | continue 89 | command = commandline.split()[0] 90 | args = commandline.split()[1:] 91 | responses = self.commands.get(command) 92 | 93 | command_exist = False 94 | if responses is not None: 95 | optional_response = [] 96 | for response in responses: 97 | argCount = response[2] 98 | if len(args) >= argCount: 99 | optional_response.append((response[2],response[0])) 100 | 101 | if len(optional_response) > 0: 102 | optional_response.sort() 103 | optional_response[-1][1](*args[:argCount]) 104 | command_exist = True 105 | 106 | 107 | if not command_exist: 108 | print('[*] 系统识别不了命令') 109 | 110 | def _logout(self): 111 | self.listenLoop.terminate() 112 | print('[*] 退出微信') 113 | exit(0) 114 | 115 | def _print_help_msg(self): 116 | msg = '=================================================\n' 117 | 118 | for command,responses in self.commands.items(): 119 | for response in responses: 120 | argCount = response[2] 121 | msg = msg + command + '\t\t-->\t' + response[1] + '\n' 122 | msg += '=================================================' 123 | 124 | print(msg) 125 | 126 | def _deleted_friends_detected(self): 127 | print('[*] 开始检测 ... ') 128 | 129 | groupCount = math.ceil(len(self.api.ContactList)/float(MAX_GROUP_NUM)) 130 | chatroomName = '' 131 | totalDeletedList = [] 132 | totalBlockList = [] 133 | 134 | for group in range(0,groupCount): 135 | userNames = [] 136 | for i in range(0,MAX_GROUP_NUM): 137 | if group * MAX_GROUP_NUM + i >= len(self.api.ContactList): 138 | break 139 | member = self.api.ContactList[group * MAX_GROUP_NUM + i] 140 | userNames.append(member['UserName']) 141 | 142 | if chatroomName == '': 143 | state,errMsg,chatroomName,deletedList,blockedList = self.api.createChatroom(userNames) 144 | self._echo('[*] 新建群聊[%s] ... '%chatroomName) 145 | if state: 146 | self._echo('成功\n') 147 | else: 148 | self._echo('失败[%s]\n'%errMsg) 149 | print('=======退出检测=========') 150 | return 151 | else: 152 | state,errMsg,deletedList,blockedList = self.api.addChatroomMember(chatroomName,userNames) 153 | self._echo('[*] 添加第%s组成员 ... '%str(group+1)) 154 | if state: 155 | self._echo('成功\n') 156 | else: 157 | self._echo('失败[%s]\n'%errMsg) 158 | continue 159 | deletedCount = len(deletedList) 160 | blockedCount = len(blockedList) 161 | if deletedCount > 0: 162 | totalDeletedList += deletedList 163 | if blockedCount > 0: 164 | totalBlockList += blockedList 165 | 166 | self.api.delChatroomMember(chatroomName,userNames) 167 | 168 | if self.DEBUG: 169 | print('[*] 群聊添加以下成员:') 170 | for m in userNames: 171 | print(self.api.getUserRemarkName(m)) 172 | if group != groupCount - 1: 173 | time.sleep(INTERFACE_CALL_INTERVAL) #接口调用间隔时间 174 | 175 | print('=======检测结束=========') 176 | print('由于微信接口限制,本功能将会有30分钟的技能冷却时间') 177 | print('[*] 检测结果如下:') 178 | print('总共有%s位联系人将你删除'%(len(totalDeletedList))) 179 | print('总共有%s位联系人将你拉入黑名单'%(len(totalBlockList))) 180 | 181 | if len(totalDeletedList) > 0: 182 | print('[*] 以下成员将你删除') 183 | for m in totalDeletedList: 184 | print(self.api.getUserRemarkName(m)) 185 | if len(totalBlockList) > 0: 186 | print('[*] 以下成员将你拉入黑名单') 187 | for m in totalBlockList: 188 | print(self.api.getUserRemarkName(m)) 189 | 190 | def _run(self, str, func, *params): 191 | self._echo(str) 192 | ret = func(*params) 193 | if isinstance(ret, tuple): 194 | (status, msg) = ret 195 | else: 196 | (status, msg) = (ret, '') 197 | 198 | if status: 199 | self._echo('成功\n') 200 | else: 201 | self._echo(msg) 202 | print(msg + '\n[退出程序]') 203 | exit(0) 204 | 205 | def _echo(self, str): 206 | sys.stdout.write(str) 207 | sys.stdout.flush() 208 | 209 | def command(self,command,helpMsg = ''): 210 | def wrapper(func): 211 | self.addCommand(command,func,helpMsg) 212 | return func 213 | return wrapper 214 | 215 | def addCommand(self,command,func,helpMsg): 216 | args = inspect.getargspec(func).args 217 | argCount = len(args) 218 | if 'self' in args: 219 | argCount -= 1 220 | if not self.commands.get(command): 221 | self.commands[command] = [] 222 | self.commands[command].append((func,helpMsg,argCount)) 223 | # self.commands[command] = (func,helpMsg,argCount) 224 | 225 | def _onPhoneExit(self): 226 | pass 227 | 228 | def _onMsgReceive(self): 229 | pass 230 | 231 | def _onPhoneInteract(self): 232 | pass 233 | 234 | def _onIdle(self): 235 | pass 236 | 237 | def _onSyncError(self): 238 | pass 239 | 240 | def onMsgReceive(self,func): 241 | self._onMsgReceive = func 242 | return func 243 | 244 | def onPhoneExit(self,func): 245 | self._onPhoneExit = func 246 | return func 247 | 248 | def onPhoneInteract(self,func): 249 | self._onPhoneInteract = func 250 | return func 251 | 252 | def onIdle(self,func): 253 | self._onIdle = func 254 | return func 255 | 256 | def onSyncError(self,func): 257 | self._onSyncError = func 258 | return func 259 | 260 | def open(self,path): 261 | pattern = path[-3:] 262 | if sys.platform.find('win') >= 0: 263 | os.startfile(path) 264 | return 265 | if pattern == 'jpg': 266 | r = self.api._safe_open(path) 267 | if not r: 268 | print('[*] 打开失败') 269 | elif pattern == 'mp3': 270 | subprocess.call(['mpg123',path]) 271 | elif pattern == 'mp4': 272 | subprocess.call(['vlc',path]) 273 | else: 274 | raise ValueError('the file format is wrong') 275 | 276 | def active_turing(self,turing_key = ''): 277 | self.turing_key = turing_key 278 | 279 | def active_simsimi(self,simsimi_key = ''): 280 | self.simsimi_key = simsimi_key 281 | 282 | #http://developer.simsimi.com/ 283 | def simsimi(self,message): #只有免费7天,不推荐 284 | if message.type != 'text': 285 | raise ValueError('auto reply request text message') 286 | text = message.content 287 | key = self.simsimi_key 288 | url = url = 'http://sandbox.api.simsimi.com/request.p?key=%s&lc=ch&ft=0.0&text=%s' % (key, text) 289 | response = requests.get(url) 290 | data = response.json() 291 | reply = data['response'] if data['result'] else '你说什么,风太大了听不清!' 292 | return '[simsimi回复]'+reply 293 | 294 | #http://www.tuling123.com/ 295 | def turing(self,message): 296 | if message.type != 'text': 297 | raise ValueError('auto reply request text message') 298 | text = message.content 299 | userid = message.fromUserId 300 | key = self.turing_key 301 | url = 'http://www.tuling123.com/openapi/api?key=%s&info=%s&userid=%s'%(key,text,userid) 302 | response = requests.get(url) 303 | data = response.json() 304 | code = data['code'] 305 | prefix = '[图灵机器人回复]' 306 | if code == 100000: #文本回复 307 | return TextReply(prefix + data['text']) 308 | elif code == 200000: #链接回复 309 | return TextReply(prefix + data['text'] + ',点击查看:' + data['url']) 310 | elif code == 302000: #新闻回复 311 | pass 312 | elif code == 308000: #菜谱回复 313 | pass 314 | elif code == 40001: #key错误 315 | pass 316 | elif code == 40002: #info为空 317 | pass 318 | elif code == 40004: #当天请求次数已使用完 319 | pass 320 | elif code == 40007: #数据格式异常 321 | pass 322 | else: 323 | return TextReply(prefix + '你说什么,风太大了听不清') 324 | return TextReply(prefix + data['text']) 325 | 326 | 327 | -------------------------------------------------------------------------------- /myrobot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | @author: sharpdeep 6 | @file:myrobot.py 7 | @time: 2016-02-15 14:49 8 | """ 9 | 10 | import sys 11 | from WxRobot.wxrobot import WxRobot 12 | from WxRobot.webwxapi import WebWxAPI 13 | 14 | 15 | api = WebWxAPI() 16 | robot = WxRobot(api) 17 | 18 | robot.active_turing('') 19 | 20 | #文本消息 21 | @api.textMsg 22 | @api.sourceFilter('腾讯新闻',beside=True) 23 | def FiltedTxtMsgHandler(message): 24 | if message.isBatch: #群消息 25 | print('-> %s(%s):%s'%(message.fromMemberName,message.fromUserName,message.content)) 26 | return 27 | else: 28 | print('-> %s:%s'%(message.fromUserName,message.content)) 29 | reply = robot.turing(message) 30 | print('[*] 自动回复:%s'%reply.content) 31 | return reply 32 | 33 | 34 | #图片消息 35 | @api.imageMsg 36 | def ImgeMsgHandler(message): 37 | print('-> %s给%s发了一张图片'%(message.fromUserName,message.toUserName)) 38 | print('[*] %s'%message.content) 39 | robot.open(message.image) 40 | 41 | #位置消息 42 | @api.location 43 | def LocationMsgHandler(message): 44 | print('-> %s给你发了一个位置:[我在%s]'%(message.fromUserName,message.location)) 45 | 46 | #语音消息 47 | @api.voiceMsg 48 | def VoiceMsgHandler(message): 49 | print('-> %s给%s发了一段语音'%(message.fromUserName,message.toUserName)) 50 | robot.open(message.voice) #打开语音消息,linux下需要安装mpg123 51 | 52 | #小视频消息 53 | @api.videoMsg 54 | def VideoMsgHandler(message): 55 | print('-> %s给%s发了一段小视频'%(message.fromUserName,message.toUserName)) 56 | robot.open(message.video) #打开视频消息,linux需要安装vlc 57 | 58 | #名片消息 59 | @api.recommend 60 | def recommendMsgHandler(message): 61 | print('-> %s给%s发了一张名片'%(message.fromUserName,message.toUserName)) 62 | print(message.recommend) 63 | 64 | #撤回消息 65 | @api.revoke 66 | def revokeMsgHandler(message): 67 | print('-> %s向%s撤回了一条消息(%s)'%(message.fromUserName,message.toUserName,message.msgId)) 68 | # return '%s撤回了一条消息%s'%(message.fromMemberName,msg[message.revokeMsg]) 69 | 70 | @robot.onPhoneExit 71 | def onPhoneExit(): 72 | print('[*] 你在手机上登出了微信,再见') 73 | exit(0) 74 | 75 | interactCount = 0 76 | 77 | @robot.onPhoneInteract 78 | def onPhoneInteract(): 79 | global interactCount 80 | interactCount += 1 81 | print('[*] 你在手机上玩了%d次微信被我发现了'%interactCount) 82 | 83 | @robot.command('->','send text msg:send [name] [msg]') 84 | def sendTextMsg(name,text): 85 | print(api.getUserId(name)) 86 | api.sendTextMsg(name,text) 87 | 88 | 89 | if __name__ == '__main__': 90 | robot.start() 91 | -------------------------------------------------------------------------------- /requirement.txt: -------------------------------------------------------------------------------- 1 | qrcode==5.2.2 2 | requests==2.9.1 3 | beautifulsoup4==4.4.1 -------------------------------------------------------------------------------- /screenshot/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharpdeep/WxRobot/7598124f914e21647c45f0749dda71fa05a6e331/screenshot/1.png -------------------------------------------------------------------------------- /screenshot/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharpdeep/WxRobot/7598124f914e21647c45f0749dda71fa05a6e331/screenshot/2.png -------------------------------------------------------------------------------- /screenshot/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharpdeep/WxRobot/7598124f914e21647c45f0749dda71fa05a6e331/screenshot/3.png -------------------------------------------------------------------------------- /screenshot/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharpdeep/WxRobot/7598124f914e21647c45f0749dda71fa05a6e331/screenshot/4.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | @author: sharpdeep 6 | @file:setup.py 7 | @time: 2016-02-24 22:15 8 | """ 9 | import codecs 10 | import os 11 | import sys 12 | 13 | try: 14 | from setuptools import setup 15 | except: 16 | from distutils.core import setup 17 | """ 18 | 打包的用的setup必须引入, 19 | """ 20 | 21 | 22 | def read(fname): 23 | return codecs.open(os.path.join(os.path.dirname(__file__), fname)).read() 24 | 25 | 26 | NAME = "wxrobot" 27 | 28 | PACKAGES = ["WxRobot", ] 29 | 30 | DESCRIPTION = "a python wechat robot framework for personal account" 31 | 32 | LONG_DESCRIPTION = read("README.rst") 33 | 34 | KEYWORDS = "wechat weixin robot wxrobot" 35 | 36 | AUTHOR = "sharpdeep" 37 | 38 | AUTHOR_EMAIL = "cairuishen@gmail.com" 39 | 40 | URL = "https://github.com/sharpdeep/WxRobot" 41 | 42 | VERSION = "0.13" 43 | 44 | LICENSE = "MIT" 45 | 46 | setup( 47 | name=NAME, 48 | version=VERSION, 49 | description=DESCRIPTION, 50 | long_description=LONG_DESCRIPTION, 51 | install_requires=['qrcode','requests','beautifulsoup4'], 52 | classifiers=[ 53 | 'License :: OSI Approved :: MIT License', 54 | 'Programming Language :: Python :: 3.4', 55 | 'Intended Audience :: Developers', 56 | 'Operating System :: MacOS', 57 | 'Operating System :: POSIX', 58 | 'Operating System :: POSIX :: Linux', 59 | 'Topic :: Software Development :: Libraries', 60 | 'Topic :: Software Development :: Libraries :: Python Modules', 61 | 'Topic :: Utilities', 62 | ], 63 | keywords=KEYWORDS, 64 | author=AUTHOR, 65 | author_email=AUTHOR_EMAIL, 66 | url=URL, 67 | license=LICENSE, 68 | packages=PACKAGES, 69 | include_package_data=True, 70 | zip_safe=True, 71 | ) 72 | --------------------------------------------------------------------------------