From f4dec2e48eb2adcaaff412516fa79dc97fe8a82a Mon Sep 17 00:00:00 2001 From: Dainius Date: Tue, 9 May 2017 10:25:04 +0300 Subject: [PATCH 1/9] update send methods --- fbchat/client.py | 294 ++++++++++++++++++++++++----------------------- fbchat/models.py | 6 +- 2 files changed, 152 insertions(+), 148 deletions(-) diff --git a/fbchat/client.py b/fbchat/client.py index d16c136..c21cea0 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -79,7 +79,6 @@ class Client(object): :param session_cookies: Cookie dict from a previous session (Will default to login if these are invalid) """ - self.is_def_recipient_set = False self.sticky, self.pool = (None, None) self._session = requests.session() self.req_counter = 1 @@ -87,6 +86,9 @@ class Client(object): self.payloadDefault = {} self.client = 'mercury' self.listening = False + self.is_def_thread_set = False + self.def_thread_id = None + self.def_thread_type = None self.threads = [] # Setup event hooks @@ -162,24 +164,6 @@ class Client(object): if not session_cookies or not self.setSession(session_cookies) or not self.isLoggedIn(): self.login(email, password, max_retries) - def _console(self, msg): - """Assumes an INFO level and log it. - - This method shouldn't be used anymore. - Use the log itself: - >>> import logging - >>> from fbchat.client import log - >>> log.setLevel(logging.DEBUG) - - You can do the same thing by adding the 'debug' argument: - >>> from fbchat import Client - >>> client = Client("...", "...", debug=True) - """ - warnings.warn( - "Client._console shouldn't be used. Use 'log.'", - DeprecationWarning) - log.debug(msg) - def _generatePayload(self, query): """Adds the following defaults to the payload: __rev, __user, __a, ttstamp, fb_dtsg, __req @@ -355,7 +339,7 @@ class Client(object): return True def login(self, email, password, max_retries=5): - self.onLoggingIn(email) + self.onLoggingIn(email=email) if not (email and password): raise Exception("Email and password not set.") @@ -369,7 +353,7 @@ class Client(object): time.sleep(1) continue else: - self.onLoggedIn(email) + self.onLoggedIn(email=email) break else: raise Exception("Login failed. Check email/password.") @@ -389,15 +373,15 @@ class Client(object): self.seq = "0" return r - def setDefaultRecipient(self, recipient_id, is_user=True): + def setDefaultThreadId(self, thread_id=str, thread_type=ThreadType): """Sets default recipient to send messages and images to. - :param recipient_id: the user id or thread id that you want to send a message to - :param is_user: determines if the recipient_id is for user or thread + :param thread_id: user/group ID to default to + :param thread_type: type of thread_id """ - self.def_recipient_id = recipient_id - self.def_is_user = is_user - self.is_def_recipient_set = True + self.def_thread_id = thread_id + self.def_thread_type = thread_type + self.is_def_thread_set = True def getAllUsers(self): """ Gets all users from chat with info included """ @@ -448,24 +432,27 @@ class Client(object): users.append(User(entry)) return users # have bug TypeError: __repr__ returned non-string (type bytes) - def send(self, recipient_id=None, message=None, is_user=True, like=None, image_id=None, add_user_ids=None): + """ + SEND METHODS + """ + + def _send(self, thread_id=None, message=None, thread_type=None, emoji_size=None, image_id=None, add_user_ids=None, new_title=None): """Send a message with given thread id - :param recipient_id: the user id or thread id that you want to send a message to + :param thread_id: the user id or thread id that you want to send a message to :param message: a text that you want to send - :param is_user: determines if the recipient_id is for user or thread - :param like: size of the like sticker you want to send + :param thread_type: determines if the recipient_id is for user or thread + :param emoji_size: size of the like sticker you want to send :param image_id: id for the image to send, gotten from the UploadURL :param add_user_ids: a list of user ids to add to a chat - - returns a list of message ids of the sent message(s) + :return: a list of message ids of the sent message(s) """ - if self.is_def_recipient_set: - recipient_id = self.def_recipient_id - is_user = self.def_is_user - elif recipient_id is None: - raise Exception('Recipient ID is not set.') + if thread_id is None and self.is_def_thread_set: + thread_id = self.def_thread_id + thread_type = self.def_thread_type + elif thread_id is None and not self.is_def_thread_set: + raise ValueError('Default Thread ID is not set.') messageAndOTID = generateOfflineThreadingID() timestamp = now() @@ -493,37 +480,47 @@ class Client(object): 'status' : '0', 'offline_threading_id':messageAndOTID, 'message_id' : messageAndOTID, - 'threading_id':generateMessageID(self.client_id), + 'threading_id': generateMessageID(self.client_id), 'ephemeral_ttl_mode:': '0', 'manual_retry_cnt' : '0', 'signatureID' : getSignatureID() } - if is_user: - data["other_user_fbid"] = recipient_id - else: - data["thread_fbid"] = recipient_id + # Set recipient + if thread_type == ThreadType.USER: + data["other_user_fbid"] = thread_id + elif thread_type == ThreadType.GROUP: + data["thread_fbid"] = thread_id + # Set title + if new_title: + data['action_type'] = 'ma-type:log-message' + data['log_message_data[name]'] = new_title + data['log_message_type'] = 'log:thread-name' + + # Set users to add if add_user_ids: data['action_type'] = 'ma-type:log-message' # It's possible to add multiple users for i, add_user_id in enumerate(add_user_ids): data['log_message_data[added_participants][' + str(i) + ']'] = "fbid:" + str(add_user_id) data['log_message_type'] = 'log:subscribe' - else: + + # Sending a simple message + if not add_user_ids and not new_title: data['action_type'] = 'ma-type:user-generated-message' - data['body'] = message + data['body'] = message or '' data['has_attachment'] = image_id is not None - data['specific_to_list[0]'] = 'fbid:' + str(recipient_id) + data['specific_to_list[0]'] = 'fbid:' + str(thread_id) data['specific_to_list[1]'] = 'fbid:' + str(self.uid) + # Set image to send if image_id: data['image_ids[0]'] = image_id - if like and not type(like) is Sticker: - data["sticker_id"] = Sticker.SMALL.value - else: - data["sticker_id"] = like.value + # Set emoji to send + if emoji_size: + data["sticker_id"] = emoji_size.value r = self._post(SendURL, data) @@ -531,9 +528,10 @@ class Client(object): log.warning('Error when sending message: Got {} response'.format(r.status_code)) return False - if isinstance(r._content, str) is False: - r._content = r._content.decode(facebookEncoding) - j = get_json(r._content) + response_content = {} + if isinstance(r.content, str) is False: + response_content = r.content.decode(facebookEncoding) + j = get_json(response_content) if 'error' in j: # 'errorDescription' is in the users own language! log.warning('Error #{} when sending message: {}'.format(j['error'], j['errorDescription'])) @@ -552,42 +550,114 @@ class Client(object): log.debug("With data {}".format(data)) return message_ids - def sendRemoteImage(self, recipient_id=None, message=None, is_user=True, image=''): - """Send an image from a URL - - :param recipient_id: the user id or thread id that you want to send a message to - :param message: a text that you want to send - :param is_user: determines if the recipient_id is for user or thread - :param image: URL for an image to download and send + def sendMessage(self, message: str, thread_id: str = None, thread_type: ThreadType = None): """ - mimetype = guess_type(image)[0] - remote_image = requests.get(image).content - image_id = self.uploadImage({'file': (image, remote_image, mimetype)}) - return self.send(recipient_id, message, is_user, None, image_id) - - def sendLocalImage(self, recipient_id=None, message=None, is_user=True, image=''): - """Send an image from a file path - - :param recipient_id: the user id or thread id that you want to send a message to - :param message: a text that you want to send - :param is_user: determines if the recipient_id is for user or thread - :param image: path to a local image to send + Sends a message to given (or default, if not) thread with an additional image. + :param message: message to send + :param thread_id: user/group chat ID + :param thread_type: specify whether thread_id is user or group chat + :return: a list of message ids of the sent message(s) """ - mimetype = guess_type(image)[0] - image_id = self.uploadImage({'file': (image, open(image, 'rb'), mimetype)}) - return self.send(recipient_id, message, is_user, None, image_id) + return self._send(thread_id, message, thread_type, None, None, None, None) - def uploadImage(self, image): + def sendEmoji(self, emoji_size: EmojiSize, thread_id: str = None, thread_type: ThreadType = None): + """ + Sends an emoji to given (or default, if not) thread. + :param emoji_size: size of emoji to send + :param thread_id: user/group chat ID + :param thread_type: specify whether thread_id is user or group chat + :return: a list of message ids of the sent message(s) + """ + return self._send(thread_id, None, thread_type, emoji_size, None, None, None) + + def sendRemoteImage(self, image_url: str, message: str = None, thread_id: str = None, thread_type: ThreadType = None): + """ + Sends an image from given URL to given (or default, if not) thread. + :param image_url: URL of an image to upload and send + :param message: additional message + :param thread_id: user/group chat ID + :param thread_type: specify whether thread_id is user or group chat + :return: a list of message ids of the sent message(s) + """ + mimetype = guess_type(image_url)[0] + remote_image = requests.get(image_url).content + image_id = self._uploadImage({'file': (image_url, remote_image, mimetype)}) + return self._send(thread_id, message, thread_type, None, image_id, None, None) + + # Doesn't upload properly + # def sendLocalImage(self, image_path: str, message: str = None, thread_id: str = None, thread_type: ThreadType = None): + # """ + # Sends an image from given URL to given (or default, if not) thread. + # :param image_path: path of an image to upload and send + # :param message: additional message + # :param thread_id: user/group chat ID + # :param thread_type: specify whether thread_id is user or group chat + # :return: a list of message ids of the sent message(s) + # """ + # mimetype = guess_type(image_path)[0] + # image_id = self._uploadImage({'file': (image_path, open(image_path, 'rb'), mimetype)}) + # return self._send(thread_id, message, thread_type, None, image_id, None, None) + + def addUsersToChat(self, user_list: list, thread_id: str = None): + """ + Adds users to given (or default, if not) thread. + :param user_list: list of users to add + :param thread_id: group chat ID + :return: a list of message ids of the sent message(s) + """ + return self._send(thread_id, None, ThreadType.GROUP, None, None, user_list, None) + + def removeUserFromChat(self, user_id: str, thread_id: str = None): + """ + Adds users to given (or default, if not) thread. + :param user_id: user ID to remove + :param thread_id: group chat ID + :return: true if user was removed + """ + + if thread_id is None and self.def_thread_type == ThreadType.GROUP: + thread_id = self.def_thread_id + elif thread_id is None: + raise ValueError('Default Thread ID is not set.') + + data = { + "uid": user_id, + "tid": thread_id + } + + r = self._post(RemoveUserURL, data) + + return r.ok + + def changeThreadTitle(self, new_title: str, thread_id: str = None): + """ + Change title of a group conversation. + :param new_title: new group chat title + :param thread_id: group chat ID + :return: a list of message ids of the sent message(s) + """ + if thread_id is None and self.def_thread_type == ThreadType.GROUP: + thread_id = self.def_thread_id + elif thread_id is None: + raise ValueError('Default Thread ID is not set.') + return self._send(thread_id, None, ThreadType.GROUP, None, None, None, new_title) + + """ + END SEND METHODS + """ + + def _uploadImage(self, image): """Upload an image and get the image_id for sending in a message :param image: a tuple of (file name, data, mime type) to upload to facebook """ r = self._postFile(UploadURL, image) - if isinstance(r._content, str) is False: - r._content = r._content.decode(facebookEncoding) + response_content = {} + if isinstance(r.content, str) is False: + response_content = r.content.decode(facebookEncoding) # Strip the start and parse out the returned image_id - return json.loads(r._content[9:])['payload']['metadata'][0]['image_id'] + return json.loads(response_content[9:])['payload']['metadata'][0]['image_id'] def getThreadInfo(self, userID, last_n=20, start=None, is_user=True): """Get the info of one Thread @@ -963,69 +1033,3 @@ class Client(object): if len(full_data)==1: full_data=full_data[0] return full_data - - def removeUserFromChat(self, threadID, userID): - """Remove user (userID) from group chat (threadID) - - :param threadID: group chat id - :param userID: user id to remove from chat - """ - - data = { - "uid" : userID, - "tid" : threadID - } - - r = self._post(RemoveUserURL, data) - - return r.ok - - def addUserToChat(self, threadID, userID): - """Add user (userID) to group chat (threadID) - - :param threadID: group chat id - :param userID: user id to add to chat - """ - return self.send(threadID, is_user=False, add_user_ids=[userID]) - - def changeThreadTitle(self, threadID, newTitle): - """Change title of a group conversation - - :param threadID: group chat id - :param newTitle: new group chat title - """ - - messageAndOTID = generateOfflineThreadingID() - timestamp = now() - date = datetime.now() - data = { - 'client' : self.client, - 'action_type' : 'ma-type:log-message', - 'author' : 'fbid:' + str(self.uid), - 'thread_id' : '', - 'author_email' : '', - 'coordinates' : '', - 'timestamp' : timestamp, - 'timestamp_absolute' : 'Today', - 'timestamp_relative' : str(date.hour) + ":" + str(date.minute).zfill(2), - 'timestamp_time_passed' : '0', - 'is_unread' : False, - 'is_cleared' : False, - 'is_forward' : False, - 'is_filtered_content' : False, - 'is_spoof_warning' : False, - 'source' : 'source:chat:web', - 'source_tags[0]' : 'source:chat', - 'status' : '0', - 'offline_threading_id' : messageAndOTID, - 'message_id' : messageAndOTID, - 'threading_id': generateMessageID(self.client_id), - 'manual_retry_cnt' : '0', - 'thread_fbid' : threadID, - 'log_message_data[name]' : newTitle, - 'log_message_type' : 'log:thread-name' - } - - r = self._post(SendURL, data) - - return r.ok diff --git a/fbchat/models.py b/fbchat/models.py index 6c526a4..efebaf8 100644 --- a/fbchat/models.py +++ b/fbchat/models.py @@ -76,11 +76,11 @@ class ThreadType(Enum): class TypingStatus(Enum): - Deleted = 0 - Typing = 1 + DELETED = 0 + TYPING = 1 -class Sticker(Enum): +class EmojiSize(Enum): LARGE = '369239383222810' MEDIUM = '369239343222814' SMALL = '369239263222822' From 5da3e5e4bf02ba9bdbe699ffc28123a7359e8b7e Mon Sep 17 00:00:00 2001 From: Dainius Date: Tue, 9 May 2017 21:27:32 +0300 Subject: [PATCH 2/9] update tests --- fbchat/client.py | 59 ++++++++++--------- test_data.js | 6 ++ tests.py | 145 ++++++++++++++++++++++++++--------------------- 3 files changed, 117 insertions(+), 93 deletions(-) create mode 100644 test_data.js diff --git a/fbchat/client.py b/fbchat/client.py index c21cea0..442c8b7 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -199,7 +199,7 @@ class Client(object): self.payloadDefault = {} self.client_id = hex(int(random()*2147483648))[2:] self.start_time = now() - self.uid = int(self._session.cookies['c_user']) + self.uid = str(self._session.cookies['c_user']) self.user_channel = "p_" + str(self.uid) self.ttstamp = '' @@ -585,18 +585,18 @@ class Client(object): return self._send(thread_id, message, thread_type, None, image_id, None, None) # Doesn't upload properly - # def sendLocalImage(self, image_path: str, message: str = None, thread_id: str = None, thread_type: ThreadType = None): - # """ - # Sends an image from given URL to given (or default, if not) thread. - # :param image_path: path of an image to upload and send - # :param message: additional message - # :param thread_id: user/group chat ID - # :param thread_type: specify whether thread_id is user or group chat - # :return: a list of message ids of the sent message(s) - # """ - # mimetype = guess_type(image_path)[0] - # image_id = self._uploadImage({'file': (image_path, open(image_path, 'rb'), mimetype)}) - # return self._send(thread_id, message, thread_type, None, image_id, None, None) + def sendLocalImage(self, image_path: str, message: str = None, thread_id: str = None, thread_type: ThreadType = None): + """ + Sends an image from given URL to given (or default, if not) thread. + :param image_path: path of an image to upload and send + :param message: additional message + :param thread_id: user/group chat ID + :param thread_type: specify whether thread_id is user or group chat + :return: a list of message ids of the sent message(s) + """ + mimetype = guess_type(image_path)[0] + image_id = self._uploadImage({'file': (image_path, open(image_path, 'rb'), mimetype)}) + return self._send(thread_id, message, thread_type, None, image_id, None, None) def addUsersToChat(self, user_list: list, thread_id: str = None): """ @@ -659,28 +659,31 @@ class Client(object): # Strip the start and parse out the returned image_id return json.loads(response_content[9:])['payload']['metadata'][0]['image_id'] - def getThreadInfo(self, userID, last_n=20, start=None, is_user=True): + def getThreadInfo(self, last_n=20, thread_id: str = None, thread_type: ThreadType = None): """Get the info of one Thread - :param userID: ID of the user you want the messages from - :param last_n: (optional) number of retrieved messages from start - :param start: (optional) the start index of a thread (Deprecated) - :param is_user: (optional) determines if the userID is for user or thread + :param last_n: number of retrieved messages from start (default 20) + :param thread_id: user/group chat ID + :param thread_type: specify whether thread_id is user or group chat + :return: a list of messages """ + if thread_id is None and self.is_def_thread_set: + thread_id = self.def_thread_id + thread_type = self.def_thread_type + elif thread_id is None and not self.is_def_thread_set: + raise ValueError('Default Thread ID is not set.') + assert last_n > 0, 'length must be positive integer, got %d' % last_n - assert start is None, '`start` is deprecated, always 0 offset querry is returned' - if is_user: + + if thread_type == ThreadType.USER: key = 'user_ids' - else: + elif thread_type == ThreadType.GROUP: key = 'thread_fbids' - # deprecated - # `start` doesn't matter, always returns from the last - # data['messages[{}][{}][offset]'.format(key, userID)] = start - data = {'messages[{}][{}][offset]'.format(key, userID): 0, - 'messages[{}][{}][limit]'.format(key, userID): last_n - 1, - 'messages[{}][{}][timestamp]'.format(key, userID): now()} + data = {'messages[{}][{}][offset]'.format(key, thread_id): 0, + 'messages[{}][{}][limit]'.format(key, thread_id): last_n - 1, + 'messages[{}][{}][timestamp]'.format(key, thread_id): now()} r = self._post(MessagesURL, query=data) if not r.ok or len(r.text) == 0: @@ -691,7 +694,7 @@ class Client(object): return None messages = [] - for message in j['payload']['actions']: + for message in j['payload'].get('actions'): messages.append(Message(**message)) return list(reversed(messages)) diff --git a/test_data.js b/test_data.js new file mode 100644 index 0000000..f6d6250 --- /dev/null +++ b/test_data.js @@ -0,0 +1,6 @@ +{ + "email": "", + "password": "", + "user_thread_id": "", + "group_thread_id": "" +} \ No newline at end of file diff --git a/tests.py b/tests.py index 533e0d2..df26469 100644 --- a/tests.py +++ b/tests.py @@ -1,7 +1,10 @@ #!/usr/bin/env python +import time +import json import logging import fbchat +from fbchat.models import * import getpass import unittest import sys @@ -20,8 +23,8 @@ To use these tests, put: - email - password - a group_uid -- a user_uid (the user will be kicked from the group and then added again) -(seperated these by a newline) in a file called `tests.data`, or type them manually in the terminal prompts +- a user_uid (the user will be kicked from the group and then added again) `test_data.js`, +or type them manually in the terminal prompts Please remember to test both python v. 2.7 and python v. 3.6! @@ -31,30 +34,36 @@ If you only want to execute specific tests, pass the function names in the comma """ class TestFbchat(unittest.TestCase): - def test_login_functions(self): - self.assertTrue(client.is_logged_in()) - + def setUp(self): + pass + + def tearDown(self): + time.sleep(3) + + def test_loginFunctions(self): + self.assertTrue(client.isLoggedIn()) + client.logout() - - self.assertFalse(client.is_logged_in()) - + + self.assertFalse(client.isLoggedIn()) + with self.assertRaises(Exception): client.login("not@email.com", "not_password", max_retries=1) - + client.login(email, password) - - self.assertTrue(client.is_logged_in()) + + self.assertTrue(client.isLoggedIn()) def test_sessions(self): global client session_cookies = client.getSession() client = fbchat.Client(email, password, session_cookies=session_cookies) - - self.assertTrue(client.is_logged_in()) - def test_setDefaultRecipient(self): - client.setDefaultRecipient(client.uid, is_user=True) - self.assertTrue(client.send(message="test_default_recipient")) + self.assertTrue(client.isLoggedIn()) + + def test_setDefaultThreadId(self): + client.setDefaultThreadId(client.uid, ThreadType.USER) + self.assertTrue(client.sendMessage("test_default_recipient")) def test_getAllUsers(self): users = client.getAllUsers() @@ -63,9 +72,9 @@ class TestFbchat(unittest.TestCase): def test_getUsers(self): users = client.getUsers("Mark Zuckerberg") self.assertGreater(len(users), 0) - + u = users[0] - + # Test if values are set correctly self.assertIsInstance(u.uid, int) self.assertEquals(u.type, 'user') @@ -73,73 +82,80 @@ class TestFbchat(unittest.TestCase): self.assertEquals(u.url[:4], 'http') self.assertEquals(u.name, 'Mark Zuckerberg') self.assertGreater(u.score, 0) - - def test_send_likes(self): - self.assertTrue(client.send(client.uid, like='s')) - self.assertTrue(client.send(client.uid, like='m')) - self.assertTrue(client.send(client.uid, like='l')) - self.assertTrue(client.send(group_uid, like='s', is_user=False)) - self.assertTrue(client.send(group_uid, like='m', is_user=False)) - self.assertTrue(client.send(group_uid, like='l', is_user=False)) - - def test_send(self): - self.assertTrue(client.send(client.uid, message='test_send_user')) - self.assertTrue(client.send(group_uid, message='test_send_group', is_user=False)) - - def test_send_images(self): + + def test_sendEmoji(self): + self.assertTrue(client.sendEmoji(EmojiSize.SMALL, user_uid, ThreadType.USER)) + self.assertTrue(client.sendEmoji(EmojiSize.MEDIUM, user_uid, ThreadType.USER)) + self.assertTrue(client.sendEmoji(EmojiSize.LARGE, user_uid, ThreadType.USER)) + self.assertTrue(client.sendEmoji(EmojiSize.SMALL, group_uid, ThreadType.GROUP)) + self.assertTrue(client.sendEmoji(EmojiSize.MEDIUM, group_uid, ThreadType.GROUP)) + self.assertTrue(client.sendEmoji(EmojiSize.LARGE, group_uid, ThreadType.GROUP)) + + def test_sendMessage(self): + self.assertTrue(client.sendMessage('test_send_user', user_uid, ThreadType.USER)) + self.assertTrue(client.sendMessage('test_send_group', group_uid, ThreadType.GROUP)) + + def test_sendImages(self): image_url = 'https://cdn4.iconfinder.com/data/icons/ionicons/512/icon-image-128.png' image_local_url = path.join(path.dirname(__file__), 'test_image.png') - self.assertTrue(client.sendRemoteImage(client.uid, message='test_send_user_images_remote', image=image_url)) - self.assertTrue(client.sendLocalImage(client.uid, message='test_send_user_images_local', image=image_local_url)) - self.assertTrue(client.sendRemoteImage(group_uid, message='test_send_group_images_remote', is_user=False, image=image_url)) - self.assertTrue(client.sendLocalImage(group_uid, message='test_send_group_images_local', is_user=False, image=image_local_url)) + self.assertTrue(client.sendRemoteImage(image_url, 'test_send_user_images_remote', user_uid, ThreadType.USER)) + self.assertTrue(client.sendRemoteImage(image_url, 'test_send_group_images_remote', group_uid, ThreadType.GROUP)) + # Idk why but doesnt work, payload is null + # self.assertTrue(client.sendLocalImage(image_local_url, 'test_send_group_images_local', user_uid, ThreadType.USER)) + # self.assertTrue(client.sendLocalImage(image_local_url, 'test_send_group_images_local', group_uid, ThreadType.GROUP)) def test_getThreadInfo(self): - info = client.getThreadInfo(client.uid, last_n=1) - self.assertEquals(info[0].author, 'fbid:' + str(client.uid)) - client.send(group_uid, message='test_getThreadInfo', is_user=False) - info = client.getThreadInfo(group_uid, last_n=1, is_user=False) - self.assertEquals(info[0].author, 'fbid:' + str(client.uid)) - self.assertEquals(info[0].body, 'test_getThreadInfo') + client.sendMessage('test_user_getThreadInfo', user_uid, ThreadType.USER) + time.sleep(3) + info = client.getThreadInfo(20, user_uid, ThreadType.USER) + self.assertEquals(info[0].author, 'fbid:' + client.uid) + self.assertEquals(info[0].body, 'test_user_getThreadInfo') - def test_markAs(self): - # To be implemented (requires some form of manual watching) - pass + client.sendMessage('test_group_getThreadInfo', group_uid, ThreadType.GROUP) + time.sleep(3) + info = client.getThreadInfo(20, group_uid, ThreadType.GROUP) + self.assertEquals(info[0].author, 'fbid:' + client.uid) + self.assertEquals(info[0].body, 'test_group_getThreadInfo') - def test_listen(self): - client.do_one_listen() + # def test_markAs(self): + # # To be implemented (requires some form of manual watching) + # pass + + # def test_listen(self): + # client.doOneListen() def test_getUserInfo(self): info = client.getUserInfo(4) self.assertEquals(info['name'], 'Mark Zuckerberg') - - def test_remove_add_from_chat(self): - self.assertTrue(client.remove_user_from_chat(group_uid, user_uid)) - self.assertTrue(client.add_users_to_chat(group_uid, user_uid)) - + + def test_removeAddFromChat(self): + self.assertTrue(client.removeUserFromChat(user_uid, group_uid)) + self.assertTrue(client.addUsersToChat([user_uid], group_uid)) + def test_changeThreadTitle(self): - self.assertTrue(client.changeThreadTitle(group_uid, 'test_changeThreadTitle')) + self.assertTrue(client.changeThreadTitle('test_changeThreadTitle', group_uid)) def start_test(param_client, param_group_uid, param_user_uid, tests=[]): global client global group_uid global user_uid - + client = param_client group_uid = param_group_uid user_uid = param_user_uid - + if len(tests) == 0: suite = unittest.TestLoader().loadTestsFromTestCase(TestFbchat) else: suite = unittest.TestSuite(map(TestFbchat, tests)) - print ('Starting test(s)') + print('Starting test(s)') unittest.TextTestRunner(verbosity=2).run(suite) +client = None -if __name__ == '__main__': +if __name__ == 'tests': # Python 3 does not use raw_input, whereas Python 2 does try: input = raw_input @@ -147,20 +163,19 @@ if __name__ == '__main__': pass try: - with open(path.join(path.dirname(__file__), 'tests.data'), 'r') as f: - content = f.readlines() - content = [x.strip() for x in content if len(x.strip()) != 0] - email = content[0] - password = content[1] - group_uid = content[2] - user_uid = content[3] + with open(path.join(path.dirname(__file__), 'test_data.js'), 'r') as f: + json = json.load(f) + email = json["email"] + password = json["password"] + user_uid = json["user_thread_id"] + group_uid = json["group_thread_id"] except (IOError, IndexError) as e: email = input('Email: ') password = getpass.getpass() group_uid = input('Please enter a group uid (To test group functionality): ') user_uid = input('Please enter a user uid (To test kicking/adding functionality): ') - print ('Logging in') + print('Logging in...') client = fbchat.Client(email, password) # Warning! Taking user input directly like this could be dangerous! Use only for testing purposes! From 0d75c09036cb530f69601195f14451e248533365 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Wed, 10 May 2017 14:54:07 +0200 Subject: [PATCH 3/9] Added support for deprecating items, and maybe support for python 2.7 - Changed `test_data.js` to `test_data.json` - Added `deprecated` decorator - Added `deprecation` function - Readded old functions, and marked them as deprecated - Changed parameters back to being type-in-specific (support for python 2.x) - Deprecated `info_log` and `debug` init paramters --- .gitignore | 1 + fbchat/__init__.py | 2 +- fbchat/client.py | 215 +++++++++++++++++++++++++++++++++++---------- fbchat/models.py | 21 ++++- fbchat/stickers.py | 8 -- fbchat/utils.py | 28 ++++++ test_data.js | 6 -- tests.py | 50 ++++++----- 8 files changed, 245 insertions(+), 86 deletions(-) delete mode 100644 fbchat/stickers.py delete mode 100644 test_data.js diff --git a/.gitignore b/.gitignore index dbca6db..08261f5 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,5 @@ develop-eggs docs/_build/ # Data for tests +test_data.json tests.data \ No newline at end of file diff --git a/fbchat/__init__.py b/fbchat/__init__.py index c58baca..e41d9f6 100644 --- a/fbchat/__init__.py +++ b/fbchat/__init__.py @@ -15,7 +15,7 @@ from .client import * __copyright__ = 'Copyright 2015 by Taehoon Kim' -__version__ = '0.10.1' +__version__ = '0.10.2' __license__ = 'BSD' __author__ = 'Taehoon Kim; Moreels Pieter-Jan' __email__ = 'carpedm20@gmail.com' diff --git a/fbchat/client.py b/fbchat/client.py index 137277f..8dd90fd 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -14,14 +14,12 @@ import requests import logging from uuid import uuid1 -import warnings from random import choice from datetime import datetime from bs4 import BeautifulSoup as bs from mimetypes import guess_type from .utils import * from .models import * -from .stickers import * import time import sys @@ -58,6 +56,9 @@ facebookEncoding = 'UTF-8' # Log settings log = logging.getLogger("client") log.setLevel(logging.DEBUG) +# Creates the console handler +handler = logging.StreamHandler() +log.addHandler(handler) class Client(object): @@ -67,16 +68,17 @@ class Client(object): documentation for the API. """ - def __init__(self, email, password, debug=True, info_log=True, user_agent=None, max_retries=5, session_cookies=None): + def __init__(self, email, password, debug=False, info_log=False, user_agent=None, max_retries=5, session_cookies=None, logging_level=logging.INFO): """A client for the Facebook Chat (Messenger). :param email: Facebook `email` or `id` or `phone number` :param password: Facebook account password - :param debug: Configures the logger to `debug` logging_level - :param info_log: Configures the logger to `info` logging_level + :param debug: Configures the logger to `debug` logging_level (deprecated) + :param info_log: Configures the logger to `info` logging_level (deprecated) :param user_agent: Custom user agent to use when sending requests. If `None`, user agent will be chosen from a premade list (see utils.py) :param max_retries: Maximum number of times to retry login :param session_cookies: Cookie dict from a previous session (Will default to login if these are invalid) + :param logging_level: Configures the logger to logging_level """ self.sticky, self.pool = (None, None) @@ -86,9 +88,8 @@ class Client(object): self.payloadDefault = {} self.client = 'mercury' self.listening = False - self.is_def_thread_set = False - self.def_thread_id = None - self.def_thread_type = None + self.default_thread_id = None + self.default_thread_type = None self.threads = [] if not user_agent: @@ -104,21 +105,39 @@ class Client(object): # Configure the logger differently based on the 'debug' and 'info_log' parameters if debug: + deprecation('Client(debug)', deprecated_in='0.6.0', details='Use Client(logging_level) instead') logging_level = logging.DEBUG elif info_log: + deprecation('Client(info_log)', deprecated_in='0.6.0', details='Use Client(logging_level) instead') logging_level = logging.INFO - else: - logging_level = logging.WARNING - # Creates the console handler - handler = logging.StreamHandler() handler.setLevel(logging_level) - log.addHandler(handler) # If session cookies aren't set, not properly loaded or gives us an invalid session, then do the login if not session_cookies or not self.setSession(session_cookies) or not self.is_logged_in(): self.login(email, password, max_retries) + @deprecated(deprecated_in='0.6.0', details='Use log. instead') + def _console(self, msg): + """Assumes an INFO level and log it. + + This method shouldn't be used anymore. + Use the log itself: + >>> import logging + >>> from fbchat.client import log + >>> log.setLevel(logging.DEBUG) + + You can do the same thing by adding the 'debug' argument: + >>> from fbchat import Client + >>> client = Client("...", "...", debug=True) + """ + log.debug(msg) + + def _setttstamp(self): + for i in self.fb_dtsg: + self.ttstamp += str(ord(i)) + self.ttstamp += '2' + def _generatePayload(self, query): """Adds the following defaults to the payload: __rev, __user, __a, ttstamp, fb_dtsg, __req @@ -154,7 +173,7 @@ class Client(object): self.payloadDefault = {} self.client_id = hex(int(random()*2147483648))[2:] self.start_time = now() - self.uid = str(self._session.cookies['c_user']) + self.uid = int(self._session.cookies['c_user']) self.user_channel = "p_" + str(self.uid) self.ttstamp = '' @@ -294,7 +313,8 @@ class Client(object): return True def login(self, email, password, max_retries=5): - self.onLoggingIn(email=email) + # Logging in + log.info("Logging in {}...".format(email)) if not (email and password): raise Exception("Email and password not set.") @@ -308,7 +328,7 @@ class Client(object): time.sleep(1) continue else: - self.onLoggedIn(email=email) + log.info("Login of {} successful.".format(email)) break else: raise Exception("Login failed. Check email/password.") @@ -327,16 +347,19 @@ class Client(object): self.req_counter = 1 self.seq = "0" return r + + @deprecated(deprecated_in='0.10.2', details='Use setDefaultThread instead') + def setDefaultRecipient(self, recipient_id, is_user=True): + self.setDefaultThread(recipient_id, thread_type=isUserToThreadType(is_user)) - def setDefaultThreadId(self, thread_id=str, thread_type=ThreadType): - """Sets default recipient to send messages and images to. + def setDefaultThread(self, thread_id=None, thread_type=ThreadType.USER): + """Sets default thread to send messages and images to. :param thread_id: user/group ID to default to :param thread_type: type of thread_id """ - self.def_thread_id = thread_id - self.def_thread_type = thread_type - self.is_def_thread_set = True + self.default_thread_id = thread_id + self.default_thread_type = thread_type def _adapt_user_in_chat_to_user_model(self, user_in_chat): """ Adapts user info from chat to User model acceptable initial dict @@ -424,7 +447,7 @@ class Client(object): SEND METHODS """ - def _send(self, thread_id=None, message=None, thread_type=None, emoji_size=None, image_id=None, add_user_ids=None, new_title=None): + def _send(self, thread_id=None, message=None, thread_type=ThreadType.USER, emoji_size=None, image_id=None, add_user_ids=None, new_title=None): """Send a message with given thread id :param thread_id: the user id or thread id that you want to send a message to @@ -436,11 +459,12 @@ class Client(object): :return: a list of message ids of the sent message(s) """ - if thread_id is None and self.is_def_thread_set: - thread_id = self.def_thread_id - thread_type = self.def_thread_type - elif thread_id is None and not self.is_def_thread_set: - raise ValueError('Default Thread ID is not set.') + if thread_id is None: + if self.default_thread_id is not None: + thread_id = self.default_thread_id + thread_type = self.default_thread_type + else: + raise ValueError('Thread ID is not set.') messageAndOTID = generateOfflineThreadingID() timestamp = now() @@ -538,7 +562,11 @@ class Client(object): log.debug("With data {}".format(data)) return message_ids - def sendMessage(self, message: str, thread_id: str = None, thread_type: ThreadType = None): + @deprecated(deprecated_in='0.10.2', details='Use specific functions (eg. sendMessage()) instead') + def send(self, recipient_id=None, message=None, is_user=True, like=None, image_id=None, add_user_ids=None): + return self._send(thread_id=recipient_id, message=message, thread_type=isUserToThreadType(is_user), emoji_size=LIKES[like], image_id=image_id, add_user_ids=add_user_ids) + + def sendMessage(self, message, thread_id=None, thread_type=ThreadType.USER): """ Sends a message to given (or default, if not) thread with an additional image. :param message: message to send @@ -546,19 +574,20 @@ class Client(object): :param thread_type: specify whether thread_id is user or group chat :return: a list of message ids of the sent message(s) """ - return self._send(thread_id, message, thread_type, None, None, None, None) + return self._send(thread_id=thread_id, message=message, thread_type=thread_type) - def sendEmoji(self, emoji_size: EmojiSize, thread_id: str = None, thread_type: ThreadType = None): + def sendEmoji(self, thread_id, size=Size.MEDIUM, emoji=None, thread_type=ThreadType.USER): """ Sends an emoji to given (or default, if not) thread. - :param emoji_size: size of emoji to send + :param size: size of emoji to send :param thread_id: user/group chat ID + :param emoji: WIP :param thread_type: specify whether thread_id is user or group chat :return: a list of message ids of the sent message(s) """ - return self._send(thread_id, None, thread_type, emoji_size, None, None, None) + return self._send(thread_id=thread_id, thread_type=thread_type, emoji_size=size) - def sendRemoteImage(self, image_url: str, message: str = None, thread_id: str = None, thread_type: ThreadType = None): + def sendRemoteImage(self, image_url, message=None, thread_id=None, thread_type=ThreadType.USER, recipient_id=None, is_user=None, image=None): """ Sends an image from given URL to given (or default, if not) thread. :param image_url: URL of an image to upload and send @@ -567,13 +596,22 @@ class Client(object): :param thread_type: specify whether thread_id is user or group chat :return: a list of message ids of the sent message(s) """ + if recipient_id is not None: + deprecation('sendRemoteImage(recipient_id)', deprecated_in='0.10.2', details='Use sendRemoteImage(thread_id) instead') + thread_id = recipient_id + if is_user is not None: + deprecation('sendRemoteImage(is_user)', deprecated_in='0.10.2', details='Use sendRemoteImage(thread_type) instead') + thread_type = isUserToThreadType(is_user) + if image is not None: + deprecation('sendRemoteImage(image)', deprecated_in='0.10.2', details='Use sendRemoteImage(image_url) instead') + image_url = image mimetype = guess_type(image_url)[0] remote_image = requests.get(image_url).content image_id = self._uploadImage({'file': (image_url, remote_image, mimetype)}) - return self._send(thread_id, message, thread_type, None, image_id, None, None) + return self._send(thread_id=thread_id, message=message, thread_type=thread_type, image_id=image_id) # Doesn't upload properly - def sendLocalImage(self, image_path: str, message: str = None, thread_id: str = None, thread_type: ThreadType = None): + def sendLocalImage(self, image_path, message=None, thread_id=None, thread_type=ThreadType.USER, recipient_id=None, is_user=None, image=None): """ Sends an image from given URL to given (or default, if not) thread. :param image_path: path of an image to upload and send @@ -582,20 +620,29 @@ class Client(object): :param thread_type: specify whether thread_id is user or group chat :return: a list of message ids of the sent message(s) """ + if recipient_id is not None: + deprecation('sendRemoteImage(recipient_id)', deprecated_in='0.10.2', details='Use sendLocalImage(thread_id) instead') + thread_id = recipient_id + if is_user is not None: + deprecation('sendRemoteImage(is_user)', deprecated_in='0.10.2', details='Use sendLocalImage(thread_type) instead') + thread_type = isUserToThreadType(is_user) + if image is not None: + deprecation('sendRemoteImage(image)', deprecated_in='0.10.2', details='Use sendLocalImage(image_path) instead') + image_path = image mimetype = guess_type(image_path)[0] image_id = self._uploadImage({'file': (image_path, open(image_path, 'rb'), mimetype)}) - return self._send(thread_id, message, thread_type, None, image_id, None, None) + return self._send(thread_id=thread_id, message=message, thread_type=thread_type, image_id=image_id) - def addUsersToChat(self, user_list: list, thread_id: str = None): + def addUsersToChat(self, user_ids, thread_id=None): """ Adds users to given (or default, if not) thread. - :param user_list: list of users to add + :param user_ids: list of user ids to add :param thread_id: group chat ID :return: a list of message ids of the sent message(s) """ - return self._send(thread_id, None, ThreadType.GROUP, None, None, user_list, None) + return self._send(thread_id=thread_id, thread_type=ThreadType.GROUP, add_user_ids=users) - def removeUserFromChat(self, user_id: str, thread_id: str = None): + def removeUserFromChat(self, user_id, thread_id=None): """ Adds users to given (or default, if not) thread. :param user_id: user ID to remove @@ -617,18 +664,28 @@ class Client(object): return r.ok - def changeThreadTitle(self, new_title: str, thread_id: str = None): + @deprecated(deprecated_in='0.10.2', details='Use removeUserFromChat() instead') + def add_users_to_chat(self, threadID, userID): + if not isinstance(userID, list): + userID = [userID] + return self.addUsersToChat(userID, thread_id=threadID) + + @deprecated(deprecated_in='0.10.2', details='Use removeUserFromChat() instead') + def remove_user_from_chat(self, threadID, userID): + return self.removeUserFromChat(userID, thread_id=threadID) + + @deprecated(deprecated_in='0.10.2', details='Use changeGroupTitle() instead') + def changeThreadTitle(self, threadID, newTitle): + return self.changeGroupTitle(newTitle, thread_id=threadID) + + def changeGroupTitle(self, title, thread_id=None): """ Change title of a group conversation. - :param new_title: new group chat title + :param title: new group chat title :param thread_id: group chat ID :return: a list of message ids of the sent message(s) """ - if thread_id is None and self.def_thread_type == ThreadType.GROUP: - thread_id = self.def_thread_id - elif thread_id is None: - raise ValueError('Default Thread ID is not set.') - return self._send(thread_id, None, ThreadType.GROUP, None, None, None, new_title) + return self._send(thread_id=thread_id, thread_type=ThreadType.GROUP, new_title=title) """ END SEND METHODS @@ -647,7 +704,7 @@ class Client(object): # Strip the start and parse out the returned image_id return json.loads(response_content[9:])['payload']['metadata'][0]['image_id'] - def getThreadInfo(self, last_n=20, thread_id: str = None, thread_type: ThreadType = None): + def getThreadInfo(self, last_n=20, thread_id=None, thread_type=ThreadType.USER): """Get the info of one Thread :param last_n: number of retrieved messages from start (default 20) @@ -969,3 +1026,65 @@ class Client(object): if len(full_data)==1: full_data=full_data[0] return full_data + + + + def on_message_new(self, mid, author_id, message, metadata, recipient_id, thread_type): + """subclass Client and override this method to add custom behavior on event + + This version of on_message recieves recipient_id and thread_type. + For backwards compatability, this data is sent directly to the old on_message. + """ + self.on_message(mid, author_id, None, message, metadata) + + @deprecated(deprecated_in='0.7.0', details='Use on_message_new() instead') + def on_message(self, mid, author_id, author_name, message, metadata): + """subclass Client and override this method to add custom behavior on event""" + self.markAsDelivered(author_id, mid) + self.markAsRead(author_id) + log.info("%s said: %s" % (author_name, message)) + + + def on_friend_request(self, from_id): + """subclass Client and override this method to add custom behavior on event""" + log.info("Friend request from %s." % from_id) + + + def on_typing(self, author_id): + """subclass Client and override this method to add custom behavior on event""" + pass + + + def on_read(self, author, reader, time): + """subclass Client and override this method to add custom behavior on event""" + pass + + + def on_people_added(self, user_ids, actor_id, thread_id): + """subclass Client and override this method to add custom behavior on event""" + log.info("User(s) {} was added to {} by {}".format(repr(user_ids), thread_id, actor_id)) + + + def on_person_removed(self, user_id, actor_id, thread_id): + """subclass Client and override this method to add custom behavior on event""" + log.info("User {} was removed from {} by {}".format(user_id, thread_id, actor_id)) + + + def on_inbox(self, viewer, unseen, unread, other_unseen, other_unread, timestamp): + """subclass Client and override this method to add custom behavior on event""" + pass + + + def on_message_error(self, exception, message): + """subclass Client and override this method to add custom behavior on event""" + log.warning("Exception:\n{}".format(exception)) + + + def on_qprimer(self, timestamp): + pass + + + def on_unknown_type(self, m): + """subclass Client and override this method to add custom behavior on event""" + log.debug("Unknown type {}".format(m)) + diff --git a/fbchat/models.py b/fbchat/models.py index 4a4400a..2b49caf 100644 --- a/fbchat/models.py +++ b/fbchat/models.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals import sys +from enum import Enum class Base(): def __repr__(self): @@ -37,7 +38,25 @@ class TypingStatus(Enum): DELETED = 0 TYPING = 1 -class EmojiSize(Enum): + +# WIP +class StickerSize(Enum): LARGE = '369239383222810' MEDIUM = '369239343222814' SMALL = '369239263222822' + +#class Size(Enum): +# LARGE = 'large' +# MEDIUM = 'medium' +# SMALL = 'small' + +Size = StickerSize + +LIKES = { + 'l': Size.LARGE, + 'm': Size.MEDIUM, + 's': Size.SMALL +} +LIKES['large'] = LIKES['l'] +LIKES['medium'] =LIKES['m'] +LIKES['small'] = LIKES['s'] diff --git a/fbchat/stickers.py b/fbchat/stickers.py deleted file mode 100644 index 6e02adb..0000000 --- a/fbchat/stickers.py +++ /dev/null @@ -1,8 +0,0 @@ -LIKES={ - 'l': '369239383222810', - 'm': '369239343222814', - 's': '369239263222822' -} -LIKES['large'] = LIKES['l'] -LIKES['medium'] =LIKES['m'] -LIKES['small'] = LIKES['s'] diff --git a/fbchat/utils.py b/fbchat/utils.py index 292d422..fb4fee0 100644 --- a/fbchat/utils.py +++ b/fbchat/utils.py @@ -2,6 +2,7 @@ import re import json from time import time from random import random +import warnings USER_AGENTS = [ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/601.1.10 (KHTML, like Gecko) Version/8.0.5 Safari/601.1.10", @@ -62,3 +63,30 @@ def generateOfflineThreadingID() : msgs = bin(ret) + string return str(int(msgs,2)) +def isUserToThreadType(is_user): + return ThreadType.USER if is_user else ThreadType.GROUP + +def deprecation(name, deprecated_in=None, details='', stacklevel=3): + """This is a function which should be used to mark parameters as deprecated. + It will result in a warning being emmitted when the parameter is used. + """ + warning = "{} is deprecated".format(name) + if deprecated_in: + warning += ' in v. {}'.format(deprecated_in) + if details: + warning += '. {}'.format(details) + + warnings.simplefilter('always', DeprecationWarning) + warnings.warn(warning, category=DeprecationWarning, stacklevel=stacklevel) + warnings.simplefilter('default', DeprecationWarning) + +def deprecated(deprecated_in=None, details=''): + """This is a decorator which can be used to mark functions as deprecated. + It will result in a warning being emmitted when the decorated function is used. + """ + def wrap(func, *args, **kwargs): + def wrapped_func(*args, **kwargs): + deprecation(func.__qualname__, deprecated_in, details, stacklevel=2) + return func(*args, **kwargs) + return wrapped_func + return wrap diff --git a/test_data.js b/test_data.js deleted file mode 100644 index f6d6250..0000000 --- a/test_data.js +++ /dev/null @@ -1,6 +0,0 @@ -{ - "email": "", - "password": "", - "user_thread_id": "", - "group_thread_id": "" -} \ No newline at end of file diff --git a/tests.py b/tests.py index df26469..b8134e1 100644 --- a/tests.py +++ b/tests.py @@ -10,21 +10,27 @@ import unittest import sys from os import path -# Disable logging -logging.basicConfig(level=100) -fbchat.log.setLevel(100) +#Setup logging +logging.basicConfig(level=logging.INFO) """ Tests for fbchat ~~~~~~~~~~~~~~~~ -To use these tests, put: -- email -- password -- a group_uid -- a user_uid (the user will be kicked from the group and then added again) `test_data.js`, -or type them manually in the terminal prompts +To use these tests, make a json file called test_data.json, put this example in it, and fill in the gaps: +{ + "email": "example@email.com", + "password": "example_password", + "group_thread_id": 0, + "user_thread_id": 0 +} +or type this information manually in the terminal prompts. + +- email: Your (or a test user's) email / phone number +- password: Your (or a test user's) password +- group_thread_id: A test group that will be used to test group functionality +- user_thread_id: A person that will be used to test kick/add functionality (This user should be in the group) Please remember to test both python v. 2.7 and python v. 3.6! @@ -101,8 +107,8 @@ class TestFbchat(unittest.TestCase): self.assertTrue(client.sendRemoteImage(image_url, 'test_send_user_images_remote', user_uid, ThreadType.USER)) self.assertTrue(client.sendRemoteImage(image_url, 'test_send_group_images_remote', group_uid, ThreadType.GROUP)) # Idk why but doesnt work, payload is null - # self.assertTrue(client.sendLocalImage(image_local_url, 'test_send_group_images_local', user_uid, ThreadType.USER)) - # self.assertTrue(client.sendLocalImage(image_local_url, 'test_send_group_images_local', group_uid, ThreadType.GROUP)) + self.assertTrue(client.sendLocalImage(image_local_url, 'test_send_group_images_local', user_uid, ThreadType.USER)) + self.assertTrue(client.sendLocalImage(image_local_url, 'test_send_group_images_local', group_uid, ThreadType.GROUP)) def test_getThreadInfo(self): client.sendMessage('test_user_getThreadInfo', user_uid, ThreadType.USER) @@ -117,12 +123,12 @@ class TestFbchat(unittest.TestCase): self.assertEquals(info[0].author, 'fbid:' + client.uid) self.assertEquals(info[0].body, 'test_group_getThreadInfo') - # def test_markAs(self): - # # To be implemented (requires some form of manual watching) - # pass + def test_markAs(self): + # To be implemented (requires some form of manual watching) + pass - # def test_listen(self): - # client.doOneListen() + def test_listen(self): + client.doOneListen() def test_getUserInfo(self): info = client.getUserInfo(4) @@ -165,15 +171,15 @@ if __name__ == 'tests': try: with open(path.join(path.dirname(__file__), 'test_data.js'), 'r') as f: json = json.load(f) - email = json["email"] - password = json["password"] - user_uid = json["user_thread_id"] - group_uid = json["group_thread_id"] + email = json['email'] + password = json['password'] + user_uid = json['user_thread_id'] + group_uid = json['group_thread_id'] except (IOError, IndexError) as e: email = input('Email: ') password = getpass.getpass() - group_uid = input('Please enter a group uid (To test group functionality): ') - user_uid = input('Please enter a user uid (To test kicking/adding functionality): ') + group_uid = input('Please enter a group thread id (To test group functionality): ') + user_uid = input('Please enter a user thread id (To test kicking/adding functionality): ') print('Logging in...') client = fbchat.Client(email, password) From 357083efce5c89e61772edf264cefb9610aec790 Mon Sep 17 00:00:00 2001 From: Dainius Date: Wed, 10 May 2017 18:16:41 +0300 Subject: [PATCH 4/9] reintroduce things skipped on conflict --- .gitignore | 2 + fbchat/client.py | 383 ++++++++++++++++++++++++++++--------------- fbchat/event_hook.py | 57 +++++++ fbchat/models.py | 71 +++++--- fbchat/utils.py | 11 +- 5 files changed, 362 insertions(+), 162 deletions(-) create mode 100644 fbchat/event_hook.py diff --git a/.gitignore b/.gitignore index 08261f5..98f10f5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ *py[co] +.idea/ + # Test scripts *.sh diff --git a/fbchat/client.py b/fbchat/client.py index 8dd90fd..1d144ad 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -20,6 +20,7 @@ from bs4 import BeautifulSoup as bs from mimetypes import guess_type from .utils import * from .models import * +from .event_hook import * import time import sys @@ -68,7 +69,8 @@ class Client(object): documentation for the API. """ - def __init__(self, email, password, debug=False, info_log=False, user_agent=None, max_retries=5, session_cookies=None, logging_level=logging.INFO): + def __init__(self, email, password, debug=False, info_log=False, user_agent=None, max_retries=5, + session_cookies=None, logging_level=logging.INFO): """A client for the Facebook Chat (Messenger). :param email: Facebook `email` or `id` or `phone number` @@ -92,6 +94,8 @@ class Client(object): self.default_thread_type = None self.threads = [] + self._setupEventHooks() + if not user_agent: user_agent = choice(USER_AGENTS) @@ -114,9 +118,55 @@ class Client(object): handler.setLevel(logging_level) # If session cookies aren't set, not properly loaded or gives us an invalid session, then do the login - if not session_cookies or not self.setSession(session_cookies) or not self.is_logged_in(): + if not session_cookies or not self.setSession(session_cookies) or not self.isLoggedIn(): self.login(email, password, max_retries) + def _setupEventHooks(self): + # Setup event hooks + self.onLoggingIn = EventHook(email=str) + self.onLoggedIn = EventHook(email=str) + self.onListening = EventHook() + + self.onMessage = EventHook(mid=str, author_id=str, message=str, thread_id=int, thread_type=ThreadType, ts=str, metadata=dict) + self.onColorChange = EventHook(mid=str, author_id=str, new_color=str, thread_id=str, thread_type=ThreadType, ts=str, metadata=dict) + self.onEmojiChange = EventHook(mid=str, author_id=str, new_emoji=str, thread_id=str, thread_type=ThreadType, ts=str, metadata=dict) + self.onTitleChange = EventHook(mid=str, author_id=str, new_title=str, thread_id=str, thread_type=ThreadType, ts=str, metadata=dict) + self.onNicknameChange = EventHook(mid=str, author_id=str, changed_for=str, new_title=str, thread_id=str, thread_type=ThreadType, ts=str, metadata=dict) + # self.onTyping = EventHook(author_id=int, typing_status=TypingStatus) + # self.onSeen = EventHook(seen_by=str, thread_id=str, timestamp=str) + + self.onInbox = EventHook(unseen=int, unread=int, recent_unread=int) + self.onPeopleAdded = EventHook(added_ids=list, author_id=str, thread_id=str) + self.onPersonRemoved = EventHook(removed_id=str, author_id=str, thread_id=str) + self.onFriendRequest = EventHook(from_id=str) + + self.onUnknownMesssageType = EventHook(msg=dict) + + # Setup event handlers + self.onLoggingIn += lambda email: log.info("Logging in %s..." % email) + self.onLoggedIn += lambda email: log.info("Login of %s successful." % email) + self.onListening += lambda: log.info("Listening...") + + self.onMessage += lambda mid, author_id, message, thread_id, thread_type, ts, metadata:\ + log.info("Message from %s in %s (%s): %s" % (author_id, thread_id, thread_type.name, message)) + + self.onColorChange += lambda mid, author_id, new_color, thread_id, thread_type, ts, metadata:\ + log.info("Color change from %s in %s (%s): %s" % (author_id, thread_id, thread_type.name, new_color)) + self.onEmojiChange += lambda mid, author_id, new_emoji, thread_id, thread_type, ts, metadata:\ + log.info("Emoji change from %s in %s (%s): %s" % (author_id, thread_id, thread_type.name, new_emoji)) + self.onTitleChange += lambda mid, author_id, new_title, thread_id, thread_type, ts, metadata:\ + log.info("Title change from %s in %s (%s): %s" % (author_id, thread_id, thread_type.name, new_title)) + self.onNicknameChange += lambda mid, author_id, new_title, changed_for, thread_id, thread_type, ts, metadata:\ + log.info("Nickname change from %s in %s (%s) for %s: %s" % (author_id, thread_id, thread_type.name, changed_for, new_title)) + + self.onPeopleAdded += lambda added_ids, author_id, thread_id:\ + log.info("%s added: %s" % (author_id, [x for x in added_ids])) + self.onPersonRemoved += lambda removed_id, author_id, thread_id:\ + log.info("%s removed: %s" % (author_id, removed_id)) + + self.onUnknownMesssageType += lambda msg:\ + log.info("Unknown message type received: %s" % msg) + @deprecated(deprecated_in='0.6.0', details='Use log. instead') def _console(self, msg): """Assumes an INFO level and log it. @@ -133,11 +183,6 @@ class Client(object): """ log.debug(msg) - def _setttstamp(self): - for i in self.fb_dtsg: - self.ttstamp += str(ord(i)) - self.ttstamp += '2' - def _generatePayload(self, query): """Adds the following defaults to the payload: __rev, __user, __a, ttstamp, fb_dtsg, __req @@ -169,7 +214,7 @@ class Client(object): payload=self._generatePayload(None) return self._session.post(url, data=payload, timeout=timeout, files=files) - def _post_login(self): + def _postLogin(self): self.payloadDefault = {} self.client_id = hex(int(random()*2147483648))[2:] self.start_time = now() @@ -183,7 +228,9 @@ class Client(object): log.debug(r.url) self.fb_dtsg = soup.find("input", {'name':'fb_dtsg'})['value'] self.fb_h = soup.find("input", {'name':'h'})['value'] - self._setttstamp() + for i in self.fb_dtsg: + self.ttstamp += str(ord(i)) + self.ttstamp += '2' # Set default payload self.payloadDefault['__rev'] = int(r.text.split('"revision":',1)[1].split(",",1)[0]) self.payloadDefault['__user'] = self.uid @@ -228,7 +275,7 @@ class Client(object): r = self._cleanGet(SaveDeviceURL) if 'home' in r.url: - self._post_login() + self._postLogin() return True else: return False @@ -284,13 +331,10 @@ class Client(object): r = self._cleanPost(CheckpointURL, data) return r - def is_logged_in(self): + def isLoggedIn(self): # Send a request to the login url, to see if we're directed to the home page. r = self._cleanGet(LoginURL) - if 'home' in r.url: - return True - else: - return False + return 'home' in r.url def getSession(self): """Returns the session cookies""" @@ -309,12 +353,11 @@ class Client(object): # Load cookies into current session self._session.cookies = requests.cookies.merge_cookies(self._session.cookies, session_cookies) - self._post_login() + self._postLogin() return True def login(self, email, password, max_retries=5): - # Logging in - log.info("Logging in {}...".format(email)) + self.onLoggingIn(email=email) if not (email and password): raise Exception("Email and password not set.") @@ -328,7 +371,7 @@ class Client(object): time.sleep(1) continue else: - log.info("Login of {} successful.".format(email)) + self.onLoggedIn(email=email) break else: raise Exception("Login failed. Check email/password.") @@ -350,49 +393,23 @@ class Client(object): @deprecated(deprecated_in='0.10.2', details='Use setDefaultThread instead') def setDefaultRecipient(self, recipient_id, is_user=True): - self.setDefaultThread(recipient_id, thread_type=isUserToThreadType(is_user)) + self.setDefaultThread(str(recipient_id), thread_type=isUserToThreadType(is_user)) - def setDefaultThread(self, thread_id=None, thread_type=ThreadType.USER): + def setDefaultThread(self, thread_id, thread_type): + # type: (str, ThreadType) -> None """Sets default thread to send messages and images to. - + :param thread_id: user/group ID to default to :param thread_type: type of thread_id """ self.default_thread_id = thread_id self.default_thread_type = thread_type - def _adapt_user_in_chat_to_user_model(self, user_in_chat): - """ Adapts user info from chat to User model acceptable initial dict - - :param user_in_chat: user info from chat - - 'dir': None, - 'mThumbSrcSmall': None, - 'is_friend': False, - 'is_nonfriend_messenger_contact': True, - 'alternateName': '', - 'i18nGender': 16777216, - 'vanity': '', - 'type': 'friend', - 'searchTokens': ['Voznesenskij', 'Sergej'], - 'thumbSrc': 'https://fb-s-b-a.akamaihd.net/h-ak-xfa1/v/t1.0-1/c9.0.32.32/p32x32/10354686_10150004552801856_220367501106153455_n.jpg?oh=71a87d76d4e4d17615a20c43fb8dbb47&oe=59118CE4&__gda__=1493753268_ae75cef40e9785398e744259ccffd7ff', - 'mThumbSrcLarge': None, - 'firstName': 'Sergej', - 'name': 'Sergej Voznesenskij', - 'uri': 'https://www.facebook.com/profile.php?id=100014812758264', - 'id': '100014812758264', - 'gender': 2 - """ - - return { - 'type': 'user', - 'uid': user_in_chat['id'], - 'photo': user_in_chat['thumbSrc'], - 'path': user_in_chat['uri'], - 'text': user_in_chat['name'], - 'score': '', - 'data': user_in_chat, - } + def resetDefaultThread(self): + # type: () -> None + """Resets default thread.""" + self.default_thread_id = None + self.default_thread_type = None def getAllUsers(self): """ Gets all users from chat with info included """ @@ -411,7 +428,7 @@ class Client(object): for k in payload.keys(): try: - user = self._adapt_user_in_chat_to_user_model(payload[k]) + user = User.adaptFromChat(payload[k]) except KeyError: continue @@ -448,6 +465,7 @@ class Client(object): """ def _send(self, thread_id=None, message=None, thread_type=ThreadType.USER, emoji_size=None, image_id=None, add_user_ids=None, new_title=None): + # type: (str, str, ThreadType, EmojiSize, str, list, str) -> list """Send a message with given thread id :param thread_id: the user id or thread id that you want to send a message to @@ -538,7 +556,7 @@ class Client(object): if not r.ok: log.warning('Error when sending message: Got {} response'.format(r.status_code)) - return False + return None response_content = {} if isinstance(r.content, str) is False: @@ -547,15 +565,15 @@ class Client(object): if 'error' in j: # 'errorDescription' is in the users own language! log.warning('Error #{} when sending message: {}'.format(j['error'], j['errorDescription'])) - return False + return None message_ids = [] try: message_ids += [action['message_id'] for action in j['payload']['actions'] if 'message_id' in action] - message_ids[0] # Try accessing element + message_ids[0] # Try accessing element except (KeyError, IndexError) as e: log.warning('Error when sending message: No message ids could be found') - return False + return None log.info('Message sent.') log.debug("Sending {}".format(r)) @@ -567,8 +585,10 @@ class Client(object): return self._send(thread_id=recipient_id, message=message, thread_type=isUserToThreadType(is_user), emoji_size=LIKES[like], image_id=image_id, add_user_ids=add_user_ids) def sendMessage(self, message, thread_id=None, thread_type=ThreadType.USER): + # type: (str, str, ThreadType) -> list """ Sends a message to given (or default, if not) thread with an additional image. + :param message: message to send :param thread_id: user/group chat ID :param thread_type: specify whether thread_id is user or group chat @@ -576,20 +596,25 @@ class Client(object): """ return self._send(thread_id=thread_id, message=message, thread_type=thread_type) - def sendEmoji(self, thread_id, size=Size.MEDIUM, emoji=None, thread_type=ThreadType.USER): + def sendEmoji(self, emoji=None, size=EmojiSize.SMALL, thread_id=None, thread_type=ThreadType.USER): + # type: (str, EmojiSize, str, ThreadType) -> list """ Sends an emoji to given (or default, if not) thread. + + :param emoji: WIP :param size: size of emoji to send :param thread_id: user/group chat ID - :param emoji: WIP :param thread_type: specify whether thread_id is user or group chat :return: a list of message ids of the sent message(s) """ return self._send(thread_id=thread_id, thread_type=thread_type, emoji_size=size) - def sendRemoteImage(self, image_url, message=None, thread_id=None, thread_type=ThreadType.USER, recipient_id=None, is_user=None, image=None): + def sendRemoteImage(self, image_url, message=None, thread_id=None, thread_type=ThreadType.USER, + recipient_id=None, is_user=None, image=None): + # type: (str, str, str, ThreadType) -> list """ Sends an image from given URL to given (or default, if not) thread. + :param image_url: URL of an image to upload and send :param message: additional message :param thread_id: user/group chat ID @@ -597,7 +622,6 @@ class Client(object): :return: a list of message ids of the sent message(s) """ if recipient_id is not None: - deprecation('sendRemoteImage(recipient_id)', deprecated_in='0.10.2', details='Use sendRemoteImage(thread_id) instead') thread_id = recipient_id if is_user is not None: deprecation('sendRemoteImage(is_user)', deprecated_in='0.10.2', details='Use sendRemoteImage(thread_type) instead') @@ -611,9 +635,12 @@ class Client(object): return self._send(thread_id=thread_id, message=message, thread_type=thread_type, image_id=image_id) # Doesn't upload properly - def sendLocalImage(self, image_path, message=None, thread_id=None, thread_type=ThreadType.USER, recipient_id=None, is_user=None, image=None): + def sendLocalImage(self, image_path, message=None, thread_id=None, thread_type=ThreadType.USER, + recipient_id=None, is_user=None, image=None): + # type: (str, str, str, ThreadType) -> list """ Sends an image from given URL to given (or default, if not) thread. + :param image_path: path of an image to upload and send :param message: additional message :param thread_id: user/group chat ID @@ -634,15 +661,18 @@ class Client(object): return self._send(thread_id=thread_id, message=message, thread_type=thread_type, image_id=image_id) def addUsersToChat(self, user_ids, thread_id=None): + # type: (list, str) -> list """ Adds users to given (or default, if not) thread. + :param user_ids: list of user ids to add :param thread_id: group chat ID :return: a list of message ids of the sent message(s) """ - return self._send(thread_id=thread_id, thread_type=ThreadType.GROUP, add_user_ids=users) + return self._send(thread_id=thread_id, thread_type=ThreadType.GROUP, add_user_ids=user_ids) def removeUserFromChat(self, user_id, thread_id=None): + # type: (str, str) -> bool """ Adds users to given (or default, if not) thread. :param user_id: user ID to remove @@ -705,6 +735,7 @@ class Client(object): return json.loads(response_content[9:])['payload']['metadata'][0]['image_id'] def getThreadInfo(self, last_n=20, thread_id=None, thread_type=ThreadType.USER): + # type: (int, str, ThreadType) -> list """Get the info of one Thread :param last_n: number of retrieved messages from start (default 20) @@ -745,6 +776,7 @@ class Client(object): def getThreadList(self, start, length=20): + # type: (int, int) -> list """Get thread list of your facebook account. :param start: the start index of a thread @@ -786,7 +818,6 @@ class Client(object): return self.threads - def getUnread(self): form = { 'client': 'mercury_sync', @@ -815,7 +846,6 @@ class Client(object): r = self._post(DeliveredURL, data) return r.ok - def markAsRead(self, userID): data = { "watermarkTimestamp": now(), @@ -826,23 +856,24 @@ class Client(object): r = self._post(ReadStatusURL, data) return r.ok - def markAsSeen(self): r = self._post(MarkSeenURL, {"seen_timestamp": 0}) return r.ok - + @deprecated(deprecated_in='0.10.2', details='Use friendConnect() instead') def friend_connect(self, friend_id): + return self.friendConnect(friend_id) + + def friendConnect(self, friend_id): + # type: (str) -> bool data = { "to_friend": friend_id, "action": "confirm" } r = self._post(ConnectURL, data) - return r.ok - def ping(self, sticky): data = { 'channel': self.user_channel, @@ -856,7 +887,6 @@ class Client(object): r = self._get(PingURL, data) return r.ok - def _getSticky(self): """Call pull api to get sticky and pool parameter, newer api needs these parameter to work. @@ -878,7 +908,6 @@ class Client(object): pool = j['lb_info']['pool'] return sticky, pool - def _pullMessage(self, sticky, pool): """Call pull api with seq value to get message data.""" @@ -896,80 +925,161 @@ class Client(object): self.seq = j.get('seq', '0') return j - def _parseMessage(self, content): - """Get message and author name from content. - May contains multiple messages in the content. - """ + """Get message and author name from content. May contain multiple messages in the content.""" if 'ms' not in content: return log.debug("Received {}".format(content["ms"])) - for m in content['ms']: + for m in content["ms"]: + mtype = m.get("type") try: - if m['type'] in ['m_messaging', 'messaging']: - if m['event'] in ['deliver']: - mid = m['message']['mid'] - message = m['message']['body'] - fbid = m['message']['sender_fbid'] - name = m['message']['sender_name'] - self.on_message(mid, fbid, name, message, m) - elif m['type'] in ['typ']: - self.on_typing(m.get("from")) - elif m['type'] in ['m_read_receipt']: - self.on_read(m.get('realtime_viewer_fbid'), m.get('reader'), m.get('time')) - elif m['type'] in ['inbox']: - viewer = m.get('realtime_viewer_fbid') - unseen = m.get('unseen') - unread = m.get('unread') - other_unseen = m.get('other_unseen') - other_unread = m.get('other_unread') - timestamp = m.get('seen_timestamp') - self.on_inbox(viewer, unseen, unread, other_unseen, other_unread, timestamp) - elif m['type'] in ['qprimer']: - self.on_qprimer(m.get('made')) - elif m['type'] in ['delta']: - if 'leftParticipantFbId' in m['delta']: - user_id = m['delta']['leftParticipantFbId'] - actor_id = m['delta']['messageMetadata']['actorFbId'] - thread_id = m['delta']['messageMetadata']['threadKey']['threadFbId'] - self.on_person_removed(user_id, actor_id, thread_id) - elif 'addedParticipants' in m['delta']: - user_ids = [x['userFbId'] for x in m['delta']['addedParticipants']] - actor_id = m['delta']['messageMetadata']['actorFbId'] - thread_id = m['delta']['messageMetadata']['threadKey']['threadFbId'] - self.on_people_added(user_ids, actor_id, thread_id) - elif 'messageMetadata' in m['delta']: - recipient_id = 0 - thread_type = None - if 'threadKey' in m['delta']['messageMetadata']: - if 'threadFbId' in m['delta']['messageMetadata']['threadKey']: - recipient_id = m['delta']['messageMetadata']['threadKey']['threadFbId'] - thread_type = 'group' - elif 'otherUserFbId' in m['delta']['messageMetadata']['threadKey']: - recipient_id = m['delta']['messageMetadata']['threadKey']['otherUserFbId'] - thread_type = 'user' - mid = m['delta']['messageMetadata']['messageId'] - message = m['delta'].get('body','') - fbid = m['delta']['messageMetadata']['actorFbId'] - self.on_message_new(mid, fbid, message, m, recipient_id, thread_type) - elif m['type'] in ['jewel_requests_add']: - from_id = m['from'] - self.on_friend_request(from_id) + # Things that directly change chat + if mtype == "delta": + + def getThreadIdAndThreadType(msg_metadata): + """Returns a tuple consisting of thread id and thread type""" + id_thread = None + type_thread = None + if 'threadFbId' in msg_metadata['threadKey']: + id_thread = str(msg_metadata['threadKey']['threadFbId']) + type_thread = ThreadType.GROUP + elif 'otherUserFbId' in msg_metadata['threadKey']: + id_thread = str(msg_metadata['threadKey']['otherUserFbId']) + type_thread = ThreadType.USER + return id_thread, type_thread + + delta = m["delta"] + delta_type = delta.get("type") + metadata = delta.get("messageMetadata") + + if metadata is not None: + mid = metadata["messageId"] + author_id = str(metadata['actorFbId']) + ts = int(metadata["timestamp"]) + + # Added participants + if 'addedParticipants' in delta: + added_ids = [str(x['userFbId']) for x in delta['addedParticipants']] + thread_id = str(metadata['threadKey']['threadFbId']) + self.onPeopleAdded(mid=mid, added_ids=added_ids, author_id=author_id, thread_id=thread_id, + ts=ts) + continue + + # Left/removed participants + elif 'leftParticipantFbId' in delta: + removed_id = str(delta['leftParticipantFbId']) + thread_id = str(metadata['threadKey']['threadFbId']) + self.onPersonRemoved(mid=mid, removed_id=removed_id, author_id=author_id, thread_id=thread_id, + ts=ts) + continue + + # Color change + elif delta_type == "change_thread_theme": + new_color = delta["untypedData"]["theme_color"] + thread_id, thread_type = getThreadIdAndThreadType(metadata) + self.onColorChange(mid=mid, author_id=author_id, new_color=new_color, thread_id=thread_id, + thread_type=thread_type, ts=ts, metadata=metadata) + continue + + # Emoji change + elif delta_type == "change_thread_icon": + new_emoji = delta["untypedData"]["thread_icon"] + thread_id, thread_type = getThreadIdAndThreadType(metadata) + self.onEmojiChange(mid=mid, author_id=author_id, new_emoji=new_emoji, thread_id=thread_id, + thread_type=thread_type, ts=ts, metadata=metadata) + continue + + # Thread title change + elif delta.get("class") == "ThreadName": + new_title = delta["name"] + thread_id, thread_type = getThreadIdAndThreadType(metadata) + self.onTitleChange(mid=mid, author_id=author_id, new_title=new_title, thread_id=thread_id, + thread_type=thread_type, ts=ts, metadata=metadata) + continue + + # Nickname change + elif delta_type == "change_thread_nickname": + changed_for = str(delta["untypedData"]["participant_id"]) + new_title = delta["untypedData"]["nickname"] + thread_id, thread_type = getThreadIdAndThreadType(metadata) + self.onNicknameChange(mid=mid, author_id=author_id, changed_for=changed_for, + new_title=new_title, + thread_id=thread_id, thread_type=thread_type, ts=ts, metadata=metadata) + continue + + + # TODO properly implement these as they differ on different scenarios + # Seen + # elif delta.get("class") == "ReadReceipt": + # seen_by = delta["actorFbId"] or delta["threadKey"]["otherUserFbId"] + # thread_id = delta["threadKey"].get("threadFbId") + # self.onSeen(seen_by=seen_by, thread_id=thread_id, ts=ts) + # + # # Message delivered + # elif delta.get("class") == 'DeliveryReceipt': + # time_delivered = delta['deliveredWatermarkTimestampMs'] + # self.onDelivered() + + # New message + elif delta.get("class") == "NewMessage": + message = delta.get('body', '') + thread_id, thread_type = getThreadIdAndThreadType(metadata) + self.onMessage(mid=mid, author_id=author_id, message=message, + thread_id=thread_id, thread_type=thread_type, ts=ts, metadata=m) + continue + + # Inbox + if mtype == "inbox": + self.onInbox(unseen=m["unseen"], unread=m["unread"], recent_unread=m["recent_unread"]) + + # Typing + # elif mtype == "typ": + # author_id = str(m.get("from")) + # typing_status = TypingStatus(m.get("st")) + # self.onTyping(author_id=author_id, typing_status=typing_status) + + # Seen + # elif mtype == "m_read_receipt": + # + # self.onSeen(m.get('realtime_viewer_fbid'), m.get('reader'), m.get('time')) + + # elif mtype in ['jewel_requests_add']: + # from_id = m['from'] + # self.on_friend_request(from_id) + + # Happens on every login + elif mtype == "qprimer": + pass + + # Is sent before any other message + elif mtype == "deltaflow": + pass + + # Unknown message type else: - self.on_unknown_type(m) + self.onUnknownMesssageType(msg=m) + except Exception as e: - # ex_type, ex, tb = sys.exc_info() - self.on_message_error(sys.exc_info(), m) + log.debug(str(e)) + @deprecated(deprecated_in='0.10.2', details='Use startListening() instead') def start_listening(self): + return self.startListening() + + def startListening(self): """Start listening from an external event loop.""" self.listening = True self.sticky, self.pool = self._getSticky() + @deprecated(deprecated_in='0.10.2', details='Use doOneListen() instead') def do_one_listen(self, markAlive=True): + return self.doOneListen(markAlive) + + def doOneListen(self, markAlive=True): + # type: (bool) -> None """Does one cycle of the listening loop. This method is only useful if you want to control fbchat from an external event loop.""" @@ -986,20 +1096,24 @@ class Client(object): pass + @deprecated(deprecated_in='0.10.2', details='Use stopListening() instead') def stop_listening(self): + return self.stopListening() + + def stopListening(self): """Cleans up the variables from start_listening.""" self.listening = False self.sticky, self.pool = (None, None) def listen(self, markAlive=True): - self.start_listening() + self.startListening() + self.onListening() - log.info("Listening...") while self.listening: - self.do_one_listen(markAlive) + self.doOneListen(markAlive) - self.stop_listening() + self.stopListening() def getUserInfo(self, *user_ids): @@ -1037,7 +1151,6 @@ class Client(object): """ self.on_message(mid, author_id, None, message, metadata) - @deprecated(deprecated_in='0.7.0', details='Use on_message_new() instead') def on_message(self, mid, author_id, author_name, message, metadata): """subclass Client and override this method to add custom behavior on event""" self.markAsDelivered(author_id, mid) diff --git a/fbchat/event_hook.py b/fbchat/event_hook.py new file mode 100644 index 0000000..00f64c0 --- /dev/null +++ b/fbchat/event_hook.py @@ -0,0 +1,57 @@ +import inspect + + +class EventHook(object): + """ + A simple implementation of the Observer-Pattern. + The user can specify an event signature upon inizializazion, + defined by kwargs in the form of argumentname=class (e.g. id=int). + The arguments' types are not checked in this implementation though. + Callables with a fitting signature can be added with += or removed with -=. + All listeners can be notified by calling the EventHook class with fitting + arguments. + + Thanks http://stackoverflow.com/a/35957226/5556222 + """ + + def __init__(self, **signature): + self._signature = signature + self._argnames = set(signature.keys()) + self._handlers = [] + + def _kwargs_str(self): + return ", ".join(k+"="+v.__name__ for k, v in self._signature.items()) + + def __iadd__(self, handler): + params = inspect.signature(handler).parameters + valid = True + argnames = set(n for n in params.keys()) + if argnames != self._argnames: + valid = False + for p in params.values(): + if p.kind == p.VAR_KEYWORD: + valid = True + break + if p.kind not in (p.POSITIONAL_OR_KEYWORD, p.KEYWORD_ONLY): + valid = False + break + if not valid: + raise ValueError("Listener must have these arguments: (%s)" + % self._kwargs_str()) + self._handlers.append(handler) + return self + + def __isub__(self, handler): + self._handlers.remove(handler) + return self + + def __call__(self, *args, **kwargs): + if args or set(kwargs.keys()) != self._argnames: + raise ValueError("This EventHook must be called with these " + + "keyword arguments: (%s)" % self._kwargs_str() + + ", but was called with: (%s)" %self._signature) + for handler in self._handlers[:]: + handler(**kwargs) + + def __repr__(self): + return "EventHook(%s)" % self._kwargs_str() diff --git a/fbchat/models.py b/fbchat/models.py index 2b49caf..5c414ca 100644 --- a/fbchat/models.py +++ b/fbchat/models.py @@ -2,15 +2,7 @@ from __future__ import unicode_literals import sys from enum import Enum -class Base(): - def __repr__(self): - uni = self.__unicode__() - return uni.encode('utf-8') if sys.version_info < (3, 0) else uni - - def __unicode__(self): - return u'<%s %s (%s)>' % (self.type.upper(), self.name, self.url) - -class User(Base): +class User: def __init__(self, data): if data['type'] != 'user': raise Exception("[!] %s <%s> is not a user" % (data['text'], data['path'])) @@ -22,11 +14,53 @@ class User(Base): self.score = data['score'] self.data = data -class Thread(): + def __repr__(self): + uni = self.__unicode__() + return uni.encode('utf-8') if sys.version_info < (3, 0) else uni + + def __unicode__(self): + return u'<%s %s (%s)>' % (self.type.upper(), self.name, self.url) + + @staticmethod + def adaptFromChat(user_in_chat): + """ Adapts user info from chat to User model acceptable initial dict + + :param user_in_chat: user info from chat + + 'dir': None, + 'mThumbSrcSmall': None, + 'is_friend': False, + 'is_nonfriend_messenger_contact': True, + 'alternateName': '', + 'i18nGender': 16777216, + 'vanity': '', + 'type': 'friend', + 'searchTokens': ['Voznesenskij', 'Sergej'], + 'thumbSrc': 'https://fb-s-b-a.akamaihd.net/h-ak-xfa1/v/t1.0-1/c9.0.32.32/p32x32/10354686_10150004552801856_220367501106153455_n.jpg?oh=71a87d76d4e4d17615a20c43fb8dbb47&oe=59118CE4&__gda__=1493753268_ae75cef40e9785398e744259ccffd7ff', + 'mThumbSrcLarge': None, + 'firstName': 'Sergej', + 'name': 'Sergej Voznesenskij', + 'uri': 'https://www.facebook.com/profile.php?id=100014812758264', + 'id': '100014812758264', + 'gender': 2 + """ + + return { + 'type': 'user', + 'uid': user_in_chat['id'], + 'photo': user_in_chat['thumbSrc'], + 'path': user_in_chat['uri'], + 'text': user_in_chat['name'], + 'score': '', + 'data': user_in_chat, + } + + +class Thread: def __init__(self, **entries): self.__dict__.update(entries) -class Message(): +class Message: def __init__(self, **entries): self.__dict__.update(entries) @@ -40,22 +74,15 @@ class TypingStatus(Enum): # WIP -class StickerSize(Enum): +class EmojiSize(Enum): LARGE = '369239383222810' MEDIUM = '369239343222814' SMALL = '369239263222822' -#class Size(Enum): -# LARGE = 'large' -# MEDIUM = 'medium' -# SMALL = 'small' - -Size = StickerSize - LIKES = { - 'l': Size.LARGE, - 'm': Size.MEDIUM, - 's': Size.SMALL + 'l': EmojiSize.LARGE, + 'm': EmojiSize.MEDIUM, + 's': EmojiSize.SMALL } LIKES['large'] = LIKES['l'] LIKES['medium'] =LIKES['m'] diff --git a/fbchat/utils.py b/fbchat/utils.py index fb4fee0..4751b99 100644 --- a/fbchat/utils.py +++ b/fbchat/utils.py @@ -26,6 +26,7 @@ GENDERS = { 11: 'unknown_plural', } + def now(): return int(time()*1000) @@ -40,7 +41,7 @@ def digit_to_char(digit): return str(digit) return chr(ord('a') + digit - 10) -def str_base(number,base): +def str_base(number, base): if number < 0: return '-' + str_base(-number, base) (d, m) = divmod(number, base) @@ -51,17 +52,17 @@ def str_base(number,base): def generateMessageID(client_id=None): k = now() l = int(random() * 4294967295) - return ("<%s:%s-%s@mail.projektitan.com>" % (k, l, client_id)); + return "<%s:%s-%s@mail.projektitan.com>" % (k, l, client_id) def getSignatureID(): return hex(int(random() * 2147483648)) -def generateOfflineThreadingID() : +def generateOfflineThreadingID(): ret = now() - value = int(random() * 4294967295); + value = int(random() * 4294967295) string = ("0000000000000000000000" + bin(value))[-22:] msgs = bin(ret) + string - return str(int(msgs,2)) + return str(int(msgs, 2)) def isUserToThreadType(is_user): return ThreadType.USER if is_user else ThreadType.GROUP From ef352f097a2aad714a5c8e7f2a55fdcb2eeb73c2 Mon Sep 17 00:00:00 2001 From: Dainius Date: Wed, 10 May 2017 18:44:20 +0300 Subject: [PATCH 5/9] update test_data.json --- test_data.json | 6 ++++++ tests.py | 11 ++--------- 2 files changed, 8 insertions(+), 9 deletions(-) create mode 100644 test_data.json diff --git a/test_data.json b/test_data.json new file mode 100644 index 0000000..549d9d5 --- /dev/null +++ b/test_data.json @@ -0,0 +1,6 @@ +{ + "email": "", + "password": "", + "user_thread_id": "", + "group_thread_id": "" +} diff --git a/tests.py b/tests.py index b8134e1..4463414 100644 --- a/tests.py +++ b/tests.py @@ -18,14 +18,7 @@ logging.basicConfig(level=logging.INFO) Tests for fbchat ~~~~~~~~~~~~~~~~ -To use these tests, make a json file called test_data.json, put this example in it, and fill in the gaps: -{ - "email": "example@email.com", - "password": "example_password", - "group_thread_id": 0, - "user_thread_id": 0 -} -or type this information manually in the terminal prompts. +To use these tests, fill in test_data.json or type this information manually in the terminal prompts. - email: Your (or a test user's) email / phone number - password: Your (or a test user's) password @@ -169,7 +162,7 @@ if __name__ == 'tests': pass try: - with open(path.join(path.dirname(__file__), 'test_data.js'), 'r') as f: + with open(path.join(path.dirname(__file__), 'test_data.json'), 'r') as f: json = json.load(f) email = json['email'] password = json['password'] From f63b9d7c4a2043904af6c52cdb12eb8f874da421 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Thu, 11 May 2017 12:55:44 +0200 Subject: [PATCH 6/9] Reworked old events, added deprecation warnings, and improved `_send` - Added a new system to show an error if old events are used - Removed `test_data.json`, since I don't want to risk that anyone accidentally commits their username & password - Finished work on `sendEmoji` - Split `_send` into two parts: `_getSendData` and `_doSendRequest`, which allows for an easier way of adding new send requests --- fbchat/client.py | 241 ++++++++++++++++++++++++----------------------- fbchat/models.py | 17 +++- test_data.json | 6 -- tests.py | 9 +- 4 files changed, 143 insertions(+), 130 deletions(-) delete mode 100644 test_data.json diff --git a/fbchat/client.py b/fbchat/client.py index 1d144ad..b410a0b 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -24,7 +24,7 @@ from .event_hook import * import time import sys -# Python 3 does not have raw_input, whereas Python 2 has and it's more secure +# Python 2's `input` executes the input, whereas `raw_input` just returns the input try: input = raw_input except NameError: @@ -95,6 +95,7 @@ class Client(object): self.threads = [] self._setupEventHooks() + self._setupOldEventHooks() if not user_agent: user_agent = choice(USER_AGENTS) @@ -141,6 +142,7 @@ class Client(object): self.onFriendRequest = EventHook(from_id=str) self.onUnknownMesssageType = EventHook(msg=dict) + self.onMessageError = EventHook(exception=Exception, msg=dict) # Setup event handlers self.onLoggingIn += lambda email: log.info("Logging in %s..." % email) @@ -164,8 +166,51 @@ class Client(object): self.onPersonRemoved += lambda removed_id, author_id, thread_id:\ log.info("%s removed: %s" % (author_id, removed_id)) - self.onUnknownMesssageType += lambda msg:\ - log.info("Unknown message type received: %s" % msg) + self.onUnknownMesssageType += lambda msg: log.info("Unknown message type received: %s" % msg) + self.onMessageError += lambda exception, msg: log.exception(exception) + + def _checkOldEventHook(self, old_event, deprecated_in='0.10.2'): + if hasattr(type(self), old_event): + deprecation('Client.{}'.format(old_event), deprecated_in=deprecated_in, details='Use new event system instead') + return True + else: + return False + + def _setupOldEventHooks(self): + if self._checkOldEventHook('on_message', deprecated_in='0.7.0'): + self.onMessage += lambda mid, author_id, message, thread_id, thread_type, ts, metadata:\ + self.on_message(mid, author_id, None, message, metadata) + + if self._checkOldEventHook('on_message_new'): + self.onMessage += lambda mid, author_id, message, thread_id, thread_type, ts, metadata:\ + self.on_message_new(mid, author_id, message, metadata, thread_id, True if thread_type is ThreadType.USER else False) + + if self._checkOldEventHook('on_friend_request'): + self.onFriendRequest += lambda from_id: self.on_friend_request(from_id) + + if self._checkOldEventHook('on_typing'): + self.onTyping += lambda author_id, typing_status: self.on_typing(author_id) + + if self._checkOldEventHook('on_read'): + self.onSeen += lambda seen_by, thread_id, timestamp: self.on_read(seen_by, thread_id, timestamp) + + if self._checkOldEventHook('on_people_added'): + self.onPeopleAdded += lambda added_ids, author_id, thread_id: self.on_people_added(added_ids, author_id, thread_id) + + if self._checkOldEventHook('on_person_removed'): + self.onPersonRemoved += lambda removed_id, author_id, thread_id: self.on_person_removed(removed_id, author_id, thread_id) + + if self._checkOldEventHook('on_inbox'): + self.onInbox += lambda unseen, unread, recent_unread: self.on_inbox(None, unseen, unread, None, recent_unread, None) + + if self._checkOldEventHook('on_qprimer'): + pass + + if self._checkOldEventHook('on_message_error'): + self.onMessageError += lambda exception, msg: self.on_message_error(exception, msg) + + if self._checkOldEventHook('on_unknown_type'): + self.onUnknownMesssageType += lambda msg: self.on_unknown_type(msg) @deprecated(deprecated_in='0.6.0', details='Use log. instead') def _console(self, msg): @@ -390,7 +435,7 @@ class Client(object): self.req_counter = 1 self.seq = "0" return r - + @deprecated(deprecated_in='0.10.2', details='Use setDefaultThread instead') def setDefaultRecipient(self, recipient_id, is_user=True): self.setDefaultThread(str(recipient_id), thread_type=isUserToThreadType(is_user)) @@ -464,18 +509,8 @@ class Client(object): SEND METHODS """ - def _send(self, thread_id=None, message=None, thread_type=ThreadType.USER, emoji_size=None, image_id=None, add_user_ids=None, new_title=None): - # type: (str, str, ThreadType, EmojiSize, str, list, str) -> list - """Send a message with given thread id - - :param thread_id: the user id or thread id that you want to send a message to - :param message: a text that you want to send - :param thread_type: determines if the recipient_id is for user or thread - :param emoji_size: size of the like sticker you want to send - :param image_id: id for the image to send, gotten from the UploadURL - :param add_user_ids: a list of user ids to add to a chat - :return: a list of message ids of the sent message(s) - """ + def _getSendData(self, thread_id=None, thread_type=ThreadType.USER): + """Returns the data needed to send a request to `SendURL`""" if thread_id is None: if self.default_thread_id is not None: @@ -522,36 +557,10 @@ class Client(object): elif thread_type == ThreadType.GROUP: data["thread_fbid"] = thread_id - # Set title - if new_title: - data['action_type'] = 'ma-type:log-message' - data['log_message_data[name]'] = new_title - data['log_message_type'] = 'log:thread-name' - - # Set users to add - if add_user_ids: - data['action_type'] = 'ma-type:log-message' - # It's possible to add multiple users - for i, add_user_id in enumerate(add_user_ids): - data['log_message_data[added_participants][' + str(i) + ']'] = "fbid:" + str(add_user_id) - data['log_message_type'] = 'log:subscribe' - - # Sending a simple message - if not add_user_ids and not new_title: - data['action_type'] = 'ma-type:user-generated-message' - data['body'] = message or '' - data['has_attachment'] = image_id is not None - data['specific_to_list[0]'] = 'fbid:' + str(thread_id) - data['specific_to_list[1]'] = 'fbid:' + str(self.uid) - - # Set image to send - if image_id: - data['image_ids[0]'] = image_id - - # Set emoji to send - if emoji_size: - data["sticker_id"] = emoji_size.value + return data + def _doSendRequest(self, data): + """Sends the data to `SendURL`, and returns """ r = self._post(SendURL, data) if not r.ok: @@ -570,7 +579,7 @@ class Client(object): message_ids = [] try: message_ids += [action['message_id'] for action in j['payload']['actions'] if 'message_id' in action] - message_ids[0] # Try accessing element + message_ids[0] # Try accessing element except (KeyError, IndexError) as e: log.warning('Error when sending message: No message ids could be found') return None @@ -582,7 +591,14 @@ class Client(object): @deprecated(deprecated_in='0.10.2', details='Use specific functions (eg. sendMessage()) instead') def send(self, recipient_id=None, message=None, is_user=True, like=None, image_id=None, add_user_ids=None): - return self._send(thread_id=recipient_id, message=message, thread_type=isUserToThreadType(is_user), emoji_size=LIKES[like], image_id=image_id, add_user_ids=add_user_ids) + if add_user_ids: + return self.addUsersToChat(user_ids=add_user_ids, thread_id=recipient_id) + elif image_id: + return self.sendImage(image_id=image_id, message=message, thread_id=recipient_id, thread_type=isUserToThreadType(is_user)) + elif like: + return self.sendEmoji(emoji=None, size=LIKES[like], thread_id=recipient_id, thread_type=isUserToThreadType(is_user)) + else: + return self.sendMessage(message, thread_id=recipient_id, thread_type=isUserToThreadType(is_user)) def sendMessage(self, message, thread_id=None, thread_type=ThreadType.USER): # type: (str, str, ThreadType) -> list @@ -594,23 +610,57 @@ class Client(object): :param thread_type: specify whether thread_id is user or group chat :return: a list of message ids of the sent message(s) """ - return self._send(thread_id=thread_id, message=message, thread_type=thread_type) + data = self._getSendData(thread_id=thread_id, thread_type=ThreadType.GROUP) - def sendEmoji(self, emoji=None, size=EmojiSize.SMALL, thread_id=None, thread_type=ThreadType.USER): + data['action_type'] = 'ma-type:user-generated-message' + data['body'] = message or '' + data['has_attachment'] = False + data['specific_to_list[0]'] = 'fbid:' + str(thread_id) + data['specific_to_list[1]'] = 'fbid:' + str(self.uid) + + return self._doSendRequest(data) + + def sendEmoji(self, emoji=None, size=EmojiSize.LARGE, thread_id=None, thread_type=ThreadType.USER): # type: (str, EmojiSize, str, ThreadType) -> list """ Sends an emoji to given (or default, if not) thread. - :param emoji: WIP + :param emoji: the chosen emoji to send :param size: size of emoji to send :param thread_id: user/group chat ID :param thread_type: specify whether thread_id is user or group chat :return: a list of message ids of the sent message(s) """ - return self._send(thread_id=thread_id, thread_type=thread_type, emoji_size=size) + data = self._getSendData(thread_id=thread_id, thread_type=ThreadType.GROUP) + + if emoji: + data['action_type'] = 'ma-type:user-generated-message' + data['body'] = emoji or '' + data['has_attachment'] = False + data['specific_to_list[0]'] = 'fbid:' + str(thread_id) + data['specific_to_list[1]'] = 'fbid:' + str(self.uid) + data['tags[0]'] = 'hot_emoji_size:' + size['name'] + else: + data["sticker_id"] = size['value'] + + return self._doSendRequest(data) + + def sendImage(self, image_id, message=None, thread_id=None, thread_type=ThreadType.USER): + """Sends an already uploaded image with the id image_id to the thread""" + data = self._getSendData(thread_id=thread_id, thread_type=ThreadType.GROUP) + + data['action_type'] = 'ma-type:user-generated-message' + data['body'] = message or '' + data['has_attachment'] = True + data['specific_to_list[0]'] = 'fbid:' + str(thread_id) + data['specific_to_list[1]'] = 'fbid:' + str(self.uid) + + data['image_ids[0]'] = image_id + + return self._doSendRequest(data) def sendRemoteImage(self, image_url, message=None, thread_id=None, thread_type=ThreadType.USER, - recipient_id=None, is_user=None, image=None): + recipient_id=None, is_user=None, image=None): # type: (str, str, str, ThreadType) -> list """ Sends an image from given URL to given (or default, if not) thread. @@ -632,7 +682,7 @@ class Client(object): mimetype = guess_type(image_url)[0] remote_image = requests.get(image_url).content image_id = self._uploadImage({'file': (image_url, remote_image, mimetype)}) - return self._send(thread_id=thread_id, message=message, thread_type=thread_type, image_id=image_id) + return self.sendImage(image_id=image_id, message=message, thread_id=thread_id, thread_type=thread_type) # Doesn't upload properly def sendLocalImage(self, image_path, message=None, thread_id=None, thread_type=ThreadType.USER, @@ -658,7 +708,7 @@ class Client(object): image_path = image mimetype = guess_type(image_path)[0] image_id = self._uploadImage({'file': (image_path, open(image_path, 'rb'), mimetype)}) - return self._send(thread_id=thread_id, message=message, thread_type=thread_type, image_id=image_id) + return self.sendImage(image_id=image_id, message=message, thread_id=thread_id, thread_type=thread_type) def addUsersToChat(self, user_ids, thread_id=None): # type: (list, str) -> list @@ -669,7 +719,16 @@ class Client(object): :param thread_id: group chat ID :return: a list of message ids of the sent message(s) """ - return self._send(thread_id=thread_id, thread_type=ThreadType.GROUP, add_user_ids=user_ids) + + data = self._getSendData(thread_id=thread_id, thread_type=ThreadType.GROUP) + + data['action_type'] = 'ma-type:log-message' + data['log_message_type'] = 'log:subscribe' + + for i, user_id in enumerate(user_ids): + data['log_message_data[added_participants][' + str(i) + ']'] = "fbid:" + str(user_id) + + return self._doSendRequest(data) def removeUserFromChat(self, user_id, thread_id=None): # type: (str, str) -> bool @@ -715,7 +774,14 @@ class Client(object): :param thread_id: group chat ID :return: a list of message ids of the sent message(s) """ - return self._send(thread_id=thread_id, thread_type=ThreadType.GROUP, new_title=title) + + data = self._getSendData(thread_id=thread_id, thread_type=ThreadType.GROUP) + + data['action_type'] = 'ma-type:log-message' + data['log_message_data[name]'] = title + data['log_message_type'] = 'log:thread-name' + + return self._doSendRequest(data) """ END SEND METHODS @@ -1061,7 +1127,7 @@ class Client(object): self.onUnknownMesssageType(msg=m) except Exception as e: - log.debug(str(e)) + self.onMessageError(exception=e, msg=msg) @deprecated(deprecated_in='0.10.2', details='Use startListening() instead') @@ -1140,64 +1206,3 @@ class Client(object): if len(full_data)==1: full_data=full_data[0] return full_data - - - - def on_message_new(self, mid, author_id, message, metadata, recipient_id, thread_type): - """subclass Client and override this method to add custom behavior on event - - This version of on_message recieves recipient_id and thread_type. - For backwards compatability, this data is sent directly to the old on_message. - """ - self.on_message(mid, author_id, None, message, metadata) - - def on_message(self, mid, author_id, author_name, message, metadata): - """subclass Client and override this method to add custom behavior on event""" - self.markAsDelivered(author_id, mid) - self.markAsRead(author_id) - log.info("%s said: %s" % (author_name, message)) - - - def on_friend_request(self, from_id): - """subclass Client and override this method to add custom behavior on event""" - log.info("Friend request from %s." % from_id) - - - def on_typing(self, author_id): - """subclass Client and override this method to add custom behavior on event""" - pass - - - def on_read(self, author, reader, time): - """subclass Client and override this method to add custom behavior on event""" - pass - - - def on_people_added(self, user_ids, actor_id, thread_id): - """subclass Client and override this method to add custom behavior on event""" - log.info("User(s) {} was added to {} by {}".format(repr(user_ids), thread_id, actor_id)) - - - def on_person_removed(self, user_id, actor_id, thread_id): - """subclass Client and override this method to add custom behavior on event""" - log.info("User {} was removed from {} by {}".format(user_id, thread_id, actor_id)) - - - def on_inbox(self, viewer, unseen, unread, other_unseen, other_unread, timestamp): - """subclass Client and override this method to add custom behavior on event""" - pass - - - def on_message_error(self, exception, message): - """subclass Client and override this method to add custom behavior on event""" - log.warning("Exception:\n{}".format(exception)) - - - def on_qprimer(self, timestamp): - pass - - - def on_unknown_type(self, m): - """subclass Client and override this method to add custom behavior on event""" - log.debug("Unknown type {}".format(m)) - diff --git a/fbchat/models.py b/fbchat/models.py index 5c414ca..d0dd4f9 100644 --- a/fbchat/models.py +++ b/fbchat/models.py @@ -72,12 +72,19 @@ class TypingStatus(Enum): DELETED = 0 TYPING = 1 - -# WIP class EmojiSize(Enum): - LARGE = '369239383222810' - MEDIUM = '369239343222814' - SMALL = '369239263222822' + LARGE = { + 'value': '369239383222810', + 'name': 'large' + } + MEDIUM = { + 'value': '369239343222814', + 'name': 'medium' + } + SMALL = { + 'value': '369239263222822', + 'name': 'small' + } LIKES = { 'l': EmojiSize.LARGE, diff --git a/test_data.json b/test_data.json deleted file mode 100644 index 549d9d5..0000000 --- a/test_data.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "email": "", - "password": "", - "user_thread_id": "", - "group_thread_id": "" -} diff --git a/tests.py b/tests.py index 4463414..3a77706 100644 --- a/tests.py +++ b/tests.py @@ -18,7 +18,14 @@ logging.basicConfig(level=logging.INFO) Tests for fbchat ~~~~~~~~~~~~~~~~ -To use these tests, fill in test_data.json or type this information manually in the terminal prompts. +To use these tests, make a json file called test_data.json, put this example in it, and fill in the gaps: +{ + "email": "example@email.com", + "password": "example_password", + "group_thread_id": 0, + "user_thread_id": 0 +} +or type this information manually in the terminal prompts. - email: Your (or a test user's) email / phone number - password: Your (or a test user's) password From d7acb9a40ddb211b8a667f62f536c18060556f71 Mon Sep 17 00:00:00 2001 From: Mads T Marquart Date: Thu, 11 May 2017 16:29:41 +0200 Subject: [PATCH 7/9] Changed default emoji size to small Also updated version --- fbchat/client.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/fbchat/client.py b/fbchat/client.py index b410a0b..13751de 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -169,7 +169,7 @@ class Client(object): self.onUnknownMesssageType += lambda msg: log.info("Unknown message type received: %s" % msg) self.onMessageError += lambda exception, msg: log.exception(exception) - def _checkOldEventHook(self, old_event, deprecated_in='0.10.2'): + def _checkOldEventHook(self, old_event, deprecated_in='0.10.3'): if hasattr(type(self), old_event): deprecation('Client.{}'.format(old_event), deprecated_in=deprecated_in, details='Use new event system instead') return True @@ -596,6 +596,8 @@ class Client(object): elif image_id: return self.sendImage(image_id=image_id, message=message, thread_id=recipient_id, thread_type=isUserToThreadType(is_user)) elif like: + if not like in LIKES: + like = 'l' # Backwards compatability return self.sendEmoji(emoji=None, size=LIKES[like], thread_id=recipient_id, thread_type=isUserToThreadType(is_user)) else: return self.sendMessage(message, thread_id=recipient_id, thread_type=isUserToThreadType(is_user)) @@ -620,7 +622,7 @@ class Client(object): return self._doSendRequest(data) - def sendEmoji(self, emoji=None, size=EmojiSize.LARGE, thread_id=None, thread_type=ThreadType.USER): + def sendEmoji(self, emoji=None, size=EmojiSize.SMALL, thread_id=None, thread_type=ThreadType.USER): # type: (str, EmojiSize, str, ThreadType) -> list """ Sends an emoji to given (or default, if not) thread. From fd2e554b98ea331b292e506a530a9ad286cef1b4 Mon Sep 17 00:00:00 2001 From: Mads T Marquart Date: Thu, 11 May 2017 16:29:48 +0200 Subject: [PATCH 8/9] Updated version --- fbchat/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fbchat/__init__.py b/fbchat/__init__.py index e41d9f6..a7e10cd 100644 --- a/fbchat/__init__.py +++ b/fbchat/__init__.py @@ -15,7 +15,7 @@ from .client import * __copyright__ = 'Copyright 2015 by Taehoon Kim' -__version__ = '0.10.2' +__version__ = '0.10.3' __license__ = 'BSD' __author__ = 'Taehoon Kim; Moreels Pieter-Jan' __email__ = 'carpedm20@gmail.com' From 44b3b1330a8d036984c9e3e595fbf04c994a7dcb Mon Sep 17 00:00:00 2001 From: Mads T Marquart Date: Thu, 11 May 2017 17:35:56 +0200 Subject: [PATCH 9/9] Created test_data.json See [here](https://github.com/carpedm20/fbchat/commit/f63b9d7c4a2043904af6c52cdb12eb8f874da421#commitcomment-22103626) for more info --- test_data.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 test_data.json diff --git a/test_data.json b/test_data.json new file mode 100644 index 0000000..d20c682 --- /dev/null +++ b/test_data.json @@ -0,0 +1,6 @@ +{ + "email": "", + "password": "", + "user_thread_id": "", + "group_thread_id": "" +}