mirror of
https://github.com/RockChinQ/QChatGPT.git
synced 2024-11-16 03:32:33 +08:00
feat: 引入 itchat 库
This commit is contained in:
parent
ea6a0af5a7
commit
f7f979fda5
9
pkg/lib/itchat/LICENSE
Normal file
9
pkg/lib/itchat/LICENSE
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
**The MIT License (MIT)**
|
||||||
|
|
||||||
|
Copyright (c) 2017 LittleCoder ([littlecodersh@Github](https://github.com/littlecodersh))
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
96
pkg/lib/itchat/__init__.py
Normal file
96
pkg/lib/itchat/__init__.py
Normal file
|
@ -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
|
12
pkg/lib/itchat/async_components/__init__.py
Normal file
12
pkg/lib/itchat/async_components/__init__.py
Normal file
|
@ -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)
|
488
pkg/lib/itchat/async_components/contact.py
Normal file
488
pkg/lib/itchat/async_components/contact.py
Normal file
|
@ -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)
|
102
pkg/lib/itchat/async_components/hotreload.py
Normal file
102
pkg/lib/itchat/async_components/hotreload.py
Normal file
|
@ -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.')
|
422
pkg/lib/itchat/async_components/login.py
Normal file
422
pkg/lib/itchat/async_components/login.py
Normal file
|
@ -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, }})
|
527
pkg/lib/itchat/async_components/messages.py
Normal file
527
pkg/lib/itchat/async_components/messages.py
Normal file
|
@ -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)
|
106
pkg/lib/itchat/async_components/register.py
Normal file
106
pkg/lib/itchat/async_components/register.py
Normal file
|
@ -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()
|
12
pkg/lib/itchat/components/__init__.py
Normal file
12
pkg/lib/itchat/components/__init__.py
Normal file
|
@ -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)
|
519
pkg/lib/itchat/components/contact.py
Normal file
519
pkg/lib/itchat/components/contact.py
Normal file
|
@ -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)
|
102
pkg/lib/itchat/components/hotreload.py
Normal file
102
pkg/lib/itchat/components/hotreload.py
Normal file
|
@ -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.')
|
418
pkg/lib/itchat/components/login.py
Normal file
418
pkg/lib/itchat/components/login.py
Normal file
|
@ -0,0 +1,418 @@
|
||||||
|
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
|
||||||
|
logger.info('Ready to login.')
|
||||||
|
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)
|
||||||
|
time.sleep(0.5)
|
||||||
|
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()
|
||||||
|
res = re.findall('<skey>(.*?)</skey>', r.text, re.S)
|
||||||
|
skey = res[0] if res else None
|
||||||
|
res = re.findall(
|
||||||
|
'<pass_ticket>(.*?)</pass_ticket>', r.text, re.S)
|
||||||
|
pass_ticket = res[0] if res else None
|
||||||
|
if skey is not None:
|
||||||
|
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"]
|
||||||
|
if pass_ticket is not None:
|
||||||
|
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:
|
||||||
|
logger.error("Having tried %s times, but still failed. " % (
|
||||||
|
retryCount) + "Stop trying...")
|
||||||
|
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.error('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, }})
|
528
pkg/lib/itchat/components/messages.py
Normal file
528
pkg/lib/itchat/components/messages.py
Normal file
|
@ -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)
|
106
pkg/lib/itchat/components/register.py
Normal file
106
pkg/lib/itchat/components/register.py
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
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:
|
||||||
|
rval=self.load_login_status(statusStorageDir,
|
||||||
|
loginCallback=loginCallback, exitCallback=exitCallback)
|
||||||
|
if rval:
|
||||||
|
return
|
||||||
|
logger.error('Hot reload failed, logging in normally, error={}'.format(rval))
|
||||||
|
self.logout()
|
||||||
|
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()
|
17
pkg/lib/itchat/config.py
Normal file
17
pkg/lib/itchat/config.py
Normal file
|
@ -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', True)
|
||||||
|
|
||||||
|
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=='
|
14
pkg/lib/itchat/content.py
Normal file
14
pkg/lib/itchat/content.py
Normal file
|
@ -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]
|
456
pkg/lib/itchat/core.py
Normal file
456
pkg/lib/itchat/core.py
Normal file
|
@ -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)
|
36
pkg/lib/itchat/log.py
Normal file
36
pkg/lib/itchat/log.py
Normal file
|
@ -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
|
67
pkg/lib/itchat/returnvalues.py
Normal file
67
pkg/lib/itchat/returnvalues.py
Normal file
|
@ -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'请求成功',
|
||||||
|
},
|
||||||
|
}
|
117
pkg/lib/itchat/storage/__init__.py
Normal file
117
pkg/lib/itchat/storage/__init__.py
Normal file
|
@ -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
|
32
pkg/lib/itchat/storage/messagequeue.py
Normal file
32
pkg/lib/itchat/storage/messagequeue.py
Normal file
|
@ -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__())
|
318
pkg/lib/itchat/storage/templates.py
Normal file
318
pkg/lib/itchat/storage/templates.py
Normal file
|
@ -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()
|
163
pkg/lib/itchat/utils.py
Normal file
163
pkg/lib/itchat/utils.py
Normal file
|
@ -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
|
|
@ -37,7 +37,7 @@ class PlatformManager:
|
||||||
|
|
||||||
async def initialize(self):
|
async def initialize(self):
|
||||||
|
|
||||||
from .sources import yirimirai, nakuru, aiocqhttp, qqbotpy
|
from .sources import yirimirai, nakuru, aiocqhttp, qqbotpy, itchat
|
||||||
|
|
||||||
async def on_friend_message(event: platform_events.FriendMessage, adapter: msadapter.MessageSourceAdapter):
|
async def on_friend_message(event: platform_events.FriendMessage, adapter: msadapter.MessageSourceAdapter):
|
||||||
|
|
||||||
|
|
62
pkg/platform/sources/itchat.py
Normal file
62
pkg/platform/sources/itchat.py
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import typing
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from ...lib import itchat
|
||||||
|
|
||||||
|
from .. import adapter
|
||||||
|
from ...core import app
|
||||||
|
from ..types import message as platform_message
|
||||||
|
from ..types import events as platform_events
|
||||||
|
from ..types import entities as platform_entities
|
||||||
|
|
||||||
|
|
||||||
|
@adapter.adapter_class("itchat")
|
||||||
|
class ItchatAdapter(adapter.MessageSourceAdapter):
|
||||||
|
|
||||||
|
bot: itchat.Core
|
||||||
|
|
||||||
|
def __init__(self, config: dict, ap: app.Application):
|
||||||
|
self.config = config
|
||||||
|
self.ap = ap
|
||||||
|
|
||||||
|
self.bot = itchat.load_async_itchat()
|
||||||
|
|
||||||
|
async def send_message(
|
||||||
|
self, target_type: str, target_id: str, message: platform_message.MessageChain
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def reply_message(
|
||||||
|
self,
|
||||||
|
message_source: platform_events.MessageEvent,
|
||||||
|
message: platform_message.MessageChain,
|
||||||
|
quote_origin: bool = False,
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def is_muted(self, group_id: int) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def register_listener(
|
||||||
|
self,
|
||||||
|
event_type: typing.Type[platform_events.Event],
|
||||||
|
callback: typing.Callable[[platform_events.Event, adapter.MessageSourceAdapter], None],
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def unregister_listener(
|
||||||
|
self,
|
||||||
|
event_type: typing.Type[platform_events.Event],
|
||||||
|
callback: typing.Callable[[platform_events.Event, adapter.MessageSourceAdapter], None],
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def run_async(self):
|
||||||
|
await self.bot.auto_login()
|
||||||
|
await self.bot.run()
|
||||||
|
|
||||||
|
async def kill(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
|
@ -32,6 +32,10 @@
|
||||||
"public_guild_messages",
|
"public_guild_messages",
|
||||||
"direct_message"
|
"direct_message"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"adapter": "itchat",
|
||||||
|
"enable": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"track-function-calls": true,
|
"track-function-calls": true,
|
||||||
|
|
Loading…
Reference in New Issue
Block a user