From e1e1a0d6118a0c5620af85834ac4ffa9a1ec1859 Mon Sep 17 00:00:00 2001 From: Dainius Date: Tue, 16 May 2017 12:03:20 +0300 Subject: [PATCH 1/6] add thread color change --- fbchat/client.py | 22 ++++++++++++++++++++-- fbchat/models.py | 32 ++++++++++++++++++++------------ tests.py | 42 +++++++++++++++++++++++++++--------------- 3 files changed, 67 insertions(+), 29 deletions(-) diff --git a/fbchat/client.py b/fbchat/client.py index 98329a2..66f34dd 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -52,6 +52,7 @@ LogoutURL ="https://www.facebook.com/logout.php" AllUsersURL ="https://www.facebook.com/chat/user_info_all" SaveDeviceURL="https://m.facebook.com/login/save-device/cancel/" CheckpointURL="https://m.facebook.com/login/checkpoint/" +ChatColorURL="https://www.facebook.com/messaging/save_thread_color/?source=thread_settings&dpr=1" facebookEncoding = 'UTF-8' # Log settings @@ -505,6 +506,7 @@ class Client(object): users.append(User(entry)) return users # have bug TypeError: __repr__ returned non-string (type bytes) + """ SEND METHODS """ @@ -612,7 +614,7 @@ 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) """ - data = self._getSendData(thread_id=thread_id, thread_type=ThreadType.GROUP) + data = self._getSendData(thread_id=thread_id, thread_type=thread_type) data['action_type'] = 'ma-type:user-generated-message' data['body'] = message or '' @@ -785,6 +787,22 @@ class Client(object): return self._doSendRequest(data) + def changeThreadColor(self, new_color, thread_id=None, thread_type=None): + # type: (ChatColor, str, ThreadType) -> bool + 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 = { + "color_choice": new_color.value, + "thread_or_other_fbid": thread_id + } + + r = self._post(ChatColorURL, data) + + return r.ok + """ END SEND METHODS """ @@ -1129,7 +1147,7 @@ class Client(object): self.onUnknownMesssageType(msg=m) except Exception as e: - self.onMessageError(exception=e, msg=msg) + self.onMessageError(exception=e, msg=m) @deprecated(deprecated_in='0.10.2', details='Use startListening() instead') diff --git a/fbchat/models.py b/fbchat/models.py index d0dd4f9..bad00f3 100644 --- a/fbchat/models.py +++ b/fbchat/models.py @@ -73,18 +73,9 @@ class TypingStatus(Enum): TYPING = 1 class EmojiSize(Enum): - LARGE = { - 'value': '369239383222810', - 'name': 'large' - } - MEDIUM = { - 'value': '369239343222814', - 'name': 'medium' - } - SMALL = { - 'value': '369239263222822', - 'name': 'small' - } + LARGE = '369239383222810' + MEDIUM = '369239343222814' + SMALL = '369239263222822' LIKES = { 'l': EmojiSize.LARGE, @@ -94,3 +85,20 @@ LIKES = { LIKES['large'] = LIKES['l'] LIKES['medium'] =LIKES['m'] LIKES['small'] = LIKES['s'] + +class ChatColor(Enum): + MESSENGER_BLUE = '' + VIKING = '#44bec7' + GOLDEN_POPPY = '#ffc300' + RADICAL_RED = '#fa3c4c' + SHOCKING = '#d696bb' + PICTON_BLUE = '#6699cc' + FREE_SPEECH_GREEN = '#13cf13' + PUMPKIN = '#ff7e29' + LIGHT_CORAL = '#e68585' + MEDIUM_SLATE_BLUE = '#7646ff' + DEEP_SKY_BLUE = '#20cef5' + FERN = '#67b868' + CAMEO = '#d4a88c' + BRILLIANT_ROSE = '#ff5ca1' + BILOBA_FLOWER = '#a695c7' diff --git a/tests.py b/tests.py index 3a77706..d106b84 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 @@ -83,10 +76,10 @@ class TestFbchat(unittest.TestCase): # Test if values are set correctly self.assertIsInstance(u.uid, int) - self.assertEquals(u.type, 'user') - self.assertEquals(u.photo[:4], 'http') - self.assertEquals(u.url[:4], 'http') - self.assertEquals(u.name, 'Mark Zuckerberg') + self.assertEqual(u.type, 'user') + self.assertEqual(u.photo[:4], 'http') + self.assertEqual(u.url[:4], 'http') + self.assertEqual(u.name, 'Mark Zuckerberg') self.assertGreater(u.score, 0) def test_sendEmoji(self): @@ -109,13 +102,13 @@ class TestFbchat(unittest.TestCase): # 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): 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') + self.assertEqual(info[0].author, 'fbid:' + client.uid) + self.assertEqual(info[0].body, 'test_user_getThreadInfo') client.sendMessage('test_group_getThreadInfo', group_uid, ThreadType.GROUP) time.sleep(3) @@ -141,6 +134,25 @@ class TestFbchat(unittest.TestCase): def test_changeThreadTitle(self): self.assertTrue(client.changeThreadTitle('test_changeThreadTitle', group_uid)) + def test_changeThreadColor(self): + self.assertTrue(client.changeThreadColor(ChatColor.BRILLIANT_ROSE, group_uid, ThreadType.GROUP)) + client.sendMessage(ChatColor.BRILLIANT_ROSE.name, group_uid, ThreadType.GROUP) + + time.sleep(1) + + self.assertTrue(client.changeThreadColor(ChatColor.MESSENGER_BLUE, group_uid, ThreadType.GROUP)) + client.sendMessage(ChatColor.MESSENGER_BLUE.name, group_uid, ThreadType.GROUP) + + time.sleep(2) + + self.assertTrue(client.changeThreadColor(ChatColor.BRILLIANT_ROSE, user_uid, ThreadType.USER)) + client.sendMessage(ChatColor.BRILLIANT_ROSE.name, user_uid, ThreadType.USER) + + time.sleep(1) + + self.assertTrue(client.changeThreadColor(ChatColor.MESSENGER_BLUE, user_uid, ThreadType.USER)) + client.sendMessage(ChatColor.MESSENGER_BLUE.name, user_uid, ThreadType.USER) + def start_test(param_client, param_group_uid, param_user_uid, tests=[]): global client From b1cccf417301b288c086fbf250f39cc463d193f7 Mon Sep 17 00:00:00 2001 From: Dainius Date: Tue, 16 May 2017 14:02:09 +0300 Subject: [PATCH 2/6] fix emoji sending and tests My bad. Test data is still being commited, changed it to a sample file instead --- .gitignore | 2 +- fbchat/client.py | 93 +++++++++++++++++++++++++++--------------------- tests.py | 31 +++++++++------- 3 files changed, 72 insertions(+), 54 deletions(-) diff --git a/.gitignore b/.gitignore index 98f10f5..a5a6ede 100644 --- a/.gitignore +++ b/.gitignore @@ -25,5 +25,5 @@ develop-eggs docs/_build/ # Data for tests -test_data.json +my_test_data.json tests.data \ No newline at end of file diff --git a/fbchat/client.py b/fbchat/client.py index 66f34dd..f043dc0 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -264,8 +264,8 @@ 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.user_channel = "p_" + str(self.uid) + self.uid = str(self._session.cookies['c_user']) + self.user_channel = "p_" + self.uid self.ttstamp = '' r = self._get(BaseURL) @@ -457,6 +457,22 @@ class Client(object): self.default_thread_id = None self.default_thread_type = None + def _setThread(self, given_thread_id, given_thread_type): + # type: (str, ThreadType) -> (str, ThreadType) + """ + Checks if thread ID is given, checks if default is set and returns correct values + + :raises ValueError: if thread ID is not given and there is no default + :return: tuple of thread ID and thread type + """ + if given_thread_id is None: + if self.default_thread_id is not None: + return self.default_thread_id, self.default_thread_type + else: + raise ValueError('Thread ID is not set.') + else: + return given_thread_id, given_thread_type + def getAllUsers(self): """ Gets all users from chat with info included """ @@ -513,14 +529,6 @@ class Client(object): 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: - thread_id = self.default_thread_id - thread_type = self.default_thread_type - else: - raise ValueError('Thread ID is not set.') - messageAndOTID = generateOfflineThreadingID() timestamp = now() date = datetime.now() @@ -545,7 +553,7 @@ class Client(object): 'html_body' : False, 'ui_push_phase' : 'V3', 'status' : '0', - 'offline_threading_id':messageAndOTID, + 'offline_threading_id': messageAndOTID, 'message_id' : messageAndOTID, 'threading_id': generateMessageID(self.client_id), 'ephemeral_ttl_mode:': '0', @@ -614,44 +622,47 @@ 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) """ - data = self._getSendData(thread_id=thread_id, thread_type=thread_type) + thread_id, thread_type = self._setThread(thread_id, thread_type) + data = self._getSendData(thread_id, thread_type) 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) + data['specific_to_list[0]'] = 'fbid:' + thread_id + data['specific_to_list[1]'] = 'fbid:' + self.uid return self._doSendRequest(data) 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. + Sends an emoji. If emoji and size are not specified a small like is sent. - :param emoji: the chosen emoji to send + :param emoji: the chosen emoji to send. If not specified, default thread emoji is sent :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) """ - data = self._getSendData(thread_id=thread_id, thread_type=ThreadType.GROUP) - + thread_id, thread_type = self._setThread(thread_id, thread_type) + data = self._getSendData(thread_id, thread_type) + data['action_type'] = 'ma-type:user-generated-message' + data['has_attachment'] = False + data['specific_to_list[0]'] = 'fbid:' + thread_id + data['specific_to_list[1]'] = 'fbid:' + self.uid + 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'] + data['body'] = emoji + data['tags[0]'] = 'hot_emoji_size:' + size.name.lower() else: - data["sticker_id"] = size['value'] + 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) + thread_id, thread_type = self._setThread(thread_id, thread_type) + data = self._getSendData(thread_id, thread_type) data['action_type'] = 'ma-type:user-generated-message' data['body'] = message or '' @@ -723,8 +734,8 @@ class Client(object): :param thread_id: group chat ID :return: a list of message ids of the sent message(s) """ - - data = self._getSendData(thread_id=thread_id, thread_type=ThreadType.GROUP) + thread_id, thread_type = self._setThread(thread_id, None) + data = self._getSendData(thread_id, ThreadType.GROUP) data['action_type'] = 'ma-type:log-message' data['log_message_type'] = 'log:subscribe' @@ -738,6 +749,7 @@ class Client(object): # type: (str, str) -> bool """ 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 @@ -774,12 +786,13 @@ class Client(object): def changeGroupTitle(self, title, thread_id=None): """ Change title of a group conversation. + :param title: new group chat title :param thread_id: group chat ID :return: a list of message ids of the sent message(s) """ - - data = self._getSendData(thread_id=thread_id, thread_type=ThreadType.GROUP) + thread_id, thread_type = self._setThread(thread_id, None) + data = self._getSendData(thread_id, ThreadType.GROUP) data['action_type'] = 'ma-type:log-message' data['log_message_data[name]'] = title @@ -787,12 +800,16 @@ class Client(object): return self._doSendRequest(data) - def changeThreadColor(self, new_color, thread_id=None, thread_type=None): + def changeThreadColor(self, new_color, thread_id=None): # type: (ChatColor, str, ThreadType) -> bool - 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.') + """ + Changes thread color to specified color. For more info about color names - see wiki. + + :param new_color: new color name + :param thread_id: user/group chat ID + :return: True if color was changed + """ + thread_id = self._setThread(thread_id, None) data = { "color_choice": new_color.value, @@ -830,11 +847,7 @@ class Client(object): :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.') + thread_id, thread_type = self._setThread(thread_id, thread_type) assert last_n > 0, 'length must be positive integer, got %d' % last_n diff --git a/tests.py b/tests.py index d106b84..c7fa59a 100644 --- a/tests.py +++ b/tests.py @@ -18,7 +18,7 @@ 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 copy test_data.json to my_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 @@ -61,9 +61,13 @@ class TestFbchat(unittest.TestCase): self.assertTrue(client.isLoggedIn()) def test_setDefaultThreadId(self): - client.setDefaultThreadId(client.uid, ThreadType.USER) + client.setDefaultThread(client.uid, ThreadType.USER) self.assertTrue(client.sendMessage("test_default_recipient")) + def test_resetDefaultThreadId(self): + client.resetDefaultThread() + self.assertRaises(ValueError, client.sendMessage("should_not_send")) + def test_getAllUsers(self): users = client.getAllUsers() self.assertGreater(len(users), 0) @@ -83,12 +87,13 @@ class TestFbchat(unittest.TestCase): self.assertGreater(u.score, 0) 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)) + self.assertTrue(client.sendEmoji(size=EmojiSize.SMALL, thread_id=user_uid, thread_type=ThreadType.USER)) + self.assertTrue(client.sendEmoji(size=EmojiSize.MEDIUM, thread_id=user_uid, thread_type=ThreadType.USER)) + self.assertTrue(client.sendEmoji("😆", EmojiSize.LARGE, user_uid, ThreadType.USER)) + + self.assertTrue(client.sendEmoji(size=EmojiSize.SMALL, thread_id=group_uid, thread_type=ThreadType.GROUP)) + self.assertTrue(client.sendEmoji(size=EmojiSize.MEDIUM, thread_id=group_uid, thread_type=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)) @@ -135,22 +140,22 @@ class TestFbchat(unittest.TestCase): self.assertTrue(client.changeThreadTitle('test_changeThreadTitle', group_uid)) def test_changeThreadColor(self): - self.assertTrue(client.changeThreadColor(ChatColor.BRILLIANT_ROSE, group_uid, ThreadType.GROUP)) + self.assertTrue(client.changeThreadColor(ChatColor.BRILLIANT_ROSE, group_uid)) client.sendMessage(ChatColor.BRILLIANT_ROSE.name, group_uid, ThreadType.GROUP) time.sleep(1) - self.assertTrue(client.changeThreadColor(ChatColor.MESSENGER_BLUE, group_uid, ThreadType.GROUP)) + self.assertTrue(client.changeThreadColor(ChatColor.MESSENGER_BLUE, group_uid)) client.sendMessage(ChatColor.MESSENGER_BLUE.name, group_uid, ThreadType.GROUP) time.sleep(2) - self.assertTrue(client.changeThreadColor(ChatColor.BRILLIANT_ROSE, user_uid, ThreadType.USER)) + self.assertTrue(client.changeThreadColor(ChatColor.BRILLIANT_ROSE, user_uid)) client.sendMessage(ChatColor.BRILLIANT_ROSE.name, user_uid, ThreadType.USER) time.sleep(1) - self.assertTrue(client.changeThreadColor(ChatColor.MESSENGER_BLUE, user_uid, ThreadType.USER)) + self.assertTrue(client.changeThreadColor(ChatColor.MESSENGER_BLUE, user_uid)) client.sendMessage(ChatColor.MESSENGER_BLUE.name, user_uid, ThreadType.USER) @@ -181,7 +186,7 @@ if __name__ == 'tests': pass try: - with open(path.join(path.dirname(__file__), 'test_data.json'), 'r') as f: + with open(path.join(path.dirname(__file__), 'my_test_data.json'), 'r') as f: json = json.load(f) email = json['email'] password = json['password'] From 4083348c409128802e7f50fbd408c6c5359c80ff Mon Sep 17 00:00:00 2001 From: Dainius Date: Tue, 16 May 2017 19:20:46 +0300 Subject: [PATCH 3/6] add reaction to messages. move request URLs to utils --- fbchat/client.py | 124 ++++++++++++++++++++++++----------------------- fbchat/models.py | 9 ++++ fbchat/utils.py | 27 +++++++++++ tests.py | 4 ++ 4 files changed, 103 insertions(+), 61 deletions(-) diff --git a/fbchat/client.py b/fbchat/client.py index f043dc0..b0f8984 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -11,6 +11,7 @@ :license: BSD, see LICENSE for more details. """ +from urllib import parse import requests import logging from uuid import uuid1 @@ -30,30 +31,7 @@ try: except NameError: pass -# URLs -LoginURL ="https://m.facebook.com/login.php?login_attempt=1" -SearchURL ="https://www.facebook.com/ajax/typeahead/search.php" -SendURL ="https://www.facebook.com/messaging/send/" -ThreadsURL ="https://www.facebook.com/ajax/mercury/threadlist_info.php" -ThreadSyncURL="https://www.facebook.com/ajax/mercury/thread_sync.php" -MessagesURL ="https://www.facebook.com/ajax/mercury/thread_info.php" -ReadStatusURL="https://www.facebook.com/ajax/mercury/change_read_status.php" -DeliveredURL ="https://www.facebook.com/ajax/mercury/delivery_receipts.php" -MarkSeenURL ="https://www.facebook.com/ajax/mercury/mark_seen.php" -BaseURL ="https://www.facebook.com" -MobileURL ="https://m.facebook.com/" -StickyURL ="https://0-edge-chat.facebook.com/pull" -PingURL ="https://0-channel-proxy-06-ash2.facebook.com/active_ping" -UploadURL ="https://upload.facebook.com/ajax/mercury/upload.php" -UserInfoURL ="https://www.facebook.com/chat/user_info/" -ConnectURL ="https://www.facebook.com/ajax/add_friend/action.php?dpr=1" -RemoveUserURL="https://www.facebook.com/chat/remove_participants/" -LogoutURL ="https://www.facebook.com/logout.php" -AllUsersURL ="https://www.facebook.com/chat/user_info_all" -SaveDeviceURL="https://m.facebook.com/login/save-device/cancel/" -CheckpointURL="https://m.facebook.com/login/checkpoint/" -ChatColorURL="https://www.facebook.com/messaging/save_thread_color/?source=thread_settings&dpr=1" -facebookEncoding = 'UTF-8' + # Log settings log = logging.getLogger("client") @@ -103,8 +81,8 @@ class Client(object): self._header = { 'Content-Type' : 'application/x-www-form-urlencoded', - 'Referer' : BaseURL, - 'Origin' : BaseURL, + 'Referer' : ReqUrl.BASE, + 'Origin' : ReqUrl.BASE, 'User-Agent' : user_agent, 'Connection' : 'keep-alive', } @@ -268,7 +246,7 @@ class Client(object): self.user_channel = "p_" + self.uid self.ttstamp = '' - r = self._get(BaseURL) + r = self._get(ReqUrl.BASE) soup = bs(r.text, "lxml") log.debug(r.text) log.debug(r.url) @@ -304,13 +282,13 @@ class Client(object): if not (self.email and self.password): raise Exception("Email and password not found.") - soup = bs(self._get(MobileURL).text, "lxml") + soup = bs(self._get(ReqUrl.MOBILE).text, "lxml") data = dict((elem['name'], elem['value']) for elem in soup.findAll("input") if elem.has_attr('value') and elem.has_attr('name')) data['email'] = self.email data['pass'] = self.password data['login'] = 'Log In' - r = self._cleanPost(LoginURL, data) + r = self._cleanPost(ReqUrl.LOGIN, data) # Usually, 'Checkpoint' will refer to 2FA if 'checkpoint' in r.url and 'Enter Security Code to Continue' in r.text: @@ -318,7 +296,7 @@ class Client(object): # Sometimes Facebook tries to show the user a "Save Device" dialog if 'save-device' in r.url: - r = self._cleanGet(SaveDeviceURL) + r = self._cleanGet(ReqUrl.SAVE_DEVICE) if 'home' in r.url: self._postLogin() @@ -338,7 +316,7 @@ class Client(object): data['codes_submitted'] = 0 log.info('Submitting 2FA code.') - r = self._cleanPost(CheckpointURL, data) + r = self._cleanPost(ReqUrl.CHECKPOINT, data) if 'home' in r.url: return r @@ -350,14 +328,14 @@ class Client(object): data['name_action_selected'] = 'save_device' data['submit[Continue]'] = 'Continue' log.info('Saving browser.') # At this stage, we have dtsg, nh, name_action_selected, submit[Continue] - r = self._cleanPost(CheckpointURL, data) + r = self._cleanPost(ReqUrl.CHECKPOINT, data) if 'home' in r.url: return r del(data['name_action_selected']) log.info('Starting Facebook checkup flow.') # At this stage, we have dtsg, nh, submit[Continue] - r = self._cleanPost(CheckpointURL, data) + r = self._cleanPost(ReqUrl.CHECKPOINT, data) if 'home' in r.url: return r @@ -365,7 +343,7 @@ class Client(object): del(data['submit[Continue]']) data['submit[This was me]'] = 'This Was Me' log.info('Verifying login attempt.') # At this stage, we have dtsg, nh, submit[This was me] - r = self._cleanPost(CheckpointURL, data) + r = self._cleanPost(ReqUrl.CHECKPOINT, data) if 'home' in r.url: return r @@ -374,12 +352,12 @@ class Client(object): data['submit[Continue]'] = 'Continue' data['name_action_selected'] = 'save_device' log.info('Saving device again.') # At this stage, we have dtsg, nh, submit[Continue], name_action_selected - r = self._cleanPost(CheckpointURL, data) + r = self._cleanPost(ReqUrl.CHECKPOINT, data) return r def isLoggedIn(self): # Send a request to the login url, to see if we're directed to the home page. - r = self._cleanGet(LoginURL) + r = self._cleanGet(ReqUrl.LOGIN) return 'home' in r.url def getSession(self): @@ -429,7 +407,7 @@ class Client(object): } payload=self._generatePayload(data) - r = self._session.get(LogoutURL, headers=self._header, params=payload, timeout=timeout) + r = self._session.get(ReqUrl.LOGOUT, headers=self._header, params=payload, timeout=timeout) # reset value self.payloadDefault={} self._session = requests.session() @@ -479,7 +457,7 @@ class Client(object): data = { 'viewer': self.uid, } - r = self._post(AllUsersURL, query=data) + r = self._post(ReqUrl.ALL_USERS, query=data) if not r.ok or len(r.text) == 0: return None j = get_json(r.text) @@ -513,7 +491,7 @@ class Client(object): 'request_id' : str(uuid1()), } - r = self._get(SearchURL, payload) + r = self._get(ReqUrl.SEARCH, payload) self.j = j = get_json(r.text) users = [] @@ -571,7 +549,7 @@ class Client(object): def _doSendRequest(self, data): """Sends the data to `SendURL`, and returns """ - r = self._post(SendURL, data) + r = self._post(ReqUrl.SEND, data) if not r.ok: log.warning('Error when sending message: Got {} response'.format(r.status_code)) @@ -755,17 +733,14 @@ class Client(object): :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.') + thread_id = self._setThread(thread_id, None) data = { "uid": user_id, "tid": thread_id } - r = self._post(RemoveUserURL, data) + r = self._post(ReqUrl.REMOVE_USER, data) return r.ok @@ -816,10 +791,37 @@ class Client(object): "thread_or_other_fbid": thread_id } - r = self._post(ChatColorURL, data) + r = self._post(ReqUrl.CHAT_COLOR, data) return r.ok + def reactToMessage(self, message_id, reaction): + # type: (str, MessageReaction) -> bool + """ + Reacts to a message. + + :param message_id: message ID to react to + :param reaction: reaction emoji to send + :return: true if reacted + """ + full_data = { + "doc_id": 1491398900900362, + "dpr": 1, + "variables": { + "data": { + "action": "ADD_REACTION", + "client_mutation_id": "1", + "actor_id": self.uid, + "message_id": message_id, + "reaction": reaction.value + } + } + } + + r = self._post(ReqUrl.MESSAGE_REACTION + "/?" + parse.urlencode(full_data)) + return r.ok + + """ END SEND METHODS """ @@ -830,7 +832,7 @@ class Client(object): :param image: a tuple of (file name, data, mime type) to upload to facebook """ - r = self._postFile(UploadURL, image) + r = self._postFile(ReqUrl.UPLOAD, image) response_content = {} if isinstance(r.content, str) is False: response_content = r.content.decode(facebookEncoding) @@ -860,13 +862,13 @@ class Client(object): 'messages[{}][{}][limit]'.format(key, thread_id): last_n - 1, 'messages[{}][{}][timestamp]'.format(key, thread_id): now()} - r = self._post(MessagesURL, query=data) + r = self._post(ReqUrl.MESSAGES, query=data) if not r.ok or len(r.text) == 0: - return None + return [] j = get_json(r.text) if not j['payload']: - return None + return [] messages = [] for message in j['payload'].get('actions'): @@ -890,9 +892,9 @@ class Client(object): 'inbox[limit]' : length, } - r = self._post(ThreadsURL, data) + r = self._post(ReqUrl.THREADS, data) if not r.ok or len(r.text) == 0: - return None + return [] j = get_json(r.text) @@ -925,7 +927,7 @@ class Client(object): # 'last_action_timestamp': 0 } - r = self._post(ThreadSyncURL, form) + r = self._post(ReqUrl.THREAD_SYNC, form) if not r.ok or len(r.text) == 0: return None @@ -942,7 +944,7 @@ class Client(object): "thread_ids[%s][0]" % userID: threadID } - r = self._post(DeliveredURL, data) + r = self._post(ReqUrl.DELIVERED, data) return r.ok def markAsRead(self, userID): @@ -952,11 +954,11 @@ class Client(object): "ids[%s]" % userID: True } - r = self._post(ReadStatusURL, data) + r = self._post(ReqUrl.READ_STATUS, data) return r.ok def markAsSeen(self): - r = self._post(MarkSeenURL, {"seen_timestamp": 0}) + r = self._post(ReqUrl.MARK_SEEN, {"seen_timestamp": 0}) return r.ok @deprecated(deprecated_in='0.10.2', details='Use friendConnect() instead') @@ -970,7 +972,7 @@ class Client(object): "action": "confirm" } - r = self._post(ConnectURL, data) + r = self._post(ReqUrl.CONNECT, data) return r.ok def ping(self, sticky): @@ -983,7 +985,7 @@ class Client(object): 'sticky': sticky, 'viewer_uid': self.uid } - r = self._get(PingURL, data) + r = self._get(ReqUrl.PING, data) return r.ok def _getSticky(self): @@ -997,7 +999,7 @@ class Client(object): "clientid": self.client_id } - r = self._get(StickyURL, data) + r = self._get(ReqUrl.STICKY, data) j = get_json(r.text) if 'lb_info' not in j: @@ -1017,7 +1019,7 @@ class Client(object): "clientid": self.client_id, } - r = self._get(StickyURL, data) + r = self._get(ReqUrl.STICKY, data) r.encoding = facebookEncoding j = get_json(r.text) @@ -1233,7 +1235,7 @@ class Client(object): data = {"ids[{}]".format(i):uid for i,uid in enumerate(user_ids)} - r = self._post(UserInfoURL, data) + r = self._post(ReqUrl.USER_INFO, data) info = get_json(r.text) full_data= [details for profile,details in info['payload']['profiles'].items()] if len(full_data)==1: diff --git a/fbchat/models.py b/fbchat/models.py index bad00f3..e704690 100644 --- a/fbchat/models.py +++ b/fbchat/models.py @@ -102,3 +102,12 @@ class ChatColor(Enum): CAMEO = '#d4a88c' BRILLIANT_ROSE = '#ff5ca1' BILOBA_FLOWER = '#a695c7' + +class MessageReaction(Enum): + LOVE = '😍' + SMILE = '😆' + WOW = '😮' + SAD = '😢' + ANGRY = '😠' + YES = '👍' + NO = '👎' diff --git a/fbchat/utils.py b/fbchat/utils.py index 4751b99..212910f 100644 --- a/fbchat/utils.py +++ b/fbchat/utils.py @@ -3,6 +3,7 @@ import json from time import time from random import random import warnings +from enum import Enum 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", @@ -26,6 +27,32 @@ GENDERS = { 11: 'unknown_plural', } +class ReqUrl: + SEARCH = "https://www.facebook.com/ajax/typeahead/search.php" + LOGIN = "https://m.facebook.com/login.php?login_attempt=1" + SEND = "https://www.facebook.com/messaging/send/" + THREAD_SYNC = "https://www.facebook.com/ajax/mercury/thread_sync.php" + THREADS = "https://www.facebook.com/ajax/mercury/threadlist_info.php" + MESSAGES = "https://www.facebook.com/ajax/mercury/thread_info.php" + READ_STATUS = "https://www.facebook.com/ajax/mercury/change_read_status.php" + DELIVERED = "https://www.facebook.com/ajax/mercury/delivery_receipts.php" + MARK_SEEN = "https://www.facebook.com/ajax/mercury/mark_seen.php" + BASE = "https://www.facebook.com" + MOBILE = "https://m.facebook.com/" + STICKY = "https://0-edge-chat.facebook.com/pull" + PING = "https://0-channel-proxy-06-ash2.facebook.com/active_ping" + UPLOAD = "https://upload.facebook.com/ajax/mercury/upload.php" + USER_INFO = "https://www.facebook.com/chat/user_info/" + CONNECT = "https://www.facebook.com/ajax/add_friend/action.php?dpr=1" + REMOVE_USER = "https://www.facebook.com/chat/remove_participants/" + LOGOUT = "https://www.facebook.com/logout.php" + ALL_USERS = "https://www.facebook.com/chat/user_info_all" + SAVE_DEVICE = "https://m.facebook.com/login/save-device/cancel/" + CHECKPOINT = "https://m.facebook.com/login/checkpoint/" + CHAT_COLOR = "https://www.facebook.com/messaging/save_thread_color/?source=thread_settings&dpr=1" + MESSAGE_REACTION = "https://www.facebook.com/webgraphql/mutation" + +facebookEncoding = 'UTF-8' def now(): return int(time()*1000) diff --git a/tests.py b/tests.py index c7fa59a..5e162ed 100644 --- a/tests.py +++ b/tests.py @@ -158,6 +158,10 @@ class TestFbchat(unittest.TestCase): self.assertTrue(client.changeThreadColor(ChatColor.MESSENGER_BLUE, user_uid)) client.sendMessage(ChatColor.MESSENGER_BLUE.name, user_uid, ThreadType.USER) + def test_reactToMessage(self): + mid = client.sendMessage("react_to_message", user_uid, ThreadType.USER)[0] + self.assertTrue(client.reactToMessage(mid, fbchat.MessageReaction.LOVE)) + def start_test(param_client, param_group_uid, param_user_uid, tests=[]): global client From c95544dcb04b21c74a11ddf24cc69ade308c7072 Mon Sep 17 00:00:00 2001 From: Dainius Date: Tue, 16 May 2017 19:35:00 +0300 Subject: [PATCH 4/6] add typing indicator --- fbchat/client.py | 20 ++++++++++++++++++++ fbchat/models.py | 2 +- fbchat/utils.py | 4 +++- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/fbchat/client.py b/fbchat/client.py index b0f8984..9a4e32c 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -821,6 +821,26 @@ class Client(object): r = self._post(ReqUrl.MESSAGE_REACTION + "/?" + parse.urlencode(full_data)) return r.ok + def setTypingStatus(self, status, thread_id=None, thread_type=None): + # type: (TypingStatus, str, ThreadType) -> bool + """ + Sets users typing status. + + :param status: typing or not typing + :param thread_id: user/group chat ID + :return: True if status changed + """ + thread_id, thread_type = self._setThread(thread_id, None) + + data = { + "typ": status.value, + "thread": thread_id, + "to": thread_id if thread_type == ThreadType.USER else "", + "source": "mercury-chat" + } + + r = self._post(ReqUrl.TYPING, data) + return r.ok """ END SEND METHODS diff --git a/fbchat/models.py b/fbchat/models.py index e704690..68d0daf 100644 --- a/fbchat/models.py +++ b/fbchat/models.py @@ -69,7 +69,7 @@ class ThreadType(Enum): GROUP = 2 class TypingStatus(Enum): - DELETED = 0 + STOPPED = 0 TYPING = 1 class EmojiSize(Enum): diff --git a/fbchat/utils.py b/fbchat/utils.py index 212910f..89d927a 100644 --- a/fbchat/utils.py +++ b/fbchat/utils.py @@ -3,7 +3,8 @@ import json from time import time from random import random import warnings -from enum import Enum +from .models import * + 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", @@ -51,6 +52,7 @@ class ReqUrl: CHECKPOINT = "https://m.facebook.com/login/checkpoint/" CHAT_COLOR = "https://www.facebook.com/messaging/save_thread_color/?source=thread_settings&dpr=1" MESSAGE_REACTION = "https://www.facebook.com/webgraphql/mutation" + TYPING = "https://www.facebook.com/ajax/messaging/typ.php" facebookEncoding = 'UTF-8' From 386cb4a6c199472f83f2a1f133641e0de31135e5 Mon Sep 17 00:00:00 2001 From: Dainius Date: Tue, 16 May 2017 21:20:46 +0300 Subject: [PATCH 5/6] fix on seen, on delivered, on marked as seen methods --- fbchat/client.py | 63 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 49 insertions(+), 14 deletions(-) diff --git a/fbchat/client.py b/fbchat/client.py index 9a4e32c..31566df 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -112,8 +112,11 @@ class Client(object): 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.onMessageSeen = EventHook(seen_by=str, thread_id=str, thread_type=ThreadType, seen_ts=int, delivered_ts=int, metadata=dict) + self.onMessageDelivered = EventHook(msg_ids=list, delivered_for=str, thread_id=str, thread_type=ThreadType, ts=int, metadata=dict) + self.onMarkedSeen = EventHook(threads=list, seen_ts=int, delivered_ts=int, metadata=dict) self.onInbox = EventHook(unseen=int, unread=int, recent_unread=int) self.onPeopleAdded = EventHook(added_ids=list, author_id=str, thread_id=str) @@ -145,6 +148,14 @@ class Client(object): self.onPersonRemoved += lambda removed_id, author_id, thread_id:\ log.info("%s removed: %s" % (author_id, removed_id)) + + self.onMessageSeen += lambda seen_by, thread_id, thread_type, seen_ts, delivered_ts, metadata:\ + log.info("Messages seen by %s in %s (%s) at %ss", seen_by, thread_id, thread_type.name, seen_ts/1000) + self.onMessageDelivered += lambda msg_ids, delivered_for, thread_id, thread_type, ts, metadata:\ + log.info("Messages %s delivered to %s in %s (%s) at %ss", msg_ids, delivered_for, thread_id, thread_type.name, ts/1000) + self.onMarkedSeen += lambda threads, seen_ts, delivered_ts, metadata:\ + log.info("Marked messages as seen in threads %s at %ss", [(x[0], x[1].name) for x in threads], seen_ts/1000) + self.onUnknownMesssageType += lambda msg: log.info("Unknown message type received: %s" % msg) self.onMessageError += lambda exception, msg: log.exception(exception) @@ -699,6 +710,8 @@ class Client(object): if image is not None: deprecation('sendRemoteImage(image)', deprecated_in='0.10.2', details='Use sendLocalImage(image_path) instead') image_path = image + + thread_id, thread_type = self._setThread(thread_id, None) mimetype = guess_type(image_path)[0] image_id = self._uploadImage({'file': (image_path, open(image_path, 'rb'), mimetype)}) return self.sendImage(image_id=image_id, message=message, thread_id=thread_id, thread_type=thread_type) @@ -1077,7 +1090,7 @@ class Client(object): if metadata is not None: mid = metadata["messageId"] author_id = str(metadata['actorFbId']) - ts = int(metadata["timestamp"]) + ts = int(metadata.get("timestamp")) # Added participants if 'addedParticipants' in delta: @@ -1129,18 +1142,38 @@ class Client(object): thread_id=thread_id, thread_type=thread_type, ts=ts, metadata=metadata) continue + # Message delivered + elif delta.get("class") == "DeliveryReceipt": + message_ids = delta["messageIds"] + delivered_for = str(delta["actorFbId"]) + ts = int(delta["deliveredWatermarkTimestampMs"]) + thread_id, thread_type = getThreadIdAndThreadType(delta) + self.onMessageDelivered(msg_ids=message_ids, delivered_for=delivered_for, + 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() + # Message seen + elif delta.get("class") == "ReadReceipt": + seen_by = str(delta.get("actorFbId") or delta["threadKey"]["otherUserFbId"]) + seen_ts = int(delta["actionTimestampMs"]) + delivered_ts = int(delta["watermarkTimestampMs"]) + thread_id, thread_type = getThreadIdAndThreadType(delta) + self.onMessageSeen(seen_by=seen_by, thread_id=thread_id, thread_type=thread_type, + seen_ts=seen_ts, delivered_ts=delivered_ts, metadata=metadata) + continue + + # Messages marked as seen + elif delta.get("class") == "MarkRead": + seen_ts = int(delta.get("actionTimestampMs") or delta.get("actionTimestamp")) + delivered_ts = int(delta.get("watermarkTimestampMs") or delta.get("watermarkTimestamp")) + + threads = [] + if "folders" not in delta: + threads = [getThreadIdAndThreadType({"threadKey": thr}) for thr in delta.get("threadKeys")] + + # thread_id, thread_type = getThreadIdAndThreadType(delta) + self.onMarkedSeen(threads=threads, seen_ts=seen_ts, delivered_ts=delivered_ts, metadata=delta) + continue # New message elif delta.get("class") == "NewMessage": @@ -1160,6 +1193,8 @@ class Client(object): # typing_status = TypingStatus(m.get("st")) # self.onTyping(author_id=author_id, typing_status=typing_status) + # Delivered + # Seen # elif mtype == "m_read_receipt": # From ed7b8488cbe6573ab2ee9508bd77485253b3eb88 Mon Sep 17 00:00:00 2001 From: Dainius Date: Wed, 17 May 2017 14:20:07 +0300 Subject: [PATCH 6/6] rename _setThread() to _getThreadId() --- fbchat/client.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/fbchat/client.py b/fbchat/client.py index 31566df..cfa4625 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -446,7 +446,7 @@ class Client(object): self.default_thread_id = None self.default_thread_type = None - def _setThread(self, given_thread_id, given_thread_type): + def _getThreadId(self, given_thread_id, given_thread_type): # type: (str, ThreadType) -> (str, ThreadType) """ Checks if thread ID is given, checks if default is set and returns correct values @@ -611,7 +611,7 @@ 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) """ - thread_id, thread_type = self._setThread(thread_id, thread_type) + thread_id, thread_type = self._getThreadId(thread_id, thread_type) data = self._getSendData(thread_id, thread_type) data['action_type'] = 'ma-type:user-generated-message' @@ -633,7 +633,7 @@ 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) """ - thread_id, thread_type = self._setThread(thread_id, thread_type) + thread_id, thread_type = self._getThreadId(thread_id, thread_type) data = self._getSendData(thread_id, thread_type) data['action_type'] = 'ma-type:user-generated-message' data['has_attachment'] = False @@ -650,7 +650,7 @@ class Client(object): 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""" - thread_id, thread_type = self._setThread(thread_id, thread_type) + thread_id, thread_type = self._getThreadId(thread_id, thread_type) data = self._getSendData(thread_id, thread_type) data['action_type'] = 'ma-type:user-generated-message' @@ -711,7 +711,7 @@ class Client(object): deprecation('sendRemoteImage(image)', deprecated_in='0.10.2', details='Use sendLocalImage(image_path) instead') image_path = image - thread_id, thread_type = self._setThread(thread_id, None) + thread_id, thread_type = self._getThreadId(thread_id, None) mimetype = guess_type(image_path)[0] image_id = self._uploadImage({'file': (image_path, open(image_path, 'rb'), mimetype)}) return self.sendImage(image_id=image_id, message=message, thread_id=thread_id, thread_type=thread_type) @@ -725,7 +725,7 @@ class Client(object): :param thread_id: group chat ID :return: a list of message ids of the sent message(s) """ - thread_id, thread_type = self._setThread(thread_id, None) + thread_id, thread_type = self._getThreadId(thread_id, None) data = self._getSendData(thread_id, ThreadType.GROUP) data['action_type'] = 'ma-type:log-message' @@ -746,7 +746,7 @@ class Client(object): :return: true if user was removed """ - thread_id = self._setThread(thread_id, None) + thread_id = self._getThreadId(thread_id, None) data = { "uid": user_id, @@ -779,7 +779,7 @@ class Client(object): :param thread_id: group chat ID :return: a list of message ids of the sent message(s) """ - thread_id, thread_type = self._setThread(thread_id, None) + thread_id, thread_type = self._getThreadId(thread_id, None) data = self._getSendData(thread_id, ThreadType.GROUP) data['action_type'] = 'ma-type:log-message' @@ -797,7 +797,7 @@ class Client(object): :param thread_id: user/group chat ID :return: True if color was changed """ - thread_id = self._setThread(thread_id, None) + thread_id = self._getThreadId(thread_id, None) data = { "color_choice": new_color.value, @@ -843,7 +843,7 @@ class Client(object): :param thread_id: user/group chat ID :return: True if status changed """ - thread_id, thread_type = self._setThread(thread_id, None) + thread_id, thread_type = self._getThreadId(thread_id, None) data = { "typ": status.value, @@ -882,7 +882,7 @@ class Client(object): :return: a list of messages """ - thread_id, thread_type = self._setThread(thread_id, thread_type) + thread_id, thread_type = self._getThreadId(thread_id, thread_type) assert last_n > 0, 'length must be positive integer, got %d' % last_n