diff --git a/README.md b/README.md index f73daa4..0d30a0b 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,11 @@ - [ ] 红包 - [ ] 转账 +- [x] 消息发送 + - [x] 文本 + - [x] 图片 + - [x] 文件 + Web微信协议参考资料: @@ -58,7 +63,7 @@ pip install Pillow ### 2.1 代码 -以下的代码对所有来自好友的文本消息回复 *hi* , 并不断向好友 *tb* 发送 *schedule* 。 +以下的代码对所有来自好友的文本消息回复文本消息 *hi* 、图片消息 *1.png* 以及文件消息 *1.png* , 并不断向好友 *tb* 发送文本 *schedule* 。 `handle_msg_all` 函数用于处理收到的每条消息,而 `schedule` 函数可以做一些任务性的工作(例如不断向好友推送信息或者一些定时任务)。 @@ -73,6 +78,8 @@ class MyWXBot(WXBot): def handle_msg_all(self, msg): if msg['msg_type_id'] == 4 and msg['content']['type'] == 0: self.send_msg_by_uid(u'hi', msg['user']['id']) + self.send_img_msg_by_uid("img/1.png", msg['user']['id']) + self.send_file_msg_by_uid("img/1.png", msg['user']['id']) def schedule(self): self.send_msg(u'tb', u'schedule') @@ -195,6 +202,9 @@ python test.py | `get_voice(msgid)` | 获取语音消息并保存到本地文件 ***voice_[msgid].mp3*** , `msgid` 为消息id(Web微信数据) | | `get_contact_name(uid)` | 获取微信id对应的名称,返回一个可能包含 `remark_name` (备注名), `nickname` (昵称), `display_name` (群名称)的字典| | `send_msg_by_uid(word, dst)` | 向好友发送消息,`word` 为消息字符串,`dst` 为好友用户id(Web微信数据) | +| `send_img_msg_by_uid(fpath, dst)` | 向好友发送图片消息,`fpath` 为本地图片文件路径,`dst` 为好友用户id(Web微信数据) | +| `send_file_msg_by_uid(fpath, dst)` | 向好友发送文件消息,`fpath` 为本地文件路径,`dst` 为好友用户id(Web微信数据) | +| `send_msg_by_uid(word, dst)` | 向好友发送消息,`word` 为消息字符串,`dst` 为好友用户id(Web微信数据) | | `send_msg(name, word, isfile)` | 向好友发送消息,`name` 为好友的备注名或者好友微信号, `isfile`为 `False` 时 `word` 为消息,`isfile` 为 `True` 时 `word` 为文件路径(此时向好友发送文件里的每一行),此方法在有重名好友时会有问题,因此更推荐使用 `send_msg_by_uid(word, dst)` | | `is_contact(uid)` | 判断id为 `uid` 的账号是否是本帐号的好友,返回 `True` (是)或 `False` (不是) | | `is_public(uid)` | 判断id为 `uid` 的账号是否是本帐号所关注的公众号,返回 `True` (是)或 `False` (不是) | @@ -245,6 +255,18 @@ python test.py python bot.py ``` -## 6 帮助项目 +## 6 类似项目 + +[feit/Weixinbot](https://github.com/feit/Weixinbot) Nodejs 封装网页版微信的接口,可编程控制微信消息 + +[littlecodersh/ItChat](https://github.com/littlecodersh/ItChat) 微信个人号接口、微信机器人及命令行微信,Command line talks through Wechat + +[Urinx/WeixinBot](https://github.com/Urinx/WeixinBot) 网页版微信API,包含终端版微信及微信机器人 + +[zixia/wechaty](https://github.com/zixia/wechaty) Wechaty is wechat for bot in Javascript(ES6). It's a Personal Account Robot Framework/Library. + +## 7 交流讨论 + +问题可以直接开 **issue** -欢迎对本项目提意见、贡献代码,参考: [如何帮助项目](https://github.com/liuwons/wxBot/wiki/How-to-contribute) +**QQ** 交流群: **429134510** diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bot.py b/bot.py index 7b8ae94..fe755c1 100644 --- a/bot.py +++ b/bot.py @@ -33,6 +33,10 @@ def tuling_auto_reply(self, uid, msg): result = respond['text'].replace('
', ' ') elif respond['code'] == 200000: result = respond['url'] + elif respond['code'] == 302000: + for k in respond['list']: + result = result + u"【" + k['source'] + u"】 " +\ + k['article'] + "\t" + k['detailurl'] + "\n" else: result = respond['text'].replace('
', ' ') diff --git a/test.py b/test.py index 86a9a56..c81a60f 100644 --- a/test.py +++ b/test.py @@ -8,6 +8,8 @@ class MyWXBot(WXBot): def handle_msg_all(self, msg): if msg['msg_type_id'] == 4 and msg['content']['type'] == 0: self.send_msg_by_uid(u'hi', msg['user']['id']) + #self.send_img_msg_by_uid("img/1.png", msg['user']['id']) + #self.send_file_msg_by_uid("img/1.png", msg['user']['id']) ''' def schedule(self): self.send_msg(u'张三', u'测试') diff --git a/wxbot.py b/wxbot.py index de9f268..3949ac2 100644 --- a/wxbot.py +++ b/wxbot.py @@ -3,28 +3,35 @@ import os import sys +import traceback import webbrowser import pyqrcode import requests +import mimetypes import json import xml.dom.minidom import urllib +import urlparse import time import re import random +from traceback import format_exc from requests.exceptions import ConnectionError, ReadTimeout +from Queue import Queue import HTMLParser +import threading +import pickle UNKONWN = 'unkonwn' SUCCESS = '200' -SCANED = '201' +SCANED = '201' TIMEOUT = '408' -def show_image(file): +def show_image(file_path): """ 跨平台显示图片文件 - :param file: 图片文件路径 + :param file_path: 图片文件路径 """ if sys.version_info >= (3, 3): from shlex import quote @@ -32,10 +39,24 @@ def show_image(file): from pipes import quote if sys.platform == "darwin": - command = "open -a /Applications/Preview.app %s&" % quote(file) + command = "open -a /Applications/Preview.app %s&" % quote(file_path) os.system(command) else: - webbrowser.open(file) + webbrowser.open(os.path.join(os.getcwd(),'temp',file_path)) + + +class SafeSession(requests.Session): + def request(self, method, url, params=None, data=None, headers=None, cookies=None, files=None, auth=None, + timeout=None, allow_redirects=True, proxies=None, hooks=None, stream=None, verify=None, cert=None, + json=None): + for i in range(3): + try: + return super(SafeSession, self).request(method, url, params, data, headers, cookies, files, auth, + timeout, + allow_redirects, proxies, hooks, stream, verify, cert, json) + except Exception as e: + print e.message, traceback.format_exc() + continue class WXBot: @@ -43,6 +64,7 @@ class WXBot: def __init__(self): self.DEBUG = False + self.SCHEDULE_INTV = 5 self.uuid = '' self.base_uri = '' self.redirect_uri = '' @@ -56,7 +78,12 @@ def __init__(self): self.sync_key = [] self.sync_host = '' - self.session = requests.Session() + #文件缓存目录 + self.temp_pwd = os.path.join(os.getcwd(), 'temp') + if os.path.exists(self.temp_pwd) == False: + os.makedirs(self.temp_pwd) + + self.session = SafeSession() self.session.headers.update({'User-Agent': 'Mozilla/5.0 (X11; Linux i686; U;) Gecko/20070322 Kazehakase/0.4.5'}) self.conf = {'qr': 'png'} @@ -77,6 +104,13 @@ def __init__(self): self.special_list = [] # 特殊账号列表 self.encry_chat_room_id_list = [] # 存储群聊的EncryChatRoomId,获取群内成员头像时需要用到 + self.file_index = 0 # 文件上传序号 + + self.msg_queue = Queue() # 消息处理队列,handle_msg_all是从此队列拿消息的 + self.msg_thread = None + self.schedule_thread = None + self.inner_proc_thread = None + @staticmethod def to_unicode(string, encoding='utf-8'): """ @@ -99,7 +133,7 @@ def get_contact(self): r = self.session.post(url, data='{}') r.encoding = 'utf-8' if self.DEBUG: - with open('contacts.json', 'w') as f: + with open(os.path.join(self.temp_pwd,'contacts.json'), 'w') as f: f.write(r.text.encode('utf-8')) dic = json.loads(r.text) self.member_list = dic['MemberList'] @@ -130,7 +164,6 @@ def get_contact(self): self.account_info['normal_member'][contact['UserName']] = {'type': 'group', 'info': contact} elif contact['UserName'] == self.my_account['UserName']: # 自己 self.account_info['normal_member'][contact['UserName']] = {'type': 'self', 'info': contact} - pass else: self.contact_list.append(contact) self.account_info['normal_member'][contact['UserName']] = {'type': 'contact', 'info': contact} @@ -142,22 +175,6 @@ def get_contact(self): if member['UserName'] not in self.account_info: self.account_info['group_member'][member['UserName']] = \ {'type': 'group_member', 'info': member, 'group': group} - - if self.DEBUG: - with open('contact_list.json', 'w') as f: - f.write(json.dumps(self.contact_list)) - with open('special_list.json', 'w') as f: - f.write(json.dumps(self.special_list)) - with open('group_list.json', 'w') as f: - f.write(json.dumps(self.group_list)) - with open('public_list.json', 'w') as f: - f.write(json.dumps(self.public_list)) - with open('member_list.json', 'w') as f: - f.write(json.dumps(self.member_list)) - with open('group_users.json', 'w') as f: - f.write(json.dumps(self.group_members)) - with open('account_info.json', 'w') as f: - f.write(json.dumps(self.account_info)) return True def batch_get_group_members(self): @@ -204,24 +221,10 @@ def get_group_member_name(self, gid, uid): return None def get_contact_info(self, uid): - if uid in self.account_info['normal_member']: - return self.account_info['normal_member'][uid] - else: - return None + return self.account_info['normal_member'].get(uid) def get_group_member_info(self, uid): - if uid in self.account_info['group_member']: - return self.account_info['group_member'][uid] - else: - return None - - def get_group_member_info(self, uid, gid): - if gid not in self.group_members: - return None - for member in self.group_members[gid]: - if member['UserName'] == uid: - return {'type': 'group_member', 'info': member} - return None + return self.account_info['group_member'].get(uid) def get_contact_name(self, uid): info = self.get_contact_info(uid) @@ -240,40 +243,6 @@ def get_contact_name(self, uid): else: return name - def get_group_member_name(self, uid): - info = self.get_group_member_info(uid) - if info is None: - return None - info = info['info'] - name = {} - if 'RemarkName' in info and info['RemarkName']: - name['remark_name'] = info['RemarkName'] - if 'NickName' in info and info['NickName']: - name['nickname'] = info['NickName'] - if 'DisplayName' in info and info['DisplayName']: - name['display_name'] = info['DisplayName'] - if len(name) == 0: - return None - else: - return name - - def get_group_member_name(self, uid, gid): - info = self.get_group_member_info(uid, gid) - if info is None: - return None - info = info['info'] - name = {} - if 'RemarkName' in info and info['RemarkName']: - name['remark_name'] = info['RemarkName'] - if 'NickName' in info and info['NickName']: - name['nickname'] = info['NickName'] - if 'DisplayName' in info and info['DisplayName']: - name['display_name'] = info['DisplayName'] - if len(name) == 0: - return None - else: - return name - @staticmethod def get_contact_prefer_name(name): if name is None: @@ -361,7 +330,7 @@ def proc_at_info(msg): str_msg = '' infos = [] if len(segs) > 1: - for i in range(0, len(segs)-1): + for i in range(0, len(segs) - 1): segs[i] += u'\u2005' pm = re.search(u'@.*\u2005', segs[i]).group() if pm: @@ -421,7 +390,7 @@ def extract_msg_content(self, msg_type_id, msg): uid = uid[:-1] name = self.get_contact_prefer_name(self.get_contact_name(uid)) if not name: - name = self.get_group_member_prefer_name(self.get_group_member_name(uid, msg['FromUserName'])) + name = self.get_group_member_prefer_name(self.get_group_member_name(msg['FromUserName'], uid)) if not name: name = 'unknown' msg_content['user'] = {'id': uid, 'name': name} @@ -461,15 +430,22 @@ def extract_msg_content(self, msg_type_id, msg): elif mtype == 3: msg_content['type'] = 3 msg_content['data'] = self.get_msg_img_url(msg_id) + msg_content['img'] = self.session.get(msg_content['data']).content.encode('hex') if self.DEBUG: image = self.get_msg_img(msg_id) print ' %s[Image] %s' % (msg_prefix, image) elif mtype == 34: msg_content['type'] = 4 msg_content['data'] = self.get_voice_url(msg_id) + msg_content['voice'] = self.session.get(msg_content['data']).content.encode('hex') if self.DEBUG: voice = self.get_voice(msg_id) print ' %s[Voice] %s' % (msg_prefix, voice) + elif mtype == 37: + msg_content['type'] = 37 + msg_content['data'] = msg['RecommendInfo'] + if self.DEBUG: + print ' %s[useradd] %s' % (msg_prefix,msg['RecommendInfo']['NickName']) elif mtype == 42: msg_content['type'] = 5 info = msg['RecommendInfo'] @@ -493,7 +469,6 @@ def extract_msg_content(self, msg_type_id, msg): print ' %s[Animation] %s' % (msg_prefix, msg_content['data']) elif mtype == 49: msg_content['type'] = 7 - app_msg_type = '' if msg['AppMsgType'] == 3: app_msg_type = 'music' elif msg['AppMsgType'] == 5: @@ -506,7 +481,9 @@ def extract_msg_content(self, msg_type_id, msg): 'title': msg['FileName'], 'desc': self.search_content('des', content, 'xml'), 'url': msg['Url'], - 'from': self.search_content('appname', content, 'xml')} + 'from': self.search_content('appname', content, 'xml'), + 'content': msg.get('Content') # 有的公众号会发一次性3 4条链接一个大图,如果只url那只能获取第一条,content里面有所有的链接 + } if self.DEBUG: print ' %s[Share] %s' % (msg_prefix, app_msg_type) print ' --------------------------' @@ -514,6 +491,7 @@ def extract_msg_content(self, msg_type_id, msg): print ' | desc: %s' % self.search_content('des', content, 'xml') print ' | link: %s' % msg['Url'] print ' | from: %s' % self.search_content('appname', content, 'xml') + print ' | content: %s' % (msg.get('content')[:20] if msg.get('content') else "unknown") print ' --------------------------' elif mtype == 62: @@ -558,11 +536,21 @@ def handle_msg(self, r): :param r: 原始微信消息 """ for msg in r['AddMsgList']: - msg_type_id = 99 user = {'id': msg['FromUserName'], 'name': 'unknown'} if msg['MsgType'] == 51: # init message msg_type_id = 0 user['name'] = 'system' + elif msg['MsgType'] == 37: # friend request + msg_type_id = 37 + pass + # content = msg['Content'] + # username = content[content.index('fromusername='): content.index('encryptusername')] + # username = username[username.index('"') + 1: username.rindex('"')] + # print u'[Friend Request]' + # print u' Nickname:' + msg['RecommendInfo']['NickName'] + # print u' 附加消息:'+msg['RecommendInfo']['Content'] + # # print u'Ticket:'+msg['RecommendInfo']['Ticket'] # Ticket添加好友时要用 + # print u' 微信号:'+username #未设置微信号的 腾讯会自动生成一段微信ID 但是无法通过搜索 搜索到此人 elif msg['FromUserName'] == self.my_account['UserName']: # Self msg_type_id = 1 user['name'] = 'self' @@ -589,14 +577,14 @@ def handle_msg(self, r): user['name'] = HTMLParser.HTMLParser().unescape(user['name']) if self.DEBUG and msg_type_id != 0: - print '[MSG] %s:' % user['name'] + print u'[MSG] %s:' % user['name'] content = self.extract_msg_content(msg_type_id, msg) message = {'msg_type_id': msg_type_id, 'msg_id': msg['MsgId'], 'content': content, 'to_user_id': msg['ToUserName'], 'user': user} - self.handle_msg_all(message) + self.msg_queue.put(message) def schedule(self): """ @@ -606,7 +594,6 @@ def schedule(self): pass def proc_msg(self): - self.test_sync_check() while True: check_time = time.time() try: @@ -625,6 +612,10 @@ def proc_msg(self): r = self.sync() if r is not None: self.handle_msg(r) + elif selector == '4': # 通讯录更新 + r = self.sync() + if r is not None: + self.get_contact() elif selector == '6': # 可能是红包 r = self.sync() if r is not None: @@ -642,13 +633,215 @@ def proc_msg(self): self.handle_msg(r) else: print '[DEBUG] sync_check:', retcode, selector - self.schedule() except: print '[ERROR] Except in proc_msg' + print format_exc() check_time = time.time() - check_time if check_time < 0.8: time.sleep(1 - check_time) + def msg_thread_proc(self): + print '[INFO] Msg thread start' + while True: + if not self.msg_queue.empty(): + msg = self.msg_queue.get() + self.handle_msg_all(msg) + else: + time.sleep(0.1) + + def schedule_thread_proc(self): + print '[INFO] Schedule thread start' + while True: + check_time = time.time() + self.schedule() + check_time = time.time() - check_time + if check_time < self.SCHEDULE_INTV: + time.sleep(self.SCHEDULE_INTV - check_time) + + def login_and_init_with_restore(self): + return self.restore_login_result() + + def login_and_init_with_qr(self): + self.get_uuid() + self.gen_qr_code(os.path.join(self.temp_pwd, 'wxqr.png')) + print '[INFO] Please use WeChat to scan the QR code .' + + result = self.wait4login() + if result != SUCCESS: + print '[ERROR] Web WeChat login failed. failed code=%s' % (result,) + return False + + if self.login(): + print '[INFO] Web WeChat login succeed .' + else: + print '[ERROR] Web WeChat login failed .' + return False + if self.init(): + print '[INFO] Web WeChat init succeed .' + else: + print '[INFO] Web WeChat init failed' + return False + self.status_notify() + self.get_contact() + self.test_sync_check() + self.save_login_result() + return True + + def run_inner(self): + print '[INFO] Get %d contacts' % len(self.contact_list) + print '[INFO] Start to process messages .' + self.proc_msg() + + def run(self): + if not self.login_and_init_with_restore(): + print '[INFO] Restore login failed !' + if not self.login_and_init_with_qr(): + print '[ERROR] Login and init failed !' + return + else: + print '[INFO Restore login succeed .' + + self.msg_thread = threading.Thread(target=self.msg_thread_proc) + self.msg_thread.setDaemon(True) + self.msg_thread.start() + + self.schedule_thread = threading.Thread(target=self.schedule_thread_proc) + self.schedule_thread.setDaemon(True) + self.schedule_thread.start() + + self.inner_proc_thread = threading.Thread(target=self.run_inner) + self.inner_proc_thread.setDaemon(True) + self.inner_proc_thread.start() + self.inner_proc_thread.join() + + while True: + if self.login_and_init_with_restore(): + self.inner_proc_thread = threading.Thread(target=self.run_inner) + self.inner_proc_thread.setDaemon(True) + self.inner_proc_thread.start() + self.inner_proc_thread.join() + else: + print '[ERROR] Try to restore from file failed !' + return + + def apply_useradd_requests(self,RecommendInfo): + url = self.base_uri + '/webwxverifyuser?r='+str(int(time.time()))+'&lang=zh_CN' + params = { + "BaseRequest": self.base_request, + "Opcode": 3, + "VerifyUserListSize": 1, + "VerifyUserList": [ + { + "Value": RecommendInfo['UserName'], + "VerifyUserTicket": RecommendInfo['Ticket'] } + ], + "VerifyContent": "", + "SceneListCount": 1, + "SceneList": [ + 33 + ], + "skey": self.skey + } + headers = {'content-type': 'application/json; charset=UTF-8'} + data = json.dumps(params, ensure_ascii=False).encode('utf8') + try: + r = self.session.post(url, data=data, headers=headers) + except (ConnectionError, ReadTimeout): + return False + dic = r.json() + return dic['BaseResponse']['Ret'] == 0 + + def add_groupuser_to_friend_by_uid(self, uid, VerifyContent): + """ + 主动向群内人员打招呼,提交添加好友请求 + uid-群内人员得uid VerifyContent-好友招呼内容 + 慎用此接口!封号后果自负!慎用此接口!封号后果自负!慎用此接口!封号后果自负! + """ + if self.is_contact(uid): + return True + url = self.base_uri + '/webwxverifyuser?r='+str(int(time.time()))+'&lang=zh_CN' + params ={ + "BaseRequest": self.base_request, + "Opcode": 2, + "VerifyUserListSize": 1, + "VerifyUserList": [ + { + "Value": uid, + "VerifyUserTicket": "" + } + ], + "VerifyContent": VerifyContent, + "SceneListCount": 1, + "SceneList": [ + 33 + ], + "skey": self.skey + } + headers = {'content-type': 'application/json; charset=UTF-8'} + data = json.dumps(params, ensure_ascii=False).encode('utf8') + try: + r = self.session.post(url, data=data, headers=headers) + except (ConnectionError, ReadTimeout): + return False + dic = r.json() + return dic['BaseResponse']['Ret'] == 0 + + def add_friend_to_group(self,uid,group_name): + """ + 将好友加入到群聊中 + """ + gid = '' + #通过群名获取群id,群没保存到通讯录中的话无法添加哦 + for group in self.group_list: + if group['NickName'] == group_name: + gid = group['UserName'] + if gid == '': + return False + #通过群id判断uid是否在群中 + for user in self.group_members[gid]: + if user['UserName'] == uid: + #已经在群里面了,不用加了 + return True + url = self.base_uri + '/webwxupdatechatroom?fun=addmember&pass_ticket=%s' % self.pass_ticket + params ={ + "AddMemberList": uid, + "ChatRoomName": gid, + "BaseRequest": self.base_request + } + headers = {'content-type': 'application/json; charset=UTF-8'} + data = json.dumps(params, ensure_ascii=False).encode('utf8') + try: + r = self.session.post(url, data=data, headers=headers) + except (ConnectionError, ReadTimeout): + return False + dic = r.json() + return dic['BaseResponse']['Ret'] == 0 + + def delete_user_from_group(self,uname,gid): + """ + 将群用户从群中剔除,只有群管理员有权限 + """ + uid = "" + for user in self.group_members[gid]: + if user['NickName'] == uname: + uid = user['UserName'] + if uid == "": + return False + url = self.base_uri + '/webwxupdatechatroom?fun=delmember&pass_ticket=%s' % self.pass_ticket + params ={ + "DelMemberList": uid, + "ChatRoomName": gid, + "BaseRequest": self.base_request + } + headers = {'content-type': 'application/json; charset=UTF-8'} + data = json.dumps(params, ensure_ascii=False).encode('utf8') + try: + r = self.session.post(url, data=data, headers=headers) + except (ConnectionError, ReadTimeout): + return False + dic = r.json() + return dic['BaseResponse']['Ret'] == 0 + def send_msg_by_uid(self, word, dst='filehelper'): url = self.base_uri + '/webwxsendmsg?pass_ticket=%s' % self.pass_ticket msg_id = str(int(time.time() * 1000)) + str(random.random())[:5].replace('.', '') @@ -673,6 +866,100 @@ def send_msg_by_uid(self, word, dst='filehelper'): dic = r.json() return dic['BaseResponse']['Ret'] == 0 + def upload_media(self, fpath, is_img=False): + if not os.path.exists(fpath): + print '[ERROR] File not exists.' + return None + url_1 = 'https://file.wx2.qq.com/cgi-bin/mmwebwx-bin/webwxuploadmedia?f=json' + url_2 = 'https://file2.wx2.qq.com/cgi-bin/mmwebwx-bin/webwxuploadmedia?f=json' + flen = str(os.path.getsize(fpath)) + ftype = mimetypes.guess_type(fpath)[0] or 'application/octet-stream' + files = { + 'id': (None, 'WU_FILE_%s' % str(self.file_index)), + 'name': (None, os.path.basename(fpath)), + 'type': (None, ftype), + 'lastModifiedDate': (None, time.strftime('%m/%d/%Y, %H:%M:%S GMT+0800 (CST)')), + 'size': (None, flen), + 'mediatype': (None, 'pic' if is_img else 'doc'), + 'uploadmediarequest': (None, json.dumps({ + 'BaseRequest': self.base_request, + 'ClientMediaId': int(time.time()), + 'TotalLen': flen, + 'StartPos': 0, + 'DataLen': flen, + 'MediaType': 4, + })), + 'webwx_data_ticket': (None, self.session.cookies['webwx_data_ticket']), + 'pass_ticket': (None, self.pass_ticket), + 'filename': (os.path.basename(fpath), open(fpath, 'rb'),ftype.split('/')[1]), + } + self.file_index += 1 + try: + r = self.session.post(url_1, files=files) + if json.loads(r.text)['BaseResponse']['Ret'] != 0: + # 当file返回值不为0时则为上传失败,尝试第二服务器上传 + r = self.session.post(url_2, files=files) + if json.loads(r.text)['BaseResponse']['Ret'] != 0: + print '[ERROR] Upload media failure.' + return None + mid = json.loads(r.text)['MediaId'] + return mid + except Exception,e: + return None + + def send_file_msg_by_uid(self, fpath, uid): + mid = self.upload_media(fpath) + if mid is None or not mid: + return False + url = self.base_uri + '/webwxsendappmsg?fun=async&f=json&pass_ticket=' + self.pass_ticket + msg_id = str(int(time.time() * 1000)) + str(random.random())[:5].replace('.', '') + data = { + 'BaseRequest': self.base_request, + 'Msg': { + 'Type': 6, + 'Content': ("%s6%s%s%s" % (os.path.basename(fpath).encode('utf-8'), str(os.path.getsize(fpath)), mid, fpath.split('.')[-1])).encode('utf8'), + 'FromUserName': self.my_account['UserName'], + 'ToUserName': uid, + 'LocalID': msg_id, + 'ClientMsgId': msg_id, }, } + try: + r = self.session.post(url, data=json.dumps(data)) + res = json.loads(r.text) + if res['BaseResponse']['Ret'] == 0: + return True + else: + return False + except Exception,e: + return False + + def send_img_msg_by_uid(self, fpath, uid): + mid = self.upload_media(fpath, is_img=True) + if mid is None: + return False + url = self.base_uri + '/webwxsendmsgimg?fun=async&f=json' + data = { + 'BaseRequest': self.base_request, + 'Msg': { + 'Type': 3, + 'MediaId': mid, + 'FromUserName': self.my_account['UserName'], + 'ToUserName': uid, + 'LocalID': str(time.time() * 1e7), + 'ClientMsgId': str(time.time() * 1e7), }, } + if fpath[-4:] == '.gif': + url = self.base_uri + '/webwxsendemoticon?fun=sys' + data['Msg']['Type'] = 47 + data['Msg']['EmojiFlag'] = 2 + try: + r = self.session.post(url, data=json.dumps(data)) + res = json.loads(r.text) + if res['BaseResponse']['Ret'] == 0: + return True + else: + return False + except Exception,e: + return False + def get_user_id(self, name): if name == '': return None @@ -684,6 +971,14 @@ def get_user_id(self, name): return contact['UserName'] elif 'DisplayName' in contact and contact['DisplayName'] == name: return contact['UserName'] + for group in self.group_list: + if 'RemarkName' in group and group['RemarkName'] == name: + return group['UserName'] + if 'NickName' in group and group['NickName'] == name: + return group['UserName'] + if 'DisplayName' in group and group['DisplayName'] == name: + return group['UserName'] + return '' def send_msg(self, name, word, isfile=False): @@ -724,33 +1019,6 @@ def search_content(key, content, fmat='attr'): return pm.group(1) return 'unknown' - def run(self): - self.get_uuid() - self.gen_qr_code('qr.png') - print '[INFO] Please use WeChat to scan the QR code .' - - result = self.wait4login() - if result != SUCCESS: - print '[ERROR] Web WeChat login failed. failed code=%s'%(result, ) - return - - if self.login(): - print '[INFO] Web WeChat login succeed .' - else: - print '[ERROR] Web WeChat login failed .' - return - - if self.init(): - print '[INFO] Web WeChat init succeed .' - else: - print '[INFO] Web WeChat init failed' - return - self.status_notify() - self.get_contact() - print '[INFO] Get %d contacts' % len(self.contact_list) - print '[INFO] Start to process messages .' - self.proc_msg() - def get_uuid(self): url = 'https://login.weixin.qq.com/jslogin' params = { @@ -820,20 +1088,85 @@ def wait4login(self): self.base_uri = redirect_uri[:redirect_uri.rfind('/')] return code elif code == TIMEOUT: - print '[ERROR] WeChat login timeout. retry in %s secs later...'%(try_later_secs, ) + print '[ERROR] WeChat login timeout. retry in %s secs later...' % (try_later_secs,) - tip = 1 # 重置 + tip = 1 # 重置 retry_time -= 1 time.sleep(try_later_secs) else: print ('[ERROR] WeChat login exception return_code=%s. retry in %s secs later...' % - (code, try_later_secs)) + (code, try_later_secs)) tip = 1 retry_time -= 1 time.sleep(try_later_secs) return code + def save_dict_to_file(self, dic, fname): + with open(os.path.join(self.temp_pwd, fname), 'w') as f: + f.write(json.dumps(dic)) + + def restore_dict_from_file(self, fname): + with open(os.path.join(self.temp_pwd, fname), 'r') as f: + fstr = f.read() + return json.loads(fstr) + + def save_login_result(self): + result = {} + result['skey'] = self.skey + result['sid'] = self.sid + result['uin'] = self.uin + result['pass_ticket'] = self.pass_ticket + result['base_request'] = self.base_request + result['redirect_uri'] = self.redirect_uri + result['base_uri'] = self.base_uri + result['sync_key'] = self.sync_key + result['sync_key_str'] = self.sync_key_str + result['my_account'] = self.my_account + result['sync_host'] = self.sync_host + with open(os.path.join(self.temp_pwd, "login_result.json"), 'w') as f: + f.write(json.dumps(result)) + + self.save_dict_to_file(self.contact_list, 'contact_list.json') + self.save_dict_to_file(self.special_list, 'special_list.json') + self.save_dict_to_file(self.group_list, 'group_list.json') + self.save_dict_to_file(self.public_list, 'public_list.json') + self.save_dict_to_file(self.member_list, 'member_list.json') + self.save_dict_to_file(self.group_members, 'group_members.json') + self.save_dict_to_file(self.account_info, 'account_info.json') + self.save_dict_to_file(self.encry_chat_room_id_list, 'encry_chat_room_id_list.json') + pickle.dump(self.session, open(os.path.join(self.temp_pwd, "session.json"), "w")) + + def restore_login_result(self): + try: + with open(os.path.join(self.temp_pwd, "login_result.json"), 'r') as f: + login_str = f.read() + result = json.loads(login_str) + self.skey = result['skey'] + self.sid = result['sid'] + self.uin = result['uin'] + self.pass_ticket = result['pass_ticket'] + self.base_request = result['base_request'] + self.redirect_uri = result['redirect_uri'] + self.base_uri = result['base_uri'] + self.sync_key = result['sync_key'] + self.sync_key_str = result['sync_key_str'] + self.my_account = result['my_account'] + self.sync_host = result['sync_host'] + + self.contact_list = self.restore_dict_from_file('contact_list.json') + self.special_list = self.restore_dict_from_file('special_list.json') + self.group_list = self.restore_dict_from_file('group_list.json') + self.public_list = self.restore_dict_from_file('public_list.json') + self.member_list = self.restore_dict_from_file('member_list.json') + self.group_members = self.restore_dict_from_file('group_members.json') + self.account_info = self.restore_dict_from_file('account_info.json') + self.encry_chat_room_id_list = self.restore_dict_from_file('encry_chat_room_id_list.json') + self.session = pickle.load(open(os.path.join(self.temp_pwd, "session.json"), "r")) + return True + except Exception, e: + print format_exc() + def login(self): if len(self.redirect_uri) < 4: print '[ERROR] Login failed due to network problem, please try again.' @@ -912,7 +1245,8 @@ def sync_check(self): 'synckey': self.sync_key_str, '_': int(time.time()), } - url = 'https://' + self.sync_host + '.weixin.qq.com/cgi-bin/mmwebwx-bin/synccheck?' + urllib.urlencode(params) + domain = urlparse.urlparse(self.redirect_uri).netloc + url = 'https://' + self.sync_host + '.' + domain + '/cgi-bin/mmwebwx-bin/synccheck?' + urllib.urlencode(params) try: r = self.session.get(url, timeout=60) r.encoding = 'utf-8' @@ -953,11 +1287,12 @@ def get_icon(self, uid, gid=None): if gid is None: url = self.base_uri + '/webwxgeticon?username=%s&skey=%s' % (uid, self.skey) else: - url = self.base_uri + '/webwxgeticon?username=%s&skey=%s&chatroomid=%s' % (uid, self.skey, self.encry_chat_room_id_list[gid]) + url = self.base_uri + '/webwxgeticon?username=%s&skey=%s&chatroomid=%s' % ( + uid, self.skey, self.encry_chat_room_id_list[gid]) r = self.session.get(url) data = r.content fn = 'icon_' + uid + '.jpg' - with open(fn, 'wb') as f: + with open(os.path.join(self.temp_pwd,fn), 'wb') as f: f.write(data) return fn @@ -970,7 +1305,7 @@ def get_head_img(self, uid): r = self.session.get(url) data = r.content fn = 'head_' + uid + '.jpg' - with open(fn, 'wb') as f: + with open(os.path.join(self.temp_pwd,fn), 'wb') as f: f.write(data) return fn @@ -987,7 +1322,7 @@ def get_msg_img(self, msgid): r = self.session.get(url) data = r.content fn = 'img_' + msgid + '.jpg' - with open(fn, 'wb') as f: + with open(os.path.join(self.temp_pwd,fn), 'wb') as f: f.write(data) return fn @@ -1004,6 +1339,23 @@ def get_voice(self, msgid): r = self.session.get(url) data = r.content fn = 'voice_' + msgid + '.mp3' - with open(fn, 'wb') as f: + with open(os.path.join(self.temp_pwd,fn), 'wb') as f: f.write(data) return fn + + def set_remark_name(self, uid, name): # 设置联系人的备注名 + url = self.base_uri + '/webwxoplog?lang=zh_CN&pass_ticket=%s' % self.pass_ticket + remark_name = self.to_unicode(name) + params = { + 'BaseRequest': self.base_request, + 'CmdId': 2, + 'RemarkName': remark_name, + 'UserName': uid + } + try: + r = self.session.post(url, data=json.dumps(params), timeout=60) + r.encoding = 'utf-8' + dic = json.loads(r.text) + return dic['BaseResponse']['ErrMsg'] + except: + return None