From f63b9d7c4a2043904af6c52cdb12eb8f874da421 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Thu, 11 May 2017 12:55:44 +0200 Subject: [PATCH] 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