diff --git a/.gitignore b/.gitignore index 969df946..c0ca475b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ dist/* tests/* itchat.egg-info/* *.pyc +*.pkl *.swp test.py itchat.pkl diff --git a/README.md b/README.md index 1ba9e11c..de8f7e00 100644 --- a/README.md +++ b/README.md @@ -59,36 +59,61 @@ itchat.run() ## 进阶应用 +### 特殊的字典使用方式 + +通过打印itchat的用户以及注册消息的参数,可以发现这些值都是字典。 + +但实际上itchat精心构造了相应的消息、用户、群聊、公众号类。 + +其所有的键值都可以通过这一方式访问: + +```python +@itchat.msg_register(TEXT) +def _(msg): + # equals to print(msg['FromUserName']) + print(msg.fromUserName) +``` + +属性名为键值首字母小写后的内容。 + +```python +author = itchat.search_friends(nickName='LittleCoder')[0] +author.send('greeting, littlecoder!') +``` + ### 各类型消息的注册 通过如下代码,微信已经可以就日常的各种信息进行获取与回复。 ```python -#coding=utf8 import itchat, time from itchat.content import * @itchat.msg_register([TEXT, MAP, CARD, NOTE, SHARING]) def text_reply(msg): - itchat.send('%s: %s' % (msg['Type'], msg['Text']), msg['FromUserName']) + msg.user.send('%s: %s' % (msg.type, msg.text)) @itchat.msg_register([PICTURE, RECORDING, ATTACHMENT, VIDEO]) def download_files(msg): - msg['Text'](msg['FileName']) - return '@%s@%s' % ({'Picture': 'img', 'Video': 'vid'}.get(msg['Type'], 'fil'), msg['FileName']) + msg.download(msg.fileName) + typeSymbol = { + PICTURE: 'img', + VIDEO: 'vid', }.get(msg.type, 'fil') + return '@%s@%s' % (typeSymbol, msg.fileName) @itchat.msg_register(FRIENDS) def add_friend(msg): - itchat.add_friend(**msg['Text']) # 该操作会自动将新好友的消息录入,不需要重载通讯录 - itchat.send_msg('Nice to meet you!', msg['RecommendInfo']['UserName']) + msg.user.verify() + msg.user.send('Nice to meet you!') @itchat.msg_register(TEXT, isGroupChat=True) def text_reply(msg): - if msg['isAt']: - itchat.send(u'@%s\u2005I received: %s' % (msg['ActualNickName'], msg['Content']), msg['FromUserName']) + if msg.isAt: + msg.user.send(u'@%s\u2005I received: %s' % ( + msg.actualNickName, msg.text)) itchat.auto_login(True) -itchat.run() +itchat.run(True) ``` ### 命令行二维码 @@ -234,6 +259,8 @@ A: 有些账号是天生无法给自己的账号发送信息的,建议使用`f ## 类似项目 +[youfou/wxpy][youfou-wxpy]: 优秀的api包装和配套插件,微信机器人/优雅的微信个人号API + [liuwons/wxBot][liuwons-wxBot]: 类似的基于Python的微信机器人 [zixia/wechaty][zixia-wechaty]: 基于Javascript(ES6)的微信个人账号机器人NodeJS框架/库 @@ -267,6 +294,7 @@ A: 有些账号是天生无法给自己的账号发送信息的,建议使用`f [littlecodersh]: https://github.com/littlecodersh [tempdban]: https://github.com/tempdban [Chyroc]: https://github.com/Chyroc +[youfou-wxpy]: https://github.com/youfou/wxpy [liuwons-wxBot]: https://github.com/liuwons/wxBot [zixia-wechaty]: https://github.com/zixia/wechaty [Mojo-Weixin]: https://github.com/sjdy521/Mojo-Weixin diff --git a/itchat/components/contact.py b/itchat/components/contact.py index 1fc98bec..95bd3b60 100644 --- a/itchat/components/contact.py +++ b/itchat/components/contact.py @@ -6,7 +6,7 @@ from .. import config, utils from ..returnvalues import ReturnValue -from ..storage import contact_change +from ..storage import contact_change, templates logger = logging.getLogger('itchat') @@ -99,8 +99,8 @@ def update_friend(self, userName): return r if len(r) != 1 else r[0] def update_info_dict(oldInfoDict, newInfoDict): - ''' - only normal values will be updated here + ''' 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))): @@ -130,8 +130,8 @@ def update_local_chatrooms(core, l): if oldChatroom: update_info_dict(oldChatroom, chatroom) # - update other values - memberList, oldMemberList = (c.get('MemberList', []) - for c in (chatroom, oldChatroom)) + memberList = chatroom.get('MemberList', []) + oldMemberList = oldChatroom.memberList if memberList: for member in memberList: oldMember = utils.search_dict_list( @@ -141,17 +141,19 @@ def update_local_chatrooms(core, l): else: oldMemberList.append(member) else: - oldChatroom = chatroom - core.chatroomList.append(chatroom) + oldChatroom = templates.wrap_user_dict(chatroom) + core.chatroomList.append(oldChatroom) # 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) + if member['UserName'] not in existsUserNames: + delList.append(i) delList.sort(reverse=True) - for i in delList: del oldChatroom['MemberList'][i] + for i in delList: + del oldChatroom['MemberList'][i] # - update OwnerUin if oldChatroom.get('ChatRoomOwner') and oldChatroom.get('MemberList'): oldChatroom['OwnerUin'] = utils.search_dict_list(oldChatroom['MemberList'], @@ -184,8 +186,8 @@ def update_local_friends(core, l): utils.emoji_formatter(friend, 'NickName') if 'DisplayName' in friend: utils.emoji_formatter(friend, 'DisplayName') - if 'RemarkName' in member: - utils.emoji_formatter(member, 'RemarkName') + if 'RemarkName' in friend: + utils.emoji_formatter(friend, 'RemarkName') oldInfoDict = utils.search_dict_list( fullList, 'UserName', friend['UserName']) if oldInfoDict is None: diff --git a/itchat/components/hotreload.py b/itchat/components/hotreload.py index f0a40d04..d34d25ee 100644 --- a/itchat/components/hotreload.py +++ b/itchat/components/hotreload.py @@ -5,6 +5,7 @@ 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 @@ -50,6 +51,8 @@ def load_login_status(self, fileDir, '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']) msgList, contactList = self.get_msg() diff --git a/itchat/components/login.py b/itchat/components/login.py index c35d1ecd..7736427c 100644 --- a/itchat/components/login.py +++ b/itchat/components/login.py @@ -9,6 +9,7 @@ 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 @@ -182,7 +183,7 @@ def web_init(self): # deal with login info utils.emoji_formatter(dic['User'], 'NickName') self.loginInfo['InviteStartCount'] = int(dic['InviteStartCount']) - self.loginInfo['User'] = utils.struct_friend_info(dic['User']) + 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']) @@ -247,6 +248,7 @@ def maintain_loop(): 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 diff --git a/itchat/components/messages.py b/itchat/components/messages.py index 9f3cd10d..c376dc59 100644 --- a/itchat/components/messages.py +++ b/itchat/components/messages.py @@ -1,438 +1,509 @@ -import os, time, re, io -import json -import mimetypes, hashlib -import traceback, logging -from collections import OrderedDict - -import requests - -from .. import config, utils -from ..returnvalues import ReturnValue -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 - -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: - if '@@' in m['FromUserName'] or '@@' in m['ToUserName']: - produce_group_chat(core, m) - else: - utils.msg_formatter(m, 'Content') - 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 - msg = { - 'Type': 'Friends', - 'Text': { - 'status' : m['Status'], - 'userName' : m['RecommendInfo']['UserName'], - 'verifyContent' : m['Ticket'], - 'autoUpdate' : m['RecommendInfo'], }, } - 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'] == 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]*?):
(.*)$', 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(msg['FromUserName']) - 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['DisplayName'] or member['NickName'] - atFlag = '@' + (chatroom['self']['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 upload_file(self, fileDir, isPicture=False, isVideo=False, - toUserName='filehelper'): - logger.debug('Request to upload a %s: %s' % ( - 'picture' if isPicture else 'video' if isVideo else 'file', fileDir)) - if not utils.check_file(fileDir): - return ReturnValue({'BaseResponse': { - 'ErrMsg': 'No file found in specific dir', - 'Ret': -1002, }}) - fileSize = os.path.getsize(fileDir) - fileSymbol = 'pic' if isPicture else 'video' if isVideo else'doc' - with open(fileDir, 'rb') as f: - fileMd5 = hashlib.md5(f.read()).hexdigest() - file_ = open(fileDir, 'rb') - 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' - files = OrderedDict([ - ('id', (None, 'WU_FILE_0')), - ('name', (None, os.path.basename(fileDir))), - ('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' , (os.path.basename(fileDir), 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 requests.post(url, files=files, headers=headers) - -def send_file(self, fileDir, toUserName=None, mediaId=None): - logger.debug('Request to send a file(mediaId: %s) to %s: %s' % ( - mediaId, toUserName, fileDir)) - if toUserName is None: toUserName = self.storageClass.userName - if mediaId is None: - r = self.upload_file(fileDir) - 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': ("%s"%os.path.basename(fileDir) + - "6" + - "%s%s"%(str(os.path.getsize(fileDir)), mediaId) + - "%s"%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, toUserName=None, mediaId=None): - logger.debug('Request to send a image(mediaId: %s) to %s: %s' % ( - mediaId, toUserName, fileDir)) - if toUserName is None: toUserName = self.storageClass.userName - if mediaId is None: - r = self.upload_file(fileDir, isPicture=not fileDir[-4:] == '.gif') - 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): - logger.debug('Request to send a video(mediaId: %s) to %s: %s' % ( - mediaId, toUserName, fileDir)) - if toUserName is None: toUserName = self.storageClass.userName - if mediaId is None: - r = self.upload_file(fileDir, isVideo=True) - 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 +import os, time, re, io +import json +import mimetypes, hashlib +import traceback, 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 + +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'] == 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]*?):
(.*)$', 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(msg['FromUserName']) + 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['DisplayName'] or member['NickName'] + atFlag = '@' + (chatroom['self']['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' + files = OrderedDict([ + ('id', (None, 'WU_FILE_0')), + ('name', (None, os.path.basename(fileDir))), + ('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' , (os.path.basename(fileDir), 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 requests.post(url, files=files, headers=headers) + +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 + if mediaId is None: + preparedFile = _prepare_file(fileDir, file_) + if not preparedFile: + return preparedFile + fileSize = preparedFile['fileSize'] + 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': ("%s" % os.path.basename(fileDir) + + "6" + + "%s%s" % (str(fileSize), mediaId) + + "%s" % 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 diff --git a/itchat/components/register.py b/itchat/components/register.py index a07c1111..86a37fcc 100644 --- a/itchat/components/register.py +++ b/itchat/components/register.py @@ -6,6 +6,7 @@ from ..log import set_logging from ..utils import test_connect +from ..storage import templates logger = logging.getLogger('itchat') @@ -46,19 +47,12 @@ def configured_reply(self): except Queue.Empty: pass else: - if msg['FromUserName'] == self.storageClass.userName: - actualOpposite = msg['ToUserName'] - else: - actualOpposite = msg['FromUserName'] - if '@@' in actualOpposite: - replyFn = self.functionDict['GroupChat'].get(msg['Type']) - elif self.search_mps(userName=msg['FromUserName']): - replyFn = self.functionDict['MpChat'].get(msg['Type']) - elif '@' in actualOpposite or \ - actualOpposite in ('filehelper', 'fmessage'): + if isinstance(msg['User'], templates.User): replyFn = self.functionDict['FriendChat'].get(msg['Type']) - else: + 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: @@ -72,7 +66,7 @@ def configured_reply(self): 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): + if not (isinstance(msgType, list) or isinstance(msgType, tuple)): msgType = [msgType] def _msg_register(fn): for _msgType in msgType: @@ -84,6 +78,7 @@ def _msg_register(fn): 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): diff --git a/itchat/config.py b/itchat/config.py index ab17ffbe..515ea463 100644 --- a/itchat/config.py +++ b/itchat/config.py @@ -1,9 +1,9 @@ -import os, platform - -VERSION = '1.2.33' -BASE_URL = 'https://login.weixin.qq.com' -OS = platform.system() #Windows, Linux, Darwin -DIR = os.getcwd() -DEFAULT_QR = 'QR.png' - -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' +import os, platform + +VERSION = '1.3.1' +BASE_URL = 'https://login.weixin.qq.com' +OS = platform.system() #Windows, Linux, Darwin +DIR = os.getcwd() +DEFAULT_QR = 'QR.png' + +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' diff --git a/itchat/core.py b/itchat/core.py index dea80903..ba384ced 100644 --- a/itchat/core.py +++ b/itchat/core.py @@ -18,7 +18,7 @@ def __init__(self): - failing is failing ''' self.alive, self.isLogging = False, False - self.storageClass = storage.Storage() + self.storageClass = storage.Storage(self) self.memberList = self.storageClass.memberList self.mpList = self.storageClass.mpList self.chatroomList = self.storageClass.chatroomList @@ -313,7 +313,7 @@ def send_msg(self, msg='Test Message', toUserName=None): ''' raise NotImplementedError() def upload_file(self, fileDir, isPicture=False, isVideo=False, - toUserName='filehelper'): + toUserName='filehelper', file_=None, preparedFile=None): ''' upload file to server and get mediaId for options - fileDir: dir for file ready for upload @@ -325,7 +325,7 @@ def upload_file(self, fileDir, isPicture=False, isVideo=False, it is defined in components/messages.py ''' raise NotImplementedError() - def send_file(self, fileDir, toUserName=None, mediaId=None): + def send_file(self, fileDir, toUserName=None, mediaId=None, file_=None): ''' send attachment for options - fileDir: dir for file ready for upload @@ -335,7 +335,7 @@ def send_file(self, fileDir, toUserName=None, mediaId=None): it is defined in components/messages.py ''' raise NotImplementedError() - def send_image(self, fileDir, toUserName=None, mediaId=None): + def send_image(self, fileDir=None, toUserName=None, mediaId=None, file_=None): ''' send image for options - fileDir: dir for file ready for upload @@ -346,7 +346,7 @@ def send_image(self, fileDir, toUserName=None, mediaId=None): it is defined in components/messages.py ''' raise NotImplementedError() - def send_video(self, fileDir=None, toUserName=None, mediaId=None): + def send_video(self, fileDir=None, toUserName=None, mediaId=None, file_=None): ''' send video for options - fileDir: dir for file ready for upload diff --git a/itchat/returnvalues.py b/itchat/returnvalues.py index e9643d08..b9600bd6 100644 --- a/itchat/returnvalues.py +++ b/itchat/returnvalues.py @@ -4,6 +4,23 @@ 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: @@ -14,7 +31,8 @@ def __init__(self, returnValueDict={}, rawResponse=None): 'Ret': -1004, 'ErrMsg': 'Unexpected return value', }, 'Data': rawResponse.content, } - for k, v in returnValueDict.items(): self[k] = v + for k, v in returnValueDict.items(): + self[k] = v if not 'BaseResponse' in self: self['BaseResponse'] = { 'ErrMsg': 'no BaseResponse in raw response', @@ -45,6 +63,7 @@ def __repr__(self): -1003: u'服务器拒绝连接', -1004: u'服务器返回异常值', -1005: u'参数错误', + -1006: u'无效操作', 0: u'请求成功', }, } diff --git a/itchat/storage.py b/itchat/storage/__init__.py similarity index 63% rename from itchat/storage.py rename to itchat/storage/__init__.py index 5bfff6f0..8010c9a1 100644 --- a/itchat/storage.py +++ b/itchat/storage/__init__.py @@ -1,10 +1,11 @@ import os, time, copy -try: - import Queue -except ImportError: - import queue as Queue 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: @@ -12,32 +13,41 @@ def _contact_change(core, *args, **kwargs): return _contact_change class Storage(object): - def __init__(self): + def __init__(self, core): self.userName = None self.nickName = None self.updateLock = Lock() - self.memberList = [] - self.mpList = [] - self.chatroomList = [] - self.msgList = Queue.Queue(-1) + 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, + 'memberList' : [dict(member) for member in self.memberList], + 'mpList' : [dict(mp) for mp in self.mpList], + 'chatroomList' : [dict(chatroom) for chatroom in self.chatroomList], 'lastInputUserName' : self.lastInputUserName, } def loads(self, j): - self.userName = j.get('userName', None) - self.nickName = j.get('nickName', None) + 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) + for i in j.get('memberList', []): + self.memberList.append(i) del self.mpList[:] - for i in j.get('mpList', []): self.mpList.append(i) + for i in j.get('mpList', []): + self.mpList.append(i) del self.chatroomList[:] - for i in j.get('chatroomList', []): self.chatroomList.append(i) + for i in j.get('chatroomList', []): + self.chatroomList.append(i) self.lastInputUserName = j.get('lastInputUserName', None) def search_friends(self, name=None, userName=None, remarkName=None, nickName=None, wechatAccount=None): @@ -75,19 +85,23 @@ 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) + 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)) + 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) + 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)) + if name in m['NickName']: + matchList.append(copy.deepcopy(m)) return matchList diff --git a/itchat/storage/messagequeue.py b/itchat/storage/messagequeue.py new file mode 100644 index 00000000..c1f6ae71 --- /dev/null +++ b/itchat/storage/messagequeue.py @@ -0,0 +1,26 @@ +try: + import Queue as queue +except ImportError: + import queue + +class Queue(queue.Queue): + def put(self, message): + if 'IsAt' in message: + message['isAt'] = message['IsAt'] + queue.Queue.put(self, Message(message)) + +class Message(dict): + def download(self, fileName): + if hasattr(self.text, '__call__'): + return self.text(fileName) + else: + return b'' + def __getattr__(self, value): + value = value[0].upper() + value[1:] + return self.get(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__()) diff --git a/itchat/storage/templates.py b/itchat/storage/templates.py new file mode 100644 index 00000000..70bf0d56 --- /dev/null +++ b/itchat/storage/templates.py @@ -0,0 +1,254 @@ +import logging, copy, pickle + +from ..returnvalues import ReturnValue + +logger = logging.getLogger('itchat') + +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 + +fakeItchat = UnInitializedItchat() + +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.contactInitFn = None + self.contactClass = User + self.core = fakeItchat + 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(contact) or contact + super(ContactList, self).append(contact) + def __deepcopy__(self, memo): + return self.__class__([copy.deepcopy(v) for v in self]) + def __getstate__(self): + return [pickle.dumps(v) for v in self] + def __setstate__(self, state): + for v in state: + super(ContactList, self).append(pickle.loads(v)) + 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__()) + +fakeContactList = ContactList + +class AbstractUserDict(dict): + def __init__(self, *args, **kwargs): + super(AbstractUserDict, self).__init__(*args, **kwargs) + self.core = fakeItchat + 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(msgType, content, 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 __getattr__(self, value): + value = value[0].upper() + value[1:] + return self.get(value, '') + def __deepcopy__(self, memo): + r = self.__class__({ + copy.deepcopy(k, memo): copy.deepcopy(v, memo) + for k, v in self.items()}) + r.core = self.core + return r + def __getstate__(self): + return dict(self) + def __setstate__(self, state): + for k, v in state.items(): + self[k] = v + 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__()) + +class User(AbstractUserDict): + def __init__(self, *args, **kwargs): + super(User, self).__init__(*args, **kwargs) + self.verifyDict = {} + self.memberList = fakeContactList + def update(self): + return self.core.update_friend(self.userName) + 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 + +class MassivePlatform(AbstractUserDict): + def __init__(self, *args, **kwargs): + super(MassivePlatform, self).__init__(*args, **kwargs) + self.memberList = fakeContactList + +class Chatroom(AbstractUserDict): + def __init__(self, *args, **kwargs): + super(Chatroom, self).__init__(*args, **kwargs) + memberList = ContactList() + def init_fn(d): + d.chatroom = self + memberList.set_default_value(init_fn, ChatroomMember) + for rawMember in self.memberList: + memberList.append(rawMember) + self['MemberList'] = memberList + def update(self, detailedMember=False): + return self.core.update_chatroom(self.userName, detailedMember) + 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) + +class ChatroomMember(AbstractUserDict): + def __init__(self, *args, **kwargs): + super(AbstractUserDict, self).__init__(*args, **kwargs) + self.core = fakeItchat + self.chatroom = self.fakeChatroom + 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 __deepcopy__(self, memo): + r = super(ChatroomMember, self).__deepcopy__(memo) + r.core = self.core + return r + +ChatroomMember.fakeChatroom = Chatroom() + +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 diff --git a/itchat/utils.py b/itchat/utils.py index a0a61a2e..202de6e7 100644 --- a/itchat/utils.py +++ b/itchat/utils.py @@ -106,7 +106,8 @@ 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 + if i.get(key) == value: + return i def print_line(msg, oneLine = False): if oneLine: