feat: intergrate itchat to lib
parent
3c91575ebe
commit
92caeed7ab
|
|
@ -5,9 +5,9 @@ wechat channel
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import itchat
|
from lib import itchat
|
||||||
import json
|
import json
|
||||||
from itchat.content import *
|
from lib.itchat.content import *
|
||||||
from bridge.reply import *
|
from bridge.reply import *
|
||||||
from bridge.context import *
|
from bridge.context import *
|
||||||
from channel.channel import Channel
|
from channel.channel import Channel
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
from .core import Core
|
||||||
|
from .config import VERSION, ASYNC_COMPONENTS
|
||||||
|
from .log import set_logging
|
||||||
|
|
||||||
|
if ASYNC_COMPONENTS:
|
||||||
|
from .async_components import load_components
|
||||||
|
else:
|
||||||
|
from .components import load_components
|
||||||
|
|
||||||
|
|
||||||
|
__version__ = VERSION
|
||||||
|
|
||||||
|
|
||||||
|
instanceList = []
|
||||||
|
|
||||||
|
def load_async_itchat() -> Core:
|
||||||
|
"""load async-based itchat instance
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Core: the abstract interface of itchat
|
||||||
|
"""
|
||||||
|
from .async_components import load_components
|
||||||
|
load_components(Core)
|
||||||
|
return Core()
|
||||||
|
|
||||||
|
|
||||||
|
def load_sync_itchat() -> Core:
|
||||||
|
"""load sync-based itchat instance
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Core: the abstract interface of itchat
|
||||||
|
"""
|
||||||
|
from .components import load_components
|
||||||
|
load_components(Core)
|
||||||
|
return Core()
|
||||||
|
|
||||||
|
|
||||||
|
if ASYNC_COMPONENTS:
|
||||||
|
instance = load_async_itchat()
|
||||||
|
else:
|
||||||
|
instance = load_sync_itchat()
|
||||||
|
|
||||||
|
|
||||||
|
instanceList = [instance]
|
||||||
|
|
||||||
|
# I really want to use sys.modules[__name__] = originInstance
|
||||||
|
# but it makes auto-fill a real mess, so forgive me for my following **
|
||||||
|
# actually it toke me less than 30 seconds, god bless Uganda
|
||||||
|
|
||||||
|
# components.login
|
||||||
|
login = instance.login
|
||||||
|
get_QRuuid = instance.get_QRuuid
|
||||||
|
get_QR = instance.get_QR
|
||||||
|
check_login = instance.check_login
|
||||||
|
web_init = instance.web_init
|
||||||
|
show_mobile_login = instance.show_mobile_login
|
||||||
|
start_receiving = instance.start_receiving
|
||||||
|
get_msg = instance.get_msg
|
||||||
|
logout = instance.logout
|
||||||
|
# components.contact
|
||||||
|
update_chatroom = instance.update_chatroom
|
||||||
|
update_friend = instance.update_friend
|
||||||
|
get_contact = instance.get_contact
|
||||||
|
get_friends = instance.get_friends
|
||||||
|
get_chatrooms = instance.get_chatrooms
|
||||||
|
get_mps = instance.get_mps
|
||||||
|
set_alias = instance.set_alias
|
||||||
|
set_pinned = instance.set_pinned
|
||||||
|
accept_friend = instance.accept_friend
|
||||||
|
get_head_img = instance.get_head_img
|
||||||
|
create_chatroom = instance.create_chatroom
|
||||||
|
set_chatroom_name = instance.set_chatroom_name
|
||||||
|
delete_member_from_chatroom = instance.delete_member_from_chatroom
|
||||||
|
add_member_into_chatroom = instance.add_member_into_chatroom
|
||||||
|
# components.messages
|
||||||
|
send_raw_msg = instance.send_raw_msg
|
||||||
|
send_msg = instance.send_msg
|
||||||
|
upload_file = instance.upload_file
|
||||||
|
send_file = instance.send_file
|
||||||
|
send_image = instance.send_image
|
||||||
|
send_video = instance.send_video
|
||||||
|
send = instance.send
|
||||||
|
revoke = instance.revoke
|
||||||
|
# components.hotreload
|
||||||
|
dump_login_status = instance.dump_login_status
|
||||||
|
load_login_status = instance.load_login_status
|
||||||
|
# components.register
|
||||||
|
auto_login = instance.auto_login
|
||||||
|
configured_reply = instance.configured_reply
|
||||||
|
msg_register = instance.msg_register
|
||||||
|
run = instance.run
|
||||||
|
# other functions
|
||||||
|
search_friends = instance.search_friends
|
||||||
|
search_chatrooms = instance.search_chatrooms
|
||||||
|
search_mps = instance.search_mps
|
||||||
|
set_logging = set_logging
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
from .contact import load_contact
|
||||||
|
from .hotreload import load_hotreload
|
||||||
|
from .login import load_login
|
||||||
|
from .messages import load_messages
|
||||||
|
from .register import load_register
|
||||||
|
|
||||||
|
def load_components(core):
|
||||||
|
load_contact(core)
|
||||||
|
load_hotreload(core)
|
||||||
|
load_login(core)
|
||||||
|
load_messages(core)
|
||||||
|
load_register(core)
|
||||||
|
|
@ -0,0 +1,488 @@
|
||||||
|
import time, re, io
|
||||||
|
import json, copy
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from .. import config, utils
|
||||||
|
from ..components.contact import accept_friend
|
||||||
|
from ..returnvalues import ReturnValue
|
||||||
|
from ..storage import contact_change
|
||||||
|
from ..utils import update_info_dict
|
||||||
|
|
||||||
|
logger = logging.getLogger('itchat')
|
||||||
|
|
||||||
|
def load_contact(core):
|
||||||
|
core.update_chatroom = update_chatroom
|
||||||
|
core.update_friend = update_friend
|
||||||
|
core.get_contact = get_contact
|
||||||
|
core.get_friends = get_friends
|
||||||
|
core.get_chatrooms = get_chatrooms
|
||||||
|
core.get_mps = get_mps
|
||||||
|
core.set_alias = set_alias
|
||||||
|
core.set_pinned = set_pinned
|
||||||
|
core.accept_friend = accept_friend
|
||||||
|
core.get_head_img = get_head_img
|
||||||
|
core.create_chatroom = create_chatroom
|
||||||
|
core.set_chatroom_name = set_chatroom_name
|
||||||
|
core.delete_member_from_chatroom = delete_member_from_chatroom
|
||||||
|
core.add_member_into_chatroom = add_member_into_chatroom
|
||||||
|
|
||||||
|
def update_chatroom(self, userName, detailedMember=False):
|
||||||
|
if not isinstance(userName, list):
|
||||||
|
userName = [userName]
|
||||||
|
url = '%s/webwxbatchgetcontact?type=ex&r=%s' % (
|
||||||
|
self.loginInfo['url'], int(time.time()))
|
||||||
|
headers = {
|
||||||
|
'ContentType': 'application/json; charset=UTF-8',
|
||||||
|
'User-Agent' : config.USER_AGENT }
|
||||||
|
data = {
|
||||||
|
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||||
|
'Count': len(userName),
|
||||||
|
'List': [{
|
||||||
|
'UserName': u,
|
||||||
|
'ChatRoomId': '', } for u in userName], }
|
||||||
|
chatroomList = json.loads(self.s.post(url, data=json.dumps(data), headers=headers
|
||||||
|
).content.decode('utf8', 'replace')).get('ContactList')
|
||||||
|
if not chatroomList:
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'ErrMsg': 'No chatroom found',
|
||||||
|
'Ret': -1001, }})
|
||||||
|
|
||||||
|
if detailedMember:
|
||||||
|
def get_detailed_member_info(encryChatroomId, memberList):
|
||||||
|
url = '%s/webwxbatchgetcontact?type=ex&r=%s' % (
|
||||||
|
self.loginInfo['url'], int(time.time()))
|
||||||
|
headers = {
|
||||||
|
'ContentType': 'application/json; charset=UTF-8',
|
||||||
|
'User-Agent' : config.USER_AGENT, }
|
||||||
|
data = {
|
||||||
|
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||||
|
'Count': len(memberList),
|
||||||
|
'List': [{
|
||||||
|
'UserName': member['UserName'],
|
||||||
|
'EncryChatRoomId': encryChatroomId} \
|
||||||
|
for member in memberList], }
|
||||||
|
return json.loads(self.s.post(url, data=json.dumps(data), headers=headers
|
||||||
|
).content.decode('utf8', 'replace'))['ContactList']
|
||||||
|
MAX_GET_NUMBER = 50
|
||||||
|
for chatroom in chatroomList:
|
||||||
|
totalMemberList = []
|
||||||
|
for i in range(int(len(chatroom['MemberList']) / MAX_GET_NUMBER + 1)):
|
||||||
|
memberList = chatroom['MemberList'][i*MAX_GET_NUMBER: (i+1)*MAX_GET_NUMBER]
|
||||||
|
totalMemberList += get_detailed_member_info(chatroom['EncryChatRoomId'], memberList)
|
||||||
|
chatroom['MemberList'] = totalMemberList
|
||||||
|
|
||||||
|
update_local_chatrooms(self, chatroomList)
|
||||||
|
r = [self.storageClass.search_chatrooms(userName=c['UserName'])
|
||||||
|
for c in chatroomList]
|
||||||
|
return r if 1 < len(r) else r[0]
|
||||||
|
|
||||||
|
def update_friend(self, userName):
|
||||||
|
if not isinstance(userName, list):
|
||||||
|
userName = [userName]
|
||||||
|
url = '%s/webwxbatchgetcontact?type=ex&r=%s' % (
|
||||||
|
self.loginInfo['url'], int(time.time()))
|
||||||
|
headers = {
|
||||||
|
'ContentType': 'application/json; charset=UTF-8',
|
||||||
|
'User-Agent' : config.USER_AGENT }
|
||||||
|
data = {
|
||||||
|
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||||
|
'Count': len(userName),
|
||||||
|
'List': [{
|
||||||
|
'UserName': u,
|
||||||
|
'EncryChatRoomId': '', } for u in userName], }
|
||||||
|
friendList = json.loads(self.s.post(url, data=json.dumps(data), headers=headers
|
||||||
|
).content.decode('utf8', 'replace')).get('ContactList')
|
||||||
|
|
||||||
|
update_local_friends(self, friendList)
|
||||||
|
r = [self.storageClass.search_friends(userName=f['UserName'])
|
||||||
|
for f in friendList]
|
||||||
|
return r if len(r) != 1 else r[0]
|
||||||
|
|
||||||
|
@contact_change
|
||||||
|
def update_local_chatrooms(core, l):
|
||||||
|
'''
|
||||||
|
get a list of chatrooms for updating local chatrooms
|
||||||
|
return a list of given chatrooms with updated info
|
||||||
|
'''
|
||||||
|
for chatroom in l:
|
||||||
|
# format new chatrooms
|
||||||
|
utils.emoji_formatter(chatroom, 'NickName')
|
||||||
|
for member in chatroom['MemberList']:
|
||||||
|
if 'NickName' in member:
|
||||||
|
utils.emoji_formatter(member, 'NickName')
|
||||||
|
if 'DisplayName' in member:
|
||||||
|
utils.emoji_formatter(member, 'DisplayName')
|
||||||
|
if 'RemarkName' in member:
|
||||||
|
utils.emoji_formatter(member, 'RemarkName')
|
||||||
|
# update it to old chatrooms
|
||||||
|
oldChatroom = utils.search_dict_list(
|
||||||
|
core.chatroomList, 'UserName', chatroom['UserName'])
|
||||||
|
if oldChatroom:
|
||||||
|
update_info_dict(oldChatroom, chatroom)
|
||||||
|
# - update other values
|
||||||
|
memberList = chatroom.get('MemberList', [])
|
||||||
|
oldMemberList = oldChatroom['MemberList']
|
||||||
|
if memberList:
|
||||||
|
for member in memberList:
|
||||||
|
oldMember = utils.search_dict_list(
|
||||||
|
oldMemberList, 'UserName', member['UserName'])
|
||||||
|
if oldMember:
|
||||||
|
update_info_dict(oldMember, member)
|
||||||
|
else:
|
||||||
|
oldMemberList.append(member)
|
||||||
|
else:
|
||||||
|
core.chatroomList.append(chatroom)
|
||||||
|
oldChatroom = utils.search_dict_list(
|
||||||
|
core.chatroomList, 'UserName', chatroom['UserName'])
|
||||||
|
# delete useless members
|
||||||
|
if len(chatroom['MemberList']) != len(oldChatroom['MemberList']) and \
|
||||||
|
chatroom['MemberList']:
|
||||||
|
existsUserNames = [member['UserName'] for member in chatroom['MemberList']]
|
||||||
|
delList = []
|
||||||
|
for i, member in enumerate(oldChatroom['MemberList']):
|
||||||
|
if member['UserName'] not in existsUserNames:
|
||||||
|
delList.append(i)
|
||||||
|
delList.sort(reverse=True)
|
||||||
|
for i in delList:
|
||||||
|
del oldChatroom['MemberList'][i]
|
||||||
|
# - update OwnerUin
|
||||||
|
if oldChatroom.get('ChatRoomOwner') and oldChatroom.get('MemberList'):
|
||||||
|
owner = utils.search_dict_list(oldChatroom['MemberList'],
|
||||||
|
'UserName', oldChatroom['ChatRoomOwner'])
|
||||||
|
oldChatroom['OwnerUin'] = (owner or {}).get('Uin', 0)
|
||||||
|
# - update IsAdmin
|
||||||
|
if 'OwnerUin' in oldChatroom and oldChatroom['OwnerUin'] != 0:
|
||||||
|
oldChatroom['IsAdmin'] = \
|
||||||
|
oldChatroom['OwnerUin'] == int(core.loginInfo['wxuin'])
|
||||||
|
else:
|
||||||
|
oldChatroom['IsAdmin'] = None
|
||||||
|
# - update Self
|
||||||
|
newSelf = utils.search_dict_list(oldChatroom['MemberList'],
|
||||||
|
'UserName', core.storageClass.userName)
|
||||||
|
oldChatroom['Self'] = newSelf or copy.deepcopy(core.loginInfo['User'])
|
||||||
|
return {
|
||||||
|
'Type' : 'System',
|
||||||
|
'Text' : [chatroom['UserName'] for chatroom in l],
|
||||||
|
'SystemInfo' : 'chatrooms',
|
||||||
|
'FromUserName' : core.storageClass.userName,
|
||||||
|
'ToUserName' : core.storageClass.userName, }
|
||||||
|
|
||||||
|
@contact_change
|
||||||
|
def update_local_friends(core, l):
|
||||||
|
'''
|
||||||
|
get a list of friends or mps for updating local contact
|
||||||
|
'''
|
||||||
|
fullList = core.memberList + core.mpList
|
||||||
|
for friend in l:
|
||||||
|
if 'NickName' in friend:
|
||||||
|
utils.emoji_formatter(friend, 'NickName')
|
||||||
|
if 'DisplayName' in friend:
|
||||||
|
utils.emoji_formatter(friend, 'DisplayName')
|
||||||
|
if 'RemarkName' in friend:
|
||||||
|
utils.emoji_formatter(friend, 'RemarkName')
|
||||||
|
oldInfoDict = utils.search_dict_list(
|
||||||
|
fullList, 'UserName', friend['UserName'])
|
||||||
|
if oldInfoDict is None:
|
||||||
|
oldInfoDict = copy.deepcopy(friend)
|
||||||
|
if oldInfoDict['VerifyFlag'] & 8 == 0:
|
||||||
|
core.memberList.append(oldInfoDict)
|
||||||
|
else:
|
||||||
|
core.mpList.append(oldInfoDict)
|
||||||
|
else:
|
||||||
|
update_info_dict(oldInfoDict, friend)
|
||||||
|
|
||||||
|
@contact_change
|
||||||
|
def update_local_uin(core, msg):
|
||||||
|
'''
|
||||||
|
content contains uins and StatusNotifyUserName contains username
|
||||||
|
they are in same order, so what I do is to pair them together
|
||||||
|
|
||||||
|
I caught an exception in this method while not knowing why
|
||||||
|
but don't worry, it won't cause any problem
|
||||||
|
'''
|
||||||
|
uins = re.search('<username>([^<]*?)<', msg['Content'])
|
||||||
|
usernameChangedList = []
|
||||||
|
r = {
|
||||||
|
'Type': 'System',
|
||||||
|
'Text': usernameChangedList,
|
||||||
|
'SystemInfo': 'uins', }
|
||||||
|
if uins:
|
||||||
|
uins = uins.group(1).split(',')
|
||||||
|
usernames = msg['StatusNotifyUserName'].split(',')
|
||||||
|
if 0 < len(uins) == len(usernames):
|
||||||
|
for uin, username in zip(uins, usernames):
|
||||||
|
if not '@' in username: continue
|
||||||
|
fullContact = core.memberList + core.chatroomList + core.mpList
|
||||||
|
userDicts = utils.search_dict_list(fullContact,
|
||||||
|
'UserName', username)
|
||||||
|
if userDicts:
|
||||||
|
if userDicts.get('Uin', 0) == 0:
|
||||||
|
userDicts['Uin'] = uin
|
||||||
|
usernameChangedList.append(username)
|
||||||
|
logger.debug('Uin fetched: %s, %s' % (username, uin))
|
||||||
|
else:
|
||||||
|
if userDicts['Uin'] != uin:
|
||||||
|
logger.debug('Uin changed: %s, %s' % (
|
||||||
|
userDicts['Uin'], uin))
|
||||||
|
else:
|
||||||
|
if '@@' in username:
|
||||||
|
core.storageClass.updateLock.release()
|
||||||
|
update_chatroom(core, username)
|
||||||
|
core.storageClass.updateLock.acquire()
|
||||||
|
newChatroomDict = utils.search_dict_list(
|
||||||
|
core.chatroomList, 'UserName', username)
|
||||||
|
if newChatroomDict is None:
|
||||||
|
newChatroomDict = utils.struct_friend_info({
|
||||||
|
'UserName': username,
|
||||||
|
'Uin': uin,
|
||||||
|
'Self': copy.deepcopy(core.loginInfo['User'])})
|
||||||
|
core.chatroomList.append(newChatroomDict)
|
||||||
|
else:
|
||||||
|
newChatroomDict['Uin'] = uin
|
||||||
|
elif '@' in username:
|
||||||
|
core.storageClass.updateLock.release()
|
||||||
|
update_friend(core, username)
|
||||||
|
core.storageClass.updateLock.acquire()
|
||||||
|
newFriendDict = utils.search_dict_list(
|
||||||
|
core.memberList, 'UserName', username)
|
||||||
|
if newFriendDict is None:
|
||||||
|
newFriendDict = utils.struct_friend_info({
|
||||||
|
'UserName': username,
|
||||||
|
'Uin': uin, })
|
||||||
|
core.memberList.append(newFriendDict)
|
||||||
|
else:
|
||||||
|
newFriendDict['Uin'] = uin
|
||||||
|
usernameChangedList.append(username)
|
||||||
|
logger.debug('Uin fetched: %s, %s' % (username, uin))
|
||||||
|
else:
|
||||||
|
logger.debug('Wrong length of uins & usernames: %s, %s' % (
|
||||||
|
len(uins), len(usernames)))
|
||||||
|
else:
|
||||||
|
logger.debug('No uins in 51 message')
|
||||||
|
logger.debug(msg['Content'])
|
||||||
|
return r
|
||||||
|
|
||||||
|
def get_contact(self, update=False):
|
||||||
|
if not update:
|
||||||
|
return utils.contact_deep_copy(self, self.chatroomList)
|
||||||
|
def _get_contact(seq=0):
|
||||||
|
url = '%s/webwxgetcontact?r=%s&seq=%s&skey=%s' % (self.loginInfo['url'],
|
||||||
|
int(time.time()), seq, self.loginInfo['skey'])
|
||||||
|
headers = {
|
||||||
|
'ContentType': 'application/json; charset=UTF-8',
|
||||||
|
'User-Agent' : config.USER_AGENT, }
|
||||||
|
try:
|
||||||
|
r = self.s.get(url, headers=headers)
|
||||||
|
except:
|
||||||
|
logger.info('Failed to fetch contact, that may because of the amount of your chatrooms')
|
||||||
|
for chatroom in self.get_chatrooms():
|
||||||
|
self.update_chatroom(chatroom['UserName'], detailedMember=True)
|
||||||
|
return 0, []
|
||||||
|
j = json.loads(r.content.decode('utf-8', 'replace'))
|
||||||
|
return j.get('Seq', 0), j.get('MemberList')
|
||||||
|
seq, memberList = 0, []
|
||||||
|
while 1:
|
||||||
|
seq, batchMemberList = _get_contact(seq)
|
||||||
|
memberList.extend(batchMemberList)
|
||||||
|
if seq == 0:
|
||||||
|
break
|
||||||
|
chatroomList, otherList = [], []
|
||||||
|
for m in memberList:
|
||||||
|
if m['Sex'] != 0:
|
||||||
|
otherList.append(m)
|
||||||
|
elif '@@' in m['UserName']:
|
||||||
|
chatroomList.append(m)
|
||||||
|
elif '@' in m['UserName']:
|
||||||
|
# mp will be dealt in update_local_friends as well
|
||||||
|
otherList.append(m)
|
||||||
|
if chatroomList:
|
||||||
|
update_local_chatrooms(self, chatroomList)
|
||||||
|
if otherList:
|
||||||
|
update_local_friends(self, otherList)
|
||||||
|
return utils.contact_deep_copy(self, chatroomList)
|
||||||
|
|
||||||
|
def get_friends(self, update=False):
|
||||||
|
if update:
|
||||||
|
self.get_contact(update=True)
|
||||||
|
return utils.contact_deep_copy(self, self.memberList)
|
||||||
|
|
||||||
|
def get_chatrooms(self, update=False, contactOnly=False):
|
||||||
|
if contactOnly:
|
||||||
|
return self.get_contact(update=True)
|
||||||
|
else:
|
||||||
|
if update:
|
||||||
|
self.get_contact(True)
|
||||||
|
return utils.contact_deep_copy(self, self.chatroomList)
|
||||||
|
|
||||||
|
def get_mps(self, update=False):
|
||||||
|
if update: self.get_contact(update=True)
|
||||||
|
return utils.contact_deep_copy(self, self.mpList)
|
||||||
|
|
||||||
|
def set_alias(self, userName, alias):
|
||||||
|
oldFriendInfo = utils.search_dict_list(
|
||||||
|
self.memberList, 'UserName', userName)
|
||||||
|
if oldFriendInfo is None:
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'Ret': -1001, }})
|
||||||
|
url = '%s/webwxoplog?lang=%s&pass_ticket=%s' % (
|
||||||
|
self.loginInfo['url'], 'zh_CN', self.loginInfo['pass_ticket'])
|
||||||
|
data = {
|
||||||
|
'UserName' : userName,
|
||||||
|
'CmdId' : 2,
|
||||||
|
'RemarkName' : alias,
|
||||||
|
'BaseRequest' : self.loginInfo['BaseRequest'], }
|
||||||
|
headers = { 'User-Agent' : config.USER_AGENT}
|
||||||
|
r = self.s.post(url, json.dumps(data, ensure_ascii=False).encode('utf8'),
|
||||||
|
headers=headers)
|
||||||
|
r = ReturnValue(rawResponse=r)
|
||||||
|
if r:
|
||||||
|
oldFriendInfo['RemarkName'] = alias
|
||||||
|
return r
|
||||||
|
|
||||||
|
def set_pinned(self, userName, isPinned=True):
|
||||||
|
url = '%s/webwxoplog?pass_ticket=%s' % (
|
||||||
|
self.loginInfo['url'], self.loginInfo['pass_ticket'])
|
||||||
|
data = {
|
||||||
|
'UserName' : userName,
|
||||||
|
'CmdId' : 3,
|
||||||
|
'OP' : int(isPinned),
|
||||||
|
'BaseRequest' : self.loginInfo['BaseRequest'], }
|
||||||
|
headers = { 'User-Agent' : config.USER_AGENT}
|
||||||
|
r = self.s.post(url, json=data, headers=headers)
|
||||||
|
return ReturnValue(rawResponse=r)
|
||||||
|
|
||||||
|
def accept_friend(self, userName, v4= '', autoUpdate=True):
|
||||||
|
url = f"{self.loginInfo['url']}/webwxverifyuser?r={int(time.time())}&pass_ticket={self.loginInfo['pass_ticket']}"
|
||||||
|
data = {
|
||||||
|
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||||
|
'Opcode': 3, # 3
|
||||||
|
'VerifyUserListSize': 1,
|
||||||
|
'VerifyUserList': [{
|
||||||
|
'Value': userName,
|
||||||
|
'VerifyUserTicket': v4, }],
|
||||||
|
'VerifyContent': '',
|
||||||
|
'SceneListCount': 1,
|
||||||
|
'SceneList': [33],
|
||||||
|
'skey': self.loginInfo['skey'], }
|
||||||
|
headers = {
|
||||||
|
'ContentType': 'application/json; charset=UTF-8',
|
||||||
|
'User-Agent' : config.USER_AGENT }
|
||||||
|
r = self.s.post(url, headers=headers,
|
||||||
|
data=json.dumps(data, ensure_ascii=False).encode('utf8', 'replace'))
|
||||||
|
if autoUpdate:
|
||||||
|
self.update_friend(userName)
|
||||||
|
return ReturnValue(rawResponse=r)
|
||||||
|
|
||||||
|
def get_head_img(self, userName=None, chatroomUserName=None, picDir=None):
|
||||||
|
''' get head image
|
||||||
|
* if you want to get chatroom header: only set chatroomUserName
|
||||||
|
* if you want to get friend header: only set userName
|
||||||
|
* if you want to get chatroom member header: set both
|
||||||
|
'''
|
||||||
|
params = {
|
||||||
|
'userName': userName or chatroomUserName or self.storageClass.userName,
|
||||||
|
'skey': self.loginInfo['skey'],
|
||||||
|
'type': 'big', }
|
||||||
|
url = '%s/webwxgeticon' % self.loginInfo['url']
|
||||||
|
if chatroomUserName is None:
|
||||||
|
infoDict = self.storageClass.search_friends(userName=userName)
|
||||||
|
if infoDict is None:
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'ErrMsg': 'No friend found',
|
||||||
|
'Ret': -1001, }})
|
||||||
|
else:
|
||||||
|
if userName is None:
|
||||||
|
url = '%s/webwxgetheadimg' % self.loginInfo['url']
|
||||||
|
else:
|
||||||
|
chatroom = self.storageClass.search_chatrooms(userName=chatroomUserName)
|
||||||
|
if chatroomUserName is None:
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'ErrMsg': 'No chatroom found',
|
||||||
|
'Ret': -1001, }})
|
||||||
|
if 'EncryChatRoomId' in chatroom:
|
||||||
|
params['chatroomid'] = chatroom['EncryChatRoomId']
|
||||||
|
params['chatroomid'] = params.get('chatroomid') or chatroom['UserName']
|
||||||
|
headers = { 'User-Agent' : config.USER_AGENT}
|
||||||
|
r = self.s.get(url, params=params, stream=True, headers=headers)
|
||||||
|
tempStorage = io.BytesIO()
|
||||||
|
for block in r.iter_content(1024):
|
||||||
|
tempStorage.write(block)
|
||||||
|
if picDir is None:
|
||||||
|
return tempStorage.getvalue()
|
||||||
|
with open(picDir, 'wb') as f:
|
||||||
|
f.write(tempStorage.getvalue())
|
||||||
|
tempStorage.seek(0)
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'ErrMsg': 'Successfully downloaded',
|
||||||
|
'Ret': 0, },
|
||||||
|
'PostFix': utils.get_image_postfix(tempStorage.read(20)), })
|
||||||
|
|
||||||
|
def create_chatroom(self, memberList, topic=''):
|
||||||
|
url = '%s/webwxcreatechatroom?pass_ticket=%s&r=%s' % (
|
||||||
|
self.loginInfo['url'], self.loginInfo['pass_ticket'], int(time.time()))
|
||||||
|
data = {
|
||||||
|
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||||
|
'MemberCount': len(memberList.split(',')),
|
||||||
|
'MemberList': [{'UserName': member} for member in memberList.split(',')],
|
||||||
|
'Topic': topic, }
|
||||||
|
headers = {
|
||||||
|
'content-type': 'application/json; charset=UTF-8',
|
||||||
|
'User-Agent' : config.USER_AGENT }
|
||||||
|
r = self.s.post(url, headers=headers,
|
||||||
|
data=json.dumps(data, ensure_ascii=False).encode('utf8', 'ignore'))
|
||||||
|
return ReturnValue(rawResponse=r)
|
||||||
|
|
||||||
|
def set_chatroom_name(self, chatroomUserName, name):
|
||||||
|
url = '%s/webwxupdatechatroom?fun=modtopic&pass_ticket=%s' % (
|
||||||
|
self.loginInfo['url'], self.loginInfo['pass_ticket'])
|
||||||
|
data = {
|
||||||
|
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||||
|
'ChatRoomName': chatroomUserName,
|
||||||
|
'NewTopic': name, }
|
||||||
|
headers = {
|
||||||
|
'content-type': 'application/json; charset=UTF-8',
|
||||||
|
'User-Agent' : config.USER_AGENT }
|
||||||
|
r = self.s.post(url, headers=headers,
|
||||||
|
data=json.dumps(data, ensure_ascii=False).encode('utf8', 'ignore'))
|
||||||
|
return ReturnValue(rawResponse=r)
|
||||||
|
|
||||||
|
def delete_member_from_chatroom(self, chatroomUserName, memberList):
|
||||||
|
url = '%s/webwxupdatechatroom?fun=delmember&pass_ticket=%s' % (
|
||||||
|
self.loginInfo['url'], self.loginInfo['pass_ticket'])
|
||||||
|
data = {
|
||||||
|
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||||
|
'ChatRoomName': chatroomUserName,
|
||||||
|
'DelMemberList': ','.join([member['UserName'] for member in memberList]), }
|
||||||
|
headers = {
|
||||||
|
'content-type': 'application/json; charset=UTF-8',
|
||||||
|
'User-Agent' : config.USER_AGENT}
|
||||||
|
r = self.s.post(url, data=json.dumps(data),headers=headers)
|
||||||
|
return ReturnValue(rawResponse=r)
|
||||||
|
|
||||||
|
def add_member_into_chatroom(self, chatroomUserName, memberList,
|
||||||
|
useInvitation=False):
|
||||||
|
''' add or invite member into chatroom
|
||||||
|
* there are two ways to get members into chatroom: invite or directly add
|
||||||
|
* but for chatrooms with more than 40 users, you can only use invite
|
||||||
|
* but don't worry we will auto-force userInvitation for you when necessary
|
||||||
|
'''
|
||||||
|
if not useInvitation:
|
||||||
|
chatroom = self.storageClass.search_chatrooms(userName=chatroomUserName)
|
||||||
|
if not chatroom: chatroom = self.update_chatroom(chatroomUserName)
|
||||||
|
if len(chatroom['MemberList']) > self.loginInfo['InviteStartCount']:
|
||||||
|
useInvitation = True
|
||||||
|
if useInvitation:
|
||||||
|
fun, memberKeyName = 'invitemember', 'InviteMemberList'
|
||||||
|
else:
|
||||||
|
fun, memberKeyName = 'addmember', 'AddMemberList'
|
||||||
|
url = '%s/webwxupdatechatroom?fun=%s&pass_ticket=%s' % (
|
||||||
|
self.loginInfo['url'], fun, self.loginInfo['pass_ticket'])
|
||||||
|
params = {
|
||||||
|
'BaseRequest' : self.loginInfo['BaseRequest'],
|
||||||
|
'ChatRoomName' : chatroomUserName,
|
||||||
|
memberKeyName : memberList, }
|
||||||
|
headers = {
|
||||||
|
'content-type': 'application/json; charset=UTF-8',
|
||||||
|
'User-Agent' : config.USER_AGENT}
|
||||||
|
r = self.s.post(url, data=json.dumps(params),headers=headers)
|
||||||
|
return ReturnValue(rawResponse=r)
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
import pickle, os
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import requests # type: ignore
|
||||||
|
|
||||||
|
from ..config import VERSION
|
||||||
|
from ..returnvalues import ReturnValue
|
||||||
|
from ..storage import templates
|
||||||
|
from .contact import update_local_chatrooms, update_local_friends
|
||||||
|
from .messages import produce_msg
|
||||||
|
|
||||||
|
logger = logging.getLogger('itchat')
|
||||||
|
|
||||||
|
def load_hotreload(core):
|
||||||
|
core.dump_login_status = dump_login_status
|
||||||
|
core.load_login_status = load_login_status
|
||||||
|
|
||||||
|
async def dump_login_status(self, fileDir=None):
|
||||||
|
fileDir = fileDir or self.hotReloadDir
|
||||||
|
try:
|
||||||
|
with open(fileDir, 'w') as f:
|
||||||
|
f.write('itchat - DELETE THIS')
|
||||||
|
os.remove(fileDir)
|
||||||
|
except:
|
||||||
|
raise Exception('Incorrect fileDir')
|
||||||
|
status = {
|
||||||
|
'version' : VERSION,
|
||||||
|
'loginInfo' : self.loginInfo,
|
||||||
|
'cookies' : self.s.cookies.get_dict(),
|
||||||
|
'storage' : self.storageClass.dumps()}
|
||||||
|
with open(fileDir, 'wb') as f:
|
||||||
|
pickle.dump(status, f)
|
||||||
|
logger.debug('Dump login status for hot reload successfully.')
|
||||||
|
|
||||||
|
async def load_login_status(self, fileDir,
|
||||||
|
loginCallback=None, exitCallback=None):
|
||||||
|
try:
|
||||||
|
with open(fileDir, 'rb') as f:
|
||||||
|
j = pickle.load(f)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug('No such file, loading login status failed.')
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'ErrMsg': 'No such file, loading login status failed.',
|
||||||
|
'Ret': -1002, }})
|
||||||
|
|
||||||
|
if j.get('version', '') != VERSION:
|
||||||
|
logger.debug(('you have updated itchat from %s to %s, ' +
|
||||||
|
'so cached status is ignored') % (
|
||||||
|
j.get('version', 'old version'), VERSION))
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'ErrMsg': 'cached status ignored because of version',
|
||||||
|
'Ret': -1005, }})
|
||||||
|
self.loginInfo = j['loginInfo']
|
||||||
|
self.loginInfo['User'] = templates.User(self.loginInfo['User'])
|
||||||
|
self.loginInfo['User'].core = self
|
||||||
|
self.s.cookies = requests.utils.cookiejar_from_dict(j['cookies'])
|
||||||
|
self.storageClass.loads(j['storage'])
|
||||||
|
try:
|
||||||
|
msgList, contactList = self.get_msg()
|
||||||
|
except:
|
||||||
|
msgList = contactList = None
|
||||||
|
if (msgList or contactList) is None:
|
||||||
|
self.logout()
|
||||||
|
await load_last_login_status(self.s, j['cookies'])
|
||||||
|
logger.debug('server refused, loading login status failed.')
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'ErrMsg': 'server refused, loading login status failed.',
|
||||||
|
'Ret': -1003, }})
|
||||||
|
else:
|
||||||
|
if contactList:
|
||||||
|
for contact in contactList:
|
||||||
|
if '@@' in contact['UserName']:
|
||||||
|
update_local_chatrooms(self, [contact])
|
||||||
|
else:
|
||||||
|
update_local_friends(self, [contact])
|
||||||
|
if msgList:
|
||||||
|
msgList = produce_msg(self, msgList)
|
||||||
|
for msg in msgList: self.msgList.put(msg)
|
||||||
|
await self.start_receiving(exitCallback)
|
||||||
|
logger.debug('loading login status succeeded.')
|
||||||
|
if hasattr(loginCallback, '__call__'):
|
||||||
|
await loginCallback(self.storageClass.userName)
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'ErrMsg': 'loading login status succeeded.',
|
||||||
|
'Ret': 0, }})
|
||||||
|
|
||||||
|
async def load_last_login_status(session, cookiesDict):
|
||||||
|
try:
|
||||||
|
session.cookies = requests.utils.cookiejar_from_dict({
|
||||||
|
'webwxuvid': cookiesDict['webwxuvid'],
|
||||||
|
'webwx_auth_ticket': cookiesDict['webwx_auth_ticket'],
|
||||||
|
'login_frequency': '2',
|
||||||
|
'last_wxuin': cookiesDict['wxuin'],
|
||||||
|
'wxloadtime': cookiesDict['wxloadtime'] + '_expired',
|
||||||
|
'wxpluginkey': cookiesDict['wxloadtime'],
|
||||||
|
'wxuin': cookiesDict['wxuin'],
|
||||||
|
'mm_lang': 'zh_CN',
|
||||||
|
'MM_WX_NOTIFY_STATE': '1',
|
||||||
|
'MM_WX_SOUND_STATE': '1', })
|
||||||
|
except:
|
||||||
|
logger.info('Load status for push login failed, we may have experienced a cookies change.')
|
||||||
|
logger.info('If you are using the newest version of itchat, you may report a bug.')
|
||||||
|
|
@ -0,0 +1,422 @@
|
||||||
|
import asyncio
|
||||||
|
import os, time, re, io
|
||||||
|
import threading
|
||||||
|
import json
|
||||||
|
import random
|
||||||
|
import traceback
|
||||||
|
import logging
|
||||||
|
try:
|
||||||
|
from httplib import BadStatusLine
|
||||||
|
except ImportError:
|
||||||
|
from http.client import BadStatusLine
|
||||||
|
|
||||||
|
import requests # type: ignore
|
||||||
|
from pyqrcode import QRCode
|
||||||
|
|
||||||
|
from .. import config, utils
|
||||||
|
from ..returnvalues import ReturnValue
|
||||||
|
from ..storage.templates import wrap_user_dict
|
||||||
|
from .contact import update_local_chatrooms, update_local_friends
|
||||||
|
from .messages import produce_msg
|
||||||
|
|
||||||
|
logger = logging.getLogger('itchat')
|
||||||
|
|
||||||
|
|
||||||
|
def load_login(core):
|
||||||
|
core.login = login
|
||||||
|
core.get_QRuuid = get_QRuuid
|
||||||
|
core.get_QR = get_QR
|
||||||
|
core.check_login = check_login
|
||||||
|
core.web_init = web_init
|
||||||
|
core.show_mobile_login = show_mobile_login
|
||||||
|
core.start_receiving = start_receiving
|
||||||
|
core.get_msg = get_msg
|
||||||
|
core.logout = logout
|
||||||
|
|
||||||
|
async def login(self, enableCmdQR=False, picDir=None, qrCallback=None, EventScanPayload=None,ScanStatus=None,event_stream=None,
|
||||||
|
loginCallback=None, exitCallback=None):
|
||||||
|
if self.alive or self.isLogging:
|
||||||
|
logger.warning('itchat has already logged in.')
|
||||||
|
return
|
||||||
|
self.isLogging = True
|
||||||
|
|
||||||
|
while self.isLogging:
|
||||||
|
uuid = await push_login(self)
|
||||||
|
if uuid:
|
||||||
|
payload = EventScanPayload(
|
||||||
|
status=ScanStatus.Waiting,
|
||||||
|
qrcode=f"qrcode/https://login.weixin.qq.com/l/{uuid}"
|
||||||
|
)
|
||||||
|
event_stream.emit('scan', payload)
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
else:
|
||||||
|
logger.info('Getting uuid of QR code.')
|
||||||
|
self.get_QRuuid()
|
||||||
|
payload = EventScanPayload(
|
||||||
|
status=ScanStatus.Waiting,
|
||||||
|
qrcode=f"https://login.weixin.qq.com/l/{self.uuid}"
|
||||||
|
)
|
||||||
|
print(f"https://wechaty.js.org/qrcode/https://login.weixin.qq.com/l/{self.uuid}")
|
||||||
|
event_stream.emit('scan', payload)
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
# logger.info('Please scan the QR code to log in.')
|
||||||
|
isLoggedIn = False
|
||||||
|
while not isLoggedIn:
|
||||||
|
status = await self.check_login()
|
||||||
|
# if hasattr(qrCallback, '__call__'):
|
||||||
|
# await qrCallback(uuid=self.uuid, status=status, qrcode=self.qrStorage.getvalue())
|
||||||
|
if status == '200':
|
||||||
|
isLoggedIn = True
|
||||||
|
payload = EventScanPayload(
|
||||||
|
status=ScanStatus.Scanned,
|
||||||
|
qrcode=f"https://login.weixin.qq.com/l/{self.uuid}"
|
||||||
|
)
|
||||||
|
event_stream.emit('scan', payload)
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
elif status == '201':
|
||||||
|
if isLoggedIn is not None:
|
||||||
|
logger.info('Please press confirm on your phone.')
|
||||||
|
isLoggedIn = None
|
||||||
|
payload = EventScanPayload(
|
||||||
|
status=ScanStatus.Waiting,
|
||||||
|
qrcode=f"https://login.weixin.qq.com/l/{self.uuid}"
|
||||||
|
)
|
||||||
|
event_stream.emit('scan', payload)
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
elif status != '408':
|
||||||
|
payload = EventScanPayload(
|
||||||
|
status=ScanStatus.Cancel,
|
||||||
|
qrcode=f"https://login.weixin.qq.com/l/{self.uuid}"
|
||||||
|
)
|
||||||
|
event_stream.emit('scan', payload)
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
break
|
||||||
|
if isLoggedIn:
|
||||||
|
payload = EventScanPayload(
|
||||||
|
status=ScanStatus.Confirmed,
|
||||||
|
qrcode=f"https://login.weixin.qq.com/l/{self.uuid}"
|
||||||
|
)
|
||||||
|
event_stream.emit('scan', payload)
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
break
|
||||||
|
elif self.isLogging:
|
||||||
|
logger.info('Log in time out, reloading QR code.')
|
||||||
|
payload = EventScanPayload(
|
||||||
|
status=ScanStatus.Timeout,
|
||||||
|
qrcode=f"https://login.weixin.qq.com/l/{self.uuid}"
|
||||||
|
)
|
||||||
|
event_stream.emit('scan', payload)
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
logger.info('Loading the contact, this may take a little while.')
|
||||||
|
await self.web_init()
|
||||||
|
await self.show_mobile_login()
|
||||||
|
self.get_contact(True)
|
||||||
|
if hasattr(loginCallback, '__call__'):
|
||||||
|
r = await loginCallback(self.storageClass.userName)
|
||||||
|
else:
|
||||||
|
utils.clear_screen()
|
||||||
|
if os.path.exists(picDir or config.DEFAULT_QR):
|
||||||
|
os.remove(picDir or config.DEFAULT_QR)
|
||||||
|
logger.info('Login successfully as %s' % self.storageClass.nickName)
|
||||||
|
await self.start_receiving(exitCallback)
|
||||||
|
self.isLogging = False
|
||||||
|
|
||||||
|
async def push_login(core):
|
||||||
|
cookiesDict = core.s.cookies.get_dict()
|
||||||
|
if 'wxuin' in cookiesDict:
|
||||||
|
url = '%s/cgi-bin/mmwebwx-bin/webwxpushloginurl?uin=%s' % (
|
||||||
|
config.BASE_URL, cookiesDict['wxuin'])
|
||||||
|
headers = { 'User-Agent' : config.USER_AGENT}
|
||||||
|
r = core.s.get(url, headers=headers).json()
|
||||||
|
if 'uuid' in r and r.get('ret') in (0, '0'):
|
||||||
|
core.uuid = r['uuid']
|
||||||
|
return r['uuid']
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_QRuuid(self):
|
||||||
|
url = '%s/jslogin' % config.BASE_URL
|
||||||
|
params = {
|
||||||
|
'appid' : 'wx782c26e4c19acffb',
|
||||||
|
'fun' : 'new',
|
||||||
|
'redirect_uri' : 'https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?mod=desktop',
|
||||||
|
'lang' : 'zh_CN' }
|
||||||
|
headers = { 'User-Agent' : config.USER_AGENT}
|
||||||
|
r = self.s.get(url, params=params, headers=headers)
|
||||||
|
regx = r'window.QRLogin.code = (\d+); window.QRLogin.uuid = "(\S+?)";'
|
||||||
|
data = re.search(regx, r.text)
|
||||||
|
if data and data.group(1) == '200':
|
||||||
|
self.uuid = data.group(2)
|
||||||
|
return self.uuid
|
||||||
|
|
||||||
|
async def get_QR(self, uuid=None, enableCmdQR=False, picDir=None, qrCallback=None):
|
||||||
|
uuid = uuid or self.uuid
|
||||||
|
picDir = picDir or config.DEFAULT_QR
|
||||||
|
qrStorage = io.BytesIO()
|
||||||
|
qrCode = QRCode('https://login.weixin.qq.com/l/' + uuid)
|
||||||
|
qrCode.png(qrStorage, scale=10)
|
||||||
|
if hasattr(qrCallback, '__call__'):
|
||||||
|
await qrCallback(uuid=uuid, status='0', qrcode=qrStorage.getvalue())
|
||||||
|
else:
|
||||||
|
with open(picDir, 'wb') as f:
|
||||||
|
f.write(qrStorage.getvalue())
|
||||||
|
if enableCmdQR:
|
||||||
|
utils.print_cmd_qr(qrCode.text(1), enableCmdQR=enableCmdQR)
|
||||||
|
else:
|
||||||
|
utils.print_qr(picDir)
|
||||||
|
return qrStorage
|
||||||
|
|
||||||
|
async def check_login(self, uuid=None):
|
||||||
|
uuid = uuid or self.uuid
|
||||||
|
url = '%s/cgi-bin/mmwebwx-bin/login' % config.BASE_URL
|
||||||
|
localTime = int(time.time())
|
||||||
|
params = 'loginicon=true&uuid=%s&tip=1&r=%s&_=%s' % (
|
||||||
|
uuid, int(-localTime / 1579), localTime)
|
||||||
|
headers = { 'User-Agent' : config.USER_AGENT}
|
||||||
|
r = self.s.get(url, params=params, headers=headers)
|
||||||
|
regx = r'window.code=(\d+)'
|
||||||
|
data = re.search(regx, r.text)
|
||||||
|
if data and data.group(1) == '200':
|
||||||
|
if await process_login_info(self, r.text):
|
||||||
|
return '200'
|
||||||
|
else:
|
||||||
|
return '400'
|
||||||
|
elif data:
|
||||||
|
return data.group(1)
|
||||||
|
else:
|
||||||
|
return '400'
|
||||||
|
|
||||||
|
async def process_login_info(core, loginContent):
|
||||||
|
''' when finish login (scanning qrcode)
|
||||||
|
* syncUrl and fileUploadingUrl will be fetched
|
||||||
|
* deviceid and msgid will be generated
|
||||||
|
* skey, wxsid, wxuin, pass_ticket will be fetched
|
||||||
|
'''
|
||||||
|
regx = r'window.redirect_uri="(\S+)";'
|
||||||
|
core.loginInfo['url'] = re.search(regx, loginContent).group(1)
|
||||||
|
headers = { 'User-Agent' : config.USER_AGENT,
|
||||||
|
'client-version' : config.UOS_PATCH_CLIENT_VERSION,
|
||||||
|
'extspam' : config.UOS_PATCH_EXTSPAM,
|
||||||
|
'referer' : 'https://wx.qq.com/?&lang=zh_CN&target=t'
|
||||||
|
}
|
||||||
|
r = core.s.get(core.loginInfo['url'], headers=headers, allow_redirects=False)
|
||||||
|
core.loginInfo['url'] = core.loginInfo['url'][:core.loginInfo['url'].rfind('/')]
|
||||||
|
for indexUrl, detailedUrl in (
|
||||||
|
("wx2.qq.com" , ("file.wx2.qq.com", "webpush.wx2.qq.com")),
|
||||||
|
("wx8.qq.com" , ("file.wx8.qq.com", "webpush.wx8.qq.com")),
|
||||||
|
("qq.com" , ("file.wx.qq.com", "webpush.wx.qq.com")),
|
||||||
|
("web2.wechat.com" , ("file.web2.wechat.com", "webpush.web2.wechat.com")),
|
||||||
|
("wechat.com" , ("file.web.wechat.com", "webpush.web.wechat.com"))):
|
||||||
|
fileUrl, syncUrl = ['https://%s/cgi-bin/mmwebwx-bin' % url for url in detailedUrl]
|
||||||
|
if indexUrl in core.loginInfo['url']:
|
||||||
|
core.loginInfo['fileUrl'], core.loginInfo['syncUrl'] = \
|
||||||
|
fileUrl, syncUrl
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
core.loginInfo['fileUrl'] = core.loginInfo['syncUrl'] = core.loginInfo['url']
|
||||||
|
core.loginInfo['deviceid'] = 'e' + repr(random.random())[2:17]
|
||||||
|
core.loginInfo['logintime'] = int(time.time() * 1e3)
|
||||||
|
core.loginInfo['BaseRequest'] = {}
|
||||||
|
cookies = core.s.cookies.get_dict()
|
||||||
|
skey = re.findall('<skey>(.*?)</skey>', r.text, re.S)[0]
|
||||||
|
pass_ticket = re.findall('<pass_ticket>(.*?)</pass_ticket>', r.text, re.S)[0]
|
||||||
|
core.loginInfo['skey'] = core.loginInfo['BaseRequest']['Skey'] = skey
|
||||||
|
core.loginInfo['wxsid'] = core.loginInfo['BaseRequest']['Sid'] = cookies["wxsid"]
|
||||||
|
core.loginInfo['wxuin'] = core.loginInfo['BaseRequest']['Uin'] = cookies["wxuin"]
|
||||||
|
core.loginInfo['pass_ticket'] = pass_ticket
|
||||||
|
|
||||||
|
# A question : why pass_ticket == DeviceID ?
|
||||||
|
# deviceID is only a randomly generated number
|
||||||
|
|
||||||
|
# UOS PATCH By luvletter2333, Sun Feb 28 10:00 PM
|
||||||
|
# for node in xml.dom.minidom.parseString(r.text).documentElement.childNodes:
|
||||||
|
# if node.nodeName == 'skey':
|
||||||
|
# core.loginInfo['skey'] = core.loginInfo['BaseRequest']['Skey'] = node.childNodes[0].data
|
||||||
|
# elif node.nodeName == 'wxsid':
|
||||||
|
# core.loginInfo['wxsid'] = core.loginInfo['BaseRequest']['Sid'] = node.childNodes[0].data
|
||||||
|
# elif node.nodeName == 'wxuin':
|
||||||
|
# core.loginInfo['wxuin'] = core.loginInfo['BaseRequest']['Uin'] = node.childNodes[0].data
|
||||||
|
# elif node.nodeName == 'pass_ticket':
|
||||||
|
# core.loginInfo['pass_ticket'] = core.loginInfo['BaseRequest']['DeviceID'] = node.childNodes[0].data
|
||||||
|
if not all([key in core.loginInfo for key in ('skey', 'wxsid', 'wxuin', 'pass_ticket')]):
|
||||||
|
logger.error('Your wechat account may be LIMITED to log in WEB wechat, error info:\n%s' % r.text)
|
||||||
|
core.isLogging = False
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def web_init(self):
|
||||||
|
url = '%s/webwxinit' % self.loginInfo['url']
|
||||||
|
params = {
|
||||||
|
'r': int(-time.time() / 1579),
|
||||||
|
'pass_ticket': self.loginInfo['pass_ticket'], }
|
||||||
|
data = { 'BaseRequest': self.loginInfo['BaseRequest'], }
|
||||||
|
headers = {
|
||||||
|
'ContentType': 'application/json; charset=UTF-8',
|
||||||
|
'User-Agent' : config.USER_AGENT, }
|
||||||
|
r = self.s.post(url, params=params, data=json.dumps(data), headers=headers)
|
||||||
|
dic = json.loads(r.content.decode('utf-8', 'replace'))
|
||||||
|
# deal with login info
|
||||||
|
utils.emoji_formatter(dic['User'], 'NickName')
|
||||||
|
self.loginInfo['InviteStartCount'] = int(dic['InviteStartCount'])
|
||||||
|
self.loginInfo['User'] = wrap_user_dict(utils.struct_friend_info(dic['User']))
|
||||||
|
self.memberList.append(self.loginInfo['User'])
|
||||||
|
self.loginInfo['SyncKey'] = dic['SyncKey']
|
||||||
|
self.loginInfo['synckey'] = '|'.join(['%s_%s' % (item['Key'], item['Val'])
|
||||||
|
for item in dic['SyncKey']['List']])
|
||||||
|
self.storageClass.userName = dic['User']['UserName']
|
||||||
|
self.storageClass.nickName = dic['User']['NickName']
|
||||||
|
# deal with contact list returned when init
|
||||||
|
contactList = dic.get('ContactList', [])
|
||||||
|
chatroomList, otherList = [], []
|
||||||
|
for m in contactList:
|
||||||
|
if m['Sex'] != 0:
|
||||||
|
otherList.append(m)
|
||||||
|
elif '@@' in m['UserName']:
|
||||||
|
m['MemberList'] = [] # don't let dirty info pollute the list
|
||||||
|
chatroomList.append(m)
|
||||||
|
elif '@' in m['UserName']:
|
||||||
|
# mp will be dealt in update_local_friends as well
|
||||||
|
otherList.append(m)
|
||||||
|
if chatroomList:
|
||||||
|
update_local_chatrooms(self, chatroomList)
|
||||||
|
if otherList:
|
||||||
|
update_local_friends(self, otherList)
|
||||||
|
return dic
|
||||||
|
|
||||||
|
async def show_mobile_login(self):
|
||||||
|
url = '%s/webwxstatusnotify?lang=zh_CN&pass_ticket=%s' % (
|
||||||
|
self.loginInfo['url'], self.loginInfo['pass_ticket'])
|
||||||
|
data = {
|
||||||
|
'BaseRequest' : self.loginInfo['BaseRequest'],
|
||||||
|
'Code' : 3,
|
||||||
|
'FromUserName' : self.storageClass.userName,
|
||||||
|
'ToUserName' : self.storageClass.userName,
|
||||||
|
'ClientMsgId' : int(time.time()), }
|
||||||
|
headers = {
|
||||||
|
'ContentType': 'application/json; charset=UTF-8',
|
||||||
|
'User-Agent' : config.USER_AGENT, }
|
||||||
|
r = self.s.post(url, data=json.dumps(data), headers=headers)
|
||||||
|
return ReturnValue(rawResponse=r)
|
||||||
|
|
||||||
|
async def start_receiving(self, exitCallback=None, getReceivingFnOnly=False):
|
||||||
|
self.alive = True
|
||||||
|
def maintain_loop():
|
||||||
|
retryCount = 0
|
||||||
|
while self.alive:
|
||||||
|
try:
|
||||||
|
i = sync_check(self)
|
||||||
|
if i is None:
|
||||||
|
self.alive = False
|
||||||
|
elif i == '0':
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
msgList, contactList = self.get_msg()
|
||||||
|
if msgList:
|
||||||
|
msgList = produce_msg(self, msgList)
|
||||||
|
for msg in msgList:
|
||||||
|
self.msgList.put(msg)
|
||||||
|
if contactList:
|
||||||
|
chatroomList, otherList = [], []
|
||||||
|
for contact in contactList:
|
||||||
|
if '@@' in contact['UserName']:
|
||||||
|
chatroomList.append(contact)
|
||||||
|
else:
|
||||||
|
otherList.append(contact)
|
||||||
|
chatroomMsg = update_local_chatrooms(self, chatroomList)
|
||||||
|
chatroomMsg['User'] = self.loginInfo['User']
|
||||||
|
self.msgList.put(chatroomMsg)
|
||||||
|
update_local_friends(self, otherList)
|
||||||
|
retryCount = 0
|
||||||
|
except requests.exceptions.ReadTimeout:
|
||||||
|
pass
|
||||||
|
except:
|
||||||
|
retryCount += 1
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
if self.receivingRetryCount < retryCount:
|
||||||
|
self.alive = False
|
||||||
|
else:
|
||||||
|
time.sleep(1)
|
||||||
|
self.logout()
|
||||||
|
if hasattr(exitCallback, '__call__'):
|
||||||
|
exitCallback(self.storageClass.userName)
|
||||||
|
else:
|
||||||
|
logger.info('LOG OUT!')
|
||||||
|
if getReceivingFnOnly:
|
||||||
|
return maintain_loop
|
||||||
|
else:
|
||||||
|
maintainThread = threading.Thread(target=maintain_loop)
|
||||||
|
maintainThread.setDaemon(True)
|
||||||
|
maintainThread.start()
|
||||||
|
|
||||||
|
def sync_check(self):
|
||||||
|
url = '%s/synccheck' % self.loginInfo.get('syncUrl', self.loginInfo['url'])
|
||||||
|
params = {
|
||||||
|
'r' : int(time.time() * 1000),
|
||||||
|
'skey' : self.loginInfo['skey'],
|
||||||
|
'sid' : self.loginInfo['wxsid'],
|
||||||
|
'uin' : self.loginInfo['wxuin'],
|
||||||
|
'deviceid' : self.loginInfo['deviceid'],
|
||||||
|
'synckey' : self.loginInfo['synckey'],
|
||||||
|
'_' : self.loginInfo['logintime'], }
|
||||||
|
headers = { 'User-Agent' : config.USER_AGENT}
|
||||||
|
self.loginInfo['logintime'] += 1
|
||||||
|
try:
|
||||||
|
r = self.s.get(url, params=params, headers=headers, timeout=config.TIMEOUT)
|
||||||
|
except requests.exceptions.ConnectionError as e:
|
||||||
|
try:
|
||||||
|
if not isinstance(e.args[0].args[1], BadStatusLine):
|
||||||
|
raise
|
||||||
|
# will return a package with status '0 -'
|
||||||
|
# and value like:
|
||||||
|
# 6f:00:8a:9c:09:74:e4:d8:e0:14:bf:96:3a:56:a0:64:1b:a4:25:5d:12:f4:31:a5:30:f1:c6:48:5f:c3:75:6a:99:93
|
||||||
|
# seems like status of typing, but before I make further achievement code will remain like this
|
||||||
|
return '2'
|
||||||
|
except:
|
||||||
|
raise
|
||||||
|
r.raise_for_status()
|
||||||
|
regx = r'window.synccheck={retcode:"(\d+)",selector:"(\d+)"}'
|
||||||
|
pm = re.search(regx, r.text)
|
||||||
|
if pm is None or pm.group(1) != '0':
|
||||||
|
logger.debug('Unexpected sync check result: %s' % r.text)
|
||||||
|
return None
|
||||||
|
return pm.group(2)
|
||||||
|
|
||||||
|
def get_msg(self):
|
||||||
|
self.loginInfo['deviceid'] = 'e' + repr(random.random())[2:17]
|
||||||
|
url = '%s/webwxsync?sid=%s&skey=%s&pass_ticket=%s' % (
|
||||||
|
self.loginInfo['url'], self.loginInfo['wxsid'],
|
||||||
|
self.loginInfo['skey'],self.loginInfo['pass_ticket'])
|
||||||
|
data = {
|
||||||
|
'BaseRequest' : self.loginInfo['BaseRequest'],
|
||||||
|
'SyncKey' : self.loginInfo['SyncKey'],
|
||||||
|
'rr' : ~int(time.time()), }
|
||||||
|
headers = {
|
||||||
|
'ContentType': 'application/json; charset=UTF-8',
|
||||||
|
'User-Agent' : config.USER_AGENT }
|
||||||
|
r = self.s.post(url, data=json.dumps(data), headers=headers, timeout=config.TIMEOUT)
|
||||||
|
dic = json.loads(r.content.decode('utf-8', 'replace'))
|
||||||
|
if dic['BaseResponse']['Ret'] != 0: return None, None
|
||||||
|
self.loginInfo['SyncKey'] = dic['SyncKey']
|
||||||
|
self.loginInfo['synckey'] = '|'.join(['%s_%s' % (item['Key'], item['Val'])
|
||||||
|
for item in dic['SyncCheckKey']['List']])
|
||||||
|
return dic['AddMsgList'], dic['ModContactList']
|
||||||
|
|
||||||
|
def logout(self):
|
||||||
|
if self.alive:
|
||||||
|
url = '%s/webwxlogout' % self.loginInfo['url']
|
||||||
|
params = {
|
||||||
|
'redirect' : 1,
|
||||||
|
'type' : 1,
|
||||||
|
'skey' : self.loginInfo['skey'], }
|
||||||
|
headers = { 'User-Agent' : config.USER_AGENT}
|
||||||
|
self.s.get(url, params=params, headers=headers)
|
||||||
|
self.alive = False
|
||||||
|
self.isLogging = False
|
||||||
|
self.s.cookies.clear()
|
||||||
|
del self.chatroomList[:]
|
||||||
|
del self.memberList[:]
|
||||||
|
del self.mpList[:]
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'ErrMsg': 'logout successfully.',
|
||||||
|
'Ret': 0, }})
|
||||||
|
|
@ -0,0 +1,527 @@
|
||||||
|
import os, time, re, io
|
||||||
|
import json
|
||||||
|
import mimetypes, hashlib
|
||||||
|
import logging
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
|
||||||
|
from .. import config, utils
|
||||||
|
from ..returnvalues import ReturnValue
|
||||||
|
from ..storage import templates
|
||||||
|
from .contact import update_local_uin
|
||||||
|
|
||||||
|
logger = logging.getLogger('itchat')
|
||||||
|
|
||||||
|
def load_messages(core):
|
||||||
|
core.send_raw_msg = send_raw_msg
|
||||||
|
core.send_msg = send_msg
|
||||||
|
core.upload_file = upload_file
|
||||||
|
core.send_file = send_file
|
||||||
|
core.send_image = send_image
|
||||||
|
core.send_video = send_video
|
||||||
|
core.send = send
|
||||||
|
core.revoke = revoke
|
||||||
|
|
||||||
|
async def get_download_fn(core, url, msgId):
|
||||||
|
async def download_fn(downloadDir=None):
|
||||||
|
params = {
|
||||||
|
'msgid': msgId,
|
||||||
|
'skey': core.loginInfo['skey'],}
|
||||||
|
headers = { 'User-Agent' : config.USER_AGENT}
|
||||||
|
r = core.s.get(url, params=params, stream=True, headers = headers)
|
||||||
|
tempStorage = io.BytesIO()
|
||||||
|
for block in r.iter_content(1024):
|
||||||
|
tempStorage.write(block)
|
||||||
|
if downloadDir is None:
|
||||||
|
return tempStorage.getvalue()
|
||||||
|
with open(downloadDir, 'wb') as f:
|
||||||
|
f.write(tempStorage.getvalue())
|
||||||
|
tempStorage.seek(0)
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'ErrMsg': 'Successfully downloaded',
|
||||||
|
'Ret': 0, },
|
||||||
|
'PostFix': utils.get_image_postfix(tempStorage.read(20)), })
|
||||||
|
return download_fn
|
||||||
|
|
||||||
|
def produce_msg(core, msgList):
|
||||||
|
''' for messages types
|
||||||
|
* 40 msg, 43 videochat, 50 VOIPMSG, 52 voipnotifymsg
|
||||||
|
* 53 webwxvoipnotifymsg, 9999 sysnotice
|
||||||
|
'''
|
||||||
|
rl = []
|
||||||
|
srl = [40, 43, 50, 52, 53, 9999]
|
||||||
|
for m in msgList:
|
||||||
|
# get actual opposite
|
||||||
|
if m['FromUserName'] == core.storageClass.userName:
|
||||||
|
actualOpposite = m['ToUserName']
|
||||||
|
else:
|
||||||
|
actualOpposite = m['FromUserName']
|
||||||
|
# produce basic message
|
||||||
|
if '@@' in m['FromUserName'] or '@@' in m['ToUserName']:
|
||||||
|
produce_group_chat(core, m)
|
||||||
|
else:
|
||||||
|
utils.msg_formatter(m, 'Content')
|
||||||
|
# set user of msg
|
||||||
|
if '@@' in actualOpposite:
|
||||||
|
m['User'] = core.search_chatrooms(userName=actualOpposite) or \
|
||||||
|
templates.Chatroom({'UserName': actualOpposite})
|
||||||
|
# we don't need to update chatroom here because we have
|
||||||
|
# updated once when producing basic message
|
||||||
|
elif actualOpposite in ('filehelper', 'fmessage'):
|
||||||
|
m['User'] = templates.User({'UserName': actualOpposite})
|
||||||
|
else:
|
||||||
|
m['User'] = core.search_mps(userName=actualOpposite) or \
|
||||||
|
core.search_friends(userName=actualOpposite) or \
|
||||||
|
templates.User(userName=actualOpposite)
|
||||||
|
# by default we think there may be a user missing not a mp
|
||||||
|
m['User'].core = core
|
||||||
|
if m['MsgType'] == 1: # words
|
||||||
|
if m['Url']:
|
||||||
|
regx = r'(.+?\(.+?\))'
|
||||||
|
data = re.search(regx, m['Content'])
|
||||||
|
data = 'Map' if data is None else data.group(1)
|
||||||
|
msg = {
|
||||||
|
'Type': 'Map',
|
||||||
|
'Text': data,}
|
||||||
|
else:
|
||||||
|
msg = {
|
||||||
|
'Type': 'Text',
|
||||||
|
'Text': m['Content'],}
|
||||||
|
elif m['MsgType'] == 3 or m['MsgType'] == 47: # picture
|
||||||
|
download_fn = get_download_fn(core,
|
||||||
|
'%s/webwxgetmsgimg' % core.loginInfo['url'], m['NewMsgId'])
|
||||||
|
msg = {
|
||||||
|
'Type' : 'Picture',
|
||||||
|
'FileName' : '%s.%s' % (time.strftime('%y%m%d-%H%M%S', time.localtime()),
|
||||||
|
'png' if m['MsgType'] == 3 else 'gif'),
|
||||||
|
'Text' : download_fn, }
|
||||||
|
elif m['MsgType'] == 34: # voice
|
||||||
|
download_fn = get_download_fn(core,
|
||||||
|
'%s/webwxgetvoice' % core.loginInfo['url'], m['NewMsgId'])
|
||||||
|
msg = {
|
||||||
|
'Type': 'Recording',
|
||||||
|
'FileName' : '%s.mp3' % time.strftime('%y%m%d-%H%M%S', time.localtime()),
|
||||||
|
'Text': download_fn,}
|
||||||
|
elif m['MsgType'] == 37: # friends
|
||||||
|
m['User']['UserName'] = m['RecommendInfo']['UserName']
|
||||||
|
msg = {
|
||||||
|
'Type': 'Friends',
|
||||||
|
'Text': {
|
||||||
|
'status' : m['Status'],
|
||||||
|
'userName' : m['RecommendInfo']['UserName'],
|
||||||
|
'verifyContent' : m['Ticket'],
|
||||||
|
'autoUpdate' : m['RecommendInfo'], }, }
|
||||||
|
m['User'].verifyDict = msg['Text']
|
||||||
|
elif m['MsgType'] == 42: # name card
|
||||||
|
msg = {
|
||||||
|
'Type': 'Card',
|
||||||
|
'Text': m['RecommendInfo'], }
|
||||||
|
elif m['MsgType'] in (43, 62): # tiny video
|
||||||
|
msgId = m['MsgId']
|
||||||
|
async def download_video(videoDir=None):
|
||||||
|
url = '%s/webwxgetvideo' % core.loginInfo['url']
|
||||||
|
params = {
|
||||||
|
'msgid': msgId,
|
||||||
|
'skey': core.loginInfo['skey'],}
|
||||||
|
headers = {'Range': 'bytes=0-', 'User-Agent' : config.USER_AGENT}
|
||||||
|
r = core.s.get(url, params=params, headers=headers, stream=True)
|
||||||
|
tempStorage = io.BytesIO()
|
||||||
|
for block in r.iter_content(1024):
|
||||||
|
tempStorage.write(block)
|
||||||
|
if videoDir is None:
|
||||||
|
return tempStorage.getvalue()
|
||||||
|
with open(videoDir, 'wb') as f:
|
||||||
|
f.write(tempStorage.getvalue())
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'ErrMsg': 'Successfully downloaded',
|
||||||
|
'Ret': 0, }})
|
||||||
|
msg = {
|
||||||
|
'Type': 'Video',
|
||||||
|
'FileName' : '%s.mp4' % time.strftime('%y%m%d-%H%M%S', time.localtime()),
|
||||||
|
'Text': download_video, }
|
||||||
|
elif m['MsgType'] == 49: # sharing
|
||||||
|
if m['AppMsgType'] == 0: # chat history
|
||||||
|
msg = {
|
||||||
|
'Type': 'Note',
|
||||||
|
'Text': m['Content'], }
|
||||||
|
elif m['AppMsgType'] == 6:
|
||||||
|
rawMsg = m
|
||||||
|
cookiesList = {name:data for name,data in core.s.cookies.items()}
|
||||||
|
async def download_atta(attaDir=None):
|
||||||
|
url = core.loginInfo['fileUrl'] + '/webwxgetmedia'
|
||||||
|
params = {
|
||||||
|
'sender': rawMsg['FromUserName'],
|
||||||
|
'mediaid': rawMsg['MediaId'],
|
||||||
|
'filename': rawMsg['FileName'],
|
||||||
|
'fromuser': core.loginInfo['wxuin'],
|
||||||
|
'pass_ticket': 'undefined',
|
||||||
|
'webwx_data_ticket': cookiesList['webwx_data_ticket'],}
|
||||||
|
headers = { 'User-Agent' : config.USER_AGENT}
|
||||||
|
r = core.s.get(url, params=params, stream=True, headers=headers)
|
||||||
|
tempStorage = io.BytesIO()
|
||||||
|
for block in r.iter_content(1024):
|
||||||
|
tempStorage.write(block)
|
||||||
|
if attaDir is None:
|
||||||
|
return tempStorage.getvalue()
|
||||||
|
with open(attaDir, 'wb') as f:
|
||||||
|
f.write(tempStorage.getvalue())
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'ErrMsg': 'Successfully downloaded',
|
||||||
|
'Ret': 0, }})
|
||||||
|
msg = {
|
||||||
|
'Type': 'Attachment',
|
||||||
|
'Text': download_atta, }
|
||||||
|
elif m['AppMsgType'] == 8:
|
||||||
|
download_fn = get_download_fn(core,
|
||||||
|
'%s/webwxgetmsgimg' % core.loginInfo['url'], m['NewMsgId'])
|
||||||
|
msg = {
|
||||||
|
'Type' : 'Picture',
|
||||||
|
'FileName' : '%s.gif' % (
|
||||||
|
time.strftime('%y%m%d-%H%M%S', time.localtime())),
|
||||||
|
'Text' : download_fn, }
|
||||||
|
elif m['AppMsgType'] == 17:
|
||||||
|
msg = {
|
||||||
|
'Type': 'Note',
|
||||||
|
'Text': m['FileName'], }
|
||||||
|
elif m['AppMsgType'] == 2000:
|
||||||
|
regx = r'\[CDATA\[(.+?)\][\s\S]+?\[CDATA\[(.+?)\]'
|
||||||
|
data = re.search(regx, m['Content'])
|
||||||
|
if data:
|
||||||
|
data = data.group(2).split(u'\u3002')[0]
|
||||||
|
else:
|
||||||
|
data = 'You may found detailed info in Content key.'
|
||||||
|
msg = {
|
||||||
|
'Type': 'Note',
|
||||||
|
'Text': data, }
|
||||||
|
else:
|
||||||
|
msg = {
|
||||||
|
'Type': 'Sharing',
|
||||||
|
'Text': m['FileName'], }
|
||||||
|
elif m['MsgType'] == 51: # phone init
|
||||||
|
msg = update_local_uin(core, m)
|
||||||
|
elif m['MsgType'] == 10000:
|
||||||
|
msg = {
|
||||||
|
'Type': 'Note',
|
||||||
|
'Text': m['Content'],}
|
||||||
|
elif m['MsgType'] == 10002:
|
||||||
|
regx = r'\[CDATA\[(.+?)\]\]'
|
||||||
|
data = re.search(regx, m['Content'])
|
||||||
|
data = 'System message' if data is None else data.group(1).replace('\\', '')
|
||||||
|
msg = {
|
||||||
|
'Type': 'Note',
|
||||||
|
'Text': data, }
|
||||||
|
elif m['MsgType'] in srl:
|
||||||
|
msg = {
|
||||||
|
'Type': 'Useless',
|
||||||
|
'Text': 'UselessMsg', }
|
||||||
|
else:
|
||||||
|
logger.debug('Useless message received: %s\n%s' % (m['MsgType'], str(m)))
|
||||||
|
msg = {
|
||||||
|
'Type': 'Useless',
|
||||||
|
'Text': 'UselessMsg', }
|
||||||
|
m = dict(m, **msg)
|
||||||
|
rl.append(m)
|
||||||
|
return rl
|
||||||
|
|
||||||
|
def produce_group_chat(core, msg):
|
||||||
|
r = re.match('(@[0-9a-z]*?):<br/>(.*)$', msg['Content'])
|
||||||
|
if r:
|
||||||
|
actualUserName, content = r.groups()
|
||||||
|
chatroomUserName = msg['FromUserName']
|
||||||
|
elif msg['FromUserName'] == core.storageClass.userName:
|
||||||
|
actualUserName = core.storageClass.userName
|
||||||
|
content = msg['Content']
|
||||||
|
chatroomUserName = msg['ToUserName']
|
||||||
|
else:
|
||||||
|
msg['ActualUserName'] = core.storageClass.userName
|
||||||
|
msg['ActualNickName'] = core.storageClass.nickName
|
||||||
|
msg['IsAt'] = False
|
||||||
|
utils.msg_formatter(msg, 'Content')
|
||||||
|
return
|
||||||
|
chatroom = core.storageClass.search_chatrooms(userName=chatroomUserName)
|
||||||
|
member = utils.search_dict_list((chatroom or {}).get(
|
||||||
|
'MemberList') or [], 'UserName', actualUserName)
|
||||||
|
if member is None:
|
||||||
|
chatroom = core.update_chatroom(chatroomUserName)
|
||||||
|
member = utils.search_dict_list((chatroom or {}).get(
|
||||||
|
'MemberList') or [], 'UserName', actualUserName)
|
||||||
|
if member is None:
|
||||||
|
logger.debug('chatroom member fetch failed with %s' % actualUserName)
|
||||||
|
msg['ActualNickName'] = ''
|
||||||
|
msg['IsAt'] = False
|
||||||
|
else:
|
||||||
|
msg['ActualNickName'] = member.get('DisplayName', '') or member['NickName']
|
||||||
|
atFlag = '@' + (chatroom['Self'].get('DisplayName', '') or core.storageClass.nickName)
|
||||||
|
msg['IsAt'] = (
|
||||||
|
(atFlag + (u'\u2005' if u'\u2005' in msg['Content'] else ' '))
|
||||||
|
in msg['Content'] or msg['Content'].endswith(atFlag))
|
||||||
|
msg['ActualUserName'] = actualUserName
|
||||||
|
msg['Content'] = content
|
||||||
|
utils.msg_formatter(msg, 'Content')
|
||||||
|
|
||||||
|
async def send_raw_msg(self, msgType, content, toUserName):
|
||||||
|
url = '%s/webwxsendmsg' % self.loginInfo['url']
|
||||||
|
data = {
|
||||||
|
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||||
|
'Msg': {
|
||||||
|
'Type': msgType,
|
||||||
|
'Content': content,
|
||||||
|
'FromUserName': self.storageClass.userName,
|
||||||
|
'ToUserName': (toUserName if toUserName else self.storageClass.userName),
|
||||||
|
'LocalID': int(time.time() * 1e4),
|
||||||
|
'ClientMsgId': int(time.time() * 1e4),
|
||||||
|
},
|
||||||
|
'Scene': 0, }
|
||||||
|
headers = { 'ContentType': 'application/json; charset=UTF-8', 'User-Agent' : config.USER_AGENT}
|
||||||
|
r = self.s.post(url, headers=headers,
|
||||||
|
data=json.dumps(data, ensure_ascii=False).encode('utf8'))
|
||||||
|
return ReturnValue(rawResponse=r)
|
||||||
|
|
||||||
|
async def send_msg(self, msg='Test Message', toUserName=None):
|
||||||
|
logger.debug('Request to send a text message to %s: %s' % (toUserName, msg))
|
||||||
|
r = await self.send_raw_msg(1, msg, toUserName)
|
||||||
|
return r
|
||||||
|
|
||||||
|
def _prepare_file(fileDir, file_=None):
|
||||||
|
fileDict = {}
|
||||||
|
if file_:
|
||||||
|
if hasattr(file_, 'read'):
|
||||||
|
file_ = file_.read()
|
||||||
|
else:
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'ErrMsg': 'file_ param should be opened file',
|
||||||
|
'Ret': -1005, }})
|
||||||
|
else:
|
||||||
|
if not utils.check_file(fileDir):
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'ErrMsg': 'No file found in specific dir',
|
||||||
|
'Ret': -1002, }})
|
||||||
|
with open(fileDir, 'rb') as f:
|
||||||
|
file_ = f.read()
|
||||||
|
fileDict['fileSize'] = len(file_)
|
||||||
|
fileDict['fileMd5'] = hashlib.md5(file_).hexdigest()
|
||||||
|
fileDict['file_'] = io.BytesIO(file_)
|
||||||
|
return fileDict
|
||||||
|
|
||||||
|
def upload_file(self, fileDir, isPicture=False, isVideo=False,
|
||||||
|
toUserName='filehelper', file_=None, preparedFile=None):
|
||||||
|
logger.debug('Request to upload a %s: %s' % (
|
||||||
|
'picture' if isPicture else 'video' if isVideo else 'file', fileDir))
|
||||||
|
if not preparedFile:
|
||||||
|
preparedFile = _prepare_file(fileDir, file_)
|
||||||
|
if not preparedFile:
|
||||||
|
return preparedFile
|
||||||
|
fileSize, fileMd5, file_ = \
|
||||||
|
preparedFile['fileSize'], preparedFile['fileMd5'], preparedFile['file_']
|
||||||
|
fileSymbol = 'pic' if isPicture else 'video' if isVideo else'doc'
|
||||||
|
chunks = int((fileSize - 1) / 524288) + 1
|
||||||
|
clientMediaId = int(time.time() * 1e4)
|
||||||
|
uploadMediaRequest = json.dumps(OrderedDict([
|
||||||
|
('UploadType', 2),
|
||||||
|
('BaseRequest', self.loginInfo['BaseRequest']),
|
||||||
|
('ClientMediaId', clientMediaId),
|
||||||
|
('TotalLen', fileSize),
|
||||||
|
('StartPos', 0),
|
||||||
|
('DataLen', fileSize),
|
||||||
|
('MediaType', 4),
|
||||||
|
('FromUserName', self.storageClass.userName),
|
||||||
|
('ToUserName', toUserName),
|
||||||
|
('FileMd5', fileMd5)]
|
||||||
|
), separators = (',', ':'))
|
||||||
|
r = {'BaseResponse': {'Ret': -1005, 'ErrMsg': 'Empty file detected'}}
|
||||||
|
for chunk in range(chunks):
|
||||||
|
r = upload_chunk_file(self, fileDir, fileSymbol, fileSize,
|
||||||
|
file_, chunk, chunks, uploadMediaRequest)
|
||||||
|
file_.close()
|
||||||
|
if isinstance(r, dict):
|
||||||
|
return ReturnValue(r)
|
||||||
|
return ReturnValue(rawResponse=r)
|
||||||
|
|
||||||
|
def upload_chunk_file(core, fileDir, fileSymbol, fileSize,
|
||||||
|
file_, chunk, chunks, uploadMediaRequest):
|
||||||
|
url = core.loginInfo.get('fileUrl', core.loginInfo['url']) + \
|
||||||
|
'/webwxuploadmedia?f=json'
|
||||||
|
# save it on server
|
||||||
|
cookiesList = {name:data for name,data in core.s.cookies.items()}
|
||||||
|
fileType = mimetypes.guess_type(fileDir)[0] or 'application/octet-stream'
|
||||||
|
fileName = utils.quote(os.path.basename(fileDir))
|
||||||
|
files = OrderedDict([
|
||||||
|
('id', (None, 'WU_FILE_0')),
|
||||||
|
('name', (None, fileName)),
|
||||||
|
('type', (None, fileType)),
|
||||||
|
('lastModifiedDate', (None, time.strftime('%a %b %d %Y %H:%M:%S GMT+0800 (CST)'))),
|
||||||
|
('size', (None, str(fileSize))),
|
||||||
|
('chunks', (None, None)),
|
||||||
|
('chunk', (None, None)),
|
||||||
|
('mediatype', (None, fileSymbol)),
|
||||||
|
('uploadmediarequest', (None, uploadMediaRequest)),
|
||||||
|
('webwx_data_ticket', (None, cookiesList['webwx_data_ticket'])),
|
||||||
|
('pass_ticket', (None, core.loginInfo['pass_ticket'])),
|
||||||
|
('filename' , (fileName, file_.read(524288), 'application/octet-stream'))])
|
||||||
|
if chunks == 1:
|
||||||
|
del files['chunk']; del files['chunks']
|
||||||
|
else:
|
||||||
|
files['chunk'], files['chunks'] = (None, str(chunk)), (None, str(chunks))
|
||||||
|
headers = { 'User-Agent' : config.USER_AGENT}
|
||||||
|
return core.s.post(url, files=files, headers=headers, timeout=config.TIMEOUT)
|
||||||
|
|
||||||
|
async def send_file(self, fileDir, toUserName=None, mediaId=None, file_=None):
|
||||||
|
logger.debug('Request to send a file(mediaId: %s) to %s: %s' % (
|
||||||
|
mediaId, toUserName, fileDir))
|
||||||
|
if hasattr(fileDir, 'read'):
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'ErrMsg': 'fileDir param should not be an opened file in send_file',
|
||||||
|
'Ret': -1005, }})
|
||||||
|
if toUserName is None:
|
||||||
|
toUserName = self.storageClass.userName
|
||||||
|
preparedFile = _prepare_file(fileDir, file_)
|
||||||
|
if not preparedFile:
|
||||||
|
return preparedFile
|
||||||
|
fileSize = preparedFile['fileSize']
|
||||||
|
if mediaId is None:
|
||||||
|
r = self.upload_file(fileDir, preparedFile=preparedFile)
|
||||||
|
if r:
|
||||||
|
mediaId = r['MediaId']
|
||||||
|
else:
|
||||||
|
return r
|
||||||
|
url = '%s/webwxsendappmsg?fun=async&f=json' % self.loginInfo['url']
|
||||||
|
data = {
|
||||||
|
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||||
|
'Msg': {
|
||||||
|
'Type': 6,
|
||||||
|
'Content': ("<appmsg appid='wxeb7ec651dd0aefa9' sdkver=''><title>%s</title>" % os.path.basename(fileDir) +
|
||||||
|
"<des></des><action></action><type>6</type><content></content><url></url><lowurl></lowurl>" +
|
||||||
|
"<appattach><totallen>%s</totallen><attachid>%s</attachid>" % (str(fileSize), mediaId) +
|
||||||
|
"<fileext>%s</fileext></appattach><extinfo></extinfo></appmsg>" % os.path.splitext(fileDir)[1].replace('.','')),
|
||||||
|
'FromUserName': self.storageClass.userName,
|
||||||
|
'ToUserName': toUserName,
|
||||||
|
'LocalID': int(time.time() * 1e4),
|
||||||
|
'ClientMsgId': int(time.time() * 1e4), },
|
||||||
|
'Scene': 0, }
|
||||||
|
headers = {
|
||||||
|
'User-Agent': config.USER_AGENT,
|
||||||
|
'Content-Type': 'application/json;charset=UTF-8', }
|
||||||
|
r = self.s.post(url, headers=headers,
|
||||||
|
data=json.dumps(data, ensure_ascii=False).encode('utf8'))
|
||||||
|
return ReturnValue(rawResponse=r)
|
||||||
|
|
||||||
|
async def send_image(self, fileDir=None, toUserName=None, mediaId=None, file_=None):
|
||||||
|
logger.debug('Request to send a image(mediaId: %s) to %s: %s' % (
|
||||||
|
mediaId, toUserName, fileDir))
|
||||||
|
if fileDir or file_:
|
||||||
|
if hasattr(fileDir, 'read'):
|
||||||
|
file_, fileDir = fileDir, None
|
||||||
|
if fileDir is None:
|
||||||
|
fileDir = 'tmp.jpg' # specific fileDir to send gifs
|
||||||
|
else:
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'ErrMsg': 'Either fileDir or file_ should be specific',
|
||||||
|
'Ret': -1005, }})
|
||||||
|
if toUserName is None:
|
||||||
|
toUserName = self.storageClass.userName
|
||||||
|
if mediaId is None:
|
||||||
|
r = self.upload_file(fileDir, isPicture=not fileDir[-4:] == '.gif', file_=file_)
|
||||||
|
if r:
|
||||||
|
mediaId = r['MediaId']
|
||||||
|
else:
|
||||||
|
return r
|
||||||
|
url = '%s/webwxsendmsgimg?fun=async&f=json' % self.loginInfo['url']
|
||||||
|
data = {
|
||||||
|
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||||
|
'Msg': {
|
||||||
|
'Type': 3,
|
||||||
|
'MediaId': mediaId,
|
||||||
|
'FromUserName': self.storageClass.userName,
|
||||||
|
'ToUserName': toUserName,
|
||||||
|
'LocalID': int(time.time() * 1e4),
|
||||||
|
'ClientMsgId': int(time.time() * 1e4), },
|
||||||
|
'Scene': 0, }
|
||||||
|
if fileDir[-4:] == '.gif':
|
||||||
|
url = '%s/webwxsendemoticon?fun=sys' % self.loginInfo['url']
|
||||||
|
data['Msg']['Type'] = 47
|
||||||
|
data['Msg']['EmojiFlag'] = 2
|
||||||
|
headers = {
|
||||||
|
'User-Agent': config.USER_AGENT,
|
||||||
|
'Content-Type': 'application/json;charset=UTF-8', }
|
||||||
|
r = self.s.post(url, headers=headers,
|
||||||
|
data=json.dumps(data, ensure_ascii=False).encode('utf8'))
|
||||||
|
return ReturnValue(rawResponse=r)
|
||||||
|
|
||||||
|
async def send_video(self, fileDir=None, toUserName=None, mediaId=None, file_=None):
|
||||||
|
logger.debug('Request to send a video(mediaId: %s) to %s: %s' % (
|
||||||
|
mediaId, toUserName, fileDir))
|
||||||
|
if fileDir or file_:
|
||||||
|
if hasattr(fileDir, 'read'):
|
||||||
|
file_, fileDir = fileDir, None
|
||||||
|
if fileDir is None:
|
||||||
|
fileDir = 'tmp.mp4' # specific fileDir to send other formats
|
||||||
|
else:
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'ErrMsg': 'Either fileDir or file_ should be specific',
|
||||||
|
'Ret': -1005, }})
|
||||||
|
if toUserName is None:
|
||||||
|
toUserName = self.storageClass.userName
|
||||||
|
if mediaId is None:
|
||||||
|
r = self.upload_file(fileDir, isVideo=True, file_=file_)
|
||||||
|
if r:
|
||||||
|
mediaId = r['MediaId']
|
||||||
|
else:
|
||||||
|
return r
|
||||||
|
url = '%s/webwxsendvideomsg?fun=async&f=json&pass_ticket=%s' % (
|
||||||
|
self.loginInfo['url'], self.loginInfo['pass_ticket'])
|
||||||
|
data = {
|
||||||
|
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||||
|
'Msg': {
|
||||||
|
'Type' : 43,
|
||||||
|
'MediaId' : mediaId,
|
||||||
|
'FromUserName' : self.storageClass.userName,
|
||||||
|
'ToUserName' : toUserName,
|
||||||
|
'LocalID' : int(time.time() * 1e4),
|
||||||
|
'ClientMsgId' : int(time.time() * 1e4), },
|
||||||
|
'Scene': 0, }
|
||||||
|
headers = {
|
||||||
|
'User-Agent' : config.USER_AGENT,
|
||||||
|
'Content-Type': 'application/json;charset=UTF-8', }
|
||||||
|
r = self.s.post(url, headers=headers,
|
||||||
|
data=json.dumps(data, ensure_ascii=False).encode('utf8'))
|
||||||
|
return ReturnValue(rawResponse=r)
|
||||||
|
|
||||||
|
async def send(self, msg, toUserName=None, mediaId=None):
|
||||||
|
if not msg:
|
||||||
|
r = ReturnValue({'BaseResponse': {
|
||||||
|
'ErrMsg': 'No message.',
|
||||||
|
'Ret': -1005, }})
|
||||||
|
elif msg[:5] == '@fil@':
|
||||||
|
if mediaId is None:
|
||||||
|
r = await self.send_file(msg[5:], toUserName)
|
||||||
|
else:
|
||||||
|
r = await self.send_file(msg[5:], toUserName, mediaId)
|
||||||
|
elif msg[:5] == '@img@':
|
||||||
|
if mediaId is None:
|
||||||
|
r = await self.send_image(msg[5:], toUserName)
|
||||||
|
else:
|
||||||
|
r = await self.send_image(msg[5:], toUserName, mediaId)
|
||||||
|
elif msg[:5] == '@msg@':
|
||||||
|
r = await self.send_msg(msg[5:], toUserName)
|
||||||
|
elif msg[:5] == '@vid@':
|
||||||
|
if mediaId is None:
|
||||||
|
r = await self.send_video(msg[5:], toUserName)
|
||||||
|
else:
|
||||||
|
r = await self.send_video(msg[5:], toUserName, mediaId)
|
||||||
|
else:
|
||||||
|
r = await self.send_msg(msg, toUserName)
|
||||||
|
return r
|
||||||
|
|
||||||
|
async def revoke(self, msgId, toUserName, localId=None):
|
||||||
|
url = '%s/webwxrevokemsg' % self.loginInfo['url']
|
||||||
|
data = {
|
||||||
|
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||||
|
"ClientMsgId": localId or str(time.time() * 1e3),
|
||||||
|
"SvrMsgId": msgId,
|
||||||
|
"ToUserName": toUserName}
|
||||||
|
headers = {
|
||||||
|
'ContentType': 'application/json; charset=UTF-8',
|
||||||
|
'User-Agent' : config.USER_AGENT }
|
||||||
|
r = self.s.post(url, headers=headers,
|
||||||
|
data=json.dumps(data, ensure_ascii=False).encode('utf8'))
|
||||||
|
return ReturnValue(rawResponse=r)
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
import logging, traceback, sys, threading
|
||||||
|
try:
|
||||||
|
import Queue
|
||||||
|
except ImportError:
|
||||||
|
import queue as Queue # type: ignore
|
||||||
|
|
||||||
|
from ..log import set_logging
|
||||||
|
from ..utils import test_connect
|
||||||
|
from ..storage import templates
|
||||||
|
|
||||||
|
logger = logging.getLogger('itchat')
|
||||||
|
|
||||||
|
def load_register(core):
|
||||||
|
core.auto_login = auto_login
|
||||||
|
core.configured_reply = configured_reply
|
||||||
|
core.msg_register = msg_register
|
||||||
|
core.run = run
|
||||||
|
|
||||||
|
async def auto_login(self, EventScanPayload=None,ScanStatus=None,event_stream=None,
|
||||||
|
hotReload=True, statusStorageDir='itchat.pkl',
|
||||||
|
enableCmdQR=False, picDir=None, qrCallback=None,
|
||||||
|
loginCallback=None, exitCallback=None):
|
||||||
|
if not test_connect():
|
||||||
|
logger.info("You can't get access to internet or wechat domain, so exit.")
|
||||||
|
sys.exit()
|
||||||
|
self.useHotReload = hotReload
|
||||||
|
self.hotReloadDir = statusStorageDir
|
||||||
|
if hotReload:
|
||||||
|
if await self.load_login_status(statusStorageDir,
|
||||||
|
loginCallback=loginCallback, exitCallback=exitCallback):
|
||||||
|
return
|
||||||
|
await self.login(enableCmdQR=enableCmdQR, picDir=picDir, qrCallback=qrCallback, EventScanPayload=EventScanPayload, ScanStatus=ScanStatus, event_stream=event_stream,
|
||||||
|
loginCallback=loginCallback, exitCallback=exitCallback)
|
||||||
|
await self.dump_login_status(statusStorageDir)
|
||||||
|
else:
|
||||||
|
await self.login(enableCmdQR=enableCmdQR, picDir=picDir, qrCallback=qrCallback, EventScanPayload=EventScanPayload, ScanStatus=ScanStatus, event_stream=event_stream,
|
||||||
|
loginCallback=loginCallback, exitCallback=exitCallback)
|
||||||
|
|
||||||
|
async def configured_reply(self, event_stream, payload, message_container):
|
||||||
|
''' determine the type of message and reply if its method is defined
|
||||||
|
however, I use a strange way to determine whether a msg is from massive platform
|
||||||
|
I haven't found a better solution here
|
||||||
|
The main problem I'm worrying about is the mismatching of new friends added on phone
|
||||||
|
If you have any good idea, pleeeease report an issue. I will be more than grateful.
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
msg = self.msgList.get(timeout=1)
|
||||||
|
if 'MsgId' in msg.keys():
|
||||||
|
message_container[msg['MsgId']] = msg
|
||||||
|
except Queue.Empty:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if isinstance(msg['User'], templates.User):
|
||||||
|
replyFn = self.functionDict['FriendChat'].get(msg['Type'])
|
||||||
|
elif isinstance(msg['User'], templates.MassivePlatform):
|
||||||
|
replyFn = self.functionDict['MpChat'].get(msg['Type'])
|
||||||
|
elif isinstance(msg['User'], templates.Chatroom):
|
||||||
|
replyFn = self.functionDict['GroupChat'].get(msg['Type'])
|
||||||
|
if replyFn is None:
|
||||||
|
r = None
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
r = await replyFn(msg)
|
||||||
|
if r is not None:
|
||||||
|
await self.send(r, msg.get('FromUserName'))
|
||||||
|
except:
|
||||||
|
logger.warning(traceback.format_exc())
|
||||||
|
|
||||||
|
def msg_register(self, msgType, isFriendChat=False, isGroupChat=False, isMpChat=False):
|
||||||
|
''' a decorator constructor
|
||||||
|
return a specific decorator based on information given '''
|
||||||
|
if not (isinstance(msgType, list) or isinstance(msgType, tuple)):
|
||||||
|
msgType = [msgType]
|
||||||
|
def _msg_register(fn):
|
||||||
|
for _msgType in msgType:
|
||||||
|
if isFriendChat:
|
||||||
|
self.functionDict['FriendChat'][_msgType] = fn
|
||||||
|
if isGroupChat:
|
||||||
|
self.functionDict['GroupChat'][_msgType] = fn
|
||||||
|
if isMpChat:
|
||||||
|
self.functionDict['MpChat'][_msgType] = fn
|
||||||
|
if not any((isFriendChat, isGroupChat, isMpChat)):
|
||||||
|
self.functionDict['FriendChat'][_msgType] = fn
|
||||||
|
return fn
|
||||||
|
return _msg_register
|
||||||
|
|
||||||
|
async def run(self, debug=False, blockThread=True):
|
||||||
|
logger.info('Start auto replying.')
|
||||||
|
if debug:
|
||||||
|
set_logging(loggingLevel=logging.DEBUG)
|
||||||
|
async def reply_fn():
|
||||||
|
try:
|
||||||
|
while self.alive:
|
||||||
|
await self.configured_reply()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
if self.useHotReload:
|
||||||
|
await self.dump_login_status()
|
||||||
|
self.alive = False
|
||||||
|
logger.debug('itchat received an ^C and exit.')
|
||||||
|
logger.info('Bye~')
|
||||||
|
if blockThread:
|
||||||
|
await reply_fn()
|
||||||
|
else:
|
||||||
|
replyThread = threading.Thread(target=reply_fn)
|
||||||
|
replyThread.setDaemon(True)
|
||||||
|
replyThread.start()
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
from .contact import load_contact
|
||||||
|
from .hotreload import load_hotreload
|
||||||
|
from .login import load_login
|
||||||
|
from .messages import load_messages
|
||||||
|
from .register import load_register
|
||||||
|
|
||||||
|
def load_components(core):
|
||||||
|
load_contact(core)
|
||||||
|
load_hotreload(core)
|
||||||
|
load_login(core)
|
||||||
|
load_messages(core)
|
||||||
|
load_register(core)
|
||||||
|
|
@ -0,0 +1,519 @@
|
||||||
|
import time
|
||||||
|
import re
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import copy
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from .. import config, utils
|
||||||
|
from ..returnvalues import ReturnValue
|
||||||
|
from ..storage import contact_change
|
||||||
|
from ..utils import update_info_dict
|
||||||
|
|
||||||
|
logger = logging.getLogger('itchat')
|
||||||
|
|
||||||
|
|
||||||
|
def load_contact(core):
|
||||||
|
core.update_chatroom = update_chatroom
|
||||||
|
core.update_friend = update_friend
|
||||||
|
core.get_contact = get_contact
|
||||||
|
core.get_friends = get_friends
|
||||||
|
core.get_chatrooms = get_chatrooms
|
||||||
|
core.get_mps = get_mps
|
||||||
|
core.set_alias = set_alias
|
||||||
|
core.set_pinned = set_pinned
|
||||||
|
core.accept_friend = accept_friend
|
||||||
|
core.get_head_img = get_head_img
|
||||||
|
core.create_chatroom = create_chatroom
|
||||||
|
core.set_chatroom_name = set_chatroom_name
|
||||||
|
core.delete_member_from_chatroom = delete_member_from_chatroom
|
||||||
|
core.add_member_into_chatroom = add_member_into_chatroom
|
||||||
|
|
||||||
|
|
||||||
|
def update_chatroom(self, userName, detailedMember=False):
|
||||||
|
if not isinstance(userName, list):
|
||||||
|
userName = [userName]
|
||||||
|
url = '%s/webwxbatchgetcontact?type=ex&r=%s' % (
|
||||||
|
self.loginInfo['url'], int(time.time()))
|
||||||
|
headers = {
|
||||||
|
'ContentType': 'application/json; charset=UTF-8',
|
||||||
|
'User-Agent': config.USER_AGENT}
|
||||||
|
data = {
|
||||||
|
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||||
|
'Count': len(userName),
|
||||||
|
'List': [{
|
||||||
|
'UserName': u,
|
||||||
|
'ChatRoomId': '', } for u in userName], }
|
||||||
|
chatroomList = json.loads(self.s.post(url, data=json.dumps(data), headers=headers
|
||||||
|
).content.decode('utf8', 'replace')).get('ContactList')
|
||||||
|
if not chatroomList:
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'ErrMsg': 'No chatroom found',
|
||||||
|
'Ret': -1001, }})
|
||||||
|
|
||||||
|
if detailedMember:
|
||||||
|
def get_detailed_member_info(encryChatroomId, memberList):
|
||||||
|
url = '%s/webwxbatchgetcontact?type=ex&r=%s' % (
|
||||||
|
self.loginInfo['url'], int(time.time()))
|
||||||
|
headers = {
|
||||||
|
'ContentType': 'application/json; charset=UTF-8',
|
||||||
|
'User-Agent': config.USER_AGENT, }
|
||||||
|
data = {
|
||||||
|
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||||
|
'Count': len(memberList),
|
||||||
|
'List': [{
|
||||||
|
'UserName': member['UserName'],
|
||||||
|
'EncryChatRoomId': encryChatroomId}
|
||||||
|
for member in memberList], }
|
||||||
|
return json.loads(self.s.post(url, data=json.dumps(data), headers=headers
|
||||||
|
).content.decode('utf8', 'replace'))['ContactList']
|
||||||
|
MAX_GET_NUMBER = 50
|
||||||
|
for chatroom in chatroomList:
|
||||||
|
totalMemberList = []
|
||||||
|
for i in range(int(len(chatroom['MemberList']) / MAX_GET_NUMBER + 1)):
|
||||||
|
memberList = chatroom['MemberList'][i *
|
||||||
|
MAX_GET_NUMBER: (i+1)*MAX_GET_NUMBER]
|
||||||
|
totalMemberList += get_detailed_member_info(
|
||||||
|
chatroom['EncryChatRoomId'], memberList)
|
||||||
|
chatroom['MemberList'] = totalMemberList
|
||||||
|
|
||||||
|
update_local_chatrooms(self, chatroomList)
|
||||||
|
r = [self.storageClass.search_chatrooms(userName=c['UserName'])
|
||||||
|
for c in chatroomList]
|
||||||
|
return r if 1 < len(r) else r[0]
|
||||||
|
|
||||||
|
|
||||||
|
def update_friend(self, userName):
|
||||||
|
if not isinstance(userName, list):
|
||||||
|
userName = [userName]
|
||||||
|
url = '%s/webwxbatchgetcontact?type=ex&r=%s' % (
|
||||||
|
self.loginInfo['url'], int(time.time()))
|
||||||
|
headers = {
|
||||||
|
'ContentType': 'application/json; charset=UTF-8',
|
||||||
|
'User-Agent': config.USER_AGENT}
|
||||||
|
data = {
|
||||||
|
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||||
|
'Count': len(userName),
|
||||||
|
'List': [{
|
||||||
|
'UserName': u,
|
||||||
|
'EncryChatRoomId': '', } for u in userName], }
|
||||||
|
friendList = json.loads(self.s.post(url, data=json.dumps(data), headers=headers
|
||||||
|
).content.decode('utf8', 'replace')).get('ContactList')
|
||||||
|
|
||||||
|
update_local_friends(self, friendList)
|
||||||
|
r = [self.storageClass.search_friends(userName=f['UserName'])
|
||||||
|
for f in friendList]
|
||||||
|
return r if len(r) != 1 else r[0]
|
||||||
|
|
||||||
|
|
||||||
|
@contact_change
|
||||||
|
def update_local_chatrooms(core, l):
|
||||||
|
'''
|
||||||
|
get a list of chatrooms for updating local chatrooms
|
||||||
|
return a list of given chatrooms with updated info
|
||||||
|
'''
|
||||||
|
for chatroom in l:
|
||||||
|
# format new chatrooms
|
||||||
|
utils.emoji_formatter(chatroom, 'NickName')
|
||||||
|
for member in chatroom['MemberList']:
|
||||||
|
if 'NickName' in member:
|
||||||
|
utils.emoji_formatter(member, 'NickName')
|
||||||
|
if 'DisplayName' in member:
|
||||||
|
utils.emoji_formatter(member, 'DisplayName')
|
||||||
|
if 'RemarkName' in member:
|
||||||
|
utils.emoji_formatter(member, 'RemarkName')
|
||||||
|
# update it to old chatrooms
|
||||||
|
oldChatroom = utils.search_dict_list(
|
||||||
|
core.chatroomList, 'UserName', chatroom['UserName'])
|
||||||
|
if oldChatroom:
|
||||||
|
update_info_dict(oldChatroom, chatroom)
|
||||||
|
# - update other values
|
||||||
|
memberList = chatroom.get('MemberList', [])
|
||||||
|
oldMemberList = oldChatroom['MemberList']
|
||||||
|
if memberList:
|
||||||
|
for member in memberList:
|
||||||
|
oldMember = utils.search_dict_list(
|
||||||
|
oldMemberList, 'UserName', member['UserName'])
|
||||||
|
if oldMember:
|
||||||
|
update_info_dict(oldMember, member)
|
||||||
|
else:
|
||||||
|
oldMemberList.append(member)
|
||||||
|
else:
|
||||||
|
core.chatroomList.append(chatroom)
|
||||||
|
oldChatroom = utils.search_dict_list(
|
||||||
|
core.chatroomList, 'UserName', chatroom['UserName'])
|
||||||
|
# delete useless members
|
||||||
|
if len(chatroom['MemberList']) != len(oldChatroom['MemberList']) and \
|
||||||
|
chatroom['MemberList']:
|
||||||
|
existsUserNames = [member['UserName']
|
||||||
|
for member in chatroom['MemberList']]
|
||||||
|
delList = []
|
||||||
|
for i, member in enumerate(oldChatroom['MemberList']):
|
||||||
|
if member['UserName'] not in existsUserNames:
|
||||||
|
delList.append(i)
|
||||||
|
delList.sort(reverse=True)
|
||||||
|
for i in delList:
|
||||||
|
del oldChatroom['MemberList'][i]
|
||||||
|
# - update OwnerUin
|
||||||
|
if oldChatroom.get('ChatRoomOwner') and oldChatroom.get('MemberList'):
|
||||||
|
owner = utils.search_dict_list(oldChatroom['MemberList'],
|
||||||
|
'UserName', oldChatroom['ChatRoomOwner'])
|
||||||
|
oldChatroom['OwnerUin'] = (owner or {}).get('Uin', 0)
|
||||||
|
# - update IsAdmin
|
||||||
|
if 'OwnerUin' in oldChatroom and oldChatroom['OwnerUin'] != 0:
|
||||||
|
oldChatroom['IsAdmin'] = \
|
||||||
|
oldChatroom['OwnerUin'] == int(core.loginInfo['wxuin'])
|
||||||
|
else:
|
||||||
|
oldChatroom['IsAdmin'] = None
|
||||||
|
# - update Self
|
||||||
|
newSelf = utils.search_dict_list(oldChatroom['MemberList'],
|
||||||
|
'UserName', core.storageClass.userName)
|
||||||
|
oldChatroom['Self'] = newSelf or copy.deepcopy(core.loginInfo['User'])
|
||||||
|
return {
|
||||||
|
'Type': 'System',
|
||||||
|
'Text': [chatroom['UserName'] for chatroom in l],
|
||||||
|
'SystemInfo': 'chatrooms',
|
||||||
|
'FromUserName': core.storageClass.userName,
|
||||||
|
'ToUserName': core.storageClass.userName, }
|
||||||
|
|
||||||
|
|
||||||
|
@contact_change
|
||||||
|
def update_local_friends(core, l):
|
||||||
|
'''
|
||||||
|
get a list of friends or mps for updating local contact
|
||||||
|
'''
|
||||||
|
fullList = core.memberList + core.mpList
|
||||||
|
for friend in l:
|
||||||
|
if 'NickName' in friend:
|
||||||
|
utils.emoji_formatter(friend, 'NickName')
|
||||||
|
if 'DisplayName' in friend:
|
||||||
|
utils.emoji_formatter(friend, 'DisplayName')
|
||||||
|
if 'RemarkName' in friend:
|
||||||
|
utils.emoji_formatter(friend, 'RemarkName')
|
||||||
|
oldInfoDict = utils.search_dict_list(
|
||||||
|
fullList, 'UserName', friend['UserName'])
|
||||||
|
if oldInfoDict is None:
|
||||||
|
oldInfoDict = copy.deepcopy(friend)
|
||||||
|
if oldInfoDict['VerifyFlag'] & 8 == 0:
|
||||||
|
core.memberList.append(oldInfoDict)
|
||||||
|
else:
|
||||||
|
core.mpList.append(oldInfoDict)
|
||||||
|
else:
|
||||||
|
update_info_dict(oldInfoDict, friend)
|
||||||
|
|
||||||
|
|
||||||
|
@contact_change
|
||||||
|
def update_local_uin(core, msg):
|
||||||
|
'''
|
||||||
|
content contains uins and StatusNotifyUserName contains username
|
||||||
|
they are in same order, so what I do is to pair them together
|
||||||
|
|
||||||
|
I caught an exception in this method while not knowing why
|
||||||
|
but don't worry, it won't cause any problem
|
||||||
|
'''
|
||||||
|
uins = re.search('<username>([^<]*?)<', msg['Content'])
|
||||||
|
usernameChangedList = []
|
||||||
|
r = {
|
||||||
|
'Type': 'System',
|
||||||
|
'Text': usernameChangedList,
|
||||||
|
'SystemInfo': 'uins', }
|
||||||
|
if uins:
|
||||||
|
uins = uins.group(1).split(',')
|
||||||
|
usernames = msg['StatusNotifyUserName'].split(',')
|
||||||
|
if 0 < len(uins) == len(usernames):
|
||||||
|
for uin, username in zip(uins, usernames):
|
||||||
|
if not '@' in username:
|
||||||
|
continue
|
||||||
|
fullContact = core.memberList + core.chatroomList + core.mpList
|
||||||
|
userDicts = utils.search_dict_list(fullContact,
|
||||||
|
'UserName', username)
|
||||||
|
if userDicts:
|
||||||
|
if userDicts.get('Uin', 0) == 0:
|
||||||
|
userDicts['Uin'] = uin
|
||||||
|
usernameChangedList.append(username)
|
||||||
|
logger.debug('Uin fetched: %s, %s' % (username, uin))
|
||||||
|
else:
|
||||||
|
if userDicts['Uin'] != uin:
|
||||||
|
logger.debug('Uin changed: %s, %s' % (
|
||||||
|
userDicts['Uin'], uin))
|
||||||
|
else:
|
||||||
|
if '@@' in username:
|
||||||
|
core.storageClass.updateLock.release()
|
||||||
|
update_chatroom(core, username)
|
||||||
|
core.storageClass.updateLock.acquire()
|
||||||
|
newChatroomDict = utils.search_dict_list(
|
||||||
|
core.chatroomList, 'UserName', username)
|
||||||
|
if newChatroomDict is None:
|
||||||
|
newChatroomDict = utils.struct_friend_info({
|
||||||
|
'UserName': username,
|
||||||
|
'Uin': uin,
|
||||||
|
'Self': copy.deepcopy(core.loginInfo['User'])})
|
||||||
|
core.chatroomList.append(newChatroomDict)
|
||||||
|
else:
|
||||||
|
newChatroomDict['Uin'] = uin
|
||||||
|
elif '@' in username:
|
||||||
|
core.storageClass.updateLock.release()
|
||||||
|
update_friend(core, username)
|
||||||
|
core.storageClass.updateLock.acquire()
|
||||||
|
newFriendDict = utils.search_dict_list(
|
||||||
|
core.memberList, 'UserName', username)
|
||||||
|
if newFriendDict is None:
|
||||||
|
newFriendDict = utils.struct_friend_info({
|
||||||
|
'UserName': username,
|
||||||
|
'Uin': uin, })
|
||||||
|
core.memberList.append(newFriendDict)
|
||||||
|
else:
|
||||||
|
newFriendDict['Uin'] = uin
|
||||||
|
usernameChangedList.append(username)
|
||||||
|
logger.debug('Uin fetched: %s, %s' % (username, uin))
|
||||||
|
else:
|
||||||
|
logger.debug('Wrong length of uins & usernames: %s, %s' % (
|
||||||
|
len(uins), len(usernames)))
|
||||||
|
else:
|
||||||
|
logger.debug('No uins in 51 message')
|
||||||
|
logger.debug(msg['Content'])
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def get_contact(self, update=False):
|
||||||
|
if not update:
|
||||||
|
return utils.contact_deep_copy(self, self.chatroomList)
|
||||||
|
|
||||||
|
def _get_contact(seq=0):
|
||||||
|
url = '%s/webwxgetcontact?r=%s&seq=%s&skey=%s' % (self.loginInfo['url'],
|
||||||
|
int(time.time()), seq, self.loginInfo['skey'])
|
||||||
|
headers = {
|
||||||
|
'ContentType': 'application/json; charset=UTF-8',
|
||||||
|
'User-Agent': config.USER_AGENT, }
|
||||||
|
try:
|
||||||
|
r = self.s.get(url, headers=headers)
|
||||||
|
except:
|
||||||
|
logger.info(
|
||||||
|
'Failed to fetch contact, that may because of the amount of your chatrooms')
|
||||||
|
for chatroom in self.get_chatrooms():
|
||||||
|
self.update_chatroom(chatroom['UserName'], detailedMember=True)
|
||||||
|
return 0, []
|
||||||
|
j = json.loads(r.content.decode('utf-8', 'replace'))
|
||||||
|
return j.get('Seq', 0), j.get('MemberList')
|
||||||
|
seq, memberList = 0, []
|
||||||
|
while 1:
|
||||||
|
seq, batchMemberList = _get_contact(seq)
|
||||||
|
memberList.extend(batchMemberList)
|
||||||
|
if seq == 0:
|
||||||
|
break
|
||||||
|
chatroomList, otherList = [], []
|
||||||
|
for m in memberList:
|
||||||
|
if m['Sex'] != 0:
|
||||||
|
otherList.append(m)
|
||||||
|
elif '@@' in m['UserName']:
|
||||||
|
chatroomList.append(m)
|
||||||
|
elif '@' in m['UserName']:
|
||||||
|
# mp will be dealt in update_local_friends as well
|
||||||
|
otherList.append(m)
|
||||||
|
if chatroomList:
|
||||||
|
update_local_chatrooms(self, chatroomList)
|
||||||
|
if otherList:
|
||||||
|
update_local_friends(self, otherList)
|
||||||
|
return utils.contact_deep_copy(self, chatroomList)
|
||||||
|
|
||||||
|
|
||||||
|
def get_friends(self, update=False):
|
||||||
|
if update:
|
||||||
|
self.get_contact(update=True)
|
||||||
|
return utils.contact_deep_copy(self, self.memberList)
|
||||||
|
|
||||||
|
|
||||||
|
def get_chatrooms(self, update=False, contactOnly=False):
|
||||||
|
if contactOnly:
|
||||||
|
return self.get_contact(update=True)
|
||||||
|
else:
|
||||||
|
if update:
|
||||||
|
self.get_contact(True)
|
||||||
|
return utils.contact_deep_copy(self, self.chatroomList)
|
||||||
|
|
||||||
|
|
||||||
|
def get_mps(self, update=False):
|
||||||
|
if update:
|
||||||
|
self.get_contact(update=True)
|
||||||
|
return utils.contact_deep_copy(self, self.mpList)
|
||||||
|
|
||||||
|
|
||||||
|
def set_alias(self, userName, alias):
|
||||||
|
oldFriendInfo = utils.search_dict_list(
|
||||||
|
self.memberList, 'UserName', userName)
|
||||||
|
if oldFriendInfo is None:
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'Ret': -1001, }})
|
||||||
|
url = '%s/webwxoplog?lang=%s&pass_ticket=%s' % (
|
||||||
|
self.loginInfo['url'], 'zh_CN', self.loginInfo['pass_ticket'])
|
||||||
|
data = {
|
||||||
|
'UserName': userName,
|
||||||
|
'CmdId': 2,
|
||||||
|
'RemarkName': alias,
|
||||||
|
'BaseRequest': self.loginInfo['BaseRequest'], }
|
||||||
|
headers = {'User-Agent': config.USER_AGENT}
|
||||||
|
r = self.s.post(url, json.dumps(data, ensure_ascii=False).encode('utf8'),
|
||||||
|
headers=headers)
|
||||||
|
r = ReturnValue(rawResponse=r)
|
||||||
|
if r:
|
||||||
|
oldFriendInfo['RemarkName'] = alias
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def set_pinned(self, userName, isPinned=True):
|
||||||
|
url = '%s/webwxoplog?pass_ticket=%s' % (
|
||||||
|
self.loginInfo['url'], self.loginInfo['pass_ticket'])
|
||||||
|
data = {
|
||||||
|
'UserName': userName,
|
||||||
|
'CmdId': 3,
|
||||||
|
'OP': int(isPinned),
|
||||||
|
'BaseRequest': self.loginInfo['BaseRequest'], }
|
||||||
|
headers = {'User-Agent': config.USER_AGENT}
|
||||||
|
r = self.s.post(url, json=data, headers=headers)
|
||||||
|
return ReturnValue(rawResponse=r)
|
||||||
|
|
||||||
|
|
||||||
|
def accept_friend(self, userName, v4='', autoUpdate=True):
|
||||||
|
url = f"{self.loginInfo['url']}/webwxverifyuser?r={int(time.time())}&pass_ticket={self.loginInfo['pass_ticket']}"
|
||||||
|
data = {
|
||||||
|
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||||
|
'Opcode': 3, # 3
|
||||||
|
'VerifyUserListSize': 1,
|
||||||
|
'VerifyUserList': [{
|
||||||
|
'Value': userName,
|
||||||
|
'VerifyUserTicket': v4, }],
|
||||||
|
'VerifyContent': '',
|
||||||
|
'SceneListCount': 1,
|
||||||
|
'SceneList': [33],
|
||||||
|
'skey': self.loginInfo['skey'], }
|
||||||
|
headers = {
|
||||||
|
'ContentType': 'application/json; charset=UTF-8',
|
||||||
|
'User-Agent': config.USER_AGENT}
|
||||||
|
r = self.s.post(url, headers=headers,
|
||||||
|
data=json.dumps(data, ensure_ascii=False).encode('utf8', 'replace'))
|
||||||
|
if autoUpdate:
|
||||||
|
self.update_friend(userName)
|
||||||
|
return ReturnValue(rawResponse=r)
|
||||||
|
|
||||||
|
|
||||||
|
def get_head_img(self, userName=None, chatroomUserName=None, picDir=None):
|
||||||
|
''' get head image
|
||||||
|
* if you want to get chatroom header: only set chatroomUserName
|
||||||
|
* if you want to get friend header: only set userName
|
||||||
|
* if you want to get chatroom member header: set both
|
||||||
|
'''
|
||||||
|
params = {
|
||||||
|
'userName': userName or chatroomUserName or self.storageClass.userName,
|
||||||
|
'skey': self.loginInfo['skey'],
|
||||||
|
'type': 'big', }
|
||||||
|
url = '%s/webwxgeticon' % self.loginInfo['url']
|
||||||
|
if chatroomUserName is None:
|
||||||
|
infoDict = self.storageClass.search_friends(userName=userName)
|
||||||
|
if infoDict is None:
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'ErrMsg': 'No friend found',
|
||||||
|
'Ret': -1001, }})
|
||||||
|
else:
|
||||||
|
if userName is None:
|
||||||
|
url = '%s/webwxgetheadimg' % self.loginInfo['url']
|
||||||
|
else:
|
||||||
|
chatroom = self.storageClass.search_chatrooms(
|
||||||
|
userName=chatroomUserName)
|
||||||
|
if chatroomUserName is None:
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'ErrMsg': 'No chatroom found',
|
||||||
|
'Ret': -1001, }})
|
||||||
|
if 'EncryChatRoomId' in chatroom:
|
||||||
|
params['chatroomid'] = chatroom['EncryChatRoomId']
|
||||||
|
params['chatroomid'] = params.get(
|
||||||
|
'chatroomid') or chatroom['UserName']
|
||||||
|
headers = {'User-Agent': config.USER_AGENT}
|
||||||
|
r = self.s.get(url, params=params, stream=True, headers=headers)
|
||||||
|
tempStorage = io.BytesIO()
|
||||||
|
for block in r.iter_content(1024):
|
||||||
|
tempStorage.write(block)
|
||||||
|
if picDir is None:
|
||||||
|
return tempStorage.getvalue()
|
||||||
|
with open(picDir, 'wb') as f:
|
||||||
|
f.write(tempStorage.getvalue())
|
||||||
|
tempStorage.seek(0)
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'ErrMsg': 'Successfully downloaded',
|
||||||
|
'Ret': 0, },
|
||||||
|
'PostFix': utils.get_image_postfix(tempStorage.read(20)), })
|
||||||
|
|
||||||
|
|
||||||
|
def create_chatroom(self, memberList, topic=''):
|
||||||
|
url = '%s/webwxcreatechatroom?pass_ticket=%s&r=%s' % (
|
||||||
|
self.loginInfo['url'], self.loginInfo['pass_ticket'], int(time.time()))
|
||||||
|
data = {
|
||||||
|
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||||
|
'MemberCount': len(memberList.split(',')),
|
||||||
|
'MemberList': [{'UserName': member} for member in memberList.split(',')],
|
||||||
|
'Topic': topic, }
|
||||||
|
headers = {
|
||||||
|
'content-type': 'application/json; charset=UTF-8',
|
||||||
|
'User-Agent': config.USER_AGENT}
|
||||||
|
r = self.s.post(url, headers=headers,
|
||||||
|
data=json.dumps(data, ensure_ascii=False).encode('utf8', 'ignore'))
|
||||||
|
return ReturnValue(rawResponse=r)
|
||||||
|
|
||||||
|
|
||||||
|
def set_chatroom_name(self, chatroomUserName, name):
|
||||||
|
url = '%s/webwxupdatechatroom?fun=modtopic&pass_ticket=%s' % (
|
||||||
|
self.loginInfo['url'], self.loginInfo['pass_ticket'])
|
||||||
|
data = {
|
||||||
|
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||||
|
'ChatRoomName': chatroomUserName,
|
||||||
|
'NewTopic': name, }
|
||||||
|
headers = {
|
||||||
|
'content-type': 'application/json; charset=UTF-8',
|
||||||
|
'User-Agent': config.USER_AGENT}
|
||||||
|
r = self.s.post(url, headers=headers,
|
||||||
|
data=json.dumps(data, ensure_ascii=False).encode('utf8', 'ignore'))
|
||||||
|
return ReturnValue(rawResponse=r)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_member_from_chatroom(self, chatroomUserName, memberList):
|
||||||
|
url = '%s/webwxupdatechatroom?fun=delmember&pass_ticket=%s' % (
|
||||||
|
self.loginInfo['url'], self.loginInfo['pass_ticket'])
|
||||||
|
data = {
|
||||||
|
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||||
|
'ChatRoomName': chatroomUserName,
|
||||||
|
'DelMemberList': ','.join([member['UserName'] for member in memberList]), }
|
||||||
|
headers = {
|
||||||
|
'content-type': 'application/json; charset=UTF-8',
|
||||||
|
'User-Agent': config.USER_AGENT}
|
||||||
|
r = self.s.post(url, data=json.dumps(data), headers=headers)
|
||||||
|
return ReturnValue(rawResponse=r)
|
||||||
|
|
||||||
|
|
||||||
|
def add_member_into_chatroom(self, chatroomUserName, memberList,
|
||||||
|
useInvitation=False):
|
||||||
|
''' add or invite member into chatroom
|
||||||
|
* there are two ways to get members into chatroom: invite or directly add
|
||||||
|
* but for chatrooms with more than 40 users, you can only use invite
|
||||||
|
* but don't worry we will auto-force userInvitation for you when necessary
|
||||||
|
'''
|
||||||
|
if not useInvitation:
|
||||||
|
chatroom = self.storageClass.search_chatrooms(
|
||||||
|
userName=chatroomUserName)
|
||||||
|
if not chatroom:
|
||||||
|
chatroom = self.update_chatroom(chatroomUserName)
|
||||||
|
if len(chatroom['MemberList']) > self.loginInfo['InviteStartCount']:
|
||||||
|
useInvitation = True
|
||||||
|
if useInvitation:
|
||||||
|
fun, memberKeyName = 'invitemember', 'InviteMemberList'
|
||||||
|
else:
|
||||||
|
fun, memberKeyName = 'addmember', 'AddMemberList'
|
||||||
|
url = '%s/webwxupdatechatroom?fun=%s&pass_ticket=%s' % (
|
||||||
|
self.loginInfo['url'], fun, self.loginInfo['pass_ticket'])
|
||||||
|
params = {
|
||||||
|
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||||
|
'ChatRoomName': chatroomUserName,
|
||||||
|
memberKeyName: memberList, }
|
||||||
|
headers = {
|
||||||
|
'content-type': 'application/json; charset=UTF-8',
|
||||||
|
'User-Agent': config.USER_AGENT}
|
||||||
|
r = self.s.post(url, data=json.dumps(params), headers=headers)
|
||||||
|
return ReturnValue(rawResponse=r)
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
import pickle, os
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from ..config import VERSION
|
||||||
|
from ..returnvalues import ReturnValue
|
||||||
|
from ..storage import templates
|
||||||
|
from .contact import update_local_chatrooms, update_local_friends
|
||||||
|
from .messages import produce_msg
|
||||||
|
|
||||||
|
logger = logging.getLogger('itchat')
|
||||||
|
|
||||||
|
def load_hotreload(core):
|
||||||
|
core.dump_login_status = dump_login_status
|
||||||
|
core.load_login_status = load_login_status
|
||||||
|
|
||||||
|
def dump_login_status(self, fileDir=None):
|
||||||
|
fileDir = fileDir or self.hotReloadDir
|
||||||
|
try:
|
||||||
|
with open(fileDir, 'w') as f:
|
||||||
|
f.write('itchat - DELETE THIS')
|
||||||
|
os.remove(fileDir)
|
||||||
|
except:
|
||||||
|
raise Exception('Incorrect fileDir')
|
||||||
|
status = {
|
||||||
|
'version' : VERSION,
|
||||||
|
'loginInfo' : self.loginInfo,
|
||||||
|
'cookies' : self.s.cookies.get_dict(),
|
||||||
|
'storage' : self.storageClass.dumps()}
|
||||||
|
with open(fileDir, 'wb') as f:
|
||||||
|
pickle.dump(status, f)
|
||||||
|
logger.debug('Dump login status for hot reload successfully.')
|
||||||
|
|
||||||
|
def load_login_status(self, fileDir,
|
||||||
|
loginCallback=None, exitCallback=None):
|
||||||
|
try:
|
||||||
|
with open(fileDir, 'rb') as f:
|
||||||
|
j = pickle.load(f)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug('No such file, loading login status failed.')
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'ErrMsg': 'No such file, loading login status failed.',
|
||||||
|
'Ret': -1002, }})
|
||||||
|
|
||||||
|
if j.get('version', '') != VERSION:
|
||||||
|
logger.debug(('you have updated itchat from %s to %s, ' +
|
||||||
|
'so cached status is ignored') % (
|
||||||
|
j.get('version', 'old version'), VERSION))
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'ErrMsg': 'cached status ignored because of version',
|
||||||
|
'Ret': -1005, }})
|
||||||
|
self.loginInfo = j['loginInfo']
|
||||||
|
self.loginInfo['User'] = templates.User(self.loginInfo['User'])
|
||||||
|
self.loginInfo['User'].core = self
|
||||||
|
self.s.cookies = requests.utils.cookiejar_from_dict(j['cookies'])
|
||||||
|
self.storageClass.loads(j['storage'])
|
||||||
|
try:
|
||||||
|
msgList, contactList = self.get_msg()
|
||||||
|
except:
|
||||||
|
msgList = contactList = None
|
||||||
|
if (msgList or contactList) is None:
|
||||||
|
self.logout()
|
||||||
|
load_last_login_status(self.s, j['cookies'])
|
||||||
|
logger.debug('server refused, loading login status failed.')
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'ErrMsg': 'server refused, loading login status failed.',
|
||||||
|
'Ret': -1003, }})
|
||||||
|
else:
|
||||||
|
if contactList:
|
||||||
|
for contact in contactList:
|
||||||
|
if '@@' in contact['UserName']:
|
||||||
|
update_local_chatrooms(self, [contact])
|
||||||
|
else:
|
||||||
|
update_local_friends(self, [contact])
|
||||||
|
if msgList:
|
||||||
|
msgList = produce_msg(self, msgList)
|
||||||
|
for msg in msgList: self.msgList.put(msg)
|
||||||
|
self.start_receiving(exitCallback)
|
||||||
|
logger.debug('loading login status succeeded.')
|
||||||
|
if hasattr(loginCallback, '__call__'):
|
||||||
|
loginCallback()
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'ErrMsg': 'loading login status succeeded.',
|
||||||
|
'Ret': 0, }})
|
||||||
|
|
||||||
|
def load_last_login_status(session, cookiesDict):
|
||||||
|
try:
|
||||||
|
session.cookies = requests.utils.cookiejar_from_dict({
|
||||||
|
'webwxuvid': cookiesDict['webwxuvid'],
|
||||||
|
'webwx_auth_ticket': cookiesDict['webwx_auth_ticket'],
|
||||||
|
'login_frequency': '2',
|
||||||
|
'last_wxuin': cookiesDict['wxuin'],
|
||||||
|
'wxloadtime': cookiesDict['wxloadtime'] + '_expired',
|
||||||
|
'wxpluginkey': cookiesDict['wxloadtime'],
|
||||||
|
'wxuin': cookiesDict['wxuin'],
|
||||||
|
'mm_lang': 'zh_CN',
|
||||||
|
'MM_WX_NOTIFY_STATE': '1',
|
||||||
|
'MM_WX_SOUND_STATE': '1', })
|
||||||
|
except:
|
||||||
|
logger.info('Load status for push login failed, we may have experienced a cookies change.')
|
||||||
|
logger.info('If you are using the newest version of itchat, you may report a bug.')
|
||||||
|
|
@ -0,0 +1,410 @@
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import re
|
||||||
|
import io
|
||||||
|
import threading
|
||||||
|
import json
|
||||||
|
import xml.dom.minidom
|
||||||
|
import random
|
||||||
|
import traceback
|
||||||
|
import logging
|
||||||
|
try:
|
||||||
|
from httplib import BadStatusLine
|
||||||
|
except ImportError:
|
||||||
|
from http.client import BadStatusLine
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from pyqrcode import QRCode
|
||||||
|
|
||||||
|
from .. import config, utils
|
||||||
|
from ..returnvalues import ReturnValue
|
||||||
|
from ..storage.templates import wrap_user_dict
|
||||||
|
from .contact import update_local_chatrooms, update_local_friends
|
||||||
|
from .messages import produce_msg
|
||||||
|
|
||||||
|
logger = logging.getLogger('itchat')
|
||||||
|
|
||||||
|
|
||||||
|
def load_login(core):
|
||||||
|
core.login = login
|
||||||
|
core.get_QRuuid = get_QRuuid
|
||||||
|
core.get_QR = get_QR
|
||||||
|
core.check_login = check_login
|
||||||
|
core.web_init = web_init
|
||||||
|
core.show_mobile_login = show_mobile_login
|
||||||
|
core.start_receiving = start_receiving
|
||||||
|
core.get_msg = get_msg
|
||||||
|
core.logout = logout
|
||||||
|
|
||||||
|
|
||||||
|
def login(self, enableCmdQR=False, picDir=None, qrCallback=None,
|
||||||
|
loginCallback=None, exitCallback=None):
|
||||||
|
if self.alive or self.isLogging:
|
||||||
|
logger.warning('itchat has already logged in.')
|
||||||
|
return
|
||||||
|
self.isLogging = True
|
||||||
|
while self.isLogging:
|
||||||
|
uuid = push_login(self)
|
||||||
|
if uuid:
|
||||||
|
qrStorage = io.BytesIO()
|
||||||
|
else:
|
||||||
|
logger.info('Getting uuid of QR code.')
|
||||||
|
while not self.get_QRuuid():
|
||||||
|
time.sleep(1)
|
||||||
|
logger.info('Downloading QR code.')
|
||||||
|
qrStorage = self.get_QR(enableCmdQR=enableCmdQR,
|
||||||
|
picDir=picDir, qrCallback=qrCallback)
|
||||||
|
logger.info('Please scan the QR code to log in.')
|
||||||
|
isLoggedIn = False
|
||||||
|
while not isLoggedIn:
|
||||||
|
status = self.check_login()
|
||||||
|
if hasattr(qrCallback, '__call__'):
|
||||||
|
qrCallback(uuid=self.uuid, status=status,
|
||||||
|
qrcode=qrStorage.getvalue())
|
||||||
|
if status == '200':
|
||||||
|
isLoggedIn = True
|
||||||
|
elif status == '201':
|
||||||
|
if isLoggedIn is not None:
|
||||||
|
logger.info('Please press confirm on your phone.')
|
||||||
|
isLoggedIn = None
|
||||||
|
time.sleep(7)
|
||||||
|
elif status != '408':
|
||||||
|
break
|
||||||
|
if isLoggedIn:
|
||||||
|
break
|
||||||
|
elif self.isLogging:
|
||||||
|
logger.info('Log in time out, reloading QR code.')
|
||||||
|
else:
|
||||||
|
return # log in process is stopped by user
|
||||||
|
logger.info('Loading the contact, this may take a little while.')
|
||||||
|
self.web_init()
|
||||||
|
self.show_mobile_login()
|
||||||
|
self.get_contact(True)
|
||||||
|
if hasattr(loginCallback, '__call__'):
|
||||||
|
r = loginCallback()
|
||||||
|
else:
|
||||||
|
utils.clear_screen()
|
||||||
|
if os.path.exists(picDir or config.DEFAULT_QR):
|
||||||
|
os.remove(picDir or config.DEFAULT_QR)
|
||||||
|
logger.info('Login successfully as %s' % self.storageClass.nickName)
|
||||||
|
self.start_receiving(exitCallback)
|
||||||
|
self.isLogging = False
|
||||||
|
|
||||||
|
|
||||||
|
def push_login(core):
|
||||||
|
cookiesDict = core.s.cookies.get_dict()
|
||||||
|
if 'wxuin' in cookiesDict:
|
||||||
|
url = '%s/cgi-bin/mmwebwx-bin/webwxpushloginurl?uin=%s' % (
|
||||||
|
config.BASE_URL, cookiesDict['wxuin'])
|
||||||
|
headers = {'User-Agent': config.USER_AGENT}
|
||||||
|
r = core.s.get(url, headers=headers).json()
|
||||||
|
if 'uuid' in r and r.get('ret') in (0, '0'):
|
||||||
|
core.uuid = r['uuid']
|
||||||
|
return r['uuid']
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_QRuuid(self):
|
||||||
|
url = '%s/jslogin' % config.BASE_URL
|
||||||
|
params = {
|
||||||
|
'appid': 'wx782c26e4c19acffb',
|
||||||
|
'fun': 'new',
|
||||||
|
'redirect_uri': 'https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?mod=desktop',
|
||||||
|
'lang': 'zh_CN'}
|
||||||
|
headers = {'User-Agent': config.USER_AGENT}
|
||||||
|
r = self.s.get(url, params=params, headers=headers)
|
||||||
|
regx = r'window.QRLogin.code = (\d+); window.QRLogin.uuid = "(\S+?)";'
|
||||||
|
data = re.search(regx, r.text)
|
||||||
|
if data and data.group(1) == '200':
|
||||||
|
self.uuid = data.group(2)
|
||||||
|
return self.uuid
|
||||||
|
|
||||||
|
|
||||||
|
def get_QR(self, uuid=None, enableCmdQR=False, picDir=None, qrCallback=None):
|
||||||
|
uuid = uuid or self.uuid
|
||||||
|
picDir = picDir or config.DEFAULT_QR
|
||||||
|
qrStorage = io.BytesIO()
|
||||||
|
qrCode = QRCode('https://login.weixin.qq.com/l/' + uuid)
|
||||||
|
qrCode.png(qrStorage, scale=10)
|
||||||
|
if hasattr(qrCallback, '__call__'):
|
||||||
|
qrCallback(uuid=uuid, status='0', qrcode=qrStorage.getvalue())
|
||||||
|
else:
|
||||||
|
with open(picDir, 'wb') as f:
|
||||||
|
f.write(qrStorage.getvalue())
|
||||||
|
if enableCmdQR:
|
||||||
|
utils.print_cmd_qr(qrCode.text(1), enableCmdQR=enableCmdQR)
|
||||||
|
else:
|
||||||
|
utils.print_qr(picDir)
|
||||||
|
return qrStorage
|
||||||
|
|
||||||
|
|
||||||
|
def check_login(self, uuid=None):
|
||||||
|
uuid = uuid or self.uuid
|
||||||
|
url = '%s/cgi-bin/mmwebwx-bin/login' % config.BASE_URL
|
||||||
|
localTime = int(time.time())
|
||||||
|
params = 'loginicon=true&uuid=%s&tip=1&r=%s&_=%s' % (
|
||||||
|
uuid, int(-localTime / 1579), localTime)
|
||||||
|
headers = {'User-Agent': config.USER_AGENT}
|
||||||
|
r = self.s.get(url, params=params, headers=headers)
|
||||||
|
regx = r'window.code=(\d+)'
|
||||||
|
data = re.search(regx, r.text)
|
||||||
|
if data and data.group(1) == '200':
|
||||||
|
if process_login_info(self, r.text):
|
||||||
|
return '200'
|
||||||
|
else:
|
||||||
|
return '400'
|
||||||
|
elif data:
|
||||||
|
return data.group(1)
|
||||||
|
else:
|
||||||
|
return '400'
|
||||||
|
|
||||||
|
|
||||||
|
def process_login_info(core, loginContent):
|
||||||
|
''' when finish login (scanning qrcode)
|
||||||
|
* syncUrl and fileUploadingUrl will be fetched
|
||||||
|
* deviceid and msgid will be generated
|
||||||
|
* skey, wxsid, wxuin, pass_ticket will be fetched
|
||||||
|
'''
|
||||||
|
regx = r'window.redirect_uri="(\S+)";'
|
||||||
|
core.loginInfo['url'] = re.search(regx, loginContent).group(1)
|
||||||
|
headers = {'User-Agent': config.USER_AGENT,
|
||||||
|
'client-version': config.UOS_PATCH_CLIENT_VERSION,
|
||||||
|
'extspam': config.UOS_PATCH_EXTSPAM,
|
||||||
|
'referer': 'https://wx.qq.com/?&lang=zh_CN&target=t'
|
||||||
|
}
|
||||||
|
r = core.s.get(core.loginInfo['url'],
|
||||||
|
headers=headers, allow_redirects=False)
|
||||||
|
core.loginInfo['url'] = core.loginInfo['url'][:core.loginInfo['url'].rfind(
|
||||||
|
'/')]
|
||||||
|
for indexUrl, detailedUrl in (
|
||||||
|
("wx2.qq.com", ("file.wx2.qq.com", "webpush.wx2.qq.com")),
|
||||||
|
("wx8.qq.com", ("file.wx8.qq.com", "webpush.wx8.qq.com")),
|
||||||
|
("qq.com", ("file.wx.qq.com", "webpush.wx.qq.com")),
|
||||||
|
("web2.wechat.com", ("file.web2.wechat.com", "webpush.web2.wechat.com")),
|
||||||
|
("wechat.com", ("file.web.wechat.com", "webpush.web.wechat.com"))):
|
||||||
|
fileUrl, syncUrl = ['https://%s/cgi-bin/mmwebwx-bin' %
|
||||||
|
url for url in detailedUrl]
|
||||||
|
if indexUrl in core.loginInfo['url']:
|
||||||
|
core.loginInfo['fileUrl'], core.loginInfo['syncUrl'] = \
|
||||||
|
fileUrl, syncUrl
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
core.loginInfo['fileUrl'] = core.loginInfo['syncUrl'] = core.loginInfo['url']
|
||||||
|
core.loginInfo['deviceid'] = 'e' + repr(random.random())[2:17]
|
||||||
|
core.loginInfo['logintime'] = int(time.time() * 1e3)
|
||||||
|
core.loginInfo['BaseRequest'] = {}
|
||||||
|
cookies = core.s.cookies.get_dict()
|
||||||
|
skey = re.findall('<skey>(.*?)</skey>', r.text, re.S)[0]
|
||||||
|
pass_ticket = re.findall(
|
||||||
|
'<pass_ticket>(.*?)</pass_ticket>', r.text, re.S)[0]
|
||||||
|
core.loginInfo['skey'] = core.loginInfo['BaseRequest']['Skey'] = skey
|
||||||
|
core.loginInfo['wxsid'] = core.loginInfo['BaseRequest']['Sid'] = cookies["wxsid"]
|
||||||
|
core.loginInfo['wxuin'] = core.loginInfo['BaseRequest']['Uin'] = cookies["wxuin"]
|
||||||
|
core.loginInfo['pass_ticket'] = pass_ticket
|
||||||
|
# A question : why pass_ticket == DeviceID ?
|
||||||
|
# deviceID is only a randomly generated number
|
||||||
|
|
||||||
|
# UOS PATCH By luvletter2333, Sun Feb 28 10:00 PM
|
||||||
|
# for node in xml.dom.minidom.parseString(r.text).documentElement.childNodes:
|
||||||
|
# if node.nodeName == 'skey':
|
||||||
|
# core.loginInfo['skey'] = core.loginInfo['BaseRequest']['Skey'] = node.childNodes[0].data
|
||||||
|
# elif node.nodeName == 'wxsid':
|
||||||
|
# core.loginInfo['wxsid'] = core.loginInfo['BaseRequest']['Sid'] = node.childNodes[0].data
|
||||||
|
# elif node.nodeName == 'wxuin':
|
||||||
|
# core.loginInfo['wxuin'] = core.loginInfo['BaseRequest']['Uin'] = node.childNodes[0].data
|
||||||
|
# elif node.nodeName == 'pass_ticket':
|
||||||
|
# core.loginInfo['pass_ticket'] = core.loginInfo['BaseRequest']['DeviceID'] = node.childNodes[0].data
|
||||||
|
if not all([key in core.loginInfo for key in ('skey', 'wxsid', 'wxuin', 'pass_ticket')]):
|
||||||
|
logger.error(
|
||||||
|
'Your wechat account may be LIMITED to log in WEB wechat, error info:\n%s' % r.text)
|
||||||
|
core.isLogging = False
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def web_init(self):
|
||||||
|
url = '%s/webwxinit' % self.loginInfo['url']
|
||||||
|
params = {
|
||||||
|
'r': int(-time.time() / 1579),
|
||||||
|
'pass_ticket': self.loginInfo['pass_ticket'], }
|
||||||
|
data = {'BaseRequest': self.loginInfo['BaseRequest'], }
|
||||||
|
headers = {
|
||||||
|
'ContentType': 'application/json; charset=UTF-8',
|
||||||
|
'User-Agent': config.USER_AGENT, }
|
||||||
|
r = self.s.post(url, params=params, data=json.dumps(data), headers=headers)
|
||||||
|
dic = json.loads(r.content.decode('utf-8', 'replace'))
|
||||||
|
# deal with login info
|
||||||
|
utils.emoji_formatter(dic['User'], 'NickName')
|
||||||
|
self.loginInfo['InviteStartCount'] = int(dic['InviteStartCount'])
|
||||||
|
self.loginInfo['User'] = wrap_user_dict(
|
||||||
|
utils.struct_friend_info(dic['User']))
|
||||||
|
self.memberList.append(self.loginInfo['User'])
|
||||||
|
self.loginInfo['SyncKey'] = dic['SyncKey']
|
||||||
|
self.loginInfo['synckey'] = '|'.join(['%s_%s' % (item['Key'], item['Val'])
|
||||||
|
for item in dic['SyncKey']['List']])
|
||||||
|
self.storageClass.userName = dic['User']['UserName']
|
||||||
|
self.storageClass.nickName = dic['User']['NickName']
|
||||||
|
# deal with contact list returned when init
|
||||||
|
contactList = dic.get('ContactList', [])
|
||||||
|
chatroomList, otherList = [], []
|
||||||
|
for m in contactList:
|
||||||
|
if m['Sex'] != 0:
|
||||||
|
otherList.append(m)
|
||||||
|
elif '@@' in m['UserName']:
|
||||||
|
m['MemberList'] = [] # don't let dirty info pollute the list
|
||||||
|
chatroomList.append(m)
|
||||||
|
elif '@' in m['UserName']:
|
||||||
|
# mp will be dealt in update_local_friends as well
|
||||||
|
otherList.append(m)
|
||||||
|
if chatroomList:
|
||||||
|
update_local_chatrooms(self, chatroomList)
|
||||||
|
if otherList:
|
||||||
|
update_local_friends(self, otherList)
|
||||||
|
return dic
|
||||||
|
|
||||||
|
|
||||||
|
def show_mobile_login(self):
|
||||||
|
url = '%s/webwxstatusnotify?lang=zh_CN&pass_ticket=%s' % (
|
||||||
|
self.loginInfo['url'], self.loginInfo['pass_ticket'])
|
||||||
|
data = {
|
||||||
|
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||||
|
'Code': 3,
|
||||||
|
'FromUserName': self.storageClass.userName,
|
||||||
|
'ToUserName': self.storageClass.userName,
|
||||||
|
'ClientMsgId': int(time.time()), }
|
||||||
|
headers = {
|
||||||
|
'ContentType': 'application/json; charset=UTF-8',
|
||||||
|
'User-Agent': config.USER_AGENT, }
|
||||||
|
r = self.s.post(url, data=json.dumps(data), headers=headers)
|
||||||
|
return ReturnValue(rawResponse=r)
|
||||||
|
|
||||||
|
|
||||||
|
def start_receiving(self, exitCallback=None, getReceivingFnOnly=False):
|
||||||
|
self.alive = True
|
||||||
|
|
||||||
|
def maintain_loop():
|
||||||
|
retryCount = 0
|
||||||
|
while self.alive:
|
||||||
|
try:
|
||||||
|
i = sync_check(self)
|
||||||
|
if i is None:
|
||||||
|
self.alive = False
|
||||||
|
elif i == '0':
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
msgList, contactList = self.get_msg()
|
||||||
|
if msgList:
|
||||||
|
msgList = produce_msg(self, msgList)
|
||||||
|
for msg in msgList:
|
||||||
|
self.msgList.put(msg)
|
||||||
|
if contactList:
|
||||||
|
chatroomList, otherList = [], []
|
||||||
|
for contact in contactList:
|
||||||
|
if '@@' in contact['UserName']:
|
||||||
|
chatroomList.append(contact)
|
||||||
|
else:
|
||||||
|
otherList.append(contact)
|
||||||
|
chatroomMsg = update_local_chatrooms(
|
||||||
|
self, chatroomList)
|
||||||
|
chatroomMsg['User'] = self.loginInfo['User']
|
||||||
|
self.msgList.put(chatroomMsg)
|
||||||
|
update_local_friends(self, otherList)
|
||||||
|
retryCount = 0
|
||||||
|
except requests.exceptions.ReadTimeout:
|
||||||
|
pass
|
||||||
|
except:
|
||||||
|
retryCount += 1
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
if self.receivingRetryCount < retryCount:
|
||||||
|
self.alive = False
|
||||||
|
else:
|
||||||
|
time.sleep(1)
|
||||||
|
self.logout()
|
||||||
|
if hasattr(exitCallback, '__call__'):
|
||||||
|
exitCallback()
|
||||||
|
else:
|
||||||
|
logger.info('LOG OUT!')
|
||||||
|
if getReceivingFnOnly:
|
||||||
|
return maintain_loop
|
||||||
|
else:
|
||||||
|
maintainThread = threading.Thread(target=maintain_loop)
|
||||||
|
maintainThread.setDaemon(True)
|
||||||
|
maintainThread.start()
|
||||||
|
|
||||||
|
|
||||||
|
def sync_check(self):
|
||||||
|
url = '%s/synccheck' % self.loginInfo.get('syncUrl', self.loginInfo['url'])
|
||||||
|
params = {
|
||||||
|
'r': int(time.time() * 1000),
|
||||||
|
'skey': self.loginInfo['skey'],
|
||||||
|
'sid': self.loginInfo['wxsid'],
|
||||||
|
'uin': self.loginInfo['wxuin'],
|
||||||
|
'deviceid': self.loginInfo['deviceid'],
|
||||||
|
'synckey': self.loginInfo['synckey'],
|
||||||
|
'_': self.loginInfo['logintime'], }
|
||||||
|
headers = {'User-Agent': config.USER_AGENT}
|
||||||
|
self.loginInfo['logintime'] += 1
|
||||||
|
try:
|
||||||
|
r = self.s.get(url, params=params, headers=headers,
|
||||||
|
timeout=config.TIMEOUT)
|
||||||
|
except requests.exceptions.ConnectionError as e:
|
||||||
|
try:
|
||||||
|
if not isinstance(e.args[0].args[1], BadStatusLine):
|
||||||
|
raise
|
||||||
|
# will return a package with status '0 -'
|
||||||
|
# and value like:
|
||||||
|
# 6f:00:8a:9c:09:74:e4:d8:e0:14:bf:96:3a:56:a0:64:1b:a4:25:5d:12:f4:31:a5:30:f1:c6:48:5f:c3:75:6a:99:93
|
||||||
|
# seems like status of typing, but before I make further achievement code will remain like this
|
||||||
|
return '2'
|
||||||
|
except:
|
||||||
|
raise
|
||||||
|
r.raise_for_status()
|
||||||
|
regx = r'window.synccheck={retcode:"(\d+)",selector:"(\d+)"}'
|
||||||
|
pm = re.search(regx, r.text)
|
||||||
|
if pm is None or pm.group(1) != '0':
|
||||||
|
logger.debug('Unexpected sync check result: %s' % r.text)
|
||||||
|
return None
|
||||||
|
return pm.group(2)
|
||||||
|
|
||||||
|
|
||||||
|
def get_msg(self):
|
||||||
|
self.loginInfo['deviceid'] = 'e' + repr(random.random())[2:17]
|
||||||
|
url = '%s/webwxsync?sid=%s&skey=%s&pass_ticket=%s' % (
|
||||||
|
self.loginInfo['url'], self.loginInfo['wxsid'],
|
||||||
|
self.loginInfo['skey'], self.loginInfo['pass_ticket'])
|
||||||
|
data = {
|
||||||
|
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||||
|
'SyncKey': self.loginInfo['SyncKey'],
|
||||||
|
'rr': ~int(time.time()), }
|
||||||
|
headers = {
|
||||||
|
'ContentType': 'application/json; charset=UTF-8',
|
||||||
|
'User-Agent': config.USER_AGENT}
|
||||||
|
r = self.s.post(url, data=json.dumps(data),
|
||||||
|
headers=headers, timeout=config.TIMEOUT)
|
||||||
|
dic = json.loads(r.content.decode('utf-8', 'replace'))
|
||||||
|
if dic['BaseResponse']['Ret'] != 0:
|
||||||
|
return None, None
|
||||||
|
self.loginInfo['SyncKey'] = dic['SyncKey']
|
||||||
|
self.loginInfo['synckey'] = '|'.join(['%s_%s' % (item['Key'], item['Val'])
|
||||||
|
for item in dic['SyncCheckKey']['List']])
|
||||||
|
return dic['AddMsgList'], dic['ModContactList']
|
||||||
|
|
||||||
|
|
||||||
|
def logout(self):
|
||||||
|
if self.alive:
|
||||||
|
url = '%s/webwxlogout' % self.loginInfo['url']
|
||||||
|
params = {
|
||||||
|
'redirect': 1,
|
||||||
|
'type': 1,
|
||||||
|
'skey': self.loginInfo['skey'], }
|
||||||
|
headers = {'User-Agent': config.USER_AGENT}
|
||||||
|
self.s.get(url, params=params, headers=headers)
|
||||||
|
self.alive = False
|
||||||
|
self.isLogging = False
|
||||||
|
self.s.cookies.clear()
|
||||||
|
del self.chatroomList[:]
|
||||||
|
del self.memberList[:]
|
||||||
|
del self.mpList[:]
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'ErrMsg': 'logout successfully.',
|
||||||
|
'Ret': 0, }})
|
||||||
|
|
@ -0,0 +1,528 @@
|
||||||
|
import os, time, re, io
|
||||||
|
import json
|
||||||
|
import mimetypes, hashlib
|
||||||
|
import logging
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from .. import config, utils
|
||||||
|
from ..returnvalues import ReturnValue
|
||||||
|
from ..storage import templates
|
||||||
|
from .contact import update_local_uin
|
||||||
|
|
||||||
|
logger = logging.getLogger('itchat')
|
||||||
|
|
||||||
|
def load_messages(core):
|
||||||
|
core.send_raw_msg = send_raw_msg
|
||||||
|
core.send_msg = send_msg
|
||||||
|
core.upload_file = upload_file
|
||||||
|
core.send_file = send_file
|
||||||
|
core.send_image = send_image
|
||||||
|
core.send_video = send_video
|
||||||
|
core.send = send
|
||||||
|
core.revoke = revoke
|
||||||
|
|
||||||
|
def get_download_fn(core, url, msgId):
|
||||||
|
def download_fn(downloadDir=None):
|
||||||
|
params = {
|
||||||
|
'msgid': msgId,
|
||||||
|
'skey': core.loginInfo['skey'],}
|
||||||
|
headers = { 'User-Agent' : config.USER_AGENT }
|
||||||
|
r = core.s.get(url, params=params, stream=True, headers = headers)
|
||||||
|
tempStorage = io.BytesIO()
|
||||||
|
for block in r.iter_content(1024):
|
||||||
|
tempStorage.write(block)
|
||||||
|
if downloadDir is None:
|
||||||
|
return tempStorage.getvalue()
|
||||||
|
with open(downloadDir, 'wb') as f:
|
||||||
|
f.write(tempStorage.getvalue())
|
||||||
|
tempStorage.seek(0)
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'ErrMsg': 'Successfully downloaded',
|
||||||
|
'Ret': 0, },
|
||||||
|
'PostFix': utils.get_image_postfix(tempStorage.read(20)), })
|
||||||
|
return download_fn
|
||||||
|
|
||||||
|
def produce_msg(core, msgList):
|
||||||
|
''' for messages types
|
||||||
|
* 40 msg, 43 videochat, 50 VOIPMSG, 52 voipnotifymsg
|
||||||
|
* 53 webwxvoipnotifymsg, 9999 sysnotice
|
||||||
|
'''
|
||||||
|
rl = []
|
||||||
|
srl = [40, 43, 50, 52, 53, 9999]
|
||||||
|
for m in msgList:
|
||||||
|
# get actual opposite
|
||||||
|
if m['FromUserName'] == core.storageClass.userName:
|
||||||
|
actualOpposite = m['ToUserName']
|
||||||
|
else:
|
||||||
|
actualOpposite = m['FromUserName']
|
||||||
|
# produce basic message
|
||||||
|
if '@@' in m['FromUserName'] or '@@' in m['ToUserName']:
|
||||||
|
produce_group_chat(core, m)
|
||||||
|
else:
|
||||||
|
utils.msg_formatter(m, 'Content')
|
||||||
|
# set user of msg
|
||||||
|
if '@@' in actualOpposite:
|
||||||
|
m['User'] = core.search_chatrooms(userName=actualOpposite) or \
|
||||||
|
templates.Chatroom({'UserName': actualOpposite})
|
||||||
|
# we don't need to update chatroom here because we have
|
||||||
|
# updated once when producing basic message
|
||||||
|
elif actualOpposite in ('filehelper', 'fmessage'):
|
||||||
|
m['User'] = templates.User({'UserName': actualOpposite})
|
||||||
|
else:
|
||||||
|
m['User'] = core.search_mps(userName=actualOpposite) or \
|
||||||
|
core.search_friends(userName=actualOpposite) or \
|
||||||
|
templates.User(userName=actualOpposite)
|
||||||
|
# by default we think there may be a user missing not a mp
|
||||||
|
m['User'].core = core
|
||||||
|
if m['MsgType'] == 1: # words
|
||||||
|
if m['Url']:
|
||||||
|
regx = r'(.+?\(.+?\))'
|
||||||
|
data = re.search(regx, m['Content'])
|
||||||
|
data = 'Map' if data is None else data.group(1)
|
||||||
|
msg = {
|
||||||
|
'Type': 'Map',
|
||||||
|
'Text': data,}
|
||||||
|
else:
|
||||||
|
msg = {
|
||||||
|
'Type': 'Text',
|
||||||
|
'Text': m['Content'],}
|
||||||
|
elif m['MsgType'] == 3 or m['MsgType'] == 47: # picture
|
||||||
|
download_fn = get_download_fn(core,
|
||||||
|
'%s/webwxgetmsgimg' % core.loginInfo['url'], m['NewMsgId'])
|
||||||
|
msg = {
|
||||||
|
'Type' : 'Picture',
|
||||||
|
'FileName' : '%s.%s' % (time.strftime('%y%m%d-%H%M%S', time.localtime()),
|
||||||
|
'png' if m['MsgType'] == 3 else 'gif'),
|
||||||
|
'Text' : download_fn, }
|
||||||
|
elif m['MsgType'] == 34: # voice
|
||||||
|
download_fn = get_download_fn(core,
|
||||||
|
'%s/webwxgetvoice' % core.loginInfo['url'], m['NewMsgId'])
|
||||||
|
msg = {
|
||||||
|
'Type': 'Recording',
|
||||||
|
'FileName' : '%s.mp3' % time.strftime('%y%m%d-%H%M%S', time.localtime()),
|
||||||
|
'Text': download_fn,}
|
||||||
|
elif m['MsgType'] == 37: # friends
|
||||||
|
m['User']['UserName'] = m['RecommendInfo']['UserName']
|
||||||
|
msg = {
|
||||||
|
'Type': 'Friends',
|
||||||
|
'Text': {
|
||||||
|
'status' : m['Status'],
|
||||||
|
'userName' : m['RecommendInfo']['UserName'],
|
||||||
|
'verifyContent' : m['Ticket'],
|
||||||
|
'autoUpdate' : m['RecommendInfo'], }, }
|
||||||
|
m['User'].verifyDict = msg['Text']
|
||||||
|
elif m['MsgType'] == 42: # name card
|
||||||
|
msg = {
|
||||||
|
'Type': 'Card',
|
||||||
|
'Text': m['RecommendInfo'], }
|
||||||
|
elif m['MsgType'] in (43, 62): # tiny video
|
||||||
|
msgId = m['MsgId']
|
||||||
|
def download_video(videoDir=None):
|
||||||
|
url = '%s/webwxgetvideo' % core.loginInfo['url']
|
||||||
|
params = {
|
||||||
|
'msgid': msgId,
|
||||||
|
'skey': core.loginInfo['skey'],}
|
||||||
|
headers = {'Range': 'bytes=0-', 'User-Agent' : config.USER_AGENT }
|
||||||
|
r = core.s.get(url, params=params, headers=headers, stream=True)
|
||||||
|
tempStorage = io.BytesIO()
|
||||||
|
for block in r.iter_content(1024):
|
||||||
|
tempStorage.write(block)
|
||||||
|
if videoDir is None:
|
||||||
|
return tempStorage.getvalue()
|
||||||
|
with open(videoDir, 'wb') as f:
|
||||||
|
f.write(tempStorage.getvalue())
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'ErrMsg': 'Successfully downloaded',
|
||||||
|
'Ret': 0, }})
|
||||||
|
msg = {
|
||||||
|
'Type': 'Video',
|
||||||
|
'FileName' : '%s.mp4' % time.strftime('%y%m%d-%H%M%S', time.localtime()),
|
||||||
|
'Text': download_video, }
|
||||||
|
elif m['MsgType'] == 49: # sharing
|
||||||
|
if m['AppMsgType'] == 0: # chat history
|
||||||
|
msg = {
|
||||||
|
'Type': 'Note',
|
||||||
|
'Text': m['Content'], }
|
||||||
|
elif m['AppMsgType'] == 6:
|
||||||
|
rawMsg = m
|
||||||
|
cookiesList = {name:data for name,data in core.s.cookies.items()}
|
||||||
|
def download_atta(attaDir=None):
|
||||||
|
url = core.loginInfo['fileUrl'] + '/webwxgetmedia'
|
||||||
|
params = {
|
||||||
|
'sender': rawMsg['FromUserName'],
|
||||||
|
'mediaid': rawMsg['MediaId'],
|
||||||
|
'filename': rawMsg['FileName'],
|
||||||
|
'fromuser': core.loginInfo['wxuin'],
|
||||||
|
'pass_ticket': 'undefined',
|
||||||
|
'webwx_data_ticket': cookiesList['webwx_data_ticket'],}
|
||||||
|
headers = { 'User-Agent' : config.USER_AGENT }
|
||||||
|
r = core.s.get(url, params=params, stream=True, headers=headers)
|
||||||
|
tempStorage = io.BytesIO()
|
||||||
|
for block in r.iter_content(1024):
|
||||||
|
tempStorage.write(block)
|
||||||
|
if attaDir is None:
|
||||||
|
return tempStorage.getvalue()
|
||||||
|
with open(attaDir, 'wb') as f:
|
||||||
|
f.write(tempStorage.getvalue())
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'ErrMsg': 'Successfully downloaded',
|
||||||
|
'Ret': 0, }})
|
||||||
|
msg = {
|
||||||
|
'Type': 'Attachment',
|
||||||
|
'Text': download_atta, }
|
||||||
|
elif m['AppMsgType'] == 8:
|
||||||
|
download_fn = get_download_fn(core,
|
||||||
|
'%s/webwxgetmsgimg' % core.loginInfo['url'], m['NewMsgId'])
|
||||||
|
msg = {
|
||||||
|
'Type' : 'Picture',
|
||||||
|
'FileName' : '%s.gif' % (
|
||||||
|
time.strftime('%y%m%d-%H%M%S', time.localtime())),
|
||||||
|
'Text' : download_fn, }
|
||||||
|
elif m['AppMsgType'] == 17:
|
||||||
|
msg = {
|
||||||
|
'Type': 'Note',
|
||||||
|
'Text': m['FileName'], }
|
||||||
|
elif m['AppMsgType'] == 2000:
|
||||||
|
regx = r'\[CDATA\[(.+?)\][\s\S]+?\[CDATA\[(.+?)\]'
|
||||||
|
data = re.search(regx, m['Content'])
|
||||||
|
if data:
|
||||||
|
data = data.group(2).split(u'\u3002')[0]
|
||||||
|
else:
|
||||||
|
data = 'You may found detailed info in Content key.'
|
||||||
|
msg = {
|
||||||
|
'Type': 'Note',
|
||||||
|
'Text': data, }
|
||||||
|
else:
|
||||||
|
msg = {
|
||||||
|
'Type': 'Sharing',
|
||||||
|
'Text': m['FileName'], }
|
||||||
|
elif m['MsgType'] == 51: # phone init
|
||||||
|
msg = update_local_uin(core, m)
|
||||||
|
elif m['MsgType'] == 10000:
|
||||||
|
msg = {
|
||||||
|
'Type': 'Note',
|
||||||
|
'Text': m['Content'],}
|
||||||
|
elif m['MsgType'] == 10002:
|
||||||
|
regx = r'\[CDATA\[(.+?)\]\]'
|
||||||
|
data = re.search(regx, m['Content'])
|
||||||
|
data = 'System message' if data is None else data.group(1).replace('\\', '')
|
||||||
|
msg = {
|
||||||
|
'Type': 'Note',
|
||||||
|
'Text': data, }
|
||||||
|
elif m['MsgType'] in srl:
|
||||||
|
msg = {
|
||||||
|
'Type': 'Useless',
|
||||||
|
'Text': 'UselessMsg', }
|
||||||
|
else:
|
||||||
|
logger.debug('Useless message received: %s\n%s' % (m['MsgType'], str(m)))
|
||||||
|
msg = {
|
||||||
|
'Type': 'Useless',
|
||||||
|
'Text': 'UselessMsg', }
|
||||||
|
m = dict(m, **msg)
|
||||||
|
rl.append(m)
|
||||||
|
return rl
|
||||||
|
|
||||||
|
def produce_group_chat(core, msg):
|
||||||
|
r = re.match('(@[0-9a-z]*?):<br/>(.*)$', msg['Content'])
|
||||||
|
if r:
|
||||||
|
actualUserName, content = r.groups()
|
||||||
|
chatroomUserName = msg['FromUserName']
|
||||||
|
elif msg['FromUserName'] == core.storageClass.userName:
|
||||||
|
actualUserName = core.storageClass.userName
|
||||||
|
content = msg['Content']
|
||||||
|
chatroomUserName = msg['ToUserName']
|
||||||
|
else:
|
||||||
|
msg['ActualUserName'] = core.storageClass.userName
|
||||||
|
msg['ActualNickName'] = core.storageClass.nickName
|
||||||
|
msg['IsAt'] = False
|
||||||
|
utils.msg_formatter(msg, 'Content')
|
||||||
|
return
|
||||||
|
chatroom = core.storageClass.search_chatrooms(userName=chatroomUserName)
|
||||||
|
member = utils.search_dict_list((chatroom or {}).get(
|
||||||
|
'MemberList') or [], 'UserName', actualUserName)
|
||||||
|
if member is None:
|
||||||
|
chatroom = core.update_chatroom(chatroomUserName)
|
||||||
|
member = utils.search_dict_list((chatroom or {}).get(
|
||||||
|
'MemberList') or [], 'UserName', actualUserName)
|
||||||
|
if member is None:
|
||||||
|
logger.debug('chatroom member fetch failed with %s' % actualUserName)
|
||||||
|
msg['ActualNickName'] = ''
|
||||||
|
msg['IsAt'] = False
|
||||||
|
else:
|
||||||
|
msg['ActualNickName'] = member.get('DisplayName', '') or member['NickName']
|
||||||
|
atFlag = '@' + (chatroom['Self'].get('DisplayName', '') or core.storageClass.nickName)
|
||||||
|
msg['IsAt'] = (
|
||||||
|
(atFlag + (u'\u2005' if u'\u2005' in msg['Content'] else ' '))
|
||||||
|
in msg['Content'] or msg['Content'].endswith(atFlag))
|
||||||
|
msg['ActualUserName'] = actualUserName
|
||||||
|
msg['Content'] = content
|
||||||
|
utils.msg_formatter(msg, 'Content')
|
||||||
|
|
||||||
|
def send_raw_msg(self, msgType, content, toUserName):
|
||||||
|
url = '%s/webwxsendmsg' % self.loginInfo['url']
|
||||||
|
data = {
|
||||||
|
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||||
|
'Msg': {
|
||||||
|
'Type': msgType,
|
||||||
|
'Content': content,
|
||||||
|
'FromUserName': self.storageClass.userName,
|
||||||
|
'ToUserName': (toUserName if toUserName else self.storageClass.userName),
|
||||||
|
'LocalID': int(time.time() * 1e4),
|
||||||
|
'ClientMsgId': int(time.time() * 1e4),
|
||||||
|
},
|
||||||
|
'Scene': 0, }
|
||||||
|
headers = { 'ContentType': 'application/json; charset=UTF-8', 'User-Agent' : config.USER_AGENT }
|
||||||
|
r = self.s.post(url, headers=headers,
|
||||||
|
data=json.dumps(data, ensure_ascii=False).encode('utf8'))
|
||||||
|
return ReturnValue(rawResponse=r)
|
||||||
|
|
||||||
|
def send_msg(self, msg='Test Message', toUserName=None):
|
||||||
|
logger.debug('Request to send a text message to %s: %s' % (toUserName, msg))
|
||||||
|
r = self.send_raw_msg(1, msg, toUserName)
|
||||||
|
return r
|
||||||
|
|
||||||
|
def _prepare_file(fileDir, file_=None):
|
||||||
|
fileDict = {}
|
||||||
|
if file_:
|
||||||
|
if hasattr(file_, 'read'):
|
||||||
|
file_ = file_.read()
|
||||||
|
else:
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'ErrMsg': 'file_ param should be opened file',
|
||||||
|
'Ret': -1005, }})
|
||||||
|
else:
|
||||||
|
if not utils.check_file(fileDir):
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'ErrMsg': 'No file found in specific dir',
|
||||||
|
'Ret': -1002, }})
|
||||||
|
with open(fileDir, 'rb') as f:
|
||||||
|
file_ = f.read()
|
||||||
|
fileDict['fileSize'] = len(file_)
|
||||||
|
fileDict['fileMd5'] = hashlib.md5(file_).hexdigest()
|
||||||
|
fileDict['file_'] = io.BytesIO(file_)
|
||||||
|
return fileDict
|
||||||
|
|
||||||
|
def upload_file(self, fileDir, isPicture=False, isVideo=False,
|
||||||
|
toUserName='filehelper', file_=None, preparedFile=None):
|
||||||
|
logger.debug('Request to upload a %s: %s' % (
|
||||||
|
'picture' if isPicture else 'video' if isVideo else 'file', fileDir))
|
||||||
|
if not preparedFile:
|
||||||
|
preparedFile = _prepare_file(fileDir, file_)
|
||||||
|
if not preparedFile:
|
||||||
|
return preparedFile
|
||||||
|
fileSize, fileMd5, file_ = \
|
||||||
|
preparedFile['fileSize'], preparedFile['fileMd5'], preparedFile['file_']
|
||||||
|
fileSymbol = 'pic' if isPicture else 'video' if isVideo else'doc'
|
||||||
|
chunks = int((fileSize - 1) / 524288) + 1
|
||||||
|
clientMediaId = int(time.time() * 1e4)
|
||||||
|
uploadMediaRequest = json.dumps(OrderedDict([
|
||||||
|
('UploadType', 2),
|
||||||
|
('BaseRequest', self.loginInfo['BaseRequest']),
|
||||||
|
('ClientMediaId', clientMediaId),
|
||||||
|
('TotalLen', fileSize),
|
||||||
|
('StartPos', 0),
|
||||||
|
('DataLen', fileSize),
|
||||||
|
('MediaType', 4),
|
||||||
|
('FromUserName', self.storageClass.userName),
|
||||||
|
('ToUserName', toUserName),
|
||||||
|
('FileMd5', fileMd5)]
|
||||||
|
), separators = (',', ':'))
|
||||||
|
r = {'BaseResponse': {'Ret': -1005, 'ErrMsg': 'Empty file detected'}}
|
||||||
|
for chunk in range(chunks):
|
||||||
|
r = upload_chunk_file(self, fileDir, fileSymbol, fileSize,
|
||||||
|
file_, chunk, chunks, uploadMediaRequest)
|
||||||
|
file_.close()
|
||||||
|
if isinstance(r, dict):
|
||||||
|
return ReturnValue(r)
|
||||||
|
return ReturnValue(rawResponse=r)
|
||||||
|
|
||||||
|
def upload_chunk_file(core, fileDir, fileSymbol, fileSize,
|
||||||
|
file_, chunk, chunks, uploadMediaRequest):
|
||||||
|
url = core.loginInfo.get('fileUrl', core.loginInfo['url']) + \
|
||||||
|
'/webwxuploadmedia?f=json'
|
||||||
|
# save it on server
|
||||||
|
cookiesList = {name:data for name,data in core.s.cookies.items()}
|
||||||
|
fileType = mimetypes.guess_type(fileDir)[0] or 'application/octet-stream'
|
||||||
|
fileName = utils.quote(os.path.basename(fileDir))
|
||||||
|
files = OrderedDict([
|
||||||
|
('id', (None, 'WU_FILE_0')),
|
||||||
|
('name', (None, fileName)),
|
||||||
|
('type', (None, fileType)),
|
||||||
|
('lastModifiedDate', (None, time.strftime('%a %b %d %Y %H:%M:%S GMT+0800 (CST)'))),
|
||||||
|
('size', (None, str(fileSize))),
|
||||||
|
('chunks', (None, None)),
|
||||||
|
('chunk', (None, None)),
|
||||||
|
('mediatype', (None, fileSymbol)),
|
||||||
|
('uploadmediarequest', (None, uploadMediaRequest)),
|
||||||
|
('webwx_data_ticket', (None, cookiesList['webwx_data_ticket'])),
|
||||||
|
('pass_ticket', (None, core.loginInfo['pass_ticket'])),
|
||||||
|
('filename' , (fileName, file_.read(524288), 'application/octet-stream'))])
|
||||||
|
if chunks == 1:
|
||||||
|
del files['chunk']; del files['chunks']
|
||||||
|
else:
|
||||||
|
files['chunk'], files['chunks'] = (None, str(chunk)), (None, str(chunks))
|
||||||
|
headers = { 'User-Agent' : config.USER_AGENT }
|
||||||
|
return core.s.post(url, files=files, headers=headers, timeout=config.TIMEOUT)
|
||||||
|
|
||||||
|
def send_file(self, fileDir, toUserName=None, mediaId=None, file_=None):
|
||||||
|
logger.debug('Request to send a file(mediaId: %s) to %s: %s' % (
|
||||||
|
mediaId, toUserName, fileDir))
|
||||||
|
if hasattr(fileDir, 'read'):
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'ErrMsg': 'fileDir param should not be an opened file in send_file',
|
||||||
|
'Ret': -1005, }})
|
||||||
|
if toUserName is None:
|
||||||
|
toUserName = self.storageClass.userName
|
||||||
|
preparedFile = _prepare_file(fileDir, file_)
|
||||||
|
if not preparedFile:
|
||||||
|
return preparedFile
|
||||||
|
fileSize = preparedFile['fileSize']
|
||||||
|
if mediaId is None:
|
||||||
|
r = self.upload_file(fileDir, preparedFile=preparedFile)
|
||||||
|
if r:
|
||||||
|
mediaId = r['MediaId']
|
||||||
|
else:
|
||||||
|
return r
|
||||||
|
url = '%s/webwxsendappmsg?fun=async&f=json' % self.loginInfo['url']
|
||||||
|
data = {
|
||||||
|
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||||
|
'Msg': {
|
||||||
|
'Type': 6,
|
||||||
|
'Content': ("<appmsg appid='wxeb7ec651dd0aefa9' sdkver=''><title>%s</title>" % os.path.basename(fileDir) +
|
||||||
|
"<des></des><action></action><type>6</type><content></content><url></url><lowurl></lowurl>" +
|
||||||
|
"<appattach><totallen>%s</totallen><attachid>%s</attachid>" % (str(fileSize), mediaId) +
|
||||||
|
"<fileext>%s</fileext></appattach><extinfo></extinfo></appmsg>" % os.path.splitext(fileDir)[1].replace('.','')),
|
||||||
|
'FromUserName': self.storageClass.userName,
|
||||||
|
'ToUserName': toUserName,
|
||||||
|
'LocalID': int(time.time() * 1e4),
|
||||||
|
'ClientMsgId': int(time.time() * 1e4), },
|
||||||
|
'Scene': 0, }
|
||||||
|
headers = {
|
||||||
|
'User-Agent': config.USER_AGENT,
|
||||||
|
'Content-Type': 'application/json;charset=UTF-8', }
|
||||||
|
r = self.s.post(url, headers=headers,
|
||||||
|
data=json.dumps(data, ensure_ascii=False).encode('utf8'))
|
||||||
|
return ReturnValue(rawResponse=r)
|
||||||
|
|
||||||
|
def send_image(self, fileDir=None, toUserName=None, mediaId=None, file_=None):
|
||||||
|
logger.debug('Request to send a image(mediaId: %s) to %s: %s' % (
|
||||||
|
mediaId, toUserName, fileDir))
|
||||||
|
if fileDir or file_:
|
||||||
|
if hasattr(fileDir, 'read'):
|
||||||
|
file_, fileDir = fileDir, None
|
||||||
|
if fileDir is None:
|
||||||
|
fileDir = 'tmp.jpg' # specific fileDir to send gifs
|
||||||
|
else:
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'ErrMsg': 'Either fileDir or file_ should be specific',
|
||||||
|
'Ret': -1005, }})
|
||||||
|
if toUserName is None:
|
||||||
|
toUserName = self.storageClass.userName
|
||||||
|
if mediaId is None:
|
||||||
|
r = self.upload_file(fileDir, isPicture=not fileDir[-4:] == '.gif', file_=file_)
|
||||||
|
if r:
|
||||||
|
mediaId = r['MediaId']
|
||||||
|
else:
|
||||||
|
return r
|
||||||
|
url = '%s/webwxsendmsgimg?fun=async&f=json' % self.loginInfo['url']
|
||||||
|
data = {
|
||||||
|
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||||
|
'Msg': {
|
||||||
|
'Type': 3,
|
||||||
|
'MediaId': mediaId,
|
||||||
|
'FromUserName': self.storageClass.userName,
|
||||||
|
'ToUserName': toUserName,
|
||||||
|
'LocalID': int(time.time() * 1e4),
|
||||||
|
'ClientMsgId': int(time.time() * 1e4), },
|
||||||
|
'Scene': 0, }
|
||||||
|
if fileDir[-4:] == '.gif':
|
||||||
|
url = '%s/webwxsendemoticon?fun=sys' % self.loginInfo['url']
|
||||||
|
data['Msg']['Type'] = 47
|
||||||
|
data['Msg']['EmojiFlag'] = 2
|
||||||
|
headers = {
|
||||||
|
'User-Agent': config.USER_AGENT,
|
||||||
|
'Content-Type': 'application/json;charset=UTF-8', }
|
||||||
|
r = self.s.post(url, headers=headers,
|
||||||
|
data=json.dumps(data, ensure_ascii=False).encode('utf8'))
|
||||||
|
return ReturnValue(rawResponse=r)
|
||||||
|
|
||||||
|
def send_video(self, fileDir=None, toUserName=None, mediaId=None, file_=None):
|
||||||
|
logger.debug('Request to send a video(mediaId: %s) to %s: %s' % (
|
||||||
|
mediaId, toUserName, fileDir))
|
||||||
|
if fileDir or file_:
|
||||||
|
if hasattr(fileDir, 'read'):
|
||||||
|
file_, fileDir = fileDir, None
|
||||||
|
if fileDir is None:
|
||||||
|
fileDir = 'tmp.mp4' # specific fileDir to send other formats
|
||||||
|
else:
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'ErrMsg': 'Either fileDir or file_ should be specific',
|
||||||
|
'Ret': -1005, }})
|
||||||
|
if toUserName is None:
|
||||||
|
toUserName = self.storageClass.userName
|
||||||
|
if mediaId is None:
|
||||||
|
r = self.upload_file(fileDir, isVideo=True, file_=file_)
|
||||||
|
if r:
|
||||||
|
mediaId = r['MediaId']
|
||||||
|
else:
|
||||||
|
return r
|
||||||
|
url = '%s/webwxsendvideomsg?fun=async&f=json&pass_ticket=%s' % (
|
||||||
|
self.loginInfo['url'], self.loginInfo['pass_ticket'])
|
||||||
|
data = {
|
||||||
|
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||||
|
'Msg': {
|
||||||
|
'Type' : 43,
|
||||||
|
'MediaId' : mediaId,
|
||||||
|
'FromUserName' : self.storageClass.userName,
|
||||||
|
'ToUserName' : toUserName,
|
||||||
|
'LocalID' : int(time.time() * 1e4),
|
||||||
|
'ClientMsgId' : int(time.time() * 1e4), },
|
||||||
|
'Scene': 0, }
|
||||||
|
headers = {
|
||||||
|
'User-Agent' : config.USER_AGENT,
|
||||||
|
'Content-Type': 'application/json;charset=UTF-8', }
|
||||||
|
r = self.s.post(url, headers=headers,
|
||||||
|
data=json.dumps(data, ensure_ascii=False).encode('utf8'))
|
||||||
|
return ReturnValue(rawResponse=r)
|
||||||
|
|
||||||
|
def send(self, msg, toUserName=None, mediaId=None):
|
||||||
|
if not msg:
|
||||||
|
r = ReturnValue({'BaseResponse': {
|
||||||
|
'ErrMsg': 'No message.',
|
||||||
|
'Ret': -1005, }})
|
||||||
|
elif msg[:5] == '@fil@':
|
||||||
|
if mediaId is None:
|
||||||
|
r = self.send_file(msg[5:], toUserName)
|
||||||
|
else:
|
||||||
|
r = self.send_file(msg[5:], toUserName, mediaId)
|
||||||
|
elif msg[:5] == '@img@':
|
||||||
|
if mediaId is None:
|
||||||
|
r = self.send_image(msg[5:], toUserName)
|
||||||
|
else:
|
||||||
|
r = self.send_image(msg[5:], toUserName, mediaId)
|
||||||
|
elif msg[:5] == '@msg@':
|
||||||
|
r = self.send_msg(msg[5:], toUserName)
|
||||||
|
elif msg[:5] == '@vid@':
|
||||||
|
if mediaId is None:
|
||||||
|
r = self.send_video(msg[5:], toUserName)
|
||||||
|
else:
|
||||||
|
r = self.send_video(msg[5:], toUserName, mediaId)
|
||||||
|
else:
|
||||||
|
r = self.send_msg(msg, toUserName)
|
||||||
|
return r
|
||||||
|
|
||||||
|
def revoke(self, msgId, toUserName, localId=None):
|
||||||
|
url = '%s/webwxrevokemsg' % self.loginInfo['url']
|
||||||
|
data = {
|
||||||
|
'BaseRequest': self.loginInfo['BaseRequest'],
|
||||||
|
"ClientMsgId": localId or str(time.time() * 1e3),
|
||||||
|
"SvrMsgId": msgId,
|
||||||
|
"ToUserName": toUserName}
|
||||||
|
headers = {
|
||||||
|
'ContentType': 'application/json; charset=UTF-8',
|
||||||
|
'User-Agent' : config.USER_AGENT }
|
||||||
|
r = self.s.post(url, headers=headers,
|
||||||
|
data=json.dumps(data, ensure_ascii=False).encode('utf8'))
|
||||||
|
return ReturnValue(rawResponse=r)
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
import logging, traceback, sys, threading
|
||||||
|
try:
|
||||||
|
import Queue
|
||||||
|
except ImportError:
|
||||||
|
import queue as Queue
|
||||||
|
|
||||||
|
from ..log import set_logging
|
||||||
|
from ..utils import test_connect
|
||||||
|
from ..storage import templates
|
||||||
|
|
||||||
|
logger = logging.getLogger('itchat')
|
||||||
|
|
||||||
|
def load_register(core):
|
||||||
|
core.auto_login = auto_login
|
||||||
|
core.configured_reply = configured_reply
|
||||||
|
core.msg_register = msg_register
|
||||||
|
core.run = run
|
||||||
|
|
||||||
|
def auto_login(self, hotReload=False, statusStorageDir='itchat.pkl',
|
||||||
|
enableCmdQR=False, picDir=None, qrCallback=None,
|
||||||
|
loginCallback=None, exitCallback=None):
|
||||||
|
if not test_connect():
|
||||||
|
logger.info("You can't get access to internet or wechat domain, so exit.")
|
||||||
|
sys.exit()
|
||||||
|
self.useHotReload = hotReload
|
||||||
|
self.hotReloadDir = statusStorageDir
|
||||||
|
if hotReload:
|
||||||
|
if self.load_login_status(statusStorageDir,
|
||||||
|
loginCallback=loginCallback, exitCallback=exitCallback):
|
||||||
|
return
|
||||||
|
self.login(enableCmdQR=enableCmdQR, picDir=picDir, qrCallback=qrCallback,
|
||||||
|
loginCallback=loginCallback, exitCallback=exitCallback)
|
||||||
|
self.dump_login_status(statusStorageDir)
|
||||||
|
else:
|
||||||
|
self.login(enableCmdQR=enableCmdQR, picDir=picDir, qrCallback=qrCallback,
|
||||||
|
loginCallback=loginCallback, exitCallback=exitCallback)
|
||||||
|
|
||||||
|
def configured_reply(self):
|
||||||
|
''' determine the type of message and reply if its method is defined
|
||||||
|
however, I use a strange way to determine whether a msg is from massive platform
|
||||||
|
I haven't found a better solution here
|
||||||
|
The main problem I'm worrying about is the mismatching of new friends added on phone
|
||||||
|
If you have any good idea, pleeeease report an issue. I will be more than grateful.
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
msg = self.msgList.get(timeout=1)
|
||||||
|
except Queue.Empty:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if isinstance(msg['User'], templates.User):
|
||||||
|
replyFn = self.functionDict['FriendChat'].get(msg['Type'])
|
||||||
|
elif isinstance(msg['User'], templates.MassivePlatform):
|
||||||
|
replyFn = self.functionDict['MpChat'].get(msg['Type'])
|
||||||
|
elif isinstance(msg['User'], templates.Chatroom):
|
||||||
|
replyFn = self.functionDict['GroupChat'].get(msg['Type'])
|
||||||
|
if replyFn is None:
|
||||||
|
r = None
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
r = replyFn(msg)
|
||||||
|
if r is not None:
|
||||||
|
self.send(r, msg.get('FromUserName'))
|
||||||
|
except:
|
||||||
|
logger.warning(traceback.format_exc())
|
||||||
|
|
||||||
|
def msg_register(self, msgType, isFriendChat=False, isGroupChat=False, isMpChat=False):
|
||||||
|
''' a decorator constructor
|
||||||
|
return a specific decorator based on information given '''
|
||||||
|
if not (isinstance(msgType, list) or isinstance(msgType, tuple)):
|
||||||
|
msgType = [msgType]
|
||||||
|
def _msg_register(fn):
|
||||||
|
for _msgType in msgType:
|
||||||
|
if isFriendChat:
|
||||||
|
self.functionDict['FriendChat'][_msgType] = fn
|
||||||
|
if isGroupChat:
|
||||||
|
self.functionDict['GroupChat'][_msgType] = fn
|
||||||
|
if isMpChat:
|
||||||
|
self.functionDict['MpChat'][_msgType] = fn
|
||||||
|
if not any((isFriendChat, isGroupChat, isMpChat)):
|
||||||
|
self.functionDict['FriendChat'][_msgType] = fn
|
||||||
|
return fn
|
||||||
|
return _msg_register
|
||||||
|
|
||||||
|
def run(self, debug=False, blockThread=True):
|
||||||
|
logger.info('Start auto replying.')
|
||||||
|
if debug:
|
||||||
|
set_logging(loggingLevel=logging.DEBUG)
|
||||||
|
def reply_fn():
|
||||||
|
try:
|
||||||
|
while self.alive:
|
||||||
|
self.configured_reply()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
if self.useHotReload:
|
||||||
|
self.dump_login_status()
|
||||||
|
self.alive = False
|
||||||
|
logger.debug('itchat received an ^C and exit.')
|
||||||
|
logger.info('Bye~')
|
||||||
|
if blockThread:
|
||||||
|
reply_fn()
|
||||||
|
else:
|
||||||
|
replyThread = threading.Thread(target=reply_fn)
|
||||||
|
replyThread.setDaemon(True)
|
||||||
|
replyThread.start()
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import os, platform
|
||||||
|
|
||||||
|
VERSION = '1.5.0.dev'
|
||||||
|
|
||||||
|
# use this envrionment to initialize the async & sync componment
|
||||||
|
ASYNC_COMPONENTS = os.environ.get('ITCHAT_UOS_ASYNC', False)
|
||||||
|
|
||||||
|
BASE_URL = 'https://login.weixin.qq.com'
|
||||||
|
OS = platform.system() # Windows, Linux, Darwin
|
||||||
|
DIR = os.getcwd()
|
||||||
|
DEFAULT_QR = 'QR.png'
|
||||||
|
TIMEOUT = (10, 60)
|
||||||
|
|
||||||
|
USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36'
|
||||||
|
|
||||||
|
UOS_PATCH_CLIENT_VERSION = '2.0.0'
|
||||||
|
UOS_PATCH_EXTSPAM = 'Go8FCIkFEokFCggwMDAwMDAwMRAGGvAESySibk50w5Wb3uTl2c2h64jVVrV7gNs06GFlWplHQbY/5FfiO++1yH4ykCyNPWKXmco+wfQzK5R98D3so7rJ5LmGFvBLjGceleySrc3SOf2Pc1gVehzJgODeS0lDL3/I/0S2SSE98YgKleq6Uqx6ndTy9yaL9qFxJL7eiA/R3SEfTaW1SBoSITIu+EEkXff+Pv8NHOk7N57rcGk1w0ZzRrQDkXTOXFN2iHYIzAAZPIOY45Lsh+A4slpgnDiaOvRtlQYCt97nmPLuTipOJ8Qc5pM7ZsOsAPPrCQL7nK0I7aPrFDF0q4ziUUKettzW8MrAaiVfmbD1/VkmLNVqqZVvBCtRblXb5FHmtS8FxnqCzYP4WFvz3T0TcrOqwLX1M/DQvcHaGGw0B0y4bZMs7lVScGBFxMj3vbFi2SRKbKhaitxHfYHAOAa0X7/MSS0RNAjdwoyGHeOepXOKY+h3iHeqCvgOH6LOifdHf/1aaZNwSkGotYnYScW8Yx63LnSwba7+hESrtPa/huRmB9KWvMCKbDThL/nne14hnL277EDCSocPu3rOSYjuB9gKSOdVmWsj9Dxb/iZIe+S6AiG29Esm+/eUacSba0k8wn5HhHg9d4tIcixrxveflc8vi2/wNQGVFNsGO6tB5WF0xf/plngOvQ1/ivGV/C1Qpdhzznh0ExAVJ6dwzNg7qIEBaw+BzTJTUuRcPk92Sn6QDn2Pu3mpONaEumacjW4w6ipPnPw+g2TfywJjeEcpSZaP4Q3YV5HG8D6UjWA4GSkBKculWpdCMadx0usMomsSS/74QgpYqcPkmamB4nVv1JxczYITIqItIKjD35IGKAUwAA=='
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
TEXT = 'Text'
|
||||||
|
MAP = 'Map'
|
||||||
|
CARD = 'Card'
|
||||||
|
NOTE = 'Note'
|
||||||
|
SHARING = 'Sharing'
|
||||||
|
PICTURE = 'Picture'
|
||||||
|
RECORDING = VOICE = 'Recording'
|
||||||
|
ATTACHMENT = 'Attachment'
|
||||||
|
VIDEO = 'Video'
|
||||||
|
FRIENDS = 'Friends'
|
||||||
|
SYSTEM = 'System'
|
||||||
|
|
||||||
|
INCOME_MSG = [TEXT, MAP, CARD, NOTE, SHARING, PICTURE,
|
||||||
|
RECORDING, VOICE, ATTACHMENT, VIDEO, FRIENDS, SYSTEM]
|
||||||
|
|
@ -0,0 +1,456 @@
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from . import storage
|
||||||
|
|
||||||
|
class Core(object):
|
||||||
|
def __init__(self):
|
||||||
|
''' init is the only method defined in core.py
|
||||||
|
alive is value showing whether core is running
|
||||||
|
- you should call logout method to change it
|
||||||
|
- after logout, a core object can login again
|
||||||
|
storageClass only uses basic python types
|
||||||
|
- so for advanced uses, inherit it yourself
|
||||||
|
receivingRetryCount is for receiving loop retry
|
||||||
|
- it's 5 now, but actually even 1 is enough
|
||||||
|
- failing is failing
|
||||||
|
'''
|
||||||
|
self.alive, self.isLogging = False, False
|
||||||
|
self.storageClass = storage.Storage(self)
|
||||||
|
self.memberList = self.storageClass.memberList
|
||||||
|
self.mpList = self.storageClass.mpList
|
||||||
|
self.chatroomList = self.storageClass.chatroomList
|
||||||
|
self.msgList = self.storageClass.msgList
|
||||||
|
self.loginInfo = {}
|
||||||
|
self.s = requests.Session()
|
||||||
|
self.uuid = None
|
||||||
|
self.functionDict = {'FriendChat': {}, 'GroupChat': {}, 'MpChat': {}}
|
||||||
|
self.useHotReload, self.hotReloadDir = False, 'itchat.pkl'
|
||||||
|
self.receivingRetryCount = 5
|
||||||
|
def login(self, enableCmdQR=False, picDir=None, qrCallback=None,
|
||||||
|
loginCallback=None, exitCallback=None):
|
||||||
|
''' log in like web wechat does
|
||||||
|
for log in
|
||||||
|
- a QR code will be downloaded and opened
|
||||||
|
- then scanning status is logged, it paused for you confirm
|
||||||
|
- finally it logged in and show your nickName
|
||||||
|
for options
|
||||||
|
- enableCmdQR: show qrcode in command line
|
||||||
|
- integers can be used to fit strange char length
|
||||||
|
- picDir: place for storing qrcode
|
||||||
|
- qrCallback: method that should accept uuid, status, qrcode
|
||||||
|
- loginCallback: callback after successfully logged in
|
||||||
|
- if not set, screen is cleared and qrcode is deleted
|
||||||
|
- exitCallback: callback after logged out
|
||||||
|
- it contains calling of logout
|
||||||
|
for usage
|
||||||
|
..code::python
|
||||||
|
|
||||||
|
import itchat
|
||||||
|
itchat.login()
|
||||||
|
|
||||||
|
it is defined in components/login.py
|
||||||
|
and of course every single move in login can be called outside
|
||||||
|
- you may scan source code to see how
|
||||||
|
- and modified according to your own demand
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
def get_QRuuid(self):
|
||||||
|
''' get uuid for qrcode
|
||||||
|
uuid is the symbol of qrcode
|
||||||
|
- for logging in, you need to get a uuid first
|
||||||
|
- for downloading qrcode, you need to pass uuid to it
|
||||||
|
- for checking login status, uuid is also required
|
||||||
|
if uuid has timed out, just get another
|
||||||
|
it is defined in components/login.py
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
def get_QR(self, uuid=None, enableCmdQR=False, picDir=None, qrCallback=None):
|
||||||
|
''' download and show qrcode
|
||||||
|
for options
|
||||||
|
- uuid: if uuid is not set, latest uuid you fetched will be used
|
||||||
|
- enableCmdQR: show qrcode in cmd
|
||||||
|
- picDir: where to store qrcode
|
||||||
|
- qrCallback: method that should accept uuid, status, qrcode
|
||||||
|
it is defined in components/login.py
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
def check_login(self, uuid=None):
|
||||||
|
''' check login status
|
||||||
|
for options:
|
||||||
|
- uuid: if uuid is not set, latest uuid you fetched will be used
|
||||||
|
for return values:
|
||||||
|
- a string will be returned
|
||||||
|
- for meaning of return values
|
||||||
|
- 200: log in successfully
|
||||||
|
- 201: waiting for press confirm
|
||||||
|
- 408: uuid timed out
|
||||||
|
- 0 : unknown error
|
||||||
|
for processing:
|
||||||
|
- syncUrl and fileUrl is set
|
||||||
|
- BaseRequest is set
|
||||||
|
blocks until reaches any of above status
|
||||||
|
it is defined in components/login.py
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
def web_init(self):
|
||||||
|
''' get info necessary for initializing
|
||||||
|
for processing:
|
||||||
|
- own account info is set
|
||||||
|
- inviteStartCount is set
|
||||||
|
- syncKey is set
|
||||||
|
- part of contact is fetched
|
||||||
|
it is defined in components/login.py
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
def show_mobile_login(self):
|
||||||
|
''' show web wechat login sign
|
||||||
|
the sign is on the top of mobile phone wechat
|
||||||
|
sign will be added after sometime even without calling this function
|
||||||
|
it is defined in components/login.py
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
def start_receiving(self, exitCallback=None, getReceivingFnOnly=False):
|
||||||
|
''' open a thread for heart loop and receiving messages
|
||||||
|
for options:
|
||||||
|
- exitCallback: callback after logged out
|
||||||
|
- it contains calling of logout
|
||||||
|
- getReceivingFnOnly: if True thread will not be created and started. Instead, receive fn will be returned.
|
||||||
|
for processing:
|
||||||
|
- messages: msgs are formatted and passed on to registered fns
|
||||||
|
- contact : chatrooms are updated when related info is received
|
||||||
|
it is defined in components/login.py
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
def get_msg(self):
|
||||||
|
''' fetch messages
|
||||||
|
for fetching
|
||||||
|
- method blocks for sometime until
|
||||||
|
- new messages are to be received
|
||||||
|
- or anytime they like
|
||||||
|
- synckey is updated with returned synccheckkey
|
||||||
|
it is defined in components/login.py
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
def logout(self):
|
||||||
|
''' logout
|
||||||
|
if core is now alive
|
||||||
|
logout will tell wechat backstage to logout
|
||||||
|
and core gets ready for another login
|
||||||
|
it is defined in components/login.py
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
def update_chatroom(self, userName, detailedMember=False):
|
||||||
|
''' update chatroom
|
||||||
|
for chatroom contact
|
||||||
|
- a chatroom contact need updating to be detailed
|
||||||
|
- detailed means members, encryid, etc
|
||||||
|
- auto updating of heart loop is a more detailed updating
|
||||||
|
- member uin will also be filled
|
||||||
|
- once called, updated info will be stored
|
||||||
|
for options
|
||||||
|
- userName: 'UserName' key of chatroom or a list of it
|
||||||
|
- detailedMember: whether to get members of contact
|
||||||
|
it is defined in components/contact.py
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
def update_friend(self, userName):
|
||||||
|
''' update chatroom
|
||||||
|
for friend contact
|
||||||
|
- once called, updated info will be stored
|
||||||
|
for options
|
||||||
|
- userName: 'UserName' key of a friend or a list of it
|
||||||
|
it is defined in components/contact.py
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
def get_contact(self, update=False):
|
||||||
|
''' fetch part of contact
|
||||||
|
for part
|
||||||
|
- all the massive platforms and friends are fetched
|
||||||
|
- if update, only starred chatrooms are fetched
|
||||||
|
for options
|
||||||
|
- update: if not set, local value will be returned
|
||||||
|
for results
|
||||||
|
- chatroomList will be returned
|
||||||
|
it is defined in components/contact.py
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
def get_friends(self, update=False):
|
||||||
|
''' fetch friends list
|
||||||
|
for options
|
||||||
|
- update: if not set, local value will be returned
|
||||||
|
for results
|
||||||
|
- a list of friends' info dicts will be returned
|
||||||
|
it is defined in components/contact.py
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
def get_chatrooms(self, update=False, contactOnly=False):
|
||||||
|
''' fetch chatrooms list
|
||||||
|
for options
|
||||||
|
- update: if not set, local value will be returned
|
||||||
|
- contactOnly: if set, only starred chatrooms will be returned
|
||||||
|
for results
|
||||||
|
- a list of chatrooms' info dicts will be returned
|
||||||
|
it is defined in components/contact.py
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
def get_mps(self, update=False):
|
||||||
|
''' fetch massive platforms list
|
||||||
|
for options
|
||||||
|
- update: if not set, local value will be returned
|
||||||
|
for results
|
||||||
|
- a list of platforms' info dicts will be returned
|
||||||
|
it is defined in components/contact.py
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
def set_alias(self, userName, alias):
|
||||||
|
''' set alias for a friend
|
||||||
|
for options
|
||||||
|
- userName: 'UserName' key of info dict
|
||||||
|
- alias: new alias
|
||||||
|
it is defined in components/contact.py
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
def set_pinned(self, userName, isPinned=True):
|
||||||
|
''' set pinned for a friend or a chatroom
|
||||||
|
for options
|
||||||
|
- userName: 'UserName' key of info dict
|
||||||
|
- isPinned: whether to pin
|
||||||
|
it is defined in components/contact.py
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
def accept_friend(self, userName, v4,autoUpdate=True):
|
||||||
|
''' accept a friend or accept a friend
|
||||||
|
for options
|
||||||
|
- userName: 'UserName' for friend's info dict
|
||||||
|
- status:
|
||||||
|
- for adding status should be 2
|
||||||
|
- for accepting status should be 3
|
||||||
|
- ticket: greeting message
|
||||||
|
- userInfo: friend's other info for adding into local storage
|
||||||
|
it is defined in components/contact.py
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
def get_head_img(self, userName=None, chatroomUserName=None, picDir=None):
|
||||||
|
''' place for docs
|
||||||
|
for options
|
||||||
|
- if you want to get chatroom header: only set chatroomUserName
|
||||||
|
- if you want to get friend header: only set userName
|
||||||
|
- if you want to get chatroom member header: set both
|
||||||
|
it is defined in components/contact.py
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
def create_chatroom(self, memberList, topic=''):
|
||||||
|
''' create a chatroom
|
||||||
|
for creating
|
||||||
|
- its calling frequency is strictly limited
|
||||||
|
for options
|
||||||
|
- memberList: list of member info dict
|
||||||
|
- topic: topic of new chatroom
|
||||||
|
it is defined in components/contact.py
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
def set_chatroom_name(self, chatroomUserName, name):
|
||||||
|
''' set chatroom name
|
||||||
|
for setting
|
||||||
|
- it makes an updating of chatroom
|
||||||
|
- which means detailed info will be returned in heart loop
|
||||||
|
for options
|
||||||
|
- chatroomUserName: 'UserName' key of chatroom info dict
|
||||||
|
- name: new chatroom name
|
||||||
|
it is defined in components/contact.py
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
def delete_member_from_chatroom(self, chatroomUserName, memberList):
|
||||||
|
''' deletes members from chatroom
|
||||||
|
for deleting
|
||||||
|
- you can't delete yourself
|
||||||
|
- if so, no one will be deleted
|
||||||
|
- strict-limited frequency
|
||||||
|
for options
|
||||||
|
- chatroomUserName: 'UserName' key of chatroom info dict
|
||||||
|
- memberList: list of members' info dict
|
||||||
|
it is defined in components/contact.py
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
def add_member_into_chatroom(self, chatroomUserName, memberList,
|
||||||
|
useInvitation=False):
|
||||||
|
''' add members into chatroom
|
||||||
|
for adding
|
||||||
|
- you can't add yourself or member already in chatroom
|
||||||
|
- if so, no one will be added
|
||||||
|
- if member will over 40 after adding, invitation must be used
|
||||||
|
- strict-limited frequency
|
||||||
|
for options
|
||||||
|
- chatroomUserName: 'UserName' key of chatroom info dict
|
||||||
|
- memberList: list of members' info dict
|
||||||
|
- useInvitation: if invitation is not required, set this to use
|
||||||
|
it is defined in components/contact.py
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
def send_raw_msg(self, msgType, content, toUserName):
|
||||||
|
''' many messages are sent in a common way
|
||||||
|
for demo
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
@itchat.msg_register(itchat.content.CARD)
|
||||||
|
def reply(msg):
|
||||||
|
itchat.send_raw_msg(msg['MsgType'], msg['Content'], msg['FromUserName'])
|
||||||
|
|
||||||
|
there are some little tricks here, you may discover them yourself
|
||||||
|
but remember they are tricks
|
||||||
|
it is defined in components/messages.py
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
def send_msg(self, msg='Test Message', toUserName=None):
|
||||||
|
''' send plain text message
|
||||||
|
for options
|
||||||
|
- msg: should be unicode if there's non-ascii words in msg
|
||||||
|
- toUserName: 'UserName' key of friend dict
|
||||||
|
it is defined in components/messages.py
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
def upload_file(self, fileDir, isPicture=False, isVideo=False,
|
||||||
|
toUserName='filehelper', file_=None, preparedFile=None):
|
||||||
|
''' upload file to server and get mediaId
|
||||||
|
for options
|
||||||
|
- fileDir: dir for file ready for upload
|
||||||
|
- isPicture: whether file is a picture
|
||||||
|
- isVideo: whether file is a video
|
||||||
|
for return values
|
||||||
|
will return a ReturnValue
|
||||||
|
if succeeded, mediaId is in r['MediaId']
|
||||||
|
it is defined in components/messages.py
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
def send_file(self, fileDir, toUserName=None, mediaId=None, file_=None):
|
||||||
|
''' send attachment
|
||||||
|
for options
|
||||||
|
- fileDir: dir for file ready for upload
|
||||||
|
- mediaId: mediaId for file.
|
||||||
|
- if set, file will not be uploaded twice
|
||||||
|
- toUserName: 'UserName' key of friend dict
|
||||||
|
it is defined in components/messages.py
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
def send_image(self, fileDir=None, toUserName=None, mediaId=None, file_=None):
|
||||||
|
''' send image
|
||||||
|
for options
|
||||||
|
- fileDir: dir for file ready for upload
|
||||||
|
- if it's a gif, name it like 'xx.gif'
|
||||||
|
- mediaId: mediaId for file.
|
||||||
|
- if set, file will not be uploaded twice
|
||||||
|
- toUserName: 'UserName' key of friend dict
|
||||||
|
it is defined in components/messages.py
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
def send_video(self, fileDir=None, toUserName=None, mediaId=None, file_=None):
|
||||||
|
''' send video
|
||||||
|
for options
|
||||||
|
- fileDir: dir for file ready for upload
|
||||||
|
- if mediaId is set, it's unnecessary to set fileDir
|
||||||
|
- mediaId: mediaId for file.
|
||||||
|
- if set, file will not be uploaded twice
|
||||||
|
- toUserName: 'UserName' key of friend dict
|
||||||
|
it is defined in components/messages.py
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
def send(self, msg, toUserName=None, mediaId=None):
|
||||||
|
''' wrapped function for all the sending functions
|
||||||
|
for options
|
||||||
|
- msg: message starts with different string indicates different type
|
||||||
|
- list of type string: ['@fil@', '@img@', '@msg@', '@vid@']
|
||||||
|
- they are for file, image, plain text, video
|
||||||
|
- if none of them matches, it will be sent like plain text
|
||||||
|
- toUserName: 'UserName' key of friend dict
|
||||||
|
- mediaId: if set, uploading will not be repeated
|
||||||
|
it is defined in components/messages.py
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
def revoke(self, msgId, toUserName, localId=None):
|
||||||
|
''' revoke message with its and msgId
|
||||||
|
for options
|
||||||
|
- msgId: message Id on server
|
||||||
|
- toUserName: 'UserName' key of friend dict
|
||||||
|
- localId: message Id at local (optional)
|
||||||
|
it is defined in components/messages.py
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
def dump_login_status(self, fileDir=None):
|
||||||
|
''' dump login status to a specific file
|
||||||
|
for option
|
||||||
|
- fileDir: dir for dumping login status
|
||||||
|
it is defined in components/hotreload.py
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
def load_login_status(self, fileDir,
|
||||||
|
loginCallback=None, exitCallback=None):
|
||||||
|
''' load login status from a specific file
|
||||||
|
for option
|
||||||
|
- fileDir: file for loading login status
|
||||||
|
- loginCallback: callback after successfully logged in
|
||||||
|
- if not set, screen is cleared and qrcode is deleted
|
||||||
|
- exitCallback: callback after logged out
|
||||||
|
- it contains calling of logout
|
||||||
|
it is defined in components/hotreload.py
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
def auto_login(self, hotReload=False, statusStorageDir='itchat.pkl',
|
||||||
|
enableCmdQR=False, picDir=None, qrCallback=None,
|
||||||
|
loginCallback=None, exitCallback=None):
|
||||||
|
''' log in like web wechat does
|
||||||
|
for log in
|
||||||
|
- a QR code will be downloaded and opened
|
||||||
|
- then scanning status is logged, it paused for you confirm
|
||||||
|
- finally it logged in and show your nickName
|
||||||
|
for options
|
||||||
|
- hotReload: enable hot reload
|
||||||
|
- statusStorageDir: dir for storing log in status
|
||||||
|
- enableCmdQR: show qrcode in command line
|
||||||
|
- integers can be used to fit strange char length
|
||||||
|
- picDir: place for storing qrcode
|
||||||
|
- loginCallback: callback after successfully logged in
|
||||||
|
- if not set, screen is cleared and qrcode is deleted
|
||||||
|
- exitCallback: callback after logged out
|
||||||
|
- it contains calling of logout
|
||||||
|
- qrCallback: method that should accept uuid, status, qrcode
|
||||||
|
for usage
|
||||||
|
..code::python
|
||||||
|
|
||||||
|
import itchat
|
||||||
|
itchat.auto_login()
|
||||||
|
|
||||||
|
it is defined in components/register.py
|
||||||
|
and of course every single move in login can be called outside
|
||||||
|
- you may scan source code to see how
|
||||||
|
- and modified according to your own demond
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
def configured_reply(self):
|
||||||
|
''' determine the type of message and reply if its method is defined
|
||||||
|
however, I use a strange way to determine whether a msg is from massive platform
|
||||||
|
I haven't found a better solution here
|
||||||
|
The main problem I'm worrying about is the mismatching of new friends added on phone
|
||||||
|
If you have any good idea, pleeeease report an issue. I will be more than grateful.
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
def msg_register(self, msgType,
|
||||||
|
isFriendChat=False, isGroupChat=False, isMpChat=False):
|
||||||
|
''' a decorator constructor
|
||||||
|
return a specific decorator based on information given
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
def run(self, debug=True, blockThread=True):
|
||||||
|
''' start auto respond
|
||||||
|
for option
|
||||||
|
- debug: if set, debug info will be shown on screen
|
||||||
|
it is defined in components/register.py
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
def search_friends(self, name=None, userName=None, remarkName=None, nickName=None,
|
||||||
|
wechatAccount=None):
|
||||||
|
return self.storageClass.search_friends(name, userName, remarkName,
|
||||||
|
nickName, wechatAccount)
|
||||||
|
def search_chatrooms(self, name=None, userName=None):
|
||||||
|
return self.storageClass.search_chatrooms(name, userName)
|
||||||
|
def search_mps(self, name=None, userName=None):
|
||||||
|
return self.storageClass.search_mps(name, userName)
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
class LogSystem(object):
|
||||||
|
handlerList = []
|
||||||
|
showOnCmd = True
|
||||||
|
loggingLevel = logging.INFO
|
||||||
|
loggingFile = None
|
||||||
|
def __init__(self):
|
||||||
|
self.logger = logging.getLogger('itchat')
|
||||||
|
self.logger.addHandler(logging.NullHandler())
|
||||||
|
self.logger.setLevel(self.loggingLevel)
|
||||||
|
self.cmdHandler = logging.StreamHandler()
|
||||||
|
self.fileHandler = None
|
||||||
|
self.logger.addHandler(self.cmdHandler)
|
||||||
|
def set_logging(self, showOnCmd=True, loggingFile=None,
|
||||||
|
loggingLevel=logging.INFO):
|
||||||
|
if showOnCmd != self.showOnCmd:
|
||||||
|
if showOnCmd:
|
||||||
|
self.logger.addHandler(self.cmdHandler)
|
||||||
|
else:
|
||||||
|
self.logger.removeHandler(self.cmdHandler)
|
||||||
|
self.showOnCmd = showOnCmd
|
||||||
|
if loggingFile != self.loggingFile:
|
||||||
|
if self.loggingFile is not None: # clear old fileHandler
|
||||||
|
self.logger.removeHandler(self.fileHandler)
|
||||||
|
self.fileHandler.close()
|
||||||
|
if loggingFile is not None: # add new fileHandler
|
||||||
|
self.fileHandler = logging.FileHandler(loggingFile)
|
||||||
|
self.logger.addHandler(self.fileHandler)
|
||||||
|
self.loggingFile = loggingFile
|
||||||
|
if loggingLevel != self.loggingLevel:
|
||||||
|
self.logger.setLevel(loggingLevel)
|
||||||
|
self.loggingLevel = loggingLevel
|
||||||
|
|
||||||
|
ls = LogSystem()
|
||||||
|
set_logging = ls.set_logging
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
#coding=utf8
|
||||||
|
TRANSLATE = 'Chinese'
|
||||||
|
|
||||||
|
class ReturnValue(dict):
|
||||||
|
''' turn return value of itchat into a boolean value
|
||||||
|
for requests:
|
||||||
|
..code::python
|
||||||
|
|
||||||
|
import requests
|
||||||
|
r = requests.get('http://httpbin.org/get')
|
||||||
|
print(ReturnValue(rawResponse=r)
|
||||||
|
|
||||||
|
for normal dict:
|
||||||
|
..code::python
|
||||||
|
|
||||||
|
returnDict = {
|
||||||
|
'BaseResponse': {
|
||||||
|
'Ret': 0,
|
||||||
|
'ErrMsg': 'My error msg', }, }
|
||||||
|
print(ReturnValue(returnDict))
|
||||||
|
'''
|
||||||
|
def __init__(self, returnValueDict={}, rawResponse=None):
|
||||||
|
if rawResponse:
|
||||||
|
try:
|
||||||
|
returnValueDict = rawResponse.json()
|
||||||
|
except ValueError:
|
||||||
|
returnValueDict = {
|
||||||
|
'BaseResponse': {
|
||||||
|
'Ret': -1004,
|
||||||
|
'ErrMsg': 'Unexpected return value', },
|
||||||
|
'Data': rawResponse.content, }
|
||||||
|
for k, v in returnValueDict.items():
|
||||||
|
self[k] = v
|
||||||
|
if not 'BaseResponse' in self:
|
||||||
|
self['BaseResponse'] = {
|
||||||
|
'ErrMsg': 'no BaseResponse in raw response',
|
||||||
|
'Ret': -1000, }
|
||||||
|
if TRANSLATE:
|
||||||
|
self['BaseResponse']['RawMsg'] = self['BaseResponse'].get('ErrMsg', '')
|
||||||
|
self['BaseResponse']['ErrMsg'] = \
|
||||||
|
TRANSLATION[TRANSLATE].get(
|
||||||
|
self['BaseResponse'].get('Ret', '')) \
|
||||||
|
or self['BaseResponse'].get('ErrMsg', u'No ErrMsg')
|
||||||
|
self['BaseResponse']['RawMsg'] = \
|
||||||
|
self['BaseResponse']['RawMsg'] or self['BaseResponse']['ErrMsg']
|
||||||
|
def __nonzero__(self):
|
||||||
|
return self['BaseResponse'].get('Ret') == 0
|
||||||
|
def __bool__(self):
|
||||||
|
return self.__nonzero__()
|
||||||
|
def __str__(self):
|
||||||
|
return '{%s}' % ', '.join(
|
||||||
|
['%s: %s' % (repr(k),repr(v)) for k,v in self.items()])
|
||||||
|
def __repr__(self):
|
||||||
|
return '<ItchatReturnValue: %s>' % self.__str__()
|
||||||
|
|
||||||
|
TRANSLATION = {
|
||||||
|
'Chinese': {
|
||||||
|
-1000: u'返回值不带BaseResponse',
|
||||||
|
-1001: u'无法找到对应的成员',
|
||||||
|
-1002: u'文件位置错误',
|
||||||
|
-1003: u'服务器拒绝连接',
|
||||||
|
-1004: u'服务器返回异常值',
|
||||||
|
-1005: u'参数错误',
|
||||||
|
-1006: u'无效操作',
|
||||||
|
0: u'请求成功',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
import os, time, copy
|
||||||
|
from threading import Lock
|
||||||
|
|
||||||
|
from .messagequeue import Queue
|
||||||
|
from .templates import (
|
||||||
|
ContactList, AbstractUserDict, User,
|
||||||
|
MassivePlatform, Chatroom, ChatroomMember)
|
||||||
|
|
||||||
|
def contact_change(fn):
|
||||||
|
def _contact_change(core, *args, **kwargs):
|
||||||
|
with core.storageClass.updateLock:
|
||||||
|
return fn(core, *args, **kwargs)
|
||||||
|
return _contact_change
|
||||||
|
|
||||||
|
class Storage(object):
|
||||||
|
def __init__(self, core):
|
||||||
|
self.userName = None
|
||||||
|
self.nickName = None
|
||||||
|
self.updateLock = Lock()
|
||||||
|
self.memberList = ContactList()
|
||||||
|
self.mpList = ContactList()
|
||||||
|
self.chatroomList = ContactList()
|
||||||
|
self.msgList = Queue(-1)
|
||||||
|
self.lastInputUserName = None
|
||||||
|
self.memberList.set_default_value(contactClass=User)
|
||||||
|
self.memberList.core = core
|
||||||
|
self.mpList.set_default_value(contactClass=MassivePlatform)
|
||||||
|
self.mpList.core = core
|
||||||
|
self.chatroomList.set_default_value(contactClass=Chatroom)
|
||||||
|
self.chatroomList.core = core
|
||||||
|
def dumps(self):
|
||||||
|
return {
|
||||||
|
'userName' : self.userName,
|
||||||
|
'nickName' : self.nickName,
|
||||||
|
'memberList' : self.memberList,
|
||||||
|
'mpList' : self.mpList,
|
||||||
|
'chatroomList' : self.chatroomList,
|
||||||
|
'lastInputUserName' : self.lastInputUserName, }
|
||||||
|
def loads(self, j):
|
||||||
|
self.userName = j.get('userName', None)
|
||||||
|
self.nickName = j.get('nickName', None)
|
||||||
|
del self.memberList[:]
|
||||||
|
for i in j.get('memberList', []):
|
||||||
|
self.memberList.append(i)
|
||||||
|
del self.mpList[:]
|
||||||
|
for i in j.get('mpList', []):
|
||||||
|
self.mpList.append(i)
|
||||||
|
del self.chatroomList[:]
|
||||||
|
for i in j.get('chatroomList', []):
|
||||||
|
self.chatroomList.append(i)
|
||||||
|
# I tried to solve everything in pickle
|
||||||
|
# but this way is easier and more storage-saving
|
||||||
|
for chatroom in self.chatroomList:
|
||||||
|
if 'MemberList' in chatroom:
|
||||||
|
for member in chatroom['MemberList']:
|
||||||
|
member.core = chatroom.core
|
||||||
|
member.chatroom = chatroom
|
||||||
|
if 'Self' in chatroom:
|
||||||
|
chatroom['Self'].core = chatroom.core
|
||||||
|
chatroom['Self'].chatroom = chatroom
|
||||||
|
self.lastInputUserName = j.get('lastInputUserName', None)
|
||||||
|
def search_friends(self, name=None, userName=None, remarkName=None, nickName=None,
|
||||||
|
wechatAccount=None):
|
||||||
|
with self.updateLock:
|
||||||
|
if (name or userName or remarkName or nickName or wechatAccount) is None:
|
||||||
|
return copy.deepcopy(self.memberList[0]) # my own account
|
||||||
|
elif userName: # return the only userName match
|
||||||
|
for m in self.memberList:
|
||||||
|
if m['UserName'] == userName:
|
||||||
|
return copy.deepcopy(m)
|
||||||
|
else:
|
||||||
|
matchDict = {
|
||||||
|
'RemarkName' : remarkName,
|
||||||
|
'NickName' : nickName,
|
||||||
|
'Alias' : wechatAccount, }
|
||||||
|
for k in ('RemarkName', 'NickName', 'Alias'):
|
||||||
|
if matchDict[k] is None:
|
||||||
|
del matchDict[k]
|
||||||
|
if name: # select based on name
|
||||||
|
contact = []
|
||||||
|
for m in self.memberList:
|
||||||
|
if any([m.get(k) == name for k in ('RemarkName', 'NickName', 'Alias')]):
|
||||||
|
contact.append(m)
|
||||||
|
else:
|
||||||
|
contact = self.memberList[:]
|
||||||
|
if matchDict: # select again based on matchDict
|
||||||
|
friendList = []
|
||||||
|
for m in contact:
|
||||||
|
if all([m.get(k) == v for k, v in matchDict.items()]):
|
||||||
|
friendList.append(m)
|
||||||
|
return copy.deepcopy(friendList)
|
||||||
|
else:
|
||||||
|
return copy.deepcopy(contact)
|
||||||
|
def search_chatrooms(self, name=None, userName=None):
|
||||||
|
with self.updateLock:
|
||||||
|
if userName is not None:
|
||||||
|
for m in self.chatroomList:
|
||||||
|
if m['UserName'] == userName:
|
||||||
|
return copy.deepcopy(m)
|
||||||
|
elif name is not None:
|
||||||
|
matchList = []
|
||||||
|
for m in self.chatroomList:
|
||||||
|
if name in m['NickName']:
|
||||||
|
matchList.append(copy.deepcopy(m))
|
||||||
|
return matchList
|
||||||
|
def search_mps(self, name=None, userName=None):
|
||||||
|
with self.updateLock:
|
||||||
|
if userName is not None:
|
||||||
|
for m in self.mpList:
|
||||||
|
if m['UserName'] == userName:
|
||||||
|
return copy.deepcopy(m)
|
||||||
|
elif name is not None:
|
||||||
|
matchList = []
|
||||||
|
for m in self.mpList:
|
||||||
|
if name in m['NickName']:
|
||||||
|
matchList.append(copy.deepcopy(m))
|
||||||
|
return matchList
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
import logging
|
||||||
|
try:
|
||||||
|
import Queue as queue
|
||||||
|
except ImportError:
|
||||||
|
import queue
|
||||||
|
|
||||||
|
from .templates import AttributeDict
|
||||||
|
|
||||||
|
logger = logging.getLogger('itchat')
|
||||||
|
|
||||||
|
class Queue(queue.Queue):
|
||||||
|
def put(self, message):
|
||||||
|
queue.Queue.put(self, Message(message))
|
||||||
|
|
||||||
|
class Message(AttributeDict):
|
||||||
|
def download(self, fileName):
|
||||||
|
if hasattr(self.text, '__call__'):
|
||||||
|
return self.text(fileName)
|
||||||
|
else:
|
||||||
|
return b''
|
||||||
|
def __getitem__(self, value):
|
||||||
|
if value in ('isAdmin', 'isAt'):
|
||||||
|
v = value[0].upper() + value[1:] # ''[1:] == ''
|
||||||
|
logger.debug('%s is expired in 1.3.0, use %s instead.' % (value, v))
|
||||||
|
value = v
|
||||||
|
return super(Message, self).__getitem__(value)
|
||||||
|
def __str__(self):
|
||||||
|
return '{%s}' % ', '.join(
|
||||||
|
['%s: %s' % (repr(k),repr(v)) for k,v in self.items()])
|
||||||
|
def __repr__(self):
|
||||||
|
return '<%s: %s>' % (self.__class__.__name__.split('.')[-1],
|
||||||
|
self.__str__())
|
||||||
|
|
@ -0,0 +1,318 @@
|
||||||
|
import logging, copy, pickle
|
||||||
|
from weakref import ref
|
||||||
|
|
||||||
|
from ..returnvalues import ReturnValue
|
||||||
|
from ..utils import update_info_dict
|
||||||
|
|
||||||
|
logger = logging.getLogger('itchat')
|
||||||
|
|
||||||
|
class AttributeDict(dict):
|
||||||
|
def __getattr__(self, value):
|
||||||
|
keyName = value[0].upper() + value[1:]
|
||||||
|
try:
|
||||||
|
return self[keyName]
|
||||||
|
except KeyError:
|
||||||
|
raise AttributeError("'%s' object has no attribute '%s'" % (
|
||||||
|
self.__class__.__name__.split('.')[-1], keyName))
|
||||||
|
def get(self, v, d=None):
|
||||||
|
try:
|
||||||
|
return self[v]
|
||||||
|
except KeyError:
|
||||||
|
return d
|
||||||
|
|
||||||
|
class UnInitializedItchat(object):
|
||||||
|
def _raise_error(self, *args, **kwargs):
|
||||||
|
logger.warning('An itchat instance is called before initialized')
|
||||||
|
def __getattr__(self, value):
|
||||||
|
return self._raise_error
|
||||||
|
|
||||||
|
class ContactList(list):
|
||||||
|
''' when a dict is append, init function will be called to format that dict '''
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(ContactList, self).__init__(*args, **kwargs)
|
||||||
|
self.__setstate__(None)
|
||||||
|
@property
|
||||||
|
def core(self):
|
||||||
|
return getattr(self, '_core', lambda: fakeItchat)() or fakeItchat
|
||||||
|
@core.setter
|
||||||
|
def core(self, value):
|
||||||
|
self._core = ref(value)
|
||||||
|
def set_default_value(self, initFunction=None, contactClass=None):
|
||||||
|
if hasattr(initFunction, '__call__'):
|
||||||
|
self.contactInitFn = initFunction
|
||||||
|
if hasattr(contactClass, '__call__'):
|
||||||
|
self.contactClass = contactClass
|
||||||
|
def append(self, value):
|
||||||
|
contact = self.contactClass(value)
|
||||||
|
contact.core = self.core
|
||||||
|
if self.contactInitFn is not None:
|
||||||
|
contact = self.contactInitFn(self, contact) or contact
|
||||||
|
super(ContactList, self).append(contact)
|
||||||
|
def __deepcopy__(self, memo):
|
||||||
|
r = self.__class__([copy.deepcopy(v) for v in self])
|
||||||
|
r.contactInitFn = self.contactInitFn
|
||||||
|
r.contactClass = self.contactClass
|
||||||
|
r.core = self.core
|
||||||
|
return r
|
||||||
|
def __getstate__(self):
|
||||||
|
return 1
|
||||||
|
def __setstate__(self, state):
|
||||||
|
self.contactInitFn = None
|
||||||
|
self.contactClass = User
|
||||||
|
def __str__(self):
|
||||||
|
return '[%s]' % ', '.join([repr(v) for v in self])
|
||||||
|
def __repr__(self):
|
||||||
|
return '<%s: %s>' % (self.__class__.__name__.split('.')[-1],
|
||||||
|
self.__str__())
|
||||||
|
|
||||||
|
class AbstractUserDict(AttributeDict):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(AbstractUserDict, self).__init__(*args, **kwargs)
|
||||||
|
@property
|
||||||
|
def core(self):
|
||||||
|
return getattr(self, '_core', lambda: fakeItchat)() or fakeItchat
|
||||||
|
@core.setter
|
||||||
|
def core(self, value):
|
||||||
|
self._core = ref(value)
|
||||||
|
def update(self):
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'Ret': -1006,
|
||||||
|
'ErrMsg': '%s can not be updated' % \
|
||||||
|
self.__class__.__name__, }, })
|
||||||
|
def set_alias(self, alias):
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'Ret': -1006,
|
||||||
|
'ErrMsg': '%s can not set alias' % \
|
||||||
|
self.__class__.__name__, }, })
|
||||||
|
def set_pinned(self, isPinned=True):
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'Ret': -1006,
|
||||||
|
'ErrMsg': '%s can not be pinned' % \
|
||||||
|
self.__class__.__name__, }, })
|
||||||
|
def verify(self):
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'Ret': -1006,
|
||||||
|
'ErrMsg': '%s do not need verify' % \
|
||||||
|
self.__class__.__name__, }, })
|
||||||
|
def get_head_image(self, imageDir=None):
|
||||||
|
return self.core.get_head_img(self.userName, picDir=imageDir)
|
||||||
|
def delete_member(self, userName):
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'Ret': -1006,
|
||||||
|
'ErrMsg': '%s can not delete member' % \
|
||||||
|
self.__class__.__name__, }, })
|
||||||
|
def add_member(self, userName):
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'Ret': -1006,
|
||||||
|
'ErrMsg': '%s can not add member' % \
|
||||||
|
self.__class__.__name__, }, })
|
||||||
|
def send_raw_msg(self, msgType, content):
|
||||||
|
return self.core.send_raw_msg(msgType, content, self.userName)
|
||||||
|
def send_msg(self, msg='Test Message'):
|
||||||
|
return self.core.send_msg(msg, self.userName)
|
||||||
|
def send_file(self, fileDir, mediaId=None):
|
||||||
|
return self.core.send_file(fileDir, self.userName, mediaId)
|
||||||
|
def send_image(self, fileDir, mediaId=None):
|
||||||
|
return self.core.send_image(fileDir, self.userName, mediaId)
|
||||||
|
def send_video(self, fileDir=None, mediaId=None):
|
||||||
|
return self.core.send_video(fileDir, self.userName, mediaId)
|
||||||
|
def send(self, msg, mediaId=None):
|
||||||
|
return self.core.send(msg, self.userName, mediaId)
|
||||||
|
def search_member(self, name=None, userName=None, remarkName=None, nickName=None,
|
||||||
|
wechatAccount=None):
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'Ret': -1006,
|
||||||
|
'ErrMsg': '%s do not have members' % \
|
||||||
|
self.__class__.__name__, }, })
|
||||||
|
def __deepcopy__(self, memo):
|
||||||
|
r = self.__class__()
|
||||||
|
for k, v in self.items():
|
||||||
|
r[copy.deepcopy(k)] = copy.deepcopy(v)
|
||||||
|
r.core = self.core
|
||||||
|
return r
|
||||||
|
def __str__(self):
|
||||||
|
return '{%s}' % ', '.join(
|
||||||
|
['%s: %s' % (repr(k),repr(v)) for k,v in self.items()])
|
||||||
|
def __repr__(self):
|
||||||
|
return '<%s: %s>' % (self.__class__.__name__.split('.')[-1],
|
||||||
|
self.__str__())
|
||||||
|
def __getstate__(self):
|
||||||
|
return 1
|
||||||
|
def __setstate__(self, state):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class User(AbstractUserDict):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(User, self).__init__(*args, **kwargs)
|
||||||
|
self.__setstate__(None)
|
||||||
|
def update(self):
|
||||||
|
r = self.core.update_friend(self.userName)
|
||||||
|
if r:
|
||||||
|
update_info_dict(self, r)
|
||||||
|
return r
|
||||||
|
def set_alias(self, alias):
|
||||||
|
return self.core.set_alias(self.userName, alias)
|
||||||
|
def set_pinned(self, isPinned=True):
|
||||||
|
return self.core.set_pinned(self.userName, isPinned)
|
||||||
|
def verify(self):
|
||||||
|
return self.core.add_friend(**self.verifyDict)
|
||||||
|
def __deepcopy__(self, memo):
|
||||||
|
r = super(User, self).__deepcopy__(memo)
|
||||||
|
r.verifyDict = copy.deepcopy(self.verifyDict)
|
||||||
|
return r
|
||||||
|
def __setstate__(self, state):
|
||||||
|
super(User, self).__setstate__(state)
|
||||||
|
self.verifyDict = {}
|
||||||
|
self['MemberList'] = fakeContactList
|
||||||
|
|
||||||
|
class MassivePlatform(AbstractUserDict):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(MassivePlatform, self).__init__(*args, **kwargs)
|
||||||
|
self.__setstate__(None)
|
||||||
|
def __setstate__(self, state):
|
||||||
|
super(MassivePlatform, self).__setstate__(state)
|
||||||
|
self['MemberList'] = fakeContactList
|
||||||
|
|
||||||
|
class Chatroom(AbstractUserDict):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(Chatroom, self).__init__(*args, **kwargs)
|
||||||
|
memberList = ContactList()
|
||||||
|
userName = self.get('UserName', '')
|
||||||
|
refSelf = ref(self)
|
||||||
|
def init_fn(parentList, d):
|
||||||
|
d.chatroom = refSelf() or \
|
||||||
|
parentList.core.search_chatrooms(userName=userName)
|
||||||
|
memberList.set_default_value(init_fn, ChatroomMember)
|
||||||
|
if 'MemberList' in self:
|
||||||
|
for member in self.memberList:
|
||||||
|
memberList.append(member)
|
||||||
|
self['MemberList'] = memberList
|
||||||
|
@property
|
||||||
|
def core(self):
|
||||||
|
return getattr(self, '_core', lambda: fakeItchat)() or fakeItchat
|
||||||
|
@core.setter
|
||||||
|
def core(self, value):
|
||||||
|
self._core = ref(value)
|
||||||
|
self.memberList.core = value
|
||||||
|
for member in self.memberList:
|
||||||
|
member.core = value
|
||||||
|
def update(self, detailedMember=False):
|
||||||
|
r = self.core.update_chatroom(self.userName, detailedMember)
|
||||||
|
if r:
|
||||||
|
update_info_dict(self, r)
|
||||||
|
self['MemberList'] = r['MemberList']
|
||||||
|
return r
|
||||||
|
def set_alias(self, alias):
|
||||||
|
return self.core.set_chatroom_name(self.userName, alias)
|
||||||
|
def set_pinned(self, isPinned=True):
|
||||||
|
return self.core.set_pinned(self.userName, isPinned)
|
||||||
|
def delete_member(self, userName):
|
||||||
|
return self.core.delete_member_from_chatroom(self.userName, userName)
|
||||||
|
def add_member(self, userName):
|
||||||
|
return self.core.add_member_into_chatroom(self.userName, userName)
|
||||||
|
def search_member(self, name=None, userName=None, remarkName=None, nickName=None,
|
||||||
|
wechatAccount=None):
|
||||||
|
with self.core.storageClass.updateLock:
|
||||||
|
if (name or userName or remarkName or nickName or wechatAccount) is None:
|
||||||
|
return None
|
||||||
|
elif userName: # return the only userName match
|
||||||
|
for m in self.memberList:
|
||||||
|
if m.userName == userName:
|
||||||
|
return copy.deepcopy(m)
|
||||||
|
else:
|
||||||
|
matchDict = {
|
||||||
|
'RemarkName' : remarkName,
|
||||||
|
'NickName' : nickName,
|
||||||
|
'Alias' : wechatAccount, }
|
||||||
|
for k in ('RemarkName', 'NickName', 'Alias'):
|
||||||
|
if matchDict[k] is None:
|
||||||
|
del matchDict[k]
|
||||||
|
if name: # select based on name
|
||||||
|
contact = []
|
||||||
|
for m in self.memberList:
|
||||||
|
if any([m.get(k) == name for k in ('RemarkName', 'NickName', 'Alias')]):
|
||||||
|
contact.append(m)
|
||||||
|
else:
|
||||||
|
contact = self.memberList[:]
|
||||||
|
if matchDict: # select again based on matchDict
|
||||||
|
friendList = []
|
||||||
|
for m in contact:
|
||||||
|
if all([m.get(k) == v for k, v in matchDict.items()]):
|
||||||
|
friendList.append(m)
|
||||||
|
return copy.deepcopy(friendList)
|
||||||
|
else:
|
||||||
|
return copy.deepcopy(contact)
|
||||||
|
def __setstate__(self, state):
|
||||||
|
super(Chatroom, self).__setstate__(state)
|
||||||
|
if not 'MemberList' in self:
|
||||||
|
self['MemberList'] = fakeContactList
|
||||||
|
|
||||||
|
class ChatroomMember(AbstractUserDict):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(AbstractUserDict, self).__init__(*args, **kwargs)
|
||||||
|
self.__setstate__(None)
|
||||||
|
@property
|
||||||
|
def chatroom(self):
|
||||||
|
r = getattr(self, '_chatroom', lambda: fakeChatroom)()
|
||||||
|
if r is None:
|
||||||
|
userName = getattr(self, '_chatroomUserName', '')
|
||||||
|
r = self.core.search_chatrooms(userName=userName)
|
||||||
|
if isinstance(r, dict):
|
||||||
|
self.chatroom = r
|
||||||
|
return r or fakeChatroom
|
||||||
|
@chatroom.setter
|
||||||
|
def chatroom(self, value):
|
||||||
|
if isinstance(value, dict) and 'UserName' in value:
|
||||||
|
self._chatroom = ref(value)
|
||||||
|
self._chatroomUserName = value['UserName']
|
||||||
|
def get_head_image(self, imageDir=None):
|
||||||
|
return self.core.get_head_img(self.userName, self.chatroom.userName, picDir=imageDir)
|
||||||
|
def delete_member(self, userName):
|
||||||
|
return self.core.delete_member_from_chatroom(self.chatroom.userName, self.userName)
|
||||||
|
def send_raw_msg(self, msgType, content):
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'Ret': -1006,
|
||||||
|
'ErrMsg': '%s can not send message directly' % \
|
||||||
|
self.__class__.__name__, }, })
|
||||||
|
def send_msg(self, msg='Test Message'):
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'Ret': -1006,
|
||||||
|
'ErrMsg': '%s can not send message directly' % \
|
||||||
|
self.__class__.__name__, }, })
|
||||||
|
def send_file(self, fileDir, mediaId=None):
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'Ret': -1006,
|
||||||
|
'ErrMsg': '%s can not send message directly' % \
|
||||||
|
self.__class__.__name__, }, })
|
||||||
|
def send_image(self, fileDir, mediaId=None):
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'Ret': -1006,
|
||||||
|
'ErrMsg': '%s can not send message directly' % \
|
||||||
|
self.__class__.__name__, }, })
|
||||||
|
def send_video(self, fileDir=None, mediaId=None):
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'Ret': -1006,
|
||||||
|
'ErrMsg': '%s can not send message directly' % \
|
||||||
|
self.__class__.__name__, }, })
|
||||||
|
def send(self, msg, mediaId=None):
|
||||||
|
return ReturnValue({'BaseResponse': {
|
||||||
|
'Ret': -1006,
|
||||||
|
'ErrMsg': '%s can not send message directly' % \
|
||||||
|
self.__class__.__name__, }, })
|
||||||
|
def __setstate__(self, state):
|
||||||
|
super(ChatroomMember, self).__setstate__(state)
|
||||||
|
self['MemberList'] = fakeContactList
|
||||||
|
|
||||||
|
def wrap_user_dict(d):
|
||||||
|
userName = d.get('UserName')
|
||||||
|
if '@@' in userName:
|
||||||
|
r = Chatroom(d)
|
||||||
|
elif d.get('VerifyFlag', 8) & 8 == 0:
|
||||||
|
r = User(d)
|
||||||
|
else:
|
||||||
|
r = MassivePlatform(d)
|
||||||
|
return r
|
||||||
|
|
||||||
|
fakeItchat = UnInitializedItchat()
|
||||||
|
fakeContactList = ContactList()
|
||||||
|
fakeChatroom = Chatroom()
|
||||||
|
|
@ -0,0 +1,163 @@
|
||||||
|
import re, os, sys, subprocess, copy, traceback, logging
|
||||||
|
|
||||||
|
try:
|
||||||
|
from HTMLParser import HTMLParser
|
||||||
|
except ImportError:
|
||||||
|
from html.parser import HTMLParser
|
||||||
|
try:
|
||||||
|
from urllib import quote as _quote
|
||||||
|
quote = lambda n: _quote(n.encode('utf8', 'replace'))
|
||||||
|
except ImportError:
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from . import config
|
||||||
|
|
||||||
|
logger = logging.getLogger('itchat')
|
||||||
|
|
||||||
|
emojiRegex = re.compile(r'<span class="emoji emoji(.{1,10})"></span>')
|
||||||
|
htmlParser = HTMLParser()
|
||||||
|
if not hasattr(htmlParser, 'unescape'):
|
||||||
|
import html
|
||||||
|
htmlParser.unescape = html.unescape
|
||||||
|
# FIX Python 3.9 HTMLParser.unescape is removed. See https://docs.python.org/3.9/whatsnew/3.9.html
|
||||||
|
try:
|
||||||
|
b = u'\u2588'
|
||||||
|
sys.stdout.write(b + '\r')
|
||||||
|
sys.stdout.flush()
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
BLOCK = 'MM'
|
||||||
|
else:
|
||||||
|
BLOCK = b
|
||||||
|
friendInfoTemplate = {}
|
||||||
|
for k in ('UserName', 'City', 'DisplayName', 'PYQuanPin', 'RemarkPYInitial', 'Province',
|
||||||
|
'KeyWord', 'RemarkName', 'PYInitial', 'EncryChatRoomId', 'Alias', 'Signature',
|
||||||
|
'NickName', 'RemarkPYQuanPin', 'HeadImgUrl'):
|
||||||
|
friendInfoTemplate[k] = ''
|
||||||
|
for k in ('UniFriend', 'Sex', 'AppAccountFlag', 'VerifyFlag', 'ChatRoomId', 'HideInputBarFlag',
|
||||||
|
'AttrStatus', 'SnsFlag', 'MemberCount', 'OwnerUin', 'ContactFlag', 'Uin',
|
||||||
|
'StarFriend', 'Statues'):
|
||||||
|
friendInfoTemplate[k] = 0
|
||||||
|
friendInfoTemplate['MemberList'] = []
|
||||||
|
|
||||||
|
def clear_screen():
|
||||||
|
os.system('cls' if config.OS == 'Windows' else 'clear')
|
||||||
|
|
||||||
|
def emoji_formatter(d, k):
|
||||||
|
''' _emoji_deebugger is for bugs about emoji match caused by wechat backstage
|
||||||
|
like :face with tears of joy: will be replaced with :cat face with tears of joy:
|
||||||
|
'''
|
||||||
|
def _emoji_debugger(d, k):
|
||||||
|
s = d[k].replace('<span class="emoji emoji1f450"></span',
|
||||||
|
'<span class="emoji emoji1f450"></span>') # fix missing bug
|
||||||
|
def __fix_miss_match(m):
|
||||||
|
return '<span class="emoji emoji%s"></span>' % ({
|
||||||
|
'1f63c': '1f601', '1f639': '1f602', '1f63a': '1f603',
|
||||||
|
'1f4ab': '1f616', '1f64d': '1f614', '1f63b': '1f60d',
|
||||||
|
'1f63d': '1f618', '1f64e': '1f621', '1f63f': '1f622',
|
||||||
|
}.get(m.group(1), m.group(1)))
|
||||||
|
return emojiRegex.sub(__fix_miss_match, s)
|
||||||
|
def _emoji_formatter(m):
|
||||||
|
s = m.group(1)
|
||||||
|
if len(s) == 6:
|
||||||
|
return ('\\U%s\\U%s'%(s[:2].rjust(8, '0'), s[2:].rjust(8, '0'))
|
||||||
|
).encode('utf8').decode('unicode-escape', 'replace')
|
||||||
|
elif len(s) == 10:
|
||||||
|
return ('\\U%s\\U%s'%(s[:5].rjust(8, '0'), s[5:].rjust(8, '0'))
|
||||||
|
).encode('utf8').decode('unicode-escape', 'replace')
|
||||||
|
else:
|
||||||
|
return ('\\U%s'%m.group(1).rjust(8, '0')
|
||||||
|
).encode('utf8').decode('unicode-escape', 'replace')
|
||||||
|
d[k] = _emoji_debugger(d, k)
|
||||||
|
d[k] = emojiRegex.sub(_emoji_formatter, d[k])
|
||||||
|
|
||||||
|
def msg_formatter(d, k):
|
||||||
|
emoji_formatter(d, k)
|
||||||
|
d[k] = d[k].replace('<br/>', '\n')
|
||||||
|
d[k] = htmlParser.unescape(d[k])
|
||||||
|
|
||||||
|
def check_file(fileDir):
|
||||||
|
try:
|
||||||
|
with open(fileDir):
|
||||||
|
pass
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def print_qr(fileDir):
|
||||||
|
if config.OS == 'Darwin':
|
||||||
|
subprocess.call(['open', fileDir])
|
||||||
|
elif config.OS == 'Linux':
|
||||||
|
subprocess.call(['xdg-open', fileDir])
|
||||||
|
else:
|
||||||
|
os.startfile(fileDir)
|
||||||
|
|
||||||
|
def print_cmd_qr(qrText, white=BLOCK, black=' ', enableCmdQR=True):
|
||||||
|
blockCount = int(enableCmdQR)
|
||||||
|
if abs(blockCount) == 0:
|
||||||
|
blockCount = 1
|
||||||
|
white *= abs(blockCount)
|
||||||
|
if blockCount < 0:
|
||||||
|
white, black = black, white
|
||||||
|
sys.stdout.write(' '*50 + '\r')
|
||||||
|
sys.stdout.flush()
|
||||||
|
qr = qrText.replace('0', white).replace('1', black)
|
||||||
|
sys.stdout.write(qr)
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
def struct_friend_info(knownInfo):
|
||||||
|
member = copy.deepcopy(friendInfoTemplate)
|
||||||
|
for k, v in copy.deepcopy(knownInfo).items(): member[k] = v
|
||||||
|
return member
|
||||||
|
|
||||||
|
def search_dict_list(l, key, value):
|
||||||
|
''' Search a list of dict
|
||||||
|
* return dict with specific value & key '''
|
||||||
|
for i in l:
|
||||||
|
if i.get(key) == value:
|
||||||
|
return i
|
||||||
|
|
||||||
|
def print_line(msg, oneLine = False):
|
||||||
|
if oneLine:
|
||||||
|
sys.stdout.write(' '*40 + '\r')
|
||||||
|
sys.stdout.flush()
|
||||||
|
else:
|
||||||
|
sys.stdout.write('\n')
|
||||||
|
sys.stdout.write(msg.encode(sys.stdin.encoding or 'utf8', 'replace'
|
||||||
|
).decode(sys.stdin.encoding or 'utf8', 'replace'))
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
def test_connect(retryTime=5):
|
||||||
|
for i in range(retryTime):
|
||||||
|
try:
|
||||||
|
r = requests.get(config.BASE_URL)
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
if i == retryTime - 1:
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return False
|
||||||
|
|
||||||
|
def contact_deep_copy(core, contact):
|
||||||
|
with core.storageClass.updateLock:
|
||||||
|
return copy.deepcopy(contact)
|
||||||
|
|
||||||
|
def get_image_postfix(data):
|
||||||
|
data = data[:20]
|
||||||
|
if b'GIF' in data:
|
||||||
|
return 'gif'
|
||||||
|
elif b'PNG' in data:
|
||||||
|
return 'png'
|
||||||
|
elif b'JFIF' in data:
|
||||||
|
return 'jpg'
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def update_info_dict(oldInfoDict, newInfoDict):
|
||||||
|
''' only normal values will be updated here
|
||||||
|
because newInfoDict is normal dict, so it's not necessary to consider templates
|
||||||
|
'''
|
||||||
|
for k, v in newInfoDict.items():
|
||||||
|
if any((isinstance(v, t) for t in (tuple, list, dict))):
|
||||||
|
pass # these values will be updated somewhere else
|
||||||
|
elif oldInfoDict.get(k) is None or v not in (None, '', '0', 0):
|
||||||
|
oldInfoDict[k] = v
|
||||||
Loading…
Reference in New Issue