@@ -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 |