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