#!/usr/bin/env python
# coding: utf-8
#===================================================
from utils import *
from wechat_apis import WXAPI
from config import ConfigManager
from config import Constant
from config import Log
#---------------------------------------------------
import json
import re
import sys
import os
import time
import random
from collections import defaultdict
from datetime import timedelta
import traceback
import Queue
import threading
#===================================================
class WeChat(WXAPI):
def __str__(self):
description = \
"=========================\n" + \
"[#] Web WeChat\n" + \
"[#] UUID: " + self.uuid + "\n" + \
"[#] Uin: " + str(self.uin) + "\n" + \
"[#] Sid: " + self.sid + "\n" + \
"[#] Skey: " + self.skey + "\n" + \
"[#] DeviceId: " + self.device_id + "\n" + \
"[#] PassTicket: " + self.pass_ticket + "\n" + \
"========================="
return description
def __init__(self, host='wx.qq.com'):
super(WeChat, self).__init__(host)
self.db = None
self.save_data_folder = '' # 保存图片,语音,小视频的文件夹
self.last_login = 0 # 上次退出的时间
self.time_out = 5 # 同步时间间隔(单位:秒)
self.msg_handler = None
self.start_time = time.time()
self.bot = None
cm = ConfigManager()
self.save_data_folders = cm.get_wechat_media_dir()
self.cookie_file = cm.get_cookie()
self.pickle_file = cm.get_pickle_files()
self.log_mode = cm.get('setting', 'log_mode') == 'True'
self.exit_code = 0
def start(self):
echo(Constant.LOG_MSG_START)
run(Constant.LOG_MSG_RECOVER, self.recover)
timeOut = time.time() - self.last_login
echo(Constant.LOG_MSG_TRY_INIT)
if self.webwxinit():
echo(Constant.LOG_MSG_SUCCESS)
run(Constant.LOG_MSG_RECOVER_CONTACT, self.recover_contacts)
else:
echo(Constant.LOG_MSG_FAIL)
while True:
# first try to login by uin without qrcode
echo(Constant.LOG_MSG_ASSOCIATION_LOGIN)
if self.association_login():
echo(Constant.LOG_MSG_SUCCESS)
else:
echo(Constant.LOG_MSG_FAIL)
# scan qrcode to login
run(Constant.LOG_MSG_GET_UUID, self.getuuid)
echo(Constant.LOG_MSG_GET_QRCODE)
self.genqrcode()
echo(Constant.LOG_MSG_SCAN_QRCODE)
if not self.waitforlogin():
continue
echo(Constant.LOG_MSG_CONFIRM_LOGIN)
if not self.waitforlogin(0):
continue
break
run(Constant.LOG_MSG_LOGIN, self.login)
run(Constant.LOG_MSG_INIT, self.webwxinit)
run(Constant.LOG_MSG_STATUS_NOTIFY, self.webwxstatusnotify)
run(Constant.LOG_MSG_GET_CONTACT, self.webwxgetcontact)
echo(Constant.LOG_MSG_CONTACT_COUNT % (
self.MemberCount, len(self.MemberList)
))
echo(Constant.LOG_MSG_OTHER_CONTACT_COUNT % (
len(self.GroupList), len(self.ContactList),
len(self.SpecialUsersList), len(self.PublicUsersList)
))
run(Constant.LOG_MSG_GET_GROUP_MEMBER, self.fetch_group_contacts)
run(Constant.LOG_MSG_SNAPSHOT, self.snapshot)
while True:
[retcode, selector] = self.synccheck()
Log.debug('retcode: %s, selector: %s' % (retcode, selector))
self.exit_code = int(retcode)
if retcode == '1100':
echo(Constant.LOG_MSG_LOGOUT)
break
if retcode == '1101':
echo(Constant.LOG_MSG_LOGIN_OTHERWHERE)
break
if retcode == '1102':
echo(Constant.LOG_MSG_QUIT_ON_PHONE)
break
elif retcode == '0':
if selector == '2':
r = self.webwxsync()
if r is not None:
try:
self.handle_msg(r)
except:
Log.error(traceback.format_exc())
elif selector == '7':
r = self.webwxsync()
elif selector == '0':
time.sleep(self.time_out)
elif selector == '4':
# 保存群聊到通讯录
# 修改群名称
# 新增或删除联系人
# 群聊成员数目变化
r = self.webwxsync()
if r is not None:
try:
self.handle_mod(r)
except:
Log.error(traceback.format_exc())
elif selector == '3' or selector == '6':
break
else:
r = self.webwxsync()
Log.debug('webwxsync: %s\n' % json.dumps(r))
# 执行定时任务
if self.msg_handler:
self.msg_handler.check_schedule_task()
# if self.bot:
# r = self.bot.time_schedule()
# if r:
# for g in self.GroupList:
# echo('[*] 推送 -> %s: %s' % (g['NickName'], r))
# g_id = g['UserName']
# self.webwxsendmsg(r, g_id)
def get_run_time(self):
"""
@brief get how long this run
@return String
"""
totalTime = int(time.time() - self.start_time)
t = timedelta(seconds=totalTime)
return '%s Day %s' % (t.days, t)
def stop(self):
"""
@brief Save some data and use shell to kill this process
"""
run(Constant.LOG_MSG_SNAPSHOT, self.snapshot)
echo(Constant.LOG_MSG_RUNTIME % self.get_run_time())
# close database connect
self.db.close()
def fetch_group_contacts(self):
"""
@brief Fetches all groups contacts.
@return Bool: whether operation succeed.
@note This function must be finished in 180s
"""
Log.debug('fetch_group_contacts')
# clean database
if self.msg_handler:
self.msg_handler.clean_db()
# sqlite
# ----------------------------------------------------
# group max_thread_num max_fetch_group_num time(s)
# 197 10 10 108
# 197 10 15 95
# 197 20 10 103
# 197 10 20 55
# 197 5 30 39
# 197 4 50 35
# ----------------------------------------------------
# mysql
# ----------------------------------------------------
# group max_thread_num max_fetch_group_num time(s)
# 197 4 50 20
# ----------------------------------------------------
max_thread_num = 4
max_fetch_group_num = 50
group_list_queue = Queue.Queue()
class GroupListThread(threading.Thread):
def __init__(self, group_list_queue, wechat):
threading.Thread.__init__(self)
self.group_list_queue = group_list_queue
self.wechat = wechat
def run(self):
while not self.group_list_queue.empty():
g_list = self.group_list_queue.get()
gid_list = []
g_dict = {}
for g in g_list:
gid = g['UserName']
gid_list.append(gid)
g_dict[gid] = g
group_member_list = self.wechat.webwxbatchgetcontact(gid_list)
for member_list in group_member_list:
gid = member_list['UserName']
g = g_dict[gid]
g['MemberCount'] = member_list['MemberCount']
g['OwnerUin'] = member_list['OwnerUin']
self.wechat.GroupMemeberList[gid] = member_list['MemberList']
# 如果使用 Mysql 则可以在多线程里操作数据库
# 否则请注释下列代码在主线程里更新群列表
# -----------------------------------
# 处理群成员
# if self.wechat.msg_handler:
# self.wechat.msg_handler.handle_group_member_list(gid, member_list['MemberList'])
# -----------------------------------
self.group_list_queue.task_done()
for g_list in split_array(self.GroupList, max_fetch_group_num):
group_list_queue.put(g_list)
for i in range(max_thread_num):
t = GroupListThread(group_list_queue, self)
t.setDaemon(True)
t.start()
group_list_queue.join()
if self.msg_handler:
# 处理群
if self.GroupList:
self.msg_handler.handle_group_list(self.GroupList)
# 这个是用 sqlite 来存储群列表,sqlite 对多线程的支持不太好
# ----------------------------------------------------
# 处理群成员
for (gid, member_list) in self.GroupMemeberList.items():
self.msg_handler.handle_group_member_list(gid, member_list)
# ----------------------------------------------------
return True
def snapshot(self):
"""
@brief Save basic infos for next login.
@return Bool: whether operation succeed.
"""
try:
conf = {
'uuid': self.uuid,
'redirect_uri': self.redirect_uri,
'uin': self.uin,
'sid': self.sid,
'skey': self.skey,
'pass_ticket': self.pass_ticket,
'synckey': self.synckey,
'device_id': self.device_id,
'last_login': time.time(),
}
cm = ConfigManager()
Log.debug('save wechat config')
cm.set_wechat_config(conf)
# save cookie
Log.debug('save cookie')
if self.cookie:
self.cookie.save(ignore_discard=True)
# save contacts
Log.debug('save contacts')
self.save_contacts()
except Exception, e:
Log.error(traceback.format_exc())
return False
return True
def recover(self):
"""
@brief Recover from snapshot data.
@return Bool: whether operation succeed.
"""
cm = ConfigManager()
[self.uuid, self.redirect_uri, self.uin,
self.sid, self.skey, self.pass_ticket,
self.synckey, device_id, self.last_login] = cm.get_wechat_config()
if device_id:
self.device_id = device_id
self.base_request = {
'Uin': int(self.uin),
'Sid': self.sid,
'Skey': self.skey,
'DeviceID': self.device_id,
}
# set cookie
Log.debug('set cookie')
self.cookie = set_cookie(self.cookie_file)
return True
def save_contacts(self):
"""
@brief Save contacts.
"""
pickle_save(self.User, self.pickle_file['User'])
pickle_save(self.MemberList, self.pickle_file['MemberList'])
pickle_save(self.GroupList, self.pickle_file['GroupList'])
pickle_save(self.GroupMemeberList, self.pickle_file['GroupMemeberList'])
pickle_save(self.SpecialUsersList, self.pickle_file['SpecialUsersList'])
def recover_contacts(self):
"""
@brief recover contacts.
@return Bool: whether operation succeed.
"""
try:
self.User = pickle_load(self.pickle_file['User'])
self.MemberList = pickle_load(self.pickle_file['MemberList'])
self.GroupList = pickle_load(self.pickle_file['GroupList'])
self.GroupMemeberList = pickle_load(self.pickle_file['GroupMemeberList'])
self.SpecialUsersList = pickle_load(self.pickle_file['SpecialUsersList'])
return True
except Exception, e:
Log.error(traceback.format_exc())
return False
def handle_mod(self, r):
# ModContactCount: 变更联系人或群聊成员数目
# ModContactList: 变更联系人或群聊列表,或群名称改变
Log.debug('handle modify')
self.handle_msg(r)
for m in r['ModContactList']:
if m['UserName'][:2] == '@@':
# group
in_list = False
g_id = m['UserName']
for g in self.GroupList:
# group member change
if g_id == g['UserName']:
g['MemberCount'] = m['MemberCount']
g['NickName'] = m['NickName']
self.GroupMemeberList[g_id] = m['MemberList']
in_list = True
if self.msg_handler:
self.msg_handler.handle_group_member_change(g_id, m['MemberList'])
break
if not in_list:
# a new group
self.GroupList.append(m)
self.GroupMemeberList[g_id] = m['MemberList']
if self.msg_handler:
self.msg_handler.handle_group_list_change(m)
self.msg_handler.handle_group_member_change(g_id, m['MemberList'])
elif m['UserName'][0] == '@':
# user
in_list = False
for u in self.MemberList:
u_id = m['UserName']
if u_id == u['UserName']:
u = m
in_list = True
break
# if don't have then add it
if not in_list:
self.MemberList.append(m)
def handle_msg(self, r):
"""
@brief Recover from snapshot data.
@param r Dict: message json
"""
Log.debug('handle message')
if self.msg_handler:
self.msg_handler.handle_wxsync(r)
n = len(r['AddMsgList'])
if n == 0:
return
if self.log_mode:
echo(Constant.LOG_MSG_NEW_MSG % n)
for msg in r['AddMsgList']:
msgType = msg['MsgType']
msgId = msg['MsgId']
content = msg['Content'].replace('<', '<').replace('>', '>')
raw_msg = None
if msgType == self.wx_conf['MSGTYPE_TEXT']:
# 地理位置消息
if content.find('pictype=location') != -1:
location = content.split('
')[1][:-1]
raw_msg = {
'raw_msg': msg,
'location': location,
'log': Constant.LOG_MSG_LOCATION % location
}
# 普通文本消息
else:
text = content.split(':
')[-1]
raw_msg = {
'raw_msg': msg,
'text': text,
'log': text.replace('
', '\n')
}
elif msgType == self.wx_conf['MSGTYPE_IMAGE']:
data = self.webwxgetmsgimg(msgId)
fn = 'img_' + msgId + '.jpg'
dir = self.save_data_folders['webwxgetmsgimg']
path = save_file(fn, data, dir)
raw_msg = {'raw_msg': msg,
'image': path,
'log': Constant.LOG_MSG_PICTURE % path}
elif msgType == self.wx_conf['MSGTYPE_VOICE']:
data = self.webwxgetvoice(msgId)
fn = 'voice_' + msgId + '.mp3'
dir = self.save_data_folders['webwxgetvoice']
path = save_file(fn, data, dir)
raw_msg = {'raw_msg': msg,
'voice': path,
'log': Constant.LOG_MSG_VOICE % path}
elif msgType == self.wx_conf['MSGTYPE_SHARECARD']:
info = msg['RecommendInfo']
card = Constant.LOG_MSG_NAME_CARD % (
info['NickName'],
info['Alias'],
info['Province'], info['City'],
Constant.LOG_MSG_SEX_OPTION[info['Sex']]
)
namecard = '%s %s %s %s %s' % (
info['NickName'], info['Alias'], info['Province'],
info['City'], Constant.LOG_MSG_SEX_OPTION[info['Sex']]
)
raw_msg = {
'raw_msg': msg,
'namecard': namecard,
'log': card
}
elif msgType == self.wx_conf['MSGTYPE_EMOTICON']:
url = search_content('cdnurl', content)
raw_msg = {'raw_msg': msg,
'emoticon': url,
'log': Constant.LOG_MSG_EMOTION % url}
elif msgType == self.wx_conf['MSGTYPE_APP']:
card = ''
# 链接, 音乐, 微博
if msg['AppMsgType'] in [
self.wx_conf['APPMSGTYPE_AUDIO'],
self.wx_conf['APPMSGTYPE_URL'],
self.wx_conf['APPMSGTYPE_OPEN']
]:
card = Constant.LOG_MSG_APP_LINK % (
Constant.LOG_MSG_APP_LINK_TYPE[msg['AppMsgType']],
msg['FileName'],
search_content('des', content, 'xml'),
msg['Url'],
search_content('appname', content, 'xml')
)
raw_msg = {
'raw_msg': msg,
'link': msg['Url'],
'log': card
}
# 图片
elif msg['AppMsgType'] == self.wx_conf['APPMSGTYPE_IMG']:
data = self.webwxgetmsgimg(msgId)
fn = 'img_' + msgId + '.jpg'
dir = self.save_data_folders['webwxgetmsgimg']
path = save_file(fn, data, dir)
card = Constant.LOG_MSG_APP_IMG % (
path,
search_content('appname', content, 'xml')
)
raw_msg = {
'raw_msg': msg,
'image': path,
'log': card
}
else:
raw_msg = {
'raw_msg': msg,
'log': Constant.LOG_MSG_UNKNOWN_MSG % (msgType, content)
}
elif msgType == self.wx_conf['MSGTYPE_STATUSNOTIFY']:
Log.info(Constant.LOG_MSG_NOTIFY_PHONE)
elif msgType == self.wx_conf['MSGTYPE_MICROVIDEO']:
data = self.webwxgetvideo(msgId)
fn = 'video_' + msgId + '.mp4'
dir = self.save_data_folders['webwxgetvideo']
path = save_file(fn, data, dir)
raw_msg = {'raw_msg': msg,
'video': path,
'log': Constant.LOG_MSG_VIDEO % path}
elif msgType == self.wx_conf['MSGTYPE_RECALLED']:
recall_id = search_content('msgid', content, 'xml')
text = Constant.LOG_MSG_RECALL
raw_msg = {
'raw_msg': msg,
'text': text,
'recall_msg_id': recall_id,
'log': text
}
elif msgType == self.wx_conf['MSGTYPE_SYS']:
raw_msg = {
'raw_msg': msg,
'sys_notif': content,
'log': content
}
elif msgType == self.wx_conf['MSGTYPE_VERIFYMSG']:
name = search_content('fromnickname', content)
raw_msg = {
'raw_msg': msg,
'log': Constant.LOG_MSG_ADD_FRIEND % name
}
else:
raw_msg = {
'raw_msg': msg,
'log': Constant.LOG_MSG_UNKNOWN_MSG % (msgType, content)
}
isGroupMsg = '@@' in msg['FromUserName']+msg['ToUserName']
if self.msg_handler and raw_msg:
if isGroupMsg:
# handle group messages
g_msg = self.make_group_msg(raw_msg)
self.msg_handler.handle_group_msg(g_msg)
else:
# handle personal messages
self.msg_handler.handle_user_msg(raw_msg)
if self.log_mode:
self.show_msg(raw_msg)
def make_group_msg(self, msg):
"""
@brief Package the group message for storage.
@param msg Dict: raw msg
@return raw_msg Dict: packged msg
"""
Log.debug('make group message')
raw_msg = {
'raw_msg': msg['raw_msg'],
'msg_id': msg['raw_msg']['MsgId'],
'group_owner_uin': '',
'group_name': '',
'group_count': '',
'from_user_name': msg['raw_msg']['FromUserName'],
'to_user_name': msg['raw_msg']['ToUserName'],
'user_attrstatus': '',
'user_display_name': '',
'user_nickname': '',
'msg_type': msg['raw_msg']['MsgType'],
'text': '',
'link': '',
'image': '',
'video': '',
'voice': '',
'emoticon': '',
'namecard': '',
'location': '',
'recall_msg_id': '',
'sys_notif': '',
'time': '',
'timestamp': '',
'log': '',
}
content = msg['raw_msg']['Content'].replace(
'<', '<').replace('>', '>')
group = None
src = None
if msg['raw_msg']['FromUserName'][:2] == '@@':
# 接收到来自群的消息
g_id = msg['raw_msg']['FromUserName']
group = self.get_group_by_id(g_id)
if re.search(":
", content, re.IGNORECASE):
u_id = content.split(':
')[0]
src = self.get_group_user_by_id(u_id, g_id)
elif msg['raw_msg']['ToUserName'][:2] == '@@':
# 自己发给群的消息
g_id = msg['raw_msg']['ToUserName']
u_id = msg['raw_msg']['FromUserName']
src = self.get_group_user_by_id(u_id, g_id)
group = self.get_group_by_id(g_id)
if src:
raw_msg['user_attrstatus'] = src['AttrStatus']
raw_msg['user_display_name'] = src['DisplayName']
raw_msg['user_nickname'] = src['NickName']
if group:
raw_msg['group_count'] = group['MemberCount']
raw_msg['group_owner_uin'] = group['OwnerUin']
raw_msg['group_name'] = group['ShowName']
raw_msg['timestamp'] = msg['raw_msg']['CreateTime']
t = time.localtime(float(raw_msg['timestamp']))
raw_msg['time'] = time.strftime("%Y-%m-%d %T", t)
for key in [
'text', 'link', 'image', 'video', 'voice',
'emoticon', 'namecard', 'location', 'log',
'recall_msg_id', 'sys_notif'
]:
if key in msg:
raw_msg[key] = msg[key]
return raw_msg
def show_msg(self, message):
"""
@brief Log the message to stdout
@param message Dict
"""
msg = message
src = None
dst = None
group = None
if msg and msg['raw_msg']:
content = msg['raw_msg']['Content']
content = content.replace('<', '<').replace('>', '>')
msg_id = msg['raw_msg']['MsgId']
if msg['raw_msg']['FromUserName'][:2] == '@@':
# 接收到来自群的消息
g_id = msg['raw_msg']['FromUserName']
group = self.get_group_by_id(g_id)
if re.search(":
", content, re.IGNORECASE):
u_id = content.split(':
')[0]
src = self.get_group_user_by_id(u_id, g_id)
dst = {'ShowName': 'GROUP'}
else:
u_id = msg['raw_msg']['ToUserName']
src = {'ShowName': 'SYSTEM'}
dst = self.get_group_user_by_id(u_id, g_id)
elif msg['raw_msg']['ToUserName'][:2] == '@@':
# 自己发给群的消息
g_id = msg['raw_msg']['ToUserName']
u_id = msg['raw_msg']['FromUserName']
group = self.get_group_by_id(g_id)
src = self.get_group_user_by_id(u_id, g_id)
dst = {'ShowName': 'GROUP'}
else:
# 非群聊消息
src = self.get_user_by_id(msg['raw_msg']['FromUserName'])
dst = self.get_user_by_id(msg['raw_msg']['ToUserName'])
if group:
echo('%s |%s| %s -> %s: %s\n' % (
msg_id,
trans_emoji(group['ShowName']),
trans_emoji(src['ShowName']),
dst['ShowName'],
trans_emoji(msg['log'])
))
else:
echo('%s %s -> %s: %s\n' % (
msg_id,
trans_emoji(src['ShowName']),
trans_emoji(dst['ShowName']),
trans_emoji(msg['log'])
))