From afaece7641003e6f73b087abd6290946fc894cb8 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Tue, 10 Apr 2018 20:18:15 +0200 Subject: [PATCH 1/9] Added `napoleon` to docs, and updated doc settings --- docs/conf.py | 16 ++++++++++++---- docs/examples.rst | 1 - docs/faq.rst | 2 -- docs/index.rst | 2 -- docs/intro.rst | 2 -- docs/testing.rst | 1 - docs/todo.rst | 2 -- 7 files changed, 12 insertions(+), 14 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index eac4081f..d80d11fb 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -21,8 +21,6 @@ import sys sys.path.insert(0, os.path.abspath('..')) -import fbchat -import tests from fbchat import __copyright__, __author__, __version__, __description__ @@ -39,7 +37,8 @@ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', - 'sphinx.ext.viewcode' + 'sphinx.ext.viewcode', + 'sphinx.ext.napoleon' ] # Add any paths that contain templates here, relative to this directory. @@ -187,5 +186,14 @@ html_show_sphinx = False html_show_sourcelink = False -autoclass_content = 'init' +autoclass_content = 'class' +autodoc_member_order = 'bysource' +autodoc_default_flags = ['members'] html_short_title = description +napoleon_numpy_docstring = False +napoleon_include_init_with_doc = True +napoleon_use_rtype = False +rst_prolog = ''' +.. module:: fbchat +''' +default_role = 'class' diff --git a/docs/examples.rst b/docs/examples.rst index 89da9dca..19ffe1ce 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -1,4 +1,3 @@ -.. highlight:: python .. _examples: Examples diff --git a/docs/faq.rst b/docs/faq.rst index 16b8c590..83996ce8 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -1,5 +1,3 @@ -.. highlight:: python -.. module:: fbchat .. _faq: FAQ diff --git a/docs/index.rst b/docs/index.rst index 67a8c119..10e713cf 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,5 +1,3 @@ -.. highlight:: python -.. module:: fbchat .. fbchat documentation master file, created by sphinx-quickstart on Thu May 25 15:43:01 2017. You can adapt this file completely to your liking, but it should at least diff --git a/docs/intro.rst b/docs/intro.rst index 6f5d58ec..6da69438 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -1,5 +1,3 @@ -.. highlight:: python -.. module:: fbchat .. _intro: Introduction diff --git a/docs/testing.rst b/docs/testing.rst index a3b2f13c..7131814f 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -1,5 +1,4 @@ .. highlight:: sh -.. module:: fbchat .. _testing: Testing diff --git a/docs/todo.rst b/docs/todo.rst index d0af13b4..9b89848b 100644 --- a/docs/todo.rst +++ b/docs/todo.rst @@ -1,5 +1,3 @@ -.. highlight:: python -.. module:: fbchat .. _todo: Todo From e392fdb13a9af042723396b0e2d8bbf2b2b5b3e6 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Tue, 10 Apr 2018 20:19:31 +0200 Subject: [PATCH 2/9] Updated examples, to illustrate how I generally picture the API --- docs/examples.rst | 8 ++-- docs/index.rst | 2 +- examples/advanced_bot.py | 69 ++++++++++++++++++++++++++++++++ examples/basic_usage.py | 14 ++++--- examples/echo_bot.py | 22 ++++++++++ examples/echobot.py | 18 --------- examples/fetch.py | 64 ----------------------------- examples/interract.py | 82 ++++++++++++++++++++------------------ examples/keep_bot.py | 59 +++++++++++++++++++++++++++ examples/keepbot.py | 54 ------------------------- examples/possible_usage.py | 40 +++++++++++++++++++ examples/remove_bot.py | 18 +++++++++ examples/removebot.py | 17 -------- examples/retrieve.py | 52 ++++++++++++++++++++++++ 14 files changed, 318 insertions(+), 201 deletions(-) create mode 100644 examples/advanced_bot.py create mode 100644 examples/echo_bot.py delete mode 100644 examples/echobot.py delete mode 100644 examples/fetch.py create mode 100644 examples/keep_bot.py delete mode 100644 examples/keepbot.py create mode 100644 examples/possible_usage.py create mode 100644 examples/remove_bot.py delete mode 100644 examples/removebot.py create mode 100644 examples/retrieve.py diff --git a/docs/examples.rst b/docs/examples.rst index 19ffe1ce..a1ef7ec8 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -27,7 +27,7 @@ Fetching Information This will show the different ways of fetching information about users and threads -.. literalinclude:: ../examples/fetch.py +.. literalinclude:: ../examples/retrieve.py Echobot @@ -35,7 +35,7 @@ Echobot This will reply to any message with the same message -.. literalinclude:: ../examples/echobot.py +.. literalinclude:: ../examples/echo_bot.py Remove Bot @@ -43,7 +43,7 @@ Remove Bot This will remove a user from a group if they write the message `Remove me!` -.. literalinclude:: ../examples/removebot.py +.. literalinclude:: ../examples/remove_bot.py "Prevent changes"-Bot @@ -52,4 +52,4 @@ This will remove a user from a group if they write the message `Remove me!` This will prevent chat color, emoji, nicknames and chat name from being changed. It will also prevent people from being added and removed -.. literalinclude:: ../examples/keepbot.py +.. literalinclude:: ../examples/keep_bot.py diff --git a/docs/index.rst b/docs/index.rst index 10e713cf..0edb53d9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -59,6 +59,6 @@ Overview intro examples testing - api + api/index todo faq diff --git a/examples/advanced_bot.py b/examples/advanced_bot.py new file mode 100644 index 00000000..9a09d12a --- /dev/null +++ b/examples/advanced_bot.py @@ -0,0 +1,69 @@ +# -*- coding: UTF-8 -*- + +from fbchat import Client, Group, Message + + +class Bot(Client): + + def on_message(self, message): + thread = message.thread + + # Prevent the bot from answering the bot's own messages + if message in self.sent_messages: + return + + # If the message-text contains the letters 'admin' + if 'admin' in message.text.lower(): + # Send various messages, based on the thread and the admin-status + if not isinstance(thread, Group): + self.send_text(thread, "This isn't a group, no admins here!") + elif self in thread.admins: + self.send_text(thread, "Yup, I'm an admin") + else: + self.send_text(thread, "Nope :'(") + + # If the message-text contains the letters 'like' + if 'like' in message.text.lower(): + # React to the message with a thumbs up + self.set_reaction(message, reaction='👍') + + # If the message contains any images + if message.images: + # Send a message with the same images + self.send(thread, Message(images=message.images, + text='Here, have your images back!')) + + # If the message contains a video + if message.video: + print(message.video, dict(message.video)) + + # If the message contains a file + if message.file: + print(message.file, dict(message.file)) + + # If you're mentioned + if self in (mention.thread for mention in message.mentions): + print('{.name} tagged me!'.format(message.author)) + + def on_user_removed(self, thread, actor, subject): + # If you're the subject or the actor + if self in [subject, actor]: + return + + # If the subject is one of your friends + if subject.is_friend: + self.send_text(thread, 'Aww, this person was my friend!') + + def on_users_added(self, thread, actor, subject): + # If you've been added to the thread + if subject == self: + self.send_text(thread, 'Thanks for adding me!') + + def on_reaction_set(self, actor, message, old_reaction): + # If old_reaction is set, the reaction was changed + if old_reaction: + self.send_text(message.thread, 'Stop changing your reactions!') + + +bot = Bot("", "") +bot.listen() diff --git a/examples/basic_usage.py b/examples/basic_usage.py index aa2afd15..722e64c5 100644 --- a/examples/basic_usage.py +++ b/examples/basic_usage.py @@ -1,12 +1,16 @@ # -*- coding: UTF-8 -*- -from fbchat import Client -from fbchat.models import * +# Imports Client, Thread, Group, Page, User and Message +from fbchat import * +# Login, using your email and credentials client = Client('', '') -print('Own id: {}'.format(client.uid)) +# Display data about you +print(client, dict(client)) -client.send(Message(text='Hi me!'), thread_id=client.uid, thread_type=ThreadType.USER) +# Send a message to yourself +m = client.send_text(client, 'Hi me!') -client.logout() +# Display data about the sent message +print(m, dict(m)) diff --git a/examples/echo_bot.py b/examples/echo_bot.py new file mode 100644 index 00000000..a439ded4 --- /dev/null +++ b/examples/echo_bot.py @@ -0,0 +1,22 @@ +# -*- coding: UTF-8 -*- + +from fbchat import * + + +# Subclass the Client +class EchoBot(Client): + + # And override the on_message method + def on_message(self, message): + # If the author isn't you + if message.author != self: + # Send the message back to the thread + self.send(message.thread, message) + + +# Login and initialize +bot = EchoBot("", "") + +# Start listening for messages, and when a message is recieved, on_message +# will be called +bot.listen() diff --git a/examples/echobot.py b/examples/echobot.py deleted file mode 100644 index 1d93407f..00000000 --- a/examples/echobot.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- coding: UTF-8 -*- - -from fbchat import log, Client - -# Subclass fbchat.Client and override required methods -class EchoBot(Client): - def onMessage(self, author_id, message_object, thread_id, thread_type, **kwargs): - self.markAsDelivered(thread_id, message_object.uid) - self.markAsRead(thread_id) - - log.info("{} from {} in {}".format(message_object, thread_id, thread_type.name)) - - # If you're not the author, echo - if author_id != self.uid: - self.send(message_object, thread_id=thread_id, thread_type=thread_type) - -client = EchoBot("", "") -client.listen() diff --git a/examples/fetch.py b/examples/fetch.py deleted file mode 100644 index e59ec63a..00000000 --- a/examples/fetch.py +++ /dev/null @@ -1,64 +0,0 @@ -# -*- coding: UTF-8 -*- - -from fbchat import Client -from fbchat.models import * - -client = Client('', '') - -# Fetches a list of all users you're currently chatting with, as `User` objects -users = client.fetchAllUsers() - -print("users' IDs: {}".format(user.uid for user in users)) -print("users' names: {}".format(user.name for user in users)) - - -# If we have a user id, we can use `fetchUserInfo` to fetch a `User` object -user = client.fetchUserInfo('')[''] -# We can also query both mutiple users together, which returns list of `User` objects -users = client.fetchUserInfo('<1st user id>', '<2nd user id>', '<3rd user id>') - -print("user's name: {}".format(user.name)) -print("users' names: {}".format(users[k].name for k in users)) - - -# `searchForUsers` searches for the user and gives us a list of the results, -# and then we just take the first one, aka. the most likely one: -user = client.searchForUsers('')[0] - -print('user ID: {}'.format(user.uid)) -print("user's name: {}".format(user.name)) -print("user's photo: {}".format(user.photo)) -print("Is user client's friend: {}".format(user.is_friend)) - - -# Fetches a list of the 20 top threads you're currently chatting with -threads = client.fetchThreadList() -# Fetches the next 10 threads -threads += client.fetchThreadList(offset=20, limit=10) - -print("Threads: {}".format(threads)) - - -# Gets the last 10 messages sent to the thread -messages = client.fetchThreadMessages(thread_id='', limit=10) -# Since the message come in reversed order, reverse them -messages.reverse() - -# Prints the content of all the messages -for message in messages: - print(message.text) - - -# If we have a thread id, we can use `fetchThreadInfo` to fetch a `Thread` object -thread = client.fetchThreadInfo('')[''] -print("thread's name: {}".format(thread.name)) -print("thread's type: {}".format(thread.type)) - - -# `searchForThreads` searches works like `searchForUsers`, but gives us a list of threads instead -thread = client.searchForThreads('')[0] -print("thread's name: {}".format(thread.name)) -print("thread's type: {}".format(thread.type)) - - -# Here should be an example of `getUnread` diff --git a/examples/interract.py b/examples/interract.py index 0b4ddb9a..08da57d9 100644 --- a/examples/interract.py +++ b/examples/interract.py @@ -1,61 +1,67 @@ # -*- coding: UTF-8 -*- -from fbchat import Client -from fbchat.models import * +from time import sleep +from fbchat import Client, Group, Size, Sticker, Mention, Color -client = Client("", "") +c = Client("", "") -thread_id = '1234567890' -thread_type = ThreadType.GROUP +# Get the thread from an ID +thread = c.get_threads_from_ids(1234567890) -# Will send a message to the thread -client.send(Message(text=''), thread_id=thread_id, thread_type=thread_type) +user = c.get_threads_from_ids(12345678901) -# Will send the default `like` emoji -client.send(Message(emoji_size=EmojiSize.LARGE), thread_id=thread_id, thread_type=thread_type) +# Send some text to the thread +c.send_text(thread, 'This is a message!') -# Will send the emoji `👍` -client.send(Message(text='👍', emoji_size=EmojiSize.LARGE), thread_id=thread_id, thread_type=thread_type) +# Send the thread's default emoji +c.send_emoji(thread) -# Will send the sticker with ID `767334476626295` -client.send(Message(sticker=Sticker('767334476626295')), thread_id=thread_id, thread_type=thread_type) +# Send the thumbs up emoji, in large version +c.send_emoji(thread, '👍', size=Size.LARGE) -# Will send a message with a mention -client.send(Message(text='This is a @mention', mentions=[Mention(thread_id, offset=10, length=8)]), thread_id=thread_id, thread_type=thread_type) +# Send the sticker with ID `767334476626295` +c.send_sticker(thread, Sticker(767334476626295)) -# Will send the image located at `` -client.sendLocalImage('', message=Message(text='This is a local image'), thread_id=thread_id, thread_type=thread_type) - -# Will download the image at the url ``, and then send it -client.sendRemoteImage('', message=Message(text='This is a remote image'), thread_id=thread_id, thread_type=thread_type) +# Send a message with a mention +c.send_text(thread, 'This is a @mention!', + mentions=[Mention(thread, offset=10, length=8)]) +# Send the image located at `` +c.send_file(thread, '', text='This is a local image') # Only do these actions if the thread is a group -if thread_type == ThreadType.GROUP: - # Will remove the user with ID `` from the thread - client.removeUserFromGroup('', thread_id=thread_id) +if isinstance(thread, Group): + # Remove the from the thread + c.remove_user(thread, user) + + # Add the user to the thread + c.add_user(thread, user) - # Will add the user with ID `` to the thread - client.addUsersToGroup('', thread_id=thread_id) + # Make the user an admin + c.add_admin(thread, user) - # Will add the users with IDs `<1st user id>`, `<2nd user id>` and `<3th user id>` to the thread - client.addUsersToGroup(['<1st user id>', '<2nd user id>', '<3rd user id>'], thread_id=thread_id) + # Make the user no longer an admin + c.remove_admin(thread, user) + # Set the nickname of the user + c.set_nickname(thread, user, 'This is a nickname!') -# Will change the nickname of the user `` to `` -client.changeNickname('', '', thread_id=thread_id, thread_type=thread_type) +# Set the title of the thread +c.set_title(thread, 'This is a title!') -# Will change the title of the thread to `` -client.changeThreadTitle('<title>', thread_id=thread_id, thread_type=thread_type) +# Make it look like you started typing, and then sent a message +c.start_typing(thread) +sleep(5) +c.send_text(thread, 'Some message') +c.stop_typing(thread) -# Will set the typing status of the thread to `TYPING` -client.setTypingStatus(TypingStatus.TYPING, thread_id=thread_id, thread_type=thread_type) +# Set the thread color to `MESSENGER_BLUE` +c.set_colour(thread, Color.MESSENGER_BLUE) -# Will change the thread color to `MESSENGER_BLUE` -client.changeThreadColor(ThreadColor.MESSENGER_BLUE, thread_id=thread_id) +# Set the thread emoji to `👍` +c.set_emoji(thread, '👍') -# Will change the thread emoji to `👍` -client.changeThreadEmoji('👍', thread_id=thread_id) +message = c.send_text(thread, 'A message') # Will react to a message with a 😍 emoji -client.reactToMessage('<message id>', MessageReaction.LOVE) +c.set_reaction(message, '😍') diff --git a/examples/keep_bot.py b/examples/keep_bot.py new file mode 100644 index 00000000..e0c5dc05 --- /dev/null +++ b/examples/keep_bot.py @@ -0,0 +1,59 @@ +# -*- coding: UTF-8 -*- + +from fbchat import Client + +# Change this to your group id +my_thread_id = 1234567890 + + +class KeepBot(Client): + def on_image_set(self, thread, actor, old_image): + if my_thread_id == thread.id and self != actor: + print('{.name} changed the thread image'.format(actor)) + self.image_set(thread, old_image) + + def on_title_set(self, thread, actor, old_title): + if my_thread_id == thread.id and self != actor: + print("{.name} changed the thread title".format(actor)) + self.set_title(thread, old_title) + + def on_nickname_set(self, thread, actor, subject, old_nickname): + if my_thread_id == thread.id and self != actor: + print("{.name}'s nickname was changed".format(subject)) + self.set_nickname(thread, subject, old_nickname) + + def on_colour_set(self, thread, actor, old_colour): + if my_thread_id == thread.id and self != actor: + print('{.name} changed the thread color'.format(actor)) + self.colour_set(thread, old_colour) + + def on_emoji_set(self, thread, actor, old_emoji): + if my_thread_id == thread.id and self != actor: + print('{.name} changed the thread emoji'.format(actor)) + self.emoji_set(thread, old_emoji) + + + def on_user_added(self, thread, actor, subject): + if my_thread_id == thread.id and self not in [actor, subject]: + print('{.name} got added'.format(subject)) + self.remove_user(thread, subject) + + def on_user_removed(self, thread, actor, subject): + if my_thread_id == thread.id and self not in [actor, subject]: + print("{.name} got removed".format(subject)) + self.add_user(thread, subject) + + + def on_admin_added(self, thread, actor, subject): + if my_thread_id == thread.id and self not in [actor, subject]: + print('{.name} got added'.format(subject)) + self.remove_admin(thread, subject) + + def on_admin_removed(self, thread, actor, subject): + if my_thread_id == thread.id and self not in [actor, subject]: + print("{.name} got removed".format(subject)) + self.add_admin(thread, subject) + + +bot = KeepBot("<email>", "<password>") +bot.listen() diff --git a/examples/keepbot.py b/examples/keepbot.py deleted file mode 100644 index 1189f7b6..00000000 --- a/examples/keepbot.py +++ /dev/null @@ -1,54 +0,0 @@ -# -*- coding: UTF-8 -*- - -from fbchat import log, Client -from fbchat.models import * - -# Change this to your group id -old_thread_id = '1234567890' - -# Change these to match your liking -old_color = ThreadColor.MESSENGER_BLUE -old_emoji = '👍' -old_title = 'Old group chat name' -old_nicknames = { - '12345678901': "User nr. 1's nickname", - '12345678902': "User nr. 2's nickname", - '12345678903': "User nr. 3's nickname", - '12345678904': "User nr. 4's nickname" -} - -class KeepBot(Client): - def onColorChange(self, author_id, new_color, thread_id, thread_type, **kwargs): - if old_thread_id == thread_id and old_color != new_color: - log.info("{} changed the thread color. It will be changed back".format(author_id)) - self.changeThreadColor(old_color, thread_id=thread_id) - - def onEmojiChange(self, author_id, new_emoji, thread_id, thread_type, **kwargs): - if old_thread_id == thread_id and new_emoji != old_emoji: - log.info("{} changed the thread emoji. It will be changed back".format(author_id)) - self.changeThreadEmoji(old_emoji, thread_id=thread_id) - - def onPeopleAdded(self, added_ids, author_id, thread_id, **kwargs): - if old_thread_id == thread_id and author_id != self.uid: - log.info("{} got added. They will be removed".format(added_ids)) - for added_id in added_ids: - self.removeUserFromGroup(added_id, thread_id=thread_id) - - def onPersonRemoved(self, removed_id, author_id, thread_id, **kwargs): - # No point in trying to add ourself - if old_thread_id == thread_id and removed_id != self.uid and author_id != self.uid: - log.info("{} got removed. They will be re-added".format(removed_id)) - self.addUsersToGroup(removed_id, thread_id=thread_id) - - def onTitleChange(self, author_id, new_title, thread_id, thread_type, **kwargs): - if old_thread_id == thread_id and old_title != new_title: - log.info("{} changed the thread title. It will be changed back".format(author_id)) - self.changeThreadTitle(old_title, thread_id=thread_id, thread_type=thread_type) - - def onNicknameChange(self, author_id, changed_for, new_nickname, thread_id, thread_type, **kwargs): - if old_thread_id == thread_id and changed_for in old_nicknames and old_nicknames[changed_for] != new_nickname: - log.info("{} changed {}'s' nickname. It will be changed back".format(author_id, changed_for)) - self.changeNickname(old_nicknames[changed_for], changed_for, thread_id=thread_id, thread_type=thread_type) - -client = KeepBot("<email>", "<password>") -client.listen() diff --git a/examples/possible_usage.py b/examples/possible_usage.py new file mode 100644 index 00000000..f1e0b42f --- /dev/null +++ b/examples/possible_usage.py @@ -0,0 +1,40 @@ +# -*- coding: UTF-8 -*- + +from fbchat import * + +c = Client('<email>', '<password>') + +# This is a possible way we *could* build the library. Not saying it's a good +# idea, but I'd like your opinions + +u = c.users[0] + +# Updates the users nickname, also on Facebook's side +u.nickname = 'New nickname' + +# Should throw an error, since we can't change a persons name +u.name = 'New name' + + +g = c.groups[0] + +# Updates the title +g.title = 'New title' + +# Adds a user to the group. If they're already in the group, nothing would +# happen? Or should an error be thrown? +g.participants += [u] + +# Makes every participant an admin +g.admins = g.participants + +# Makes yourself the only admin in the group +g.admins = [c] + + +# Deletes the group +c.threads -= g + + +# Unfriends the user +c.friends -= u diff --git a/examples/remove_bot.py b/examples/remove_bot.py new file mode 100644 index 00000000..00034fbd --- /dev/null +++ b/examples/remove_bot.py @@ -0,0 +1,18 @@ +# -*- coding: UTF-8 -*- + +from fbchat import * + + +class RemoveBot(Client): + + def on_text(self, thread, author, text, mentions): + # We can only kick people from group chats, so no need to try if it's a + # user chat + if text == 'Remove me!' and isinstance(thread, Group): + print('{.name} asked to be removed from {.name}' + .format(author, thread)) + self.remove_user(thread, author) + + +bot = RemoveBot("<email>", "<password>") +bot.listen() diff --git a/examples/removebot.py b/examples/removebot.py deleted file mode 100644 index b6e7d512..00000000 --- a/examples/removebot.py +++ /dev/null @@ -1,17 +0,0 @@ -# -*- coding: UTF-8 -*- - -from fbchat import log, Client -from fbchat.models import * - -class RemoveBot(Client): - def onMessage(self, author_id, message_object, thread_id, thread_type, **kwargs): - # We can only kick people from group chats, so no need to try if it's a user chat - if message_object.text == 'Remove me!' and thread_type == ThreadType.GROUP: - log.info('{} will be removed from {}'.format(author_id, thread_id)) - self.removeUserFromGroup(author_id, thread_id=thread_id) - else: - # Sends the data to the inherited onMessage, so that we can still see when a message is recieved - super(RemoveBot, self).onMessage(author_id=author_id, message_object=message_object, thread_id=thread_id, thread_type=thread_type, **kwargs) - -client = RemoveBot("<email>", "<password>") -client.listen() diff --git a/examples/retrieve.py b/examples/retrieve.py new file mode 100644 index 00000000..306b868c --- /dev/null +++ b/examples/retrieve.py @@ -0,0 +1,52 @@ +# -*- coding: UTF-8 -*- + +from fbchat import * + +c = Client('<email>', '<password>') + +# Display the first 10 threads in your chat window, newly updated first +threads = c.get_threads(limit=10) +print(threads) + +# Do some action for a while +sleep(10) + +# Update the internal cache with the new messages that may have been +# sent/recieved in the meantime +c.update() + +# Update the threads +threads = c.get_threads(limit=10) + +# If we found any threads +if len(threads) > 0: + # Select the lastest thread + t = threads[0] + + # Display the type of the thread + print(type(t)) + + # Display the last 10 messages in that thread, newest first + for m in c.get_messages(t, limit=10): + print(m) + + # Same as above, alternate method using iterables + from itertools import islice + for m in islice(c.get_messages(t), 10): + print(m) + + # If the message contains an attachment, for example an image, we can + # retrieve the full url to that attachment + m, = c.get_messages(t, limit=1) + if m.images: + print(c.get_url(m.images[0])) + +# Display the name of all your friends +print([f.name for f in c.get_friends()]) + +# If we have the name of a thread in advance +thread = c.get_threads_from_ids(1234567890) + +# Otherwise we could try a search +possible_threads = c.search_for_thread('<thread name>', limit=10) +print(possible_threads) From 7ba038a6dff364cc777ab53603d3b2b766df0538 Mon Sep 17 00:00:00 2001 From: Mads Marquart <madsmtm@gmail.com> Date: Tue, 10 Apr 2018 20:26:42 +0200 Subject: [PATCH 3/9] Added first iteration of new API Most of the code has been removed, and the functionality of the client has been split up into a bunch of classes and methods. These are still without implementation, which will come later --- fbchat/base.py | 64 ++ fbchat/client.py | 1895 --------------------------------- fbchat/get.py | 154 +++ fbchat/graphql.py | 422 -------- fbchat/listener.py | 56 + fbchat/message_management.py | 42 + fbchat/models.py | 496 --------- fbchat/search.py | 69 ++ fbchat/send.py | 143 +++ fbchat/thread_control.py | 141 +++ fbchat/thread_interraction.py | 182 ++++ fbchat/thread_options.py | 78 ++ fbchat/utils.py | 235 ---- 13 files changed, 929 insertions(+), 3048 deletions(-) create mode 100644 fbchat/base.py delete mode 100644 fbchat/client.py create mode 100644 fbchat/get.py delete mode 100644 fbchat/graphql.py create mode 100644 fbchat/listener.py create mode 100644 fbchat/message_management.py delete mode 100644 fbchat/models.py create mode 100644 fbchat/search.py create mode 100644 fbchat/send.py create mode 100644 fbchat/thread_control.py create mode 100644 fbchat/thread_interraction.py create mode 100644 fbchat/thread_options.py delete mode 100644 fbchat/utils.py diff --git a/fbchat/base.py b/fbchat/base.py new file mode 100644 index 00000000..7d148177 --- /dev/null +++ b/fbchat/base.py @@ -0,0 +1,64 @@ +# -*- coding: UTF-8 -*- + +from __future__ import unicode_literals +from .models import User + + +class Base(User): + """Base Facebook client""" + + def __init__(self, email, password, session=None, user_agent=None, + max_tries=5): + """Initialize and login the Facebook client + + Args: + email: Facebook `email`, `id` or `phone number` + password: Facebook account password + session (dict): Previous session to attempt to load + user_agent: Custom user agent to use when sending requests. If + ``None``, the user agent will be chosen randomly + max_tries (int): Maximum number of times to try logging in + """ + + def logout(self): + """Properly log out the client, invalidating the session + + Warning: + Using the client after this method is called results in undefined + behaviour + """ + + def is_logged_in(self): + """Check the login status + + Return: + Whether the client is still logged in + """ + + def on_2fa(self): + """Will be called when a two-factor authentication code is needed + + By default, this will call ``input``, and wait for the authentication + code + + Return: + The expected return is a two-factor authentication code, or + ``None`` if not available + """ + + def get_session(self): + """Retrieve session + + The session can then be serialised, stored, and reused the next time + the client wants to log in + + Return: + A dict containing the session + """ + + def set_session(self, session): + """Validate session and load it into the client + + Args: + session (dict): A dictionay containing the session + """ diff --git a/fbchat/client.py b/fbchat/client.py deleted file mode 100644 index 46981550..00000000 --- a/fbchat/client.py +++ /dev/null @@ -1,1895 +0,0 @@ -# -*- coding: UTF-8 -*- - -from __future__ import unicode_literals -import requests -import urllib -from uuid import uuid1 -from random import choice -from bs4 import BeautifulSoup as bs -from mimetypes import guess_type -from .utils import * -from .models import * -from .graphql import * -import time - - - -class Client(object): - """A client for the Facebook Chat (Messenger). - - See https://fbchat.readthedocs.io for complete documentation of the API. - """ - - ssl_verify = True - """Verify ssl certificate, set to False to allow debugging with a proxy""" - listening = False - """Whether the client is listening. Used when creating an external event loop to determine when to stop listening""" - uid = None - """ - The ID of the client. - Can be used as `thread_id`. See :ref:`intro_threads` for more info. - - Note: Modifying this results in undefined behaviour - """ - - def __init__(self, email, password, user_agent=None, max_tries=5, session_cookies=None, logging_level=logging.INFO): - """Initializes and logs in the client - - :param email: Facebook `email`, `id` or `phone number` - :param password: Facebook account password - :param user_agent: Custom user agent to use when sending requests. If `None`, user agent will be chosen from a premade list (see :any:`utils.USER_AGENTS`) - :param max_tries: Maximum number of times to try logging in - :param session_cookies: Cookies from a previous session (Will default to login if these are invalid) - :param logging_level: Configures the `logging level <https://docs.python.org/3/library/logging.html#logging-levels>`_. Defaults to `INFO` - :type max_tries: int - :type session_cookies: dict - :type logging_level: int - :raises: FBchatException on failed login - """ - - self.sticky, self.pool = (None, None) - self._session = requests.session() - self.req_counter = 1 - self.seq = "0" - self.payloadDefault = {} - self.client = 'mercury' - self.default_thread_id = None - self.default_thread_type = None - self.req_url = ReqUrl() - - if not user_agent: - user_agent = choice(USER_AGENTS) - - self._header = { - 'Content-Type' : 'application/x-www-form-urlencoded', - 'Referer' : self.req_url.BASE, - 'Origin' : self.req_url.BASE, - 'User-Agent' : user_agent, - 'Connection' : 'keep-alive', - } - - 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.isLoggedIn(): - self.login(email, password, max_tries) - else: - self.email = email - self.password = password - - """ - INTERNAL REQUEST METHODS - """ - - def _generatePayload(self, query): - """Adds the following defaults to the payload: - __rev, __user, __a, ttstamp, fb_dtsg, __req - """ - payload = self.payloadDefault.copy() - if query: - payload.update(query) - payload['__req'] = str_base(self.req_counter, 36) - payload['seq'] = self.seq - self.req_counter += 1 - return payload - - def _fix_fb_errors(self, error_code): - """ - This fixes "Please try closing and re-opening your browser window" errors (1357004) - This error usually happens after 1-2 days of inactivity - It may be a bad idea to do this in an exception handler, if you have a better method, please suggest it! - """ - if error_code == '1357004': - log.warning('Got error #1357004. Doing a _postLogin, and resending request') - self._postLogin() - return True - return False - - def _get(self, url, query=None, timeout=30, fix_request=False, as_json=False, error_retries=3): - payload = self._generatePayload(query) - r = self._session.get(url, headers=self._header, params=payload, timeout=timeout, verify=self.ssl_verify) - if not fix_request: - return r - try: - return check_request(r, as_json=as_json) - except FBchatFacebookError as e: - if error_retries > 0 and self._fix_fb_errors(e.fb_error_code): - return self._get(url, query=query, timeout=timeout, fix_request=fix_request, as_json=as_json, error_retries=error_retries-1) - raise e - - def _post(self, url, query=None, timeout=30, fix_request=False, as_json=False, error_retries=3): - payload = self._generatePayload(query) - r = self._session.post(url, headers=self._header, data=payload, timeout=timeout, verify=self.ssl_verify) - if not fix_request: - return r - try: - return check_request(r, as_json=as_json) - except FBchatFacebookError as e: - if error_retries > 0 and self._fix_fb_errors(e.fb_error_code): - return self._post(url, query=query, timeout=timeout, fix_request=fix_request, as_json=as_json, error_retries=error_retries-1) - raise e - - def _graphql(self, payload, error_retries=3): - content = self._post(self.req_url.GRAPHQL, payload, fix_request=True, as_json=False) - try: - return graphql_response_to_json(content) - except FBchatFacebookError as e: - if error_retries > 0 and self._fix_fb_errors(e.fb_error_code): - return self._graphql(payload, error_retries=error_retries-1) - raise e - - def _cleanGet(self, url, query=None, timeout=30): - return self._session.get(url, headers=self._header, params=query, timeout=timeout, verify=self.ssl_verify) - - def _cleanPost(self, url, query=None, timeout=30): - self.req_counter += 1 - return self._session.post(url, headers=self._header, data=query, timeout=timeout, verify=self.ssl_verify) - - def _postFile(self, url, files=None, query=None, timeout=30, fix_request=False, as_json=False, error_retries=3): - payload=self._generatePayload(query) - # Removes 'Content-Type' from the header - headers = dict((i, self._header[i]) for i in self._header if i != 'Content-Type') - r = self._session.post(url, headers=headers, data=payload, timeout=timeout, files=files, verify=self.ssl_verify) - if not fix_request: - return r - try: - return check_request(r, as_json=as_json) - except FBchatFacebookError as e: - if error_retries > 0 and self._fix_fb_errors(e.fb_error_code): - return self._postFile(url, files=files, query=query, timeout=timeout, fix_request=fix_request, as_json=as_json, error_retries=error_retries-1) - raise e - - def graphql_requests(self, *queries): - """ - .. todo:: - Documenting this - - :raises: FBchatException if request failed - """ - - return tuple(self._graphql({ - 'method': 'GET', - 'response_format': 'json', - 'queries': graphql_queries_to_json(*queries) - })) - - def graphql_request(self, query): - """ - Shorthand for `graphql_requests(query)[0]` - - :raises: FBchatException if request failed - """ - return self.graphql_requests(query)[0] - - """ - END INTERNAL REQUEST METHODS - """ - - """ - LOGIN METHODS - """ - - def _resetValues(self): - self.payloadDefault={} - self._session = requests.session() - self.req_counter = 1 - self.seq = "0" - self.uid = None - - def _postLogin(self): - self.payloadDefault = {} - self.client_id = hex(int(random()*2147483648))[2:] - self.start_time = now() - self.uid = self._session.cookies.get_dict().get('c_user') - if self.uid is None: - raise FBchatException('Could not find c_user cookie') - self.uid = str(self.uid) - self.user_channel = "p_" + self.uid - self.ttstamp = '' - - r = self._get(self.req_url.BASE) - soup = bs(r.text, "lxml") - self.fb_dtsg = soup.find("input", {'name':'fb_dtsg'})['value'] - self.fb_h = soup.find("input", {'name':'h'})['value'] - for i in self.fb_dtsg: - self.ttstamp += str(ord(i)) - self.ttstamp += '2' - # Set default payload - self.payloadDefault['__rev'] = int(r.text.split('"client_revision":',1)[1].split(",",1)[0]) - self.payloadDefault['__user'] = self.uid - self.payloadDefault['__a'] = '1' - self.payloadDefault['ttstamp'] = self.ttstamp - self.payloadDefault['fb_dtsg'] = self.fb_dtsg - - self.form = { - 'channel' : self.user_channel, - 'partition' : '-2', - 'clientid' : self.client_id, - 'viewer_uid' : self.uid, - 'uid' : self.uid, - 'state' : 'active', - 'format' : 'json', - 'idle' : 0, - 'cap' : '8' - } - - self.prev = now() - self.tmp_prev = now() - self.last_sync = now() - - def _login(self): - if not (self.email and self.password): - raise FBchatUserError("Email and password not found.") - - soup = bs(self._get(self.req_url.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(self.req_url.LOGIN, data) - - # Usually, 'Checkpoint' will refer to 2FA - if ('checkpoint' in r.url - and ('enter security code to continue' in r.text.lower() - or 'enter login code to continue' in r.text.lower())): - r = self._2FA(r) - - # Sometimes Facebook tries to show the user a "Save Device" dialog - if 'save-device' in r.url: - r = self._cleanGet(self.req_url.SAVE_DEVICE) - - if 'home' in r.url: - self._postLogin() - return True, r.url - else: - return False, r.url - - def _2FA(self, r): - soup = bs(r.text, "lxml") - data = dict() - - s = self.on2FACode() - - data['approvals_code'] = s - data['fb_dtsg'] = soup.find("input", {'name':'fb_dtsg'})['value'] - data['nh'] = soup.find("input", {'name':'nh'})['value'] - data['submit[Submit Code]'] = 'Submit Code' - data['codes_submitted'] = 0 - log.info('Submitting 2FA code.') - - r = self._cleanPost(self.req_url.CHECKPOINT, data) - - if 'home' in r.url: - return r - - del(data['approvals_code']) - del(data['submit[Submit Code]']) - del(data['codes_submitted']) - - 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(self.req_url.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(self.req_url.CHECKPOINT, data) - - if 'home' in r.url: - return r - - 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(self.req_url.CHECKPOINT, data) - - if 'home' in r.url: - return r - - del(data['submit[This was me]']) - 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(self.req_url.CHECKPOINT, data) - return r - - def isLoggedIn(self): - """ - Sends a request to Facebook to check the login status - - :return: True if the client is still logged in - :rtype: bool - """ - # Send a request to the login url, to see if we're directed to the home page - r = self._cleanGet(self.req_url.LOGIN) - return 'home' in r.url - - def getSession(self): - """Retrieves session cookies - - :return: A dictionay containing session cookies - :rtype: dict - """ - return self._session.cookies.get_dict() - - def setSession(self, session_cookies): - """Loads session cookies - - :param session_cookies: A dictionay containing session cookies - :type session_cookies: dict - :return: False if `session_cookies` does not contain proper cookies - :rtype: bool - """ - - # Quick check to see if session_cookies is formatted properly - if not session_cookies or 'c_user' not in session_cookies: - return False - - try: - # Load cookies into current session - self._session.cookies = requests.cookies.merge_cookies(self._session.cookies, session_cookies) - self._postLogin() - except Exception as e: - log.exception('Failed loading session') - self._resetValues() - return False - return True - - def login(self, email, password, max_tries=5): - """ - Uses `email` and `password` to login the user (If the user is already logged in, this will do a re-login) - - :param email: Facebook `email` or `id` or `phone number` - :param password: Facebook account password - :param max_tries: Maximum number of times to try logging in - :type max_tries: int - :raises: FBchatException on failed login - """ - self.onLoggingIn(email=email) - - if max_tries < 1: - raise FBchatUserError('Cannot login: max_tries should be at least one') - - if not (email and password): - raise FBchatUserError('Email and password not set') - - self.email = email - self.password = password - - for i in range(1, max_tries+1): - login_successful, login_url = self._login() - if not login_successful: - log.warning('Attempt #{} failed{}'.format(i, {True:', retrying'}.get(i < max_tries, ''))) - time.sleep(1) - continue - else: - self.onLoggedIn(email=email) - break - else: - raise FBchatUserError('Login failed. Check email/password. (Failed on url: {})'.format(login_url)) - - def logout(self): - """ - Safely logs out the client - - :param timeout: See `requests timeout <http://docs.python-requests.org/en/master/user/advanced/#timeouts>`_ - :return: True if the action was successful - :rtype: bool - """ - data = { - 'ref': "mb", - 'h': self.fb_h - } - - r = self._get(self.req_url.LOGOUT, data) - - self._resetValues() - - return r.ok - - """ - END LOGIN METHODS - """ - - """ - DEFAULT THREAD METHODS - """ - - def _getThread(self, given_thread_id=None, given_thread_type=None): - """ - 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: Thread ID and thread type - :rtype: tuple - """ - 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 setDefaultThread(self, thread_id, thread_type): - """Sets default thread to send messages to - - :param thread_id: User/Group ID to default to. See :ref:`intro_threads` - :param thread_type: See :ref:`intro_threads` - :type thread_type: models.ThreadType - """ - self.default_thread_id = thread_id - self.default_thread_type = thread_type - - def resetDefaultThread(self): - """Resets default thread""" - self.setDefaultThread(None, None) - - """ - END DEFAULT THREAD METHODS - """ - - """ - FETCH METHODS - """ - - def fetchAllUsers(self): - """ - Gets all users the client is currently chatting with - - :return: :class:`models.User` objects - :rtype: list - :raises: FBchatException if request failed - """ - - data = { - 'viewer': self.uid, - } - j = self._post(self.req_url.ALL_USERS, query=data, fix_request=True, as_json=True) - if j.get('payload') is None: - raise FBchatException('Missing payload while fetching users: {}'.format(j)) - - users = [] - - for key in j['payload']: - k = j['payload'][key] - if k['type'] in ['user', 'friend']: - if k['id'] in ['0', 0]: - # Skip invalid users - pass - users.append(User(k['id'], first_name=k.get('firstName'), url=k.get('uri'), photo=k.get('thumbSrc'), name=k.get('name'), is_friend=k.get('is_friend'), gender=GENDERS.get(k.get('gender')))) - - return users - - def searchForUsers(self, name, limit=1): - """ - Find and get user by his/her name - - :param name: Name of the user - :param limit: The max. amount of users to fetch - :return: :class:`models.User` objects, ordered by relevance - :rtype: list - :raises: FBchatException if request failed - """ - - j = self.graphql_request(GraphQL(query=GraphQL.SEARCH_USER, params={'search': name, 'limit': limit})) - - return [graphql_to_user(node) for node in j[name]['users']['nodes']] - - def searchForPages(self, name, limit=1): - """ - Find and get page by its name - - :param name: Name of the page - :return: :class:`models.Page` objects, ordered by relevance - :rtype: list - :raises: FBchatException if request failed - """ - - j = self.graphql_request(GraphQL(query=GraphQL.SEARCH_PAGE, params={'search': name, 'limit': limit})) - - return [graphql_to_page(node) for node in j[name]['pages']['nodes']] - - # TODO intergrate Rooms - def searchForGroups(self, name, limit=1): - """ - Find and get group thread by its name - - :param name: Name of the group thread - :param limit: The max. amount of groups to fetch - :return: :class:`models.Group` objects, ordered by relevance - :rtype: list - :raises: FBchatException if request failed - """ - - j = self.graphql_request(GraphQL(query=GraphQL.SEARCH_GROUP, params={'search': name, 'limit': limit})) - - return [graphql_to_group(node) for node in j['viewer']['groups']['nodes']] - - def searchForThreads(self, name, limit=1): - """ - Find and get a thread by its name - - :param name: Name of the thread - :param limit: The max. amount of groups to fetch - :return: :class:`models.User`, :class:`models.Group` and :class:`models.Page` objects, ordered by relevance - :rtype: list - :raises: FBchatException if request failed - """ - - j = self.graphql_request(GraphQL(query=GraphQL.SEARCH_THREAD, params={'search': name, 'limit': limit})) - - rtn = [] - for node in j[name]['threads']['nodes']: - if node['__typename'] == 'User': - rtn.append(graphql_to_user(node)) - elif node['__typename'] == 'MessageThread': - # MessageThread => Group thread - rtn.append(graphql_to_group(node)) - elif node['__typename'] == 'Page': - rtn.append(graphql_to_page(node)) - elif node['__typename'] == 'Group': - # We don't handle Facebook "Groups" - pass - # TODO Add Rooms - else: - log.warning('Unknown __typename: {} in {}'.format(repr(node['__typename']), node)) - - return rtn - - def _fetchInfo(self, *ids): - data = { - "ids[{}]".format(i): _id for i, _id in enumerate(ids) - } - j = self._post(self.req_url.INFO, data, fix_request=True, as_json=True) - - if j.get('payload') is None or j['payload'].get('profiles') is None: - raise FBchatException('No users/pages returned: {}'.format(j)) - - entries = {} - for _id in j['payload']['profiles']: - k = j['payload']['profiles'][_id] - if k['type'] in ['user', 'friend']: - entries[_id] = { - 'id': _id, - 'type': ThreadType.USER, - 'url': k.get('uri'), - 'first_name': k.get('firstName'), - 'is_viewer_friend': k.get('is_friend'), - 'gender': k.get('gender'), - 'profile_picture': {'uri': k.get('thumbSrc')}, - 'name': k.get('name') - } - elif k['type'] == 'page': - entries[_id] = { - 'id': _id, - 'type': ThreadType.PAGE, - 'url': k.get('uri'), - 'profile_picture': {'uri': k.get('thumbSrc')}, - 'name': k.get('name') - } - else: - raise FBchatException('{} had an unknown thread type: {}'.format(_id, k)) - - log.debug(entries) - return entries - - def fetchUserInfo(self, *user_ids): - """ - Get users' info from IDs, unordered - - .. warning:: - Sends two requests, to fetch all available info! - - :param user_ids: One or more user ID(s) to query - :return: :class:`models.User` objects, labeled by their ID - :rtype: dict - :raises: FBchatException if request failed - """ - - threads = self.fetchThreadInfo(*user_ids) - users = {} - for k in threads: - if threads[k].type == ThreadType.USER: - users[k] = threads[k] - else: - raise FBchatUserError('Thread {} was not a user'.format(threads[k])) - - return users - - def fetchPageInfo(self, *page_ids): - """ - Get pages' info from IDs, unordered - - .. warning:: - Sends two requests, to fetch all available info! - - :param page_ids: One or more page ID(s) to query - :return: :class:`models.Page` objects, labeled by their ID - :rtype: dict - :raises: FBchatException if request failed - """ - - threads = self.fetchThreadInfo(*page_ids) - pages = {} - for k in threads: - if threads[k].type == ThreadType.PAGE: - pages[k] = threads[k] - else: - raise FBchatUserError('Thread {} was not a page'.format(threads[k])) - - return pages - - def fetchGroupInfo(self, *group_ids): - """ - Get groups' info from IDs, unordered - - :param group_ids: One or more group ID(s) to query - :return: :class:`models.Group` objects, labeled by their ID - :rtype: dict - :raises: FBchatException if request failed - """ - - threads = self.fetchThreadInfo(*group_ids) - groups = {} - for k in threads: - if threads[k].type == ThreadType.GROUP: - groups[k] = threads[k] - else: - raise FBchatUserError('Thread {} was not a group'.format(threads[k])) - - return groups - - def fetchThreadInfo(self, *thread_ids): - """ - Get threads' info from IDs, unordered - - .. warning:: - Sends two requests if users or pages are present, to fetch all available info! - - :param thread_ids: One or more thread ID(s) to query - :return: :class:`models.Thread` objects, labeled by their ID - :rtype: dict - :raises: FBchatException if request failed - """ - - queries = [] - for thread_id in thread_ids: - queries.append(GraphQL(doc_id='1386147188135407', params={ - 'id': thread_id, - 'message_limit': 0, - 'load_messages': False, - 'load_read_receipts': False, - 'before': None - })) - - j = self.graphql_requests(*queries) - - for i, entry in enumerate(j): - if entry.get('message_thread') is None: - # If you don't have an existing thread with this person, attempt to retrieve user data anyways - j[i]['message_thread'] = { - 'thread_key': { - 'other_user_id': thread_ids[i] - }, - 'thread_type': 'ONE_TO_ONE' - } - - pages_and_user_ids = [k['message_thread']['thread_key']['other_user_id'] for k in j if k['message_thread'].get('thread_type') == 'ONE_TO_ONE'] - pages_and_users = {} - if len(pages_and_user_ids) != 0: - pages_and_users = self._fetchInfo(*pages_and_user_ids) - - rtn = {} - for i, entry in enumerate(j): - entry = entry['message_thread'] - if entry.get('thread_type') == 'GROUP': - _id = entry['thread_key']['thread_fbid'] - rtn[_id] = graphql_to_group(entry) - elif entry.get('thread_type') == 'ROOM': - _id = entry['thread_key']['thread_fbid'] - rtn[_id] = graphql_to_room(entry) - elif entry.get('thread_type') == 'ONE_TO_ONE': - _id = entry['thread_key']['other_user_id'] - if pages_and_users.get(_id) is None: - raise FBchatException('Could not fetch thread {}'.format(_id)) - entry.update(pages_and_users[_id]) - if entry['type'] == ThreadType.USER: - rtn[_id] = graphql_to_user(entry) - else: - rtn[_id] = graphql_to_page(entry) - else: - raise FBchatException('{} had an unknown thread type: {}'.format(thread_ids[i], entry)) - - return rtn - - def fetchThreadMessages(self, thread_id=None, limit=20, before=None): - """ - Get the last messages in a thread - - :param thread_id: User/Group ID to get messages from. See :ref:`intro_threads` - :param limit: Max. number of messages to retrieve - :param before: A timestamp, indicating from which point to retrieve messages - :type limit: int - :type before: int - :return: :class:`models.Message` objects - :rtype: list - :raises: FBchatException if request failed - """ - - thread_id, thread_type = self._getThread(thread_id, None) - - j = self.graphql_request(GraphQL(doc_id='1386147188135407', params={ - 'id': thread_id, - 'message_limit': limit, - 'load_messages': True, - 'load_read_receipts': False, - 'before': before - })) - - if j.get('message_thread') is None: - raise FBchatException('Could not fetch thread {}: {}'.format(thread_id, j)) - - return list(reversed([graphql_to_message(message) for message in j['message_thread']['messages']['nodes']])) - - def fetchThreadList(self, offset=None, limit=20, thread_location=ThreadLocation.INBOX, before=None): - """Get thread list of your facebook account - - :param offset: Deprecated. Do not use! - :param limit: Max. number of threads to retrieve. Capped at 20 - :param thread_location: models.ThreadLocation: INBOX, PENDING, ARCHIVED or OTHER - :param before: A timestamp (in milliseconds), indicating from which point to retrieve threads - :type limit: int - :type before: int - :return: :class:`models.Thread` objects - :rtype: list - :raises: FBchatException if request failed - """ - - if offset is not None: - log.warning('Using `offset` in `fetchThreadList` is no longer supported, since Facebook migrated to the use of GraphQL in this request. Use `before` instead') - - if limit > 20 or limit < 1: - raise FBchatUserError('`limit` should be between 1 and 20') - - if thread_location in ThreadLocation: - loc_str = thread_location.value - else: - raise FBchatUserError('"thread_location" must be a value of ThreadLocation') - - j = self.graphql_request(GraphQL(doc_id='1349387578499440', params={ - 'limit': limit, - 'tags': [loc_str], - 'before': before, - 'includeDeliveryReceipts': True, - 'includeSeqID': False - })) - - return [graphql_to_thread(node) for node in j['viewer']['message_threads']['nodes']] - - def fetchUnread(self): - """ - Get the unread thread list - - :return: List of unread thread ids - :rtype: list - :raises: FBchatException if request failed - """ - form = { - 'folders[0]': 'inbox', - 'client': 'mercury', - 'last_action_timestamp': now() - 60*1000 - # 'last_action_timestamp': 0 - } - - j = self._post(self.req_url.UNREAD_THREADS, form, fix_request=True, as_json=True) - - return j['payload']['unread_thread_fbids'][0]['other_user_fbids'] - - def fetchUnseen(self): - """ - Get the unseen (new) thread list - - :return: List of unseen thread ids - :rtype: list - :raises: FBchatException if request failed - """ - j = self._post(self.req_url.UNSEEN_THREADS, None, fix_request=True, as_json=True) - - return j['payload']['unseen_thread_fbids'][0]['other_user_fbids'] - - def fetchImageUrl(self, image_id): - """Fetches the url to the original image from an image attachment ID - - :param image_id: The image you want to fethc - :type image_id: str - :return: An url where you can download the original image - :rtype: str - :raises: FBChatException if request failed - """ - image_id = str(image_id) - j = check_request(self._get(ReqUrl.ATTACHMENT_PHOTO, query={'photo_id': str(image_id)})) - - url = get_jsmods_require(j, 3) - if url is None: - raise FBChatException('Could not fetch image url from: {}'.format(j)) - return url - - """ - END FETCH METHODS - """ - - """ - SEND METHODS - """ - - def _oldMessage(self, message): - return message if isinstance(message, Message) else Message(text=message) - - def _getSendData(self, message=None, thread_id=None, thread_type=ThreadType.USER): - """Returns the data needed to send a request to `SendURL`""" - messageAndOTID = generateOfflineThreadingID() - timestamp = now() - data = { - 'client': self.client, - 'author' : 'fbid:' + str(self.uid), - 'timestamp' : timestamp, - 'source' : 'source:chat:web', - 'offline_threading_id': messageAndOTID, - 'message_id' : messageAndOTID, - 'threading_id': generateMessageID(self.client_id), - 'ephemeral_ttl_mode:': '0' - } - - # Set recipient - if thread_type in [ThreadType.USER, ThreadType.PAGE]: - data['other_user_fbid'] = thread_id - elif thread_type == ThreadType.GROUP: - data['thread_fbid'] = thread_id - - if message is None: - message = Message() - - if message.text or message.sticker or message.emoji_size: - data['action_type'] = 'ma-type:user-generated-message' - - if message.text: - data['body'] = message.text - - for i, mention in enumerate(message.mentions): - data['profile_xmd[{}][id]'.format(i)] = mention.thread_id - data['profile_xmd[{}][offset]'.format(i)] = mention.offset - data['profile_xmd[{}][length]'.format(i)] = mention.length - data['profile_xmd[{}][type]'.format(i)] = 'p' - - if message.emoji_size: - if message.text: - data['tags[0]'] = 'hot_emoji_size:' + message.emoji_size.name.lower() - else: - data['sticker_id'] = message.emoji_size.value - - if message.sticker: - data['sticker_id'] = message.sticker.uid - - return data - - def _doSendRequest(self, data): - """Sends the data to `SendURL`, and returns the message ID or None on failure""" - j = self._post(self.req_url.SEND, data, fix_request=True, as_json=True) - - try: - message_ids = [action['message_id'] for action in j['payload']['actions'] if 'message_id' in action] - if len(message_ids) != 1: - log.warning("Got multiple message ids' back: {}".format(message_ids)) - message_id = message_ids[0] - except (KeyError, IndexError) as e: - raise FBchatException('Error when sending message: No message IDs could be found: {}'.format(j)) - - # update JS token if received in response - fb_dtsg = get_jsmods_require(j, 2) - if fb_dtsg is not None: - self.payloadDefault['fb_dtsg'] = fb_dtsg - - return message_id - - def send(self, message, thread_id=None, thread_type=ThreadType.USER): - """ - Sends a message to a thread - - :param message: Message to send - :param thread_id: User/Group ID to send to. See :ref:`intro_threads` - :param thread_type: See :ref:`intro_threads` - :type message: models.Message - :type thread_type: models.ThreadType - :return: :ref:`Message ID <intro_message_ids>` of the sent message - :raises: FBchatException if request failed - """ - thread_id, thread_type = self._getThread(thread_id, thread_type) - data = self._getSendData(message=message, thread_id=thread_id, thread_type=thread_type) - - return self._doSendRequest(data) - - def sendMessage(self, message, thread_id=None, thread_type=ThreadType.USER): - """ - Deprecated. Use :func:`fbchat.Client.send` instead - """ - return self.send(Message(text=message), thread_id=thread_id, thread_type=thread_type) - - def sendEmoji(self, emoji=None, size=EmojiSize.SMALL, thread_id=None, thread_type=ThreadType.USER): - """ - Deprecated. Use :func:`fbchat.Client.send` instead - """ - return self.send(Message(text=emoji, emoji_size=size), thread_id=thread_id, thread_type=thread_type) - - def _uploadImage(self, image_path, data, mimetype): - """Upload an image and get the image_id for sending in a message""" - - j = self._postFile(self.req_url.UPLOAD, { - 'file': ( - image_path, - data, - mimetype - ) - }, fix_request=True, as_json=True) - # Return the image_id - if not mimetype == 'image/gif': - return j['payload']['metadata'][0]['image_id'] - else: - return j['payload']['metadata'][0]['gif_id'] - - def sendImage(self, image_id, message=None, thread_id=None, thread_type=ThreadType.USER, is_gif=False): - """ - Deprecated. Use :func:`fbchat.Client.send` instead - """ - thread_id, thread_type = self._getThread(thread_id, thread_type) - data = self._getSendData(message=self._oldMessage(message), thread_id=thread_id, thread_type=thread_type) - - data['action_type'] = 'ma-type:user-generated-message' - data['has_attachment'] = True - - if not is_gif: - data['image_ids[0]'] = image_id - else: - data['gif_ids[0]'] = image_id - - return self._doSendRequest(data) - - def sendRemoteImage(self, image_url, message=None, thread_id=None, thread_type=ThreadType.USER): - """ - Sends an image from a URL to a thread - - :param image_url: URL of an image to upload and send - :param message: Additional message - :param thread_id: User/Group ID to send to. See :ref:`intro_threads` - :param thread_type: See :ref:`intro_threads` - :type thread_type: models.ThreadType - :return: :ref:`Message ID <intro_message_ids>` of the sent image - :raises: FBchatException if request failed - """ - thread_id, thread_type = self._getThread(thread_id, thread_type) - mimetype = guess_type(image_url)[0] - is_gif = (mimetype == 'image/gif') - remote_image = requests.get(image_url).content - image_id = self._uploadImage(image_url, remote_image, mimetype) - return self.sendImage(image_id=image_id, message=message, thread_id=thread_id, thread_type=thread_type, is_gif=is_gif) - - def sendLocalImage(self, image_path, message=None, thread_id=None, thread_type=ThreadType.USER): - """ - Sends a local image to a thread - - :param image_path: Path of an image to upload and send - :param message: Additional message - :param thread_id: User/Group ID to send to. See :ref:`intro_threads` - :param thread_type: See :ref:`intro_threads` - :type thread_type: models.ThreadType - :return: :ref:`Message ID <intro_message_ids>` of the sent image - :raises: FBchatException if request failed - """ - thread_id, thread_type = self._getThread(thread_id, thread_type) - mimetype = guess_type(image_path)[0] - is_gif = (mimetype == 'image/gif') - image_id = self._uploadImage(image_path, open(image_path, 'rb'), mimetype) - return self.sendImage(image_id=image_id, message=message, thread_id=thread_id, thread_type=thread_type, is_gif=is_gif) - - def addUsersToGroup(self, user_ids, thread_id=None): - """ - Adds users to a group. - - :param user_ids: One or more user IDs to add - :param thread_id: Group ID to add people to. See :ref:`intro_threads` - :type user_ids: list - :return: :ref:`Message ID <intro_message_ids>` of the executed action - :raises: FBchatException if request failed - """ - thread_id, thread_type = self._getThread(thread_id, None) - data = self._getSendData(thread_id=thread_id, thread_type=ThreadType.GROUP) - - data['action_type'] = 'ma-type:log-message' - data['log_message_type'] = 'log:subscribe' - - if type(user_ids) is not list: - user_ids = [user_ids] - - # Make list of users unique - user_ids = set(user_ids) - - for i, user_id in enumerate(user_ids): - if user_id == self.uid: - raise FBchatUserError('Error when adding users: Cannot add self to group thread') - else: - data['log_message_data[added_participants][' + str(i) + ']'] = "fbid:" + str(user_id) - - return self._doSendRequest(data) - - def removeUserFromGroup(self, user_id, thread_id=None): - """ - Removes users from a group. - - :param user_id: User ID to remove - :param thread_id: Group ID to remove people from. See :ref:`intro_threads` - :raises: FBchatException if request failed - """ - - thread_id, thread_type = self._getThread(thread_id, None) - - data = { - "uid": user_id, - "tid": thread_id - } - - j = self._post(self.req_url.REMOVE_USER, data, fix_request=True, as_json=True) - - def changeThreadTitle(self, title, thread_id=None, thread_type=ThreadType.USER): - """ - Changes title of a thread. - If this is executed on a user thread, this will change the nickname of that user, effectively changing the title - - :param title: New group thread title - :param thread_id: Group ID to change title of. See :ref:`intro_threads` - :param thread_type: See :ref:`intro_threads` - :type thread_type: models.ThreadType - :raises: FBchatException if request failed - """ - - thread_id, thread_type = self._getThread(thread_id, thread_type) - - if thread_type == ThreadType.USER: - # The thread is a user, so we change the user's nickname - return self.changeNickname(title, thread_id, thread_id=thread_id, thread_type=thread_type) - else: - data = self._getSendData(thread_id=thread_id, thread_type=thread_type) - - data['action_type'] = 'ma-type:log-message' - data['log_message_data[name]'] = title - data['log_message_type'] = 'log:thread-name' - - return self._doSendRequest(data) - - def changeNickname(self, nickname, user_id, thread_id=None, thread_type=ThreadType.USER): - """ - Changes the nickname of a user in a thread - - :param nickname: New nickname - :param user_id: User that will have their nickname changed - :param thread_id: User/Group ID to change color of. See :ref:`intro_threads` - :param thread_type: See :ref:`intro_threads` - :type thread_type: models.ThreadType - :raises: FBchatException if request failed - """ - thread_id, thread_type = self._getThread(thread_id, thread_type) - - data = { - 'nickname': nickname, - 'participant_id': user_id, - 'thread_or_other_fbid': thread_id - } - - j = self._post(self.req_url.THREAD_NICKNAME, data, fix_request=True, as_json=True) - - def changeThreadColor(self, color, thread_id=None): - """ - Changes thread color - - :param color: New thread color - :param thread_id: User/Group ID to change color of. See :ref:`intro_threads` - :type color: models.ThreadColor - :raises: FBchatException if request failed - """ - thread_id, thread_type = self._getThread(thread_id, None) - - data = { - 'color_choice': color.value, - 'thread_or_other_fbid': thread_id - } - - j = self._post(self.req_url.THREAD_COLOR, data, fix_request=True, as_json=True) - - def changeThreadEmoji(self, emoji, thread_id=None): - """ - Changes thread color - - Trivia: While changing the emoji, the Facebook web client actually sends multiple different requests, though only this one is required to make the change - - :param color: New thread emoji - :param thread_id: User/Group ID to change emoji of. See :ref:`intro_threads` - :raises: FBchatException if request failed - """ - thread_id, thread_type = self._getThread(thread_id, None) - - data = { - 'emoji_choice': emoji, - 'thread_or_other_fbid': thread_id - } - - j = self._post(self.req_url.THREAD_EMOJI, data, fix_request=True, as_json=True) - - def reactToMessage(self, message_id, reaction): - """ - Reacts to a message - - :param message_id: :ref:`Message ID <intro_message_ids>` to react to - :param reaction: Reaction emoji to use - :type reaction: models.MessageReaction - :raises: FBchatException if request failed - """ - full_data = { - "doc_id": 1491398900900362, - "dpr": 1, - "variables": { - "data": { - "action": "ADD_REACTION", - "client_mutation_id": "1", - "actor_id": self.uid, - "message_id": str(message_id), - "reaction": reaction.value - } - } - } - try: - url_part = urllib.parse.urlencode(full_data) - except AttributeError: - # This is a very hacky solution for python 2 support, please suggest a better one ;) - url_part = urllib.urlencode(full_data)\ - .replace('u%27', '%27')\ - .replace('%5CU{}'.format(MessageReactionFix[reaction.value][0]), MessageReactionFix[reaction.value][1]) - - j = self._post('{}/?{}'.format(self.req_url.MESSAGE_REACTION, url_part), fix_request=True, as_json=True) - - def eventReminder(self, thread_id, time, title, location='', location_id=''): - """ - Sets an event reminder - - ..warning:: - Does not work in Python2.7 - - ..todo:: - Make this work in Python2.7 - - :param thread_id: User/Group ID to send event to. See :ref:`intro_threads` - :param time: Event time (unix time stamp) - :param title: Event title - :param location: Event location name - :param location_id: Event location ID - :raises: FBchatException if request failed - """ - full_data = { - "event_type": "EVENT", - "dpr": 1, - "event_time" : time, - "title" : title, - "thread_id" : thread_id, - "location_id" : location_id, - "location_name" : location, - "acontext": { - "action_history": [{ - "surface": "messenger_chat_tab", - "mechanism": "messenger_composer" - }] - } - } - url_part = urllib.parse.urlencode(full_data) - - j = self._post('{}/?{}'.format(self.req_url.EVENT_REMINDER, url_part), fix_request=True, as_json=True) - - - def setTypingStatus(self, status, thread_id=None, thread_type=None): - """ - Sets users typing status in a thread - - :param status: Specify the typing status - :param thread_id: User/Group ID to change status in. See :ref:`intro_threads` - :param thread_type: See :ref:`intro_threads` - :type status: models.TypingStatus - :type thread_type: models.ThreadType - :raises: FBchatException if request failed - """ - thread_id, thread_type = self._getThread(thread_id, thread_type) - - data = { - "typ": status.value, - "thread": thread_id, - "to": thread_id if thread_type == ThreadType.USER else "", - "source": "mercury-chat" - } - - j = self._post(self.req_url.TYPING, data, fix_request=True, as_json=True) - - """ - END SEND METHODS - """ - - def markAsDelivered(self, thread_id, message_id): - """ - Mark a message as delivered - - :param thread_id: User/Group ID to which the message belongs. See :ref:`intro_threads` - :param message_id: Message ID to set as delivered. See :ref:`intro_threads` - :return: Whether the request was successful - :raises: FBchatException if request failed - """ - data = { - "message_ids[0]": message_id, - "thread_ids[%s][0]" % thread_id: message_id - } - - r = self._post(self.req_url.DELIVERED, data) - return r.ok - - def markAsRead(self, thread_id): - """ - Mark a thread as read - All messages inside the thread will be marked as read - - :param thread_id: User/Group ID to set as read. See :ref:`intro_threads` - :return: Whether the request was successful - :raises: FBchatException if request failed - """ - data = { - "ids[%s]" % thread_id: 'true', - "watermarkTimestamp": now(), - "shouldSendReadReceipt": 'true', - } - - r = self._post(self.req_url.READ_STATUS, data) - return r.ok - - def markAsSeen(self): - """ - .. todo:: - Documenting this - """ - r = self._post(self.req_url.MARK_SEEN, {"seen_timestamp": 0}) - return r.ok - - def friendConnect(self, friend_id): - """ - .. todo:: - Documenting this - """ - data = { - "to_friend": friend_id, - "action": "confirm" - } - - r = self._post(self.req_url.CONNECT, data) - return r.ok - - - """ - LISTEN METHODS - """ - - def _ping(self, sticky, pool): - data = { - 'channel': self.user_channel, - 'clientid': self.client_id, - 'partition': -2, - 'cap': 0, - 'uid': self.uid, - 'sticky_token': sticky, - 'sticky_pool': pool, - 'viewer_uid': self.uid, - 'state': 'active' - } - self._get(self.req_url.PING, data, fix_request=True, as_json=False) - - def _fetchSticky(self): - """Call pull api to get sticky and pool parameter, newer api needs these parameters to work""" - - data = { - "msgs_recv": 0, - "channel": self.user_channel, - "clientid": self.client_id - } - - j = self._get(self.req_url.STICKY, data, fix_request=True, as_json=True) - - if j.get('lb_info') is None: - raise FBchatException('Missing lb_info: {}'.format(j)) - - return j['lb_info']['sticky'], j['lb_info']['pool'] - - def _pullMessage(self, sticky, pool): - """Call pull api with seq value to get message data.""" - - data = { - "msgs_recv": 0, - "sticky_token": sticky, - "sticky_pool": pool, - "clientid": self.client_id, - } - - j = self._get(ReqUrl.STICKY, data, fix_request=True, as_json=True) - - self.seq = j.get('seq', '0') - return j - - def _parseMessage(self, content): - """Get message and author name from content. May contain multiple messages in the content.""" - - if 'ms' not in content: return - - for m in content["ms"]: - mtype = m.get("type") - try: - # 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: - mid = metadata["messageId"] - author_id = str(metadata['actorFbId']) - ts = int(metadata.get("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, msg=m) - - # 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, msg=m) - - # Color change - elif delta_type == "change_thread_theme": - new_color = graphql_color_to_enum(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, msg=m) - - # 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, msg=m) - - # 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, msg=m) - - # Nickname change - elif delta_type == "change_thread_nickname": - changed_for = str(delta["untypedData"]["participant_id"]) - new_nickname = delta["untypedData"]["nickname"] - thread_id, thread_type = getThreadIdAndThreadType(metadata) - self.onNicknameChange(mid=mid, author_id=author_id, changed_for=changed_for, - new_nickname=new_nickname, - thread_id=thread_id, thread_type=thread_type, ts=ts, metadata=metadata, msg=m) - - # Message delivered - elif delta.get("class") == "DeliveryReceipt": - message_ids = delta["messageIds"] - delivered_for = str(delta.get("actorFbId") or delta["threadKey"]["otherUserFbId"]) - 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, msg=m) - - # 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, ts=delivered_ts, metadata=metadata, msg=m) - - # 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, ts=delivered_ts, metadata=delta, msg=m) - - # New message - elif delta.get("class") == "NewMessage": - mentions = [] - if delta.get('data') and delta['data'].get('prng'): - try: - mentions = [Mention(str(mention.get('i')), offset=mention.get('o'), length=mention.get('l')) for mention in parse_json(delta['data']['prng'])] - except Exception: - log.exception('An exception occured while reading attachments') - - sticker = None - attachments = [] - if delta.get('attachments'): - try: - for a in delta['attachments']: - mercury = a['mercury'] - if mercury.get('blob_attachment'): - image_metadata = a.get('imageMetadata', {}) - attach_type = mercury['blob_attachment']['__typename'] - attachment = graphql_to_attachment(mercury.get('blob_attachment', {})) - - if attach_type == ['MessageFile', 'MessageVideo', 'MessageAudio']: - # TODO: Add more data here for audio files - attachment.size = int(a['fileSize']) - attachments.append(attachment) - elif mercury.get('sticker_attachment'): - sticker = graphql_to_sticker(a['mercury']['sticker_attachment']) - elif mercury.get('extensible_attachment'): - # TODO: Add more data here for shared stuff (URLs, events and so on) - pass - except Exception: - log.exception('An exception occured while reading attachments: {}'.format(delta['attachments'])) - - if metadata and metadata.get('tags'): - emoji_size = get_emojisize_from_tags(metadata.get('tags')) - - message = Message( - text=delta.get('body'), - mentions=mentions, - emoji_size=emoji_size, - sticker=sticker, - attachments=attachments - ) - message.uid = mid - message.author = author_id - message.timestamp = ts - #message.reactions = {} - thread_id, thread_type = getThreadIdAndThreadType(metadata) - self.onMessage(mid=mid, author_id=author_id, message=delta.get('body', ''), message_object=message, - thread_id=thread_id, thread_type=thread_type, ts=ts, metadata=metadata, msg=m) - - # Unknown message type - else: - self.onUnknownMesssageType(msg=m) - - # Inbox - elif mtype == "inbox": - self.onInbox(unseen=m["unseen"], unread=m["unread"], recent_unread=m["recent_unread"], msg=m) - - # Typing - elif mtype == "typ": - author_id = str(m.get("from")) - thread_id = str(m.get("to")) - if thread_id == self.uid: - thread_type = ThreadType.USER - else: - thread_type = ThreadType.GROUP - typing_status = TypingStatus(m.get("st")) - self.onTyping(author_id=author_id, status=typing_status, thread_id=thread_id, thread_type=thread_type, msg=m) - - # Delivered - - # 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.onFriendRequest(from_id=from_id, msg=m) - - # Happens on every login - elif mtype == "qprimer": - self.onQprimer(ts=m.get("made"), msg=m) - - # Is sent before any other message - elif mtype == "deltaflow": - pass - - # Chat timestamp - elif mtype == "chatproxy-presence": - buddylist = {} - for _id in m.get('buddyList', {}): - payload = m['buddyList'][_id] - buddylist[_id] = payload.get('lat') - self.onChatTimestamp(buddylist=buddylist, msg=m) - - # Unknown message type - else: - self.onUnknownMesssageType(msg=m) - - except Exception as e: - self.onMessageError(exception=e, msg=m) - - def startListening(self): - """ - Start listening from an external event loop - - :raises: FBchatException if request failed - """ - self.listening = True - self.sticky, self.pool = self._fetchSticky() - - def doOneListen(self, markAlive=True): - """ - Does one cycle of the listening loop. - This method is useful if you want to control fbchat from an external event loop - - :param markAlive: Whether this should ping the Facebook server before running - :type markAlive: bool - :return: Whether the loop should keep running - :rtype: bool - """ - try: - if markAlive: - self._ping(self.sticky, self.pool) - content = self._pullMessage(self.sticky, self.pool) - if content: - self._parseMessage(content) - except KeyboardInterrupt: - return False - except requests.Timeout: - pass - except requests.ConnectionError: - # If the client has lost their internet connection, keep trying every 30 seconds - time.sleep(30) - except FBchatFacebookError as e: - # Fix 502 and 503 pull errors - if e.request_status_code in [502, 503]: - self.req_url.change_pull_channel() - self.startListening() - else: - raise e - except Exception as e: - return self.onListenError(exception=e) - - return True - - def stopListening(self): - """Cleans up the variables from startListening""" - self.listening = False - self.sticky, self.pool = (None, None) - - def listen(self, markAlive=True): - """ - Initializes and runs the listening loop continually - - :param markAlive: Whether this should ping the Facebook server each time the loop runs - :type markAlive: bool - """ - self.startListening() - self.onListening() - - while self.listening and self.doOneListen(markAlive): - pass - - self.stopListening() - - """ - END LISTEN METHODS - """ - - """ - EVENTS - """ - - def onLoggingIn(self, email=None): - """ - Called when the client is logging in - - :param email: The email of the client - """ - log.info("Logging in {}...".format(email)) - - def on2FACode(self): - """Called when a 2FA code is needed to progress""" - return input('Please enter your 2FA code --> ') - - def onLoggedIn(self, email=None): - """ - Called when the client is successfully logged in - - :param email: The email of the client - """ - log.info("Login of {} successful.".format(email)) - - def onListening(self): - """Called when the client is listening""" - log.info("Listening...") - - def onListenError(self, exception=None): - """ - Called when an error was encountered while listening - - :param exception: The exception that was encountered - :return: Whether the loop should keep running - """ - log.exception('Got exception while listening') - return True - - - def onMessage(self, mid=None, author_id=None, message=None, message_object=None, thread_id=None, thread_type=ThreadType.USER, ts=None, metadata=None, msg=None): - """ - Called when the client is listening, and somebody sends a message - - :param mid: The message ID - :param author_id: The ID of the author - :param message: (deprecated. Use `message_object.text` instead) - :param message_object: The message (As a `Message` object) - :param thread_id: Thread ID that the message was sent to. See :ref:`intro_threads` - :param thread_type: Type of thread that the message was sent to. See :ref:`intro_threads` - :param ts: The timestamp of the message - :param metadata: Extra metadata about the message - :param msg: A full set of the data recieved - :type message_object: models.Message - :type thread_type: models.ThreadType - """ - log.info("{} from {} in {}".format(message_object, thread_id, thread_type.name)) - - def onColorChange(self, mid=None, author_id=None, new_color=None, thread_id=None, thread_type=ThreadType.USER, ts=None, metadata=None, msg=None): - """ - Called when the client is listening, and somebody changes a thread's color - - :param mid: The action ID - :param author_id: The ID of the person who changed the color - :param new_color: The new color - :param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` - :param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads` - :param ts: A timestamp of the action - :param metadata: Extra metadata about the action - :param msg: A full set of the data recieved - :type new_color: models.ThreadColor - :type thread_type: models.ThreadType - """ - log.info("Color change from {} in {} ({}): {}".format(author_id, thread_id, thread_type.name, new_color)) - - def onEmojiChange(self, mid=None, author_id=None, new_emoji=None, thread_id=None, thread_type=ThreadType.USER, ts=None, metadata=None, msg=None): - """ - Called when the client is listening, and somebody changes a thread's emoji - - :param mid: The action ID - :param author_id: The ID of the person who changed the emoji - :param new_emoji: The new emoji - :param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` - :param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads` - :param ts: A timestamp of the action - :param metadata: Extra metadata about the action - :param msg: A full set of the data recieved - :type thread_type: models.ThreadType - """ - log.info("Emoji change from {} in {} ({}): {}".format(author_id, thread_id, thread_type.name, new_emoji)) - - def onTitleChange(self, mid=None, author_id=None, new_title=None, thread_id=None, thread_type=ThreadType.USER, ts=None, metadata=None, msg=None): - """ - Called when the client is listening, and somebody changes the title of a thread - - :param mid: The action ID - :param author_id: The ID of the person who changed the title - :param new_title: The new title - :param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` - :param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads` - :param ts: A timestamp of the action - :param metadata: Extra metadata about the action - :param msg: A full set of the data recieved - :type thread_type: models.ThreadType - """ - log.info("Title change from {} in {} ({}): {}".format(author_id, thread_id, thread_type.name, new_title)) - - def onNicknameChange(self, mid=None, author_id=None, changed_for=None, new_nickname=None, thread_id=None, thread_type=ThreadType.USER, ts=None, metadata=None, msg=None): - """ - Called when the client is listening, and somebody changes the nickname of a person - - :param mid: The action ID - :param author_id: The ID of the person who changed the nickname - :param changed_for: The ID of the person whom got their nickname changed - :param new_nickname: The new nickname - :param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` - :param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads` - :param ts: A timestamp of the action - :param metadata: Extra metadata about the action - :param msg: A full set of the data recieved - :type thread_type: models.ThreadType - """ - log.info("Nickname change from {} in {} ({}) for {}: {}".format(author_id, thread_id, thread_type.name, changed_for, new_nickname)) - - - def onMessageSeen(self, seen_by=None, thread_id=None, thread_type=ThreadType.USER, seen_ts=None, ts=None, metadata=None, msg=None): - """ - Called when the client is listening, and somebody marks a message as seen - - :param seen_by: The ID of the person who marked the message as seen - :param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` - :param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads` - :param seen_ts: A timestamp of when the person saw the message - :param ts: A timestamp of the action - :param metadata: Extra metadata about the action - :param msg: A full set of the data recieved - :type thread_type: models.ThreadType - """ - log.info("Messages seen by {} in {} ({}) at {}s".format(seen_by, thread_id, thread_type.name, seen_ts/1000)) - - def onMessageDelivered(self, msg_ids=None, delivered_for=None, thread_id=None, thread_type=ThreadType.USER, ts=None, metadata=None, msg=None): - """ - Called when the client is listening, and somebody marks messages as delivered - - :param msg_ids: The messages that are marked as delivered - :param delivered_for: The person that marked the messages as delivered - :param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` - :param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads` - :param ts: A timestamp of the action - :param metadata: Extra metadata about the action - :param msg: A full set of the data recieved - :type thread_type: models.ThreadType - """ - log.info("Messages {} delivered to {} in {} ({}) at {}s".format(msg_ids, delivered_for, thread_id, thread_type.name, ts/1000)) - - def onMarkedSeen(self, threads=None, seen_ts=None, ts=None, metadata=None, msg=None): - """ - Called when the client is listening, and the client has successfully marked threads as seen - - :param threads: The threads that were marked - :param author_id: The ID of the person who changed the emoji - :param seen_ts: A timestamp of when the threads were seen - :param ts: A timestamp of the action - :param metadata: Extra metadata about the action - :param msg: A full set of the data recieved - :type thread_type: models.ThreadType - """ - log.info("Marked messages as seen in threads {} at {}s".format([(x[0], x[1].name) for x in threads], seen_ts/1000)) - - - def onPeopleAdded(self, mid=None, added_ids=None, author_id=None, thread_id=None, ts=None, msg=None): - """ - Called when the client is listening, and somebody adds people to a group thread - - :param mid: The action ID - :param added_ids: The IDs of the people who got added - :param author_id: The ID of the person who added the people - :param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` - :param ts: A timestamp of the action - :param msg: A full set of the data recieved - """ - log.info("{} added: {}".format(author_id, ', '.join(added_ids))) - - def onPersonRemoved(self, mid=None, removed_id=None, author_id=None, thread_id=None, ts=None, msg=None): - """ - Called when the client is listening, and somebody removes a person from a group thread - - :param mid: The action ID - :param removed_id: The ID of the person who got removed - :param author_id: The ID of the person who removed the person - :param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` - :param ts: A timestamp of the action - :param msg: A full set of the data recieved - """ - log.info("{} removed: {}".format(author_id, removed_id)) - - def onFriendRequest(self, from_id=None, msg=None): - """ - Called when the client is listening, and somebody sends a friend request - - :param from_id: The ID of the person that sent the request - :param msg: A full set of the data recieved - """ - log.info("Friend request from {}".format(from_id)) - - def onInbox(self, unseen=None, unread=None, recent_unread=None, msg=None): - """ - .. todo:: - Documenting this - - :param unseen: -- - :param unread: -- - :param recent_unread: -- - :param msg: A full set of the data recieved - """ - log.info('Inbox event: {}, {}, {}'.format(unseen, unread, recent_unread)) - - def onTyping(self, author_id=None, status=None, thread_id=None, thread_type=None, msg=None): - """ - Called when the client is listening, and somebody starts or stops typing into a chat - - :param author_id: The ID of the person who sent the action - :param status: The typing status - :param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` - :param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads` - :param msg: A full set of the data recieved - :type typing_status: models.TypingStatus - :type thread_type: models.ThreadType - """ - pass - - def onQprimer(self, ts=None, msg=None): - """ - Called when the client just started listening - - :param ts: A timestamp of the action - :param msg: A full set of the data recieved - """ - pass - - def onChatTimestamp(self, buddylist=None, msg=None): - """ - Called when the client receives chat online presence update - - :param buddylist: A list of dicts with friend id and last seen timestamp - :param msg: A full set of the data recieved - """ - log.debug('Chat Timestamps received: {}'.format(buddylist)) - - def onUnknownMesssageType(self, msg=None): - """ - Called when the client is listening, and some unknown data was recieved - - :param msg: A full set of the data recieved - """ - log.debug('Unknown message received: {}'.format(msg)) - - def onMessageError(self, exception=None, msg=None): - """ - Called when an error was encountered while parsing recieved data - - :param exception: The exception that was encountered - :param msg: A full set of the data recieved - """ - log.exception('Exception in parsing of {}'.format(msg)) - - """ - END EVENTS - """ diff --git a/fbchat/get.py b/fbchat/get.py new file mode 100644 index 00000000..cff19baf --- /dev/null +++ b/fbchat/get.py @@ -0,0 +1,154 @@ +# -*- coding: UTF-8 -*- + +from __future__ import unicode_literals +from .base import Base + + +class Get(Base): + """Enables retrieving information about threads and messages""" + + def get_threads(self, limit=None): + """Retrieve the threads that the client is currently chatting with + + If ``limit`` is ``None``, the result will be a generator, otherwise the + result will be a list with maximum length of ``limit`` + + Args: + limit (int): Max. number of threads to retrieve + + Return: + List of `Thread` objects or a generator yielding `Thread` objects + """ + + def get_archived_threads(self, limit=None): + """Retrieve the client's archived threads + + ``limit`` and return values behave as in `get_messages` + """ + + def get_filtered_threads(self, limit=None): + """Retrieve the client's filtered threads + + ``limit`` and return values behave as in `get_messages` + """ + + def get_unread_threads(self, limit=None): + """Retrieve the client's unread threads + + ``limit`` and return values behave as in `get_messages` + """ + + def get_friends(self, limit=None): + """Retrieve the users that the client is friends with + + ``limit`` and return values behave as in `get_messages` + """ + + + def get_threads_from_ids(self, *thread_ids): + """Retrieve threads based on their IDs + + If ``thread_ids`` contains a single iterable, then a generator yielding + the threads is returned. + + If ``thread_ids`` is a single value or multiple values, then a list + containing the threads is returned. + + Args: + *thread_ids: One or more thread IDs to query + + Return: + `Thread` objects, in the order and format their IDs were supplied + """ + + def get_messages_from_ids(self, *message_ids): + """Retrieve messages based on their IDs + + If ``message_ids`` contains a single iterable, then a generator + yielding the messages is returned. + + If ``message_ids`` is a single value or multiple values, then a list + containing the messages is returned. + + Args: + *message_ids: One or more message IDs to query + + Return: + `Message` objects, in the order and format their IDs were supplied + """ + + + def get_messages(self, thread, limit=None): + """Retrieve messages from a thread, starting from the newest + + Like in `get_threads`, if ``limit`` is ``None``, the result will be a + generator, otherwise the result will be a list with maximum length of + ``limit`` + + Args: + thread (`Thread`): Thread to retrieve messages from + limit (int): Max. number of messages to retrieve + + Return: + List of `Message` objects or a generator yielding `Message` objects + """ + + def get_messages_before(self, message, limit=None): + """Retrieve messages *before* a specific message + + ``limit`` and return values behave as in `get_messages` + + Args: + message (`Message`): A message, indicating from which point to + retrieve messages + """ + + def get_messages_after(self, message, limit=None): + """Retrieve messages *after* a specific message + + ``limit`` and return values behave as in `get_messages` + + Args: + message (`Message`): A message, indicating from which point to + retrieve messages + """ + + + def get_thread_images(self, thread, limit=None): + """Retrieve sent images in a thread + + Like in `get_threads`, if ``limit`` is ``None``, the result will be a + generator, otherwise the result will be a list with maximum length of + ``limit`` + + Args: + thread (`Thread`): Thread to fetch images from + limit (int): Max. number of images to retrieve + + Return: + List of `ImageAttachment` objects or a generator yielding + `ImageAttachment` objects + """ + + def get_thread_files(self, thread, limit=None): + """Retrieve sent files in a thread + + ``limit`` and return values behave as in `get_messages` + + Args: + thread (`Thread`): Thread to fetch files from + + Return: + List of `FileAttachment` objects or a generator yielding + `FileAttachment` objects + """ + + def get_url(self, attachment): + """Fetch an url from an attachment, where you can download the original + + Args: + attachment (`Attachment`): The attachment to be fetched + + Return: + An url where you can download the original attachment + """ diff --git a/fbchat/graphql.py b/fbchat/graphql.py deleted file mode 100644 index 323a0467..00000000 --- a/fbchat/graphql.py +++ /dev/null @@ -1,422 +0,0 @@ -# -*- coding: UTF-8 -*- - -from __future__ import unicode_literals -import json -import re -from .models import * -from .utils import * - -# Shameless copy from https://stackoverflow.com/a/8730674 -FLAGS = re.VERBOSE | re.MULTILINE | re.DOTALL -WHITESPACE = re.compile(r'[ \t\n\r]*', FLAGS) - -class ConcatJSONDecoder(json.JSONDecoder): - def decode(self, s, _w=WHITESPACE.match): - s_len = len(s) - - objs = [] - end = 0 - while end != s_len: - obj, end = self.raw_decode(s, idx=_w(s, end).end()) - end = _w(s, end).end() - objs.append(obj) - return objs -# End shameless copy - -def graphql_color_to_enum(color): - if color is None: - return None - if len(color) == 0: - return ThreadColor.MESSENGER_BLUE - try: - return ThreadColor('#{}'.format(color[2:].lower())) - except ValueError: - raise FBchatException('Could not get ThreadColor from color: {}'.format(color)) - -def get_customization_info(thread): - if thread is None or thread.get('customization_info') is None: - return {} - info = thread['customization_info'] - - rtn = { - 'emoji': info.get('emoji'), - 'color': graphql_color_to_enum(info.get('outgoing_bubble_color')) - } - if thread.get('thread_type') in ('GROUP', 'ROOM') or thread.get('is_group_thread') or thread.get('thread_key', {}).get('thread_fbid'): - rtn['nicknames'] = {} - for k in info.get('participant_customizations', []): - rtn['nicknames'][k['participant_id']] = k.get('nickname') - elif info.get('participant_customizations'): - uid = thread.get('thread_key', {}).get('other_user_id') or thread.get('id') - pc = info['participant_customizations'] - if len(pc) > 0: - if pc[0].get('participant_id') == uid: - rtn['nickname'] = pc[0].get('nickname') - else: - rtn['own_nickname'] = pc[0].get('nickname') - if len(pc) > 1: - if pc[1].get('participant_id') == uid: - rtn['nickname'] = pc[1].get('nickname') - else: - rtn['own_nickname'] = pc[1].get('nickname') - return rtn - - -def graphql_to_sticker(s): - if not s: - return None - sticker = Sticker( - uid=s['id'] - ) - if s.get('pack'): - sticker.pack = s['pack'].get('id') - if s.get('sprite_image'): - sticker.is_animated = True - sticker.medium_sprite_image = s['sprite_image'].get('uri') - sticker.large_sprite_image = s['sprite_image_2x'].get('uri') - sticker.frames_per_row = s.get('frames_per_row') - sticker.frames_per_col = s.get('frames_per_column') - sticker.frame_rate = s.get('frame_rate') - sticker.url = s.get('url') - sticker.width = s.get('width') - sticker.height = s.get('height') - if s.get('label'): - sticker.label = s['label'] - return sticker - -def graphql_to_attachment(a): - _type = a['__typename'] - if _type in ['MessageImage', 'MessageAnimatedImage']: - return ImageAttachment( - original_extension=a.get('original_extension') or (a['filename'].split('-')[0] if a.get('filename') else None), - width=a.get('original_dimensions', {}).get('width'), - height=a.get('original_dimensions', {}).get('height'), - is_animated=_type=='MessageAnimatedImage', - thumbnail_url=a.get('thumbnail', {}).get('uri'), - preview=a.get('preview') or a.get('preview_image'), - large_preview=a.get('large_preview'), - animated_preview=a.get('animated_image'), - uid=a.get('legacy_attachment_id') - ) - elif _type == 'MessageVideo': - return VideoAttachment( - width=a.get('original_dimensions', {}).get('width'), - height=a.get('original_dimensions', {}).get('height'), - duration=a.get('playable_duration_in_ms'), - preview_url=a.get('playable_url'), - small_image=a.get('chat_image'), - medium_image=a.get('inbox_image'), - large_image=a.get('large_image'), - uid=a.get('legacy_attachment_id') - ) - elif _type == 'MessageAudio': - return AudioAttachment( - filename=a.get('filename'), - url=a.get('playable_url'), - duration=a.get('playable_duration_in_ms'), - audio_type=a.get('audio_type') - ) - elif _type == 'MessageFile': - return FileAttachment( - url=a.get('url'), - name=a.get('filename'), - is_malicious=a.get('is_malicious'), - uid=a.get('message_file_fbid') - ) - else: - return Attachment( - uid=a.get('legacy_attachment_id') - ) - -def graphql_to_message(message): - if message.get('message_sender') is None: - message['message_sender'] = {} - if message.get('message') is None: - message['message'] = {} - rtn = Message( - text=message.get('message').get('text'), - mentions=[Mention(m.get('entity', {}).get('id'), offset=m.get('offset'), length=m.get('length')) for m in message.get('message').get('ranges', [])], - emoji_size=get_emojisize_from_tags(message.get('tags_list')), - sticker=graphql_to_sticker(message.get('sticker')) - ) - rtn.uid = str(message.get('message_id')) - rtn.author = str(message.get('message_sender').get('id')) - rtn.timestamp = message.get('timestamp_precise') - if message.get('unread') is not None: - rtn.is_read = not message['unread'] - rtn.reactions = {str(r['user']['id']):MessageReaction(r['reaction']) for r in message.get('message_reactions')} - if message.get('blob_attachments') is not None: - rtn.attachments = [graphql_to_attachment(attachment) for attachment in message['blob_attachments']] - # TODO: This is still missing parsing: - # message.get('extensible_attachment') - return rtn - -def graphql_to_user(user): - if user.get('profile_picture') is None: - user['profile_picture'] = {} - c_info = get_customization_info(user) - return User( - user['id'], - url=user.get('url'), - first_name=user.get('first_name'), - last_name=user.get('last_name'), - is_friend=user.get('is_viewer_friend'), - gender=GENDERS.get(user.get('gender')), - affinity=user.get('affinity'), - nickname=c_info.get('nickname'), - color=c_info.get('color'), - emoji=c_info.get('emoji'), - own_nickname=c_info.get('own_nickname'), - photo=user['profile_picture'].get('uri'), - name=user.get('name'), - message_count=user.get('messages_count') - ) - -def graphql_to_thread(thread): - if thread['thread_type'] == 'GROUP': - return graphql_to_group(thread) - elif thread['thread_type'] == 'ONE_TO_ONE': - if thread.get('big_image_src') is None: - thread['big_image_src'] = {} - c_info = get_customization_info(thread) - participants = [node['messaging_actor'] for node in thread['all_participants']['nodes']] - user = next(p for p in participants if p['id'] == thread['thread_key']['other_user_id']) - last_message_timestamp = None - if 'last_message' in thread: - last_message_timestamp = thread['last_message']['nodes'][0]['timestamp_precise'] - - return User( - user['id'], - url=user.get('url'), - name=user.get('name'), - first_name=user.get('short_name'), - last_name=user.get('name').split(user.get('short_name'),1)[1].strip(), - is_friend=user.get('is_viewer_friend'), - gender=GENDERS.get(user.get('gender')), - affinity=user.get('affinity'), - nickname=c_info.get('nickname'), - color=c_info.get('color'), - emoji=c_info.get('emoji'), - own_nickname=c_info.get('own_nickname'), - photo=user['big_image_src'].get('uri'), - message_count=thread.get('messages_count'), - last_message_timestamp=last_message_timestamp - ) - else: - raise FBchatException('Unknown thread type: {}, with data: {}'.format(thread.get('thread_type'), thread)) - -def graphql_to_group(group): - if group.get('image') is None: - group['image'] = {} - c_info = get_customization_info(group) - last_message_timestamp = None - if 'last_message' in group: - last_message_timestamp = group['last_message']['nodes'][0]['timestamp_precise'] - return Group( - group['thread_key']['thread_fbid'], - participants=set([node['messaging_actor']['id'] for node in group['all_participants']['nodes']]), - nicknames=c_info.get('nicknames'), - color=c_info.get('color'), - emoji=c_info.get('emoji'), - photo=group['image'].get('uri'), - name=group.get('name'), - message_count=group.get('messages_count'), - last_message_timestamp=last_message_timestamp - ) - -def graphql_to_room(room): - if room.get('image') is None: - room['image'] = {} - c_info = get_customization_info(room) - return Room( - room['thread_key']['thread_fbid'], - participants=set([node['messaging_actor']['id'] for node in room['all_participants']['nodes']]), - nicknames=c_info.get('nicknames'), - color=c_info.get('color'), - emoji=c_info.get('emoji'), - photo=room['image'].get('uri'), - name=room.get('name'), - message_count=room.get('messages_count'), - admins = set([node.get('id') for node in room.get('thread_admins')]), - approval_mode = bool(room.get('approval_mode')), - approval_requests = set(node.get('id') for node in room['thread_queue_metadata'].get('approval_requests', {}).get('nodes')), - join_link = room['joinable_mode'].get('link'), - privacy_mode = bool(room.get('privacy_mode')), - ) - -def graphql_to_page(page): - if page.get('profile_picture') is None: - page['profile_picture'] = {} - if page.get('city') is None: - page['city'] = {} - return Page( - page['id'], - url=page.get('url'), - city=page.get('city').get('name'), - category=page.get('category_type'), - photo=page['profile_picture'].get('uri'), - name=page.get('name'), - message_count=page.get('messages_count') - ) - -def graphql_queries_to_json(*queries): - """ - Queries should be a list of GraphQL objects - """ - rtn = {} - for i, query in enumerate(queries): - rtn['q{}'.format(i)] = query.value - return json.dumps(rtn) - -def graphql_response_to_json(content): - content = strip_to_json(content) # Usually only needed in some error cases - try: - j = json.loads(content, cls=ConcatJSONDecoder) - except Exception: - raise FBchatException('Error while parsing JSON: {}'.format(repr(content))) - - rtn = [None]*(len(j)) - for x in j: - if 'error_results' in x: - del rtn[-1] - continue - check_json(x) - [(key, value)] = x.items() - check_json(value) - if 'response' in value: - rtn[int(key[1:])] = value['response'] - else: - rtn[int(key[1:])] = value['data'] - - log.debug(rtn) - - return rtn - -class GraphQL(object): - def __init__(self, query=None, doc_id=None, params=None): - if params is None: - params = {} - if query is not None: - self.value = { - 'priority': 0, - 'q': query, - 'query_params': params - } - elif doc_id is not None: - self.value = { - 'doc_id': doc_id, - 'query_params': params - } - else: - raise FBchatUserError('A query or doc_id must be specified') - - - FRAGMENT_USER = """ - QueryFragment User: User { - id, - name, - first_name, - last_name, - profile_picture.width(<pic_size>).height(<pic_size>) { - uri - }, - is_viewer_friend, - url, - gender, - viewer_affinity - } - """ - - FRAGMENT_GROUP = """ - QueryFragment Group: MessageThread { - name, - thread_key { - thread_fbid - }, - image { - uri - }, - is_group_thread, - all_participants { - nodes { - messaging_actor { - id - } - } - }, - customization_info { - participant_customizations { - participant_id, - nickname - }, - outgoing_bubble_color, - emoji - } - } - """ - - FRAGMENT_PAGE = """ - QueryFragment Page: Page { - id, - name, - profile_picture.width(32).height(32) { - uri - }, - url, - category_type, - city { - name - } - } - """ - - SEARCH_USER = """ - Query SearchUser(<search> = '', <limit> = 1) { - entities_named(<search>) { - search_results.of_type(user).first(<limit>) as users { - nodes { - @User - } - } - } - } - """ + FRAGMENT_USER - - SEARCH_GROUP = """ - Query SearchGroup(<search> = '', <limit> = 1, <pic_size> = 32) { - viewer() { - message_threads.with_thread_name(<search>).last(<limit>) as groups { - nodes { - @Group - } - } - } - } - """ + FRAGMENT_GROUP - - SEARCH_PAGE = """ - Query SearchPage(<search> = '', <limit> = 1) { - entities_named(<search>) { - search_results.of_type(page).first(<limit>) as pages { - nodes { - @Page - } - } - } - } - """ + FRAGMENT_PAGE - - SEARCH_THREAD = """ - Query SearchThread(<search> = '', <limit> = 1) { - entities_named(<search>) { - search_results.first(<limit>) as threads { - nodes { - __typename, - @User, - @Group, - @Page - } - } - } - } - """ + FRAGMENT_USER + FRAGMENT_GROUP + FRAGMENT_PAGE diff --git a/fbchat/listener.py b/fbchat/listener.py new file mode 100644 index 00000000..17b1c36e --- /dev/null +++ b/fbchat/listener.py @@ -0,0 +1,56 @@ +# -*- coding: UTF-8 -*- + +from __future__ import unicode_literals +from .base import Base + + +class Listener(Base): + """Enables basic listening""" + + def listen(self): + """Start listening for incoming messages or events + + When this method recieves a message/an event, it will parse the + message/event, and call the corresponding `on_` method + """ + + + def start_listen(self): + """Prepare the event listener""" + + def step_listen(self): + """Do one cycle of the listening loop. + + This method is useful if you want to control the listener from an + external event loop + """ + + def stop_listen(self): + """Cleanup the event listener""" + + + def on_error(self, exception, msg): + """Called when an error was encountered while listening + + Args: + exception (Exception): The exception that was encountered + msg (dict): Dictionary containing the full json data recieved + """ + + def on_unknown(self, msg): + """Called when some unknown data was recieved while listening + + Useful for debugging, and figuring out missing features + + Args: + msg (dict): Dictionary containing the full json data recieved + """ + + def on_raw(self, msg): + """Called when data is recieved while listening + + This method is overwritten by other classes, to parse the relevant data + + Args: + msg (dict): Dictionary containing the full json data recieved + """ diff --git a/fbchat/message_management.py b/fbchat/message_management.py new file mode 100644 index 00000000..77257991 --- /dev/null +++ b/fbchat/message_management.py @@ -0,0 +1,42 @@ +# -*- coding: UTF-8 -*- + +from __future__ import unicode_literals +from .listener import Listener + + +class MessageManagement(Listener): + """Enables the client to manage previous messages""" + + def set_reaction(self, message, reaction): + """React to a message + + If ``reaction`` is ``None``, the reaction will be removed + + Args: + message (`Message`): Message to react to + reaction: Reaction emoji to use. Can be one of 😍, 😆, 😮, 😢, + 😠, 👍, 👎 or ``None`` + """ + + def on_reaction_set(self, actor, message, old_reaction): + """Called when somebody reacts to/changes their reaction to a message + + Args: + actor (`Thread`): Person that caused the action + message (`Message`): Message that was reacted to + old_reaction: Previous reaction emoji + """ + + def remove_message(self, message): + """Remove/delete a message + + Warning: + Facebook only deletes messages for the user that is deleting them. + This means that others will still be able to view the message. + + Furthermore, this deletes the message without any further warning. + Use with caution! + + Args: + message (`Message`): Message to delete + """ diff --git a/fbchat/models.py b/fbchat/models.py deleted file mode 100644 index 947d943d..00000000 --- a/fbchat/models.py +++ /dev/null @@ -1,496 +0,0 @@ -# -*- coding: UTF-8 -*- - -from __future__ import unicode_literals -import enum - - -class FBchatException(Exception): - """Custom exception thrown by fbchat. All exceptions in the fbchat module inherits this""" - -class FBchatFacebookError(FBchatException): - #: The error code that Facebook returned - fb_error_code = None - #: The error message that Facebook returned (In the user's own language) - fb_error_message = None - #: The status code that was sent in the http response (eg. 404) (Usually only set if not successful, aka. not 200) - request_status_code = None - def __init__(self, message, fb_error_code=None, fb_error_message=None, request_status_code=None): - super(FBchatFacebookError, self).__init__(message) - """Thrown by fbchat when Facebook returns an error""" - self.fb_error_code = str(fb_error_code) - self.fb_error_message = fb_error_message - self.request_status_code = request_status_code - -class FBchatUserError(FBchatException): - """Thrown by fbchat when wrong values are entered""" - -class Thread(object): - #: The unique identifier of the thread. Can be used a `thread_id`. See :ref:`intro_threads` for more info - uid = None - #: Specifies the type of thread. Can be used a `thread_type`. See :ref:`intro_threads` for more info - type = None - #: A url to the thread's picture - photo = None - #: The name of the thread - name = None - #: Timestamp of last message - last_message_timestamp = None - #: Number of messages in the thread - message_count = None - def __init__(self, _type, uid, photo=None, name=None, last_message_timestamp=None, message_count=None): - """Represents a Facebook thread""" - self.uid = str(uid) - self.type = _type - self.photo = photo - self.name = name - self.last_message_timestamp = last_message_timestamp - self.message_count = message_count - - def __repr__(self): - return self.__unicode__() - - def __unicode__(self): - return '<{} {} ({})>'.format(self.type.name, self.name, self.uid) - - -class User(Thread): - #: The profile url - url = None - #: The users first name - first_name = None - #: The users last name - last_name = None - #: Whether the user and the client are friends - is_friend = None - #: The user's gender - gender = None - #: From 0 to 1. How close the client is to the user - affinity = None - #: The user's nickname - nickname = None - #: The clients nickname, as seen by the user - own_nickname = None - #: A :class:`ThreadColor`. The message color - color = None - #: The default emoji - emoji = None - - def __init__(self, uid, url=None, first_name=None, last_name=None, is_friend=None, gender=None, affinity=None, nickname=None, own_nickname=None, color=None, emoji=None, **kwargs): - """Represents a Facebook user. Inherits `Thread`""" - super(User, self).__init__(ThreadType.USER, uid, **kwargs) - self.url = url - self.first_name = first_name - self.last_name = last_name - self.is_friend = is_friend - self.gender = gender - self.affinity = affinity - self.nickname = nickname - self.own_nickname = own_nickname - self.color = color - self.emoji = emoji - - -class Group(Thread): - #: Unique list (set) of the group thread's participant user IDs - participants = None - #: A dict, containing user nicknames mapped to their IDs - nicknames = None - #: A :class:`ThreadColor`. The groups's message color - color = None - #: The groups's default emoji - emoji = None - - def __init__(self, uid, participants=None, nicknames=None, color=None, emoji=None, **kwargs): - """Represents a Facebook group. Inherits `Thread`""" - super(Group, self).__init__(ThreadType.GROUP, uid, **kwargs) - if participants is None: - participants = set() - self.participants = participants - if nicknames is None: - nicknames = [] - self.nicknames = nicknames - self.color = color - self.emoji = emoji - - -class Room(Group): - # Set containing user IDs of thread admins - admins = None - # True if users need approval to join - approval_mode = None - # Set containing user IDs requesting to join - approval_requests = None - # Link for joining room - join_link = None - # True is room is not discoverable - privacy_mode = None - - def __init__(self, uid, admins=None, approval_mode=None, approval_requests=None, join_link=None, privacy_mode=None, **kwargs): - """Represents a Facebook room. Inherits `Group`""" - super(Room, self).__init__(uid, **kwargs) - self.type = ThreadType.ROOM - if admins is None: - admins = set() - self.admins = admins - self.approval_mode = approval_mode - if approval_requests is None: - approval_requests = set() - self.approval_requests = approval_requests - self.join_link = join_link - self.privacy_mode = privacy_mode - - -class Page(Thread): - #: The page's custom url - url = None - #: The name of the page's location city - city = None - #: Amount of likes the page has - likes = None - #: Some extra information about the page - sub_title = None - #: The page's category - category = None - - def __init__(self, uid, url=None, city=None, likes=None, sub_title=None, category=None, **kwargs): - """Represents a Facebook page. Inherits `Thread`""" - super(Page, self).__init__(ThreadType.PAGE, uid, **kwargs) - self.url = url - self.city = city - self.likes = likes - self.sub_title = sub_title - self.category = category - - -class Message(object): - #: The actual message - text = None - #: A list of :class:`Mention` objects - mentions = None - #: A :class:`EmojiSize`. Size of a sent emoji - emoji_size = None - #: The message ID - uid = None - #: ID of the sender - author = None - #: Timestamp of when the message was sent - timestamp = None - #: Whether the message is read - is_read = None - #: A dict with user's IDs as keys, and their :class:`MessageReaction` as values - reactions = None - #: The actual message - text = None - #: A :class:`Sticker` - sticker = None - #: A list of attachments - attachments = None - - def __init__(self, text=None, mentions=None, emoji_size=None, sticker=None, attachments=None): - """Represents a Facebook message""" - self.text = text - if mentions is None: - mentions = [] - self.mentions = mentions - self.emoji_size = emoji_size - self.sticker = sticker - if attachments is None: - attachments = [] - self.attachments = attachments - self.reactions = {} - - def __repr__(self): - return self.__unicode__() - - def __unicode__(self): - return '<Message ({}): {}, mentions={} emoji_size={} attachments={}>'.format(self.uid, repr(self.text), self.mentions, self.emoji_size, self.attachments) - -class Attachment(object): - #: The attachment ID - uid = None - - def __init__(self, uid=None): - """Represents a Facebook attachment""" - self.uid = uid - -class Sticker(Attachment): - #: The sticker-pack's ID - pack = None - #: Whether the sticker is animated - is_animated = False - - # If the sticker is animated, the following should be present - #: URL to a medium spritemap - medium_sprite_image = None - #: URL to a large spritemap - large_sprite_image = None - #: The amount of frames present in the spritemap pr. row - frames_per_row = None - #: The amount of frames present in the spritemap pr. coloumn - frames_per_col = None - #: The frame rate the spritemap is intended to be played in - frame_rate = None - - #: URL to the sticker's image - url = None - #: Width of the sticker - width = None - #: Height of the sticker - height = None - #: The sticker's label/name - label = None - - def __init__(self, *args, **kwargs): - """Represents a Facebook sticker that has been sent to a Facebook thread as an attachment""" - super(Sticker, self).__init__(*args, **kwargs) - -class ShareAttachment(Attachment): - def __init__(self, **kwargs): - """Represents a shared item (eg. URL) that has been sent as a Facebook attachment - *Currently Incomplete!*""" - super(ShareAttachment, self).__init__(**kwargs) - -class FileAttachment(Attachment): - #: Url where you can download the file - url = None - #: Size of the file in bytes - size = None - #: Name of the file - name = None - #: Whether Facebook determines that this file may be harmful - is_malicious = None - - def __init__(self, url=None, size=None, name=None, is_malicious=None, **kwargs): - """Represents a file that has been sent as a Facebook attachment""" - super(FileAttachment, self).__init__(**kwargs) - self.url = url - self.size = size - self.name = name - self.is_malicious = is_malicious - -class AudioAttachment(Attachment): - #: Name of the file - filename = None - #: Url of the audio file - url = None - #: Duration of the audioclip in milliseconds - duration = None - #: Audio type - audio_type = None - - def __init__(self, filename=None, url=None, duration=None, audio_type=None, **kwargs): - """Represents an audio file that has been sent as a Facebook attachment""" - super(AudioAttachment, self).__init__(**kwargs) - self.filename = filename - self.url = url - self.duration = duration - self.audio_type = audio_type - -class ImageAttachment(Attachment): - #: The extension of the original image (eg. 'png') - original_extension = None - #: Width of original image - width = None - #: Height of original image - height = None - - #: Whether the image is animated - is_animated = None - - #: URL to a thumbnail of the image - thumbnail_url = None - - #: URL to a medium preview of the image - preview_url = None - #: Width of the medium preview image - preview_width = None - #: Height of the medium preview image - preview_height = None - - #: URL to a large preview of the image - large_preview_url = None - #: Width of the large preview image - large_preview_width = None - #: Height of the large preview image - large_preview_height = None - - #: URL to an animated preview of the image (eg. for gifs) - animated_preview_url = None - #: Width of the animated preview image - animated_preview_width = None - #: Height of the animated preview image - animated_preview_height = None - - def __init__(self, original_extension=None, width=None, height=None, is_animated=None, thumbnail_url=None, preview=None, large_preview=None, animated_preview=None, **kwargs): - """ - Represents an image that has been sent as a Facebook attachment - To retrieve the full image url, use: :func:`fbchat.Client.fetchImageUrl`, - and pass it the uid of the image attachment - """ - super(ImageAttachment, self).__init__(**kwargs) - self.original_extension = original_extension - if width is not None: - width = int(width) - self.width = width - if height is not None: - height = int(height) - self.height = height - self.is_animated = is_animated - self.thumbnail_url = thumbnail_url - - if preview is None: - preview = {} - self.preview_url = preview.get('uri') - self.preview_width = preview.get('width') - self.preview_height = preview.get('height') - - if large_preview is None: - large_preview = {} - self.large_preview_url = large_preview.get('uri') - self.large_preview_width = large_preview.get('width') - self.large_preview_height = large_preview.get('height') - - if animated_preview is None: - animated_preview = {} - self.animated_preview_url = animated_preview.get('uri') - self.animated_preview_width = animated_preview.get('width') - self.animated_preview_height = animated_preview.get('height') - -class VideoAttachment(Attachment): - #: Size of the original video in bytes - size = None - #: Width of original video - width = None - #: Height of original video - height = None - #: Length of video in milliseconds - duration = None - #: URL to very compressed preview video - preview_url = None - - #: URL to a small preview image of the video - small_image_url = None - #: Width of the small preview image - small_image_width = None - #: Height of the small preview image - small_image_height = None - - #: URL to a medium preview image of the video - medium_image_url = None - #: Width of the medium preview image - medium_image_width = None - #: Height of the medium preview image - medium_image_height = None - - #: URL to a large preview image of the video - large_image_url = None - #: Width of the large preview image - large_image_width = None - #: Height of the large preview image - large_image_height = None - - def __init__(self, size=None, width=None, height=None, duration=None, preview_url=None, small_image=None, medium_image=None, large_image=None, **kwargs): - """Represents a video that has been sent as a Facebook attachment""" - super(VideoAttachment, self).__init__(**kwargs) - self.size = size - self.width = width - self.height = height - self.duration = duration - self.preview_url = preview_url - - if small_image is None: - small_image = {} - self.small_image_url = small_image.get('uri') - self.small_image_width = small_image.get('width') - self.small_image_height = small_image.get('height') - - if medium_image is None: - medium_image = {} - self.medium_image_url = medium_image.get('uri') - self.medium_image_width = medium_image.get('width') - self.medium_image_height = medium_image.get('height') - - if large_image is None: - large_image = {} - self.large_image_url = large_image.get('uri') - self.large_image_width = large_image.get('width') - self.large_image_height = large_image.get('height') - - -class Mention(object): - #: The thread ID the mention is pointing at - thread_id = None - #: The character where the mention starts - offset = None - #: The length of the mention - length = None - - def __init__(self, thread_id, offset=0, length=10): - """Represents a @mention""" - self.thread_id = thread_id - self.offset = offset - self.length = length - - def __repr__(self): - return self.__unicode__() - - def __unicode__(self): - return '<Mention {}: offset={} length={}>'.format(self.thread_id, self.offset, self.length) - -class Enum(enum.Enum): - """Used internally by fbchat to support enumerations""" - def __repr__(self): - # For documentation: - return '{}.{}'.format(type(self).__name__, self.name) - -class ThreadType(Enum): - """Used to specify what type of Facebook thread is being used. See :ref:`intro_threads` for more info""" - USER = 1 - GROUP = 2 - PAGE = 3 - ROOM = 4 - -class ThreadLocation(Enum): - """Used to specify where a thread is located (inbox, pending, archived, other).""" - INBOX = 'INBOX' - PENDING = 'PENDING' - ARCHIVED = 'ARCHIVED' - OTHER = 'OTHER' - -class TypingStatus(Enum): - """Used to specify whether the user is typing or has stopped typing""" - STOPPED = 0 - TYPING = 1 - -class EmojiSize(Enum): - """Used to specify the size of a sent emoji""" - LARGE = '369239383222810' - MEDIUM = '369239343222814' - SMALL = '369239263222822' - -class ThreadColor(Enum): - """Used to specify a thread colors""" - 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' - -class MessageReaction(Enum): - """Used to specify a message reaction""" - LOVE = '😍' - SMILE = '😆' - WOW = '😮' - SAD = '😢' - ANGRY = '😠' - YES = '👍' - NO = '👎' diff --git a/fbchat/search.py b/fbchat/search.py new file mode 100644 index 00000000..ce3b2797 --- /dev/null +++ b/fbchat/search.py @@ -0,0 +1,69 @@ +# -*- coding: UTF-8 -*- + +from __future__ import unicode_literals +from .base import Base + + +class Search(Base): + """Enables fetching information about threads""" + + def search_for_thread(self, name, limit=None): + """Search for and retrieve threads by their name + + If ``limit`` is ``None``, the result will be a generator, otherwise the + result will be a list with maximum length of ``limit`` + + Args: + name: Name of the thread you want to search for + limit (int): The max. amount of threads to retrieve + + Return: + List of `Thread` objects or a generator yielding `Thread` objects, + ordered by relevance + """ + + def search_for_group(self, name, limit=None): + """Search for and retrieve groups by their name + + Arguments behave as in `search_for_thread` + + Return: + List of `Group` objects or a generator yielding `Group` objects, + ordered by relevance + """ + + def search_for_page(self, name, limit=None): + """Search for and retrieve pages by their name + + Arguments behave as in `search_for_thread` + + Return: + List of `Page` objects or a generator yielding `Page` objects, + ordered by relevance + """ + + def search_for_user(self, name, limit=None): + """Search for and retrieve users by their name + + Arguments behave as in `search_for_thread` + + Return: + List of `User` objects or a generator yielding `User` objects, + ordered by relevance + """ + + def search_for_messages(self, thread, text, limit=None): + """Search for and retrieve messages in a thread by their text-contents + + If ``limit`` is ``None``, the result will be a generator, otherwise the + result will be a list with maximum length of ``limit`` + + Args: + thread: Thread to search in + text: Text-content to search for + limit (int): The max. amount of messages to retrieve + + Return: + List of `Message` objects or a generator yielding `Message` + objects, ordered by relevance + """ diff --git a/fbchat/send.py b/fbchat/send.py new file mode 100644 index 00000000..f36f8e52 --- /dev/null +++ b/fbchat/send.py @@ -0,0 +1,143 @@ +# -*- coding: UTF-8 -*- + +from __future__ import unicode_literals +from .listener import Listener +from .models import Size + + +class Send(Listener): + """Enables sending various messages to threads + + Every method in this class, except `send`, `on_message` and `upload`, are + shortcuts of the aforementioned + """ + + #: Contains a list of all messages sent by the client + sent_messages = None + + def send(self, thread, message): + """Send the contents specified in a message object to a thread + + If ``message.thread``, ``message.author`` or ``message.id`` is set, + they will be ignored (Which means it's safe to send a previously + recieved message) + + Args: + thread (`Thread`): Thread to send the message to + message (`Message`): Message to send + + Return: + New `Message`, denoting the sent message + """ + + def on_message(self, message): + """Called when someone sends a message + + Args: + message (`Message`): Message that was sent + """ + + def upload(self, paths, names=None): + """Upload a set of files to Facebook, to later send them in messages + + If ``names`` is shorter than ``paths`` or ``None``, the remainding + files will get default names. If it's longer, it's truncated + + Args: + paths (list): Paths of the files to send + names (list): Names of the files + + Return: + `Attachment`, denoting the uploaded file + """ + + def send_text(self, thread, text, mentions=None): + """Send a piece of text to a thread. Shortcut of `send` + + If ``mentions`` is ``None``, it will act like an empty list + + Args: + thread (`Thread`): Thread to send the text to + text: Text to send + mentions (list): `Mention` objects + + Return: + `Message`, denoting the sent message + """ + + def on_text(self, thread, author, text, mentions): + """Called when text is recieved. Shortcut of `on_message` + + Args: + thread (`Thread`): Thread that the text was sent to + author (`Thread`): User that sent the text + text: Text that was sent + mentions (list): `Mention` objects + """ + + def send_sticker(self, thread, sticker): + """Send a sticker to a thread. Shortcut of `send` + + Args: + thread (`Thread`): Thread to send the sticker to + sticker (`Sticker`): Sticker to send + + Return: + `Message`, denoting the sent message + """ + + def on_sticker(self, thread, author, sticker): + """Called when a sticker is recieved. Shortcut of `on_message` + + Args: + thread (`Thread`): Thread that the sticker was sent to + author (`Thread`): User that sent the sticker + sticker (`Sticker`): Sticker that was sent + """ + + def send_emoji(self, thread, emoji=None, size=Size.SMALL): + """Send a emoji to a thread. Shortcut of `send` + + If ``emoji`` is ``None``, the thread's default emoji will be sent + + Args: + thread (`Thread`): Thread to send the emoji to + emoji: Emoji to send + size (`Size`): Size of the emoji + + Return: + `Message`, denoting the sent message + """ + + def on_emoji(self, thread, author, emoji, size): + """Called when an emoji is recieved. Shortcut of `on_message` + + Args: + thread (`Thread`): Thread that the emoji was sent to + author (`Thread`): User that sent the emoji + emoji: Emoji that was sent + size (`Size`): Size of the sent emoji + """ + + def send_file(self, thread, path, name=None): + """Send a file to a thread. Shortcut combination of `send` and `upload` + + Args: + thread: Thread to send the files to + path: Path to the file which should be sent + name: Name of the file which is sent + + Return: + `Message`, denoting the sent message + """ + + def on_file(self, thread, author, file): + """Called when a file is recieved. Shortcut of `on_message` + + Args: + thread (`Thread`): Thread that the file was sent to + author (`Thread`): User that sent the file + file (`Attachment`): Recieved file + """ + + # More shortcuts could be added diff --git a/fbchat/thread_control.py b/fbchat/thread_control.py new file mode 100644 index 00000000..1d984b34 --- /dev/null +++ b/fbchat/thread_control.py @@ -0,0 +1,141 @@ +# -*- coding: UTF-8 -*- + +from __future__ import unicode_literals +from .listener import Listener + + +class ThreadControl(Listener): + """Enables the client to control and listen on threads""" + + def add_users(self, thread, users): + """Add users to a group + + Args: + thread (`Thread`): Group to add the users to + users (list): `Thread` objects, denoting the users to add + """ + + def on_users_added(self, thread, actor, subjects): + """Called when users are added to a thread + + Args: + thread (`Thread`): Group that the users were added to + actor (`Thread`): Person that added the users + subjects (list): `Thread` objects, denoting the added users + """ + + def add_user(self, thread, user): + """Add a user to a group. Shortcut of `add_users` + + Args: + thread (`Thread`): Group to add the user to + user (`Thread`): User to add + """ + + def on_user_added(self, thread, actor, subject): + """Called when a user is added. Shortcut of `on_users_added` + + Args: + thread (`Thread`): Group that the user were added to + actor (`Thread`): Person that added the user + subject (`Thread`)): The added user + """ + + def remove_user(self, thread, user): + """Remove/kick a user from a group + + Args: + thread (`Thread`): Group to remove the user from + user (`Thread`): User to remove + """ + + def on_user_removed(self, thread, actor, subject): + """Called when a user is removed/kicked + + Args: + thread (`Thread`): Group that the user were removed from + actor (`Thread`): Person that removed the user + subject (`Thread`)): The removed user + """ + + + def add_admin(self, thread, user): + """Promote a user to admin in a group + + Args: + thread (`Thread`): Group to promote the user in + user (`Thread`): User to promote + """ + + def on_admin_added(self, thread, actor, subject): + """Called when a user is promoted to admin + + Args: + thread (`Thread`): Group that the user were promoted in + actor (`Thread`): Person that promoted the user + subject (`Thread`)): The promoted user + """ + + def remove_admin(self, thread, user): + """Demote a user from being an admin + + Args: + thread (`Thread`): Group to demote the user from being an admin in + user (`Thread`): User to demote + """ + + def on_admin_removed(self, thread, actor, subject): + """Called when a user is demoted as an admin + + Args: + thread (`Thread`): Group that the user were demoted in + actor (`Thread`): Person that demoted the user + subject (`Thread`)): The demoted user + """ + + + def add_thread(self, users): + """Add/create a group-thread + + Args: + users (list): `Thread` objects, denoting the users that the group + should be created with + + Return: + `Thread`, denoting the newly created group + """ + + def on_thread_added(self, thread, actor): + """Called when a new thread is added/created + + Args: + thread (`Thread`): The newly created thread + actor (`Thread`): Person that created the thread + """ + + def remove_thread(self, thread): + """Remove/delete a thread + + Warning: + Will delete the thread without any further warning. Use with + caution! + + Args: + thread (`Thread`): Thread to delete + """ + + def on_thread_removed(self, thread, actor): + """Called when a thread is removed/deleted + + Args: + thread (`Thread`): The deleted thread + actor (`Thread`): Person that deleted the thread + """ + + + def leave_thread(self, thread): + """Leave a group. Shortcut of `remove_user` + + Args: + thread (`Thread`): Group to leave + """ diff --git a/fbchat/thread_interraction.py b/fbchat/thread_interraction.py new file mode 100644 index 00000000..9ca744c9 --- /dev/null +++ b/fbchat/thread_interraction.py @@ -0,0 +1,182 @@ +# -*- coding: UTF-8 -*- + +from __future__ import unicode_literals +from .listener import Listener + + +class ThreadInterraction(Listener): + """Enables the client to interract with and listen on threads""" + + def set_image(self, thread, image): + """Set a group's image + + Args: + thread (`Thread`): Group whose image to change + image (`Image`): New image + """ + + def on_image_set(self, thread, actor, old_image): + """Called when a group's image is set + + Args: + thread (`Thread`): Group whose image was changed + actor (`Thread`): User that set the image + old_image (`Image`): The previous image + """ + + def set_title(self, thread, title=None): + """Set a group's title + + If ``title`` is ``None``, then the title is removed + + Args: + thread (`Thread`): Group whose title to change + title: New title + """ + + def on_title_set(self, thread, actor, old_title): + """Called when a group's title is set + + Args: + thread (`Thread`): Group whose title was changed + actor (`Thread`): User that set the title + old_title: The previous title + """ + + def set_nickname(self, thread, user, nickname=None): + """Set the nickname of a user in a group + + If ``nickname`` is ``None``, then the nickname is removed + + Args: + thread (`Thread`): Group in which the user's nickname will be set + user (`Thread`): User whose nickname to change + nickname: User's new nickname + """ + + def on_nickname_set(self, thread, actor, subject, old_nickname): + """Called when a user's nickname is set in a group + + Args: + thread (`Thread`): Group where the nickname was changed in + actor (`Thread`): User that set the nickname + subject (`Thread`): User whose nickname was changed + old_nickname: The previous nickname + """ + + def set_colour(self, thread, colour): + """Set a thread's colour + + Args: + thread (`Thread`): Thread whose colour to change + colour (`Colour`): New colour + """ + + def on_colour_set(self, thread, actor, old_colour): + """Called when a group's colour is set + + Args: + thread (`Thread`): Group whose colour was changed + actor (`Thread`): User that set the colour + old_colour (`Colour`): The previous colour + """ + + def set_color(self, thread, color): + """Alias of `set_colour`""" + + def on_color_set(self, thread, actor, old_color): + """Alias of `on_colour_set`""" + + def set_emoji(self, thread, emoji=None): + """Set a thread's emoji + + Args: + thread (`Thread`): Thread whose emoji to change + emoji: New emoji + """ + + def on_emoji_set(self, thread, actor, old_emoji): + """Called when a group's emoji is set + + Args: + thread (`Thread`): Group whose emoji was changed + actor (`Thread`): User that set the emoji + old_emoji: The previous emoji + """ + + + def start_typing(self, thread): + """Notify the thread that the client is currently typing + + Args: + thread (`Thread`): Thread to notify + """ + + def on_typing_started(self, thread, actor): + """Called when someone starts typing in a thread + + Args: + thread (`Thread`): Thread where the user started typing + actor (`Thread`): User that started typing + """ + + def stop_typing(self, thread): + """Notify the thread that the client is no longer typing + + Args: + thread (`Thread`): Thread to notify + """ + + def on_typing_stopped(self, thread, actor): + """Called when someone stops typing in a thread + + Args: + thread (`Thread`): Thread where the user stopped typing + actor (`Thread`): User that stopped typing + """ + + + def mark_delivered(self, thread): + """Notify the thread that the last messages have been delivered + + Args: + thread (`Thread`): Thread to notify + """ + + def on_marked_delivered(self, thread, actor): + """Called when the thread is notified that messages have been delivered + + Args: + thread (`Thread`): Thread where the messages were delivered + actor (`Thread`): User that marked the messages as delivered + """ + + def mark_read(self, thread): + """Notify the thread that the last messages have been read + + Args: + thread (`Thread`): Thread to notify + """ + + def on_marked_read(self, thread, actor): + """Called when the thread is notified that messages have been read + + Args: + thread (`Thread`): Thread where the messages were marked as read + actor (`Thread`): User that marked the messages as read + """ + + def mark_unread(self, thread): + """Notify the thread that the last messages have been marked as unread + + Args: + thread (`Thread`): Thread to notify + """ + + def on_marked_unread(self, thread, actor): + """Called when the thread is notified that messages have been unread + + Args: + thread (`Thread`): Thread where the messages were marked as unread + actor (`Thread`): User that marked the messages as unread + """ diff --git a/fbchat/thread_options.py b/fbchat/thread_options.py new file mode 100644 index 00000000..53790a17 --- /dev/null +++ b/fbchat/thread_options.py @@ -0,0 +1,78 @@ +# -*- coding: UTF-8 -*- + +from __future__ import unicode_literals +from .base import Base + + +class ThreadOptions(Base): + """Enables the client to configure threads""" + + def archive(self, thread): + """Archive a thread + + Args: + thread (`Thread`): Thread to archive + """ + + + def ignore(self, thread): + """Ignore a thread + + Args: + thread (`Thread`): Thread to ignore + """ + + def un_ignore(self, thread): + """Un-ignore a thread + + Args: + thread (`Thread`): Thread to stop ignoring + """ + + + def mute(self, thread, time=None): + """Mute a thread for a specific amount of time + + If ``time`` is ``None``, the thread will be muted indefinitely + + Args: + thread (`Thread`): Thread to mute + time (float): Amount of time to mute the thread, in seconds + """ + + def unmute(self, thread): + """Unmute a thread + + Args: + thread (`Thread`): Thread to unmute + """ + + + def mute_reactions(self, thread): + """Mute reactions in a thread + + Args: + thread (`Thread`): Thread to mute reactions in + """ + + def unmute_reactions(self, thread): + """Unmute reactions in a thread + + Args: + thread (`Thread`): Thread to unmute reactions in + """ + + + def mute_mentions(self, thread): + """Mute mentions in a thread + + Args: + thread (`Thread`): Thread to mute mentions in + """ + + def unmute_mentions(self, thread): + """Unmute mentions in a thread + + Args: + thread (`Thread`): Thread to unmute mentions in + """ diff --git a/fbchat/utils.py b/fbchat/utils.py deleted file mode 100644 index 87eef262..00000000 --- a/fbchat/utils.py +++ /dev/null @@ -1,235 +0,0 @@ -# -*- coding: UTF-8 -*- - -from __future__ import unicode_literals -import re -import json -from time import time -from random import random -import warnings -import logging -from .models import * - -try: - from urllib.parse import urlencode - basestring = (str, bytes) -except ImportError: - from urllib import urlencode - basestring = basestring - -# Python 2's `input` executes the input, whereas `raw_input` just returns the input -try: - input = raw_input -except NameError: - pass - -# Log settings -log = logging.getLogger("client") -log.setLevel(logging.DEBUG) -# Creates the console handler -handler = logging.StreamHandler() -log.addHandler(handler) - -#: Default list of user agents -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", - "Mozilla/5.0 (Windows NT 6.3; WOW64; ; NCT50_AAP285C84A1328) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1", - "Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6" -] - -LIKES = { - 'large': EmojiSize.LARGE, - 'medium': EmojiSize.MEDIUM, - 'small': EmojiSize.SMALL, - 'l': EmojiSize.LARGE, - 'm': EmojiSize.MEDIUM, - 's': EmojiSize.SMALL -} - -MessageReactionFix = { - '😍': ('0001f60d', '%F0%9F%98%8D'), - '😆': ('0001f606', '%F0%9F%98%86'), - '😮': ('0001f62e', '%F0%9F%98%AE'), - '😢': ('0001f622', '%F0%9F%98%A2'), - '😠': ('0001f620', '%F0%9F%98%A0'), - '👍': ('0001f44d', '%F0%9F%91%8D'), - '👎': ('0001f44e', '%F0%9F%91%8E') -} - - -GENDERS = { - # For standard requests - 0: 'unknown', - 1: 'female_singular', - 2: 'male_singular', - 3: 'female_singular_guess', - 4: 'male_singular_guess', - 5: 'mixed', - 6: 'neuter_singular', - 7: 'unknown_singular', - 8: 'female_plural', - 9: 'male_plural', - 10: 'neuter_plural', - 11: 'unknown_plural', - - # For graphql requests - 'UNKNOWN': 'unknown', - 'FEMALE': 'female_singular', - 'MALE': 'male_singular', - #'': 'female_singular_guess', - #'': 'male_singular_guess', - #'': 'mixed', - 'NEUTER': 'neuter_singular', - #'': 'unknown_singular', - #'': 'female_plural', - #'': 'male_plural', - #'': 'neuter_plural', - #'': 'unknown_plural', -} - -class ReqUrl(object): - """A class containing all urls used by `fbchat`""" - 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/" - UNREAD_THREADS = "https://www.facebook.com/ajax/mercury/unread_threads.php" - UNSEEN_THREADS = "https://www.facebook.com/mercury/unseen_thread_ids/" - 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-edge-chat.facebook.com/active_ping" - UPLOAD = "https://upload.facebook.com/ajax/mercury/upload.php" - 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/" - THREAD_COLOR = "https://www.facebook.com/messaging/save_thread_color/?source=thread_settings&dpr=1" - THREAD_NICKNAME = "https://www.facebook.com/messaging/save_thread_nickname/?source=thread_settings&dpr=1" - THREAD_EMOJI = "https://www.facebook.com/messaging/save_thread_emoji/?source=thread_settings&dpr=1" - MESSAGE_REACTION = "https://www.facebook.com/webgraphql/mutation" - TYPING = "https://www.facebook.com/ajax/messaging/typ.php" - GRAPHQL = "https://www.facebook.com/api/graphqlbatch/" - ATTACHMENT_PHOTO = "https://www.facebook.com/mercury/attachments/photo/" - EVENT_REMINDER = "https://www.facebook.com/ajax/eventreminder/create" - - pull_channel = 0 - - def change_pull_channel(self, channel=None): - if channel is None: - self.pull_channel = (self.pull_channel + 1) % 5 # Pull channel will be 0-4 - else: - self.pull_channel = channel - self.STICKY = "https://{}-edge-chat.facebook.com/pull".format(self.pull_channel) - self.PING = "https://{}-edge-chat.facebook.com/active_ping".format(self.pull_channel) - - -facebookEncoding = 'UTF-8' - -def now(): - return int(time()*1000) - -def strip_to_json(text): - try: - return text[text.index('{'):] - except ValueError: - raise FBchatException('No JSON object found: {}, {}'.format(repr(text), text.index('{'))) - -def get_decoded_r(r): - return get_decoded(r._content) - -def get_decoded(content): - return content.decode(facebookEncoding) - -def parse_json(content): - return json.loads(content) - -def get_json(r): - return json.loads(strip_to_json(get_decoded_r(r))) - -def digitToChar(digit): - if digit < 10: - return str(digit) - return chr(ord('a') + digit - 10) - -def str_base(number, base): - if number < 0: - return '-' + str_base(-number, base) - (d, m) = divmod(number, base) - if d > 0: - return str_base(d, base) + digitToChar(m) - return digitToChar(m) - -def generateMessageID(client_id=None): - k = now() - l = int(random() * 4294967295) - return "<{}:{}-{}@mail.projektitan.com>".format(k, l, client_id) - -def getSignatureID(): - return hex(int(random() * 2147483648)) - -def generateOfflineThreadingID(): - ret = now() - value = int(random() * 4294967295) - string = ("0000000000000000000000" + format(value, 'b'))[-22:] - msgs = format(ret, 'b') + string - return str(int(msgs, 2)) - -def check_json(j): - if j.get('error') is None: - return - if 'errorDescription' in j: - # 'errorDescription' is in the users own language! - raise FBchatFacebookError('Error #{} when sending request: {}'.format(j['error'], j['errorDescription']), fb_error_code=j['error'], fb_error_message=j['errorDescription']) - elif 'debug_info' in j['error'] and 'code' in j['error']: - raise FBchatFacebookError('Error #{} when sending request: {}'.format(j['error']['code'], repr(j['error']['debug_info'])), fb_error_code=j['error']['code'], fb_error_message=j['error']['debug_info']) - else: - raise FBchatFacebookError('Error {} when sending request'.format(j['error']), fb_error_code=j['error']) - -def check_request(r, as_json=True): - if not r.ok: - raise FBchatFacebookError('Error when sending request: Got {} response'.format(r.status_code), request_status_code=r.status_code) - - content = get_decoded_r(r) - - if content is None or len(content) == 0: - raise FBchatFacebookError('Error when sending request: Got empty response') - - if as_json: - content = strip_to_json(content) - try: - j = json.loads(content) - except ValueError: - raise FBchatFacebookError('Error while parsing JSON: {}'.format(repr(content))) - check_json(j) - return j - else: - return content - -def get_jsmods_require(j, index): - if j.get('jsmods') and j['jsmods'].get('require'): - try: - return j['jsmods']['require'][0][index][0] - except (KeyError, IndexError) as e: - log.warning('Error when getting jsmods_require: {}. Facebook might have changed protocol'.format(j)) - return None - -def get_emojisize_from_tags(tags): - if tags is None: - return None - tmp = [tag for tag in tags if tag.startswith('hot_emoji_size:')] - if len(tmp) > 0: - try: - return LIKES[tmp[0].split(':')[1]] - except (KeyError, IndexError): - log.exception('Could not determine emoji size from {} - {}'.format(tags, tmp)) - return None From 78044149174c29431b2ad71a4ecf6b1101f6e6cb Mon Sep 17 00:00:00 2001 From: Mads Marquart <madsmtm@gmail.com> Date: Tue, 10 Apr 2018 20:30:12 +0200 Subject: [PATCH 4/9] Combined all classes into a single class and readded models --- fbchat/__init__.py | 47 ++++++--- fbchat/client.py | 18 ++++ fbchat/models.py | 252 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 303 insertions(+), 14 deletions(-) create mode 100644 fbchat/client.py create mode 100644 fbchat/models.py diff --git a/fbchat/__init__.py b/fbchat/__init__.py index 329459d8..5d586817 100644 --- a/fbchat/__init__.py +++ b/fbchat/__init__.py @@ -1,29 +1,48 @@ # -*- coding: UTF-8 -*- -from __future__ import unicode_literals -from datetime import datetime -from .client import * - +"""fbchat: Facebook Chat (Messenger) for Python. +:copyright: (c) 2015 - 2018 by Taehoon Kim. +:license: BSD, see LICENSE.txt for more details. """ - fbchat - ~~~~~~ - Facebook Chat (Messenger) for Python +from __future__ import unicode_literals +import logging - :copyright: (c) 2015 by Taehoon Kim. - :license: BSD, see LICENSE for more details. -""" +from .models import * +from .base import Base +from .get import Get +from .listener import Listener +from .message_management import MessageManagement +from .search import Search +from .send import Send +from .thread_control import ThreadControl +from .thread_interraction import ThreadInterraction +from .thread_options import ThreadOptions -__copyright__ = 'Copyright 2015 - {} by Taehoon Kim'.format(datetime.now().year) -__version__ = '1.3.6' +from .client import Client + +__copyright__ = 'Copyright 2015 - 2018 by Taehoon Kim' +__version__ = '2.0.0' __license__ = 'BSD' __author__ = 'Taehoon Kim; Moreels Pieter-Jan; Mads Marquart' -__email__ = 'carpedm20@gmail.com' +__email__ = 'carpedm20@gmail.com; madsmtm@gmail.com' __source__ = 'https://github.com/carpedm20/fbchat/' __description__ = 'Facebook Chat (Messenger) for Python' __all__ = [ - 'Client', + 'Message', + 'Mention', + + 'Size', + 'Colour', + 'Color', + + 'Thread', + 'User', + 'Group', + 'Page', + + 'Client' ] diff --git a/fbchat/client.py b/fbchat/client.py new file mode 100644 index 00000000..d5977df5 --- /dev/null +++ b/fbchat/client.py @@ -0,0 +1,18 @@ +# -*- coding: UTF-8 -*- + +from __future__ import unicode_literals +from .base import Base +from .get import Get +from .listener import Listener +from .message_management import MessageManagement +from .search import Search +from .send import Send +from .thread_control import ThreadControl +from .thread_interraction import ThreadInterraction +from .thread_options import ThreadOptions + + +# Actual order here is still to be determined +class Client(ThreadOptions, ThreadInterraction, ThreadControl, Send, Search, + MessageManagement, Get, Listener, Base): + pass diff --git a/fbchat/models.py b/fbchat/models.py new file mode 100644 index 00000000..6b785cb5 --- /dev/null +++ b/fbchat/models.py @@ -0,0 +1,252 @@ +# -*- coding: UTF-8 -*- + +from __future__ import unicode_literals +import enum + + +class Enum(enum.Enum): + """Used internally by fbchat to support enumerations""" + def __repr__(self): + # For documentation: + return '{}.{}'.format(type(self).__name__, self.name) + + +class Thread(object): + """Represents a Facebook thread""" + + #: The unique identifier of the thread + id = None + #: The thread's Facebook url + url = None + #: A url to the thread's picture + image = None + #: The name of the thread + name = None + #: Timestamp of last message + last_message_timestamp = None + #: Number of messages in the thread + message_count = None + #: A dict, containing `User`\s and `Page`\s, mapped to their nicknames + nicknames = None + #: Unique list of `User`\s and `Page`\s, denoting the thread's participants + participants = None + #: The thread `Colour` + colour = None + #: Alias of :attr:`colour` + color = None + #: The thread's default emoji + emoji = None + + +class User(Thread): + """Represents a Facebook user""" + + #: The user's first name + first_name = None + #: The user's last name + last_name = None + #: Whether the user and the client are friends + is_friend = None + #: The user's gender + gender = None + #: From 0 to 1. How close the client is to the user + affinity = None + + +class Group(Thread): + """Represents a Facebook group-thread""" + + #: Unique list of `User`\s, denoting the group's admins + admins = None + + +class Page(Thread): + """Represents a Facebook page""" + + #: The name of the page's location city + city = None + #: Amount of likes that the page has + likes = None + #: Some extra information about the page + sub_title = None + #: The page's category + category = None + + +class Message(object): + """Represents a Facebook message""" + + #: The unique identifier of the message + id = None + #: The text-contents + text = None + #: A list of `Mention`\s + mentions = None + #: `Size` of a sent emoji + size = None + #: `User` or `Page`, denoting the sender + author = None + #: Float unix timestamp of when the message was sent + timestamp = None + #: Whether the message is read + is_read = None + #: A dict with `User`\s, mapped to their reaction + reactions = None + #: A `Sticker` + sticker = None + #: A list of `File`\s + files = None + #: A list of `Image`s. Subset of :attr:`files` + images = None + #: A list of `Video`s. Subset of :attr:`files` + videos = None + + +class Mention(object): + """Represents a @mention""" + + #: `Thread` that the mention is pointing at + thread = None + #: The character where the mention starts + offset = None + #: The length of the mention + length = None + + +class Attachment(object): + """""" + + #: The attachment ID + id = None + #: URL to the attachment + url = None + + +class Sticker(Attachment): + """""" + + #: The sticker-pack's ID + pack = None + #: Width of the sticker + width = None + #: Height of the sticker + height = None + #: The sticker's label/name + label = None + + +class AnimatedSticker(Sticker): + """""" + + # If the sticker is animated, the following should be present + #: URL to a medium spritemap + medium_sprite = None + #: URL to a large spritemap + large_sprite = None + #: The amount of frames present in the spritemap pr. row + frames_per_row = None + #: The amount of frames present in the spritemap pr. coloumn + frames_per_col = None + #: The frame rate the spritemap is intended to be played in + frame_rate = None + + +class File(Attachment): + """""" + + #: Url where you can download the file + url = None + #: Size of the file in bytes + size = None + #: Name of the file + name = None + #: Whether Facebook determines that this file may be harmful + is_malicious = None + + +class Audio(File): + """""" + + #: Name of the file + filename = None + #: Url of the audio file + url = None + #: Duration of the audioclip in milliseconds + duration = None + #: Audio type + audio_type = None + + +class Image(File): + """""" + + #: Width of original image + width = None + #: Height of original image + height = None + #: URL to a thumbnail of the image + thumbnail = None + #: URL to a medium preview of the image + preview = None + #: URL to a large preview of the image + large_preview = None + #: The extension of the original image (eg. 'png') + original_extension = None + + +class AnimatedImage(Image): + """""" + + #: URL to an animated preview of the image + animated_preview = None + + +class Video(File): + """""" + + #: Width of original video + width = None + #: Height of original video + height = None + #: Length of video in milliseconds + duration = None + #: URL to very compressed preview video + preview = None + #: URL to a small preview image of the video + small_image = None + #: URL to a medium preview image of the video + medium_image = None + #: URL to a large preview image of the video + large_image = None + + +class Size(Enum): + """Used to specify the size an emoji""" + SMALL = 1 + MEDIUM = 2 + LARGE = 3 + + +class Colour(Enum): + """Used to specify thread colours + + See #220 before implementing this + """ + 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' + + +Color = Colour From 416832b8b625243df067632f2ef07549079d2bfc Mon Sep 17 00:00:00 2001 From: Mads Marquart <madsmtm@gmail.com> Date: Tue, 10 Apr 2018 20:30:31 +0200 Subject: [PATCH 5/9] Properly added logger, fixes #229 --- fbchat/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/fbchat/__init__.py b/fbchat/__init__.py index 5d586817..515b2ed1 100644 --- a/fbchat/__init__.py +++ b/fbchat/__init__.py @@ -46,3 +46,13 @@ 'Client' ] + + +try: # Python 2.7+ + from logging import NullHandler +except ImportError: + class NullHandler(logging.Handler): + def emit(self, record): + pass + +logging.getLogger(__name__).addHandler(NullHandler()) From 99599814ae9b950cd0f414c283051179745a39fc Mon Sep 17 00:00:00 2001 From: Mads Marquart <madsmtm@gmail.com> Date: Tue, 10 Apr 2018 20:31:38 +0200 Subject: [PATCH 6/9] Added .rst for each new class --- docs/api.rst | 44 -------------------------------- docs/api/base.rst | 4 +++ docs/api/get.rst | 4 +++ docs/api/index.rst | 16 ++++++++++++ docs/api/listener.rst | 4 +++ docs/api/message_management.rst | 4 +++ docs/api/models.rst | 5 ++++ docs/api/search.rst | 4 +++ docs/api/send.rst | 4 +++ docs/api/thread_control.rst | 4 +++ docs/api/thread_interraction.rst | 4 +++ docs/api/thread_options.rst | 4 +++ 12 files changed, 57 insertions(+), 44 deletions(-) delete mode 100644 docs/api.rst create mode 100644 docs/api/base.rst create mode 100644 docs/api/get.rst create mode 100644 docs/api/index.rst create mode 100644 docs/api/listener.rst create mode 100644 docs/api/message_management.rst create mode 100644 docs/api/models.rst create mode 100644 docs/api/search.rst create mode 100644 docs/api/send.rst create mode 100644 docs/api/thread_control.rst create mode 100644 docs/api/thread_interraction.rst create mode 100644 docs/api/thread_options.rst diff --git a/docs/api.rst b/docs/api.rst deleted file mode 100644 index e08f19b0..00000000 --- a/docs/api.rst +++ /dev/null @@ -1,44 +0,0 @@ -.. module:: fbchat -.. highlight:: python -.. _api: - -Full API -======== - -If you are looking for information on a specific function, class, or method, this part of the documentation is for you. - - -.. _api_client: - -Client ------- - -This is the main class of `fbchat`, which contains all the methods you use to interract with Facebook. -You can extend this class, and overwrite the events, to provide custom event handling (mainly used while listening) - -.. autoclass:: Client(email, password, user_agent=None, max_tries=5, session_cookies=None, logging_level=logging.INFO) - :members: - - -.. _api_models: - -Models ------- - -These models are used in various functions, both as inputs and return values. -A good tip is to write ``from fbchat.models import *`` at the start of your source, so you can use these models freely - -.. automodule:: fbchat.models - :members: - :undoc-members: - - -.. _api_utils: - -Utils ------ - -These functions and values are used internally by fbchat, and are subject to change. Do **NOT** rely on these to be backwards compatible! - -.. automodule:: fbchat.utils - :members: diff --git a/docs/api/base.rst b/docs/api/base.rst new file mode 100644 index 00000000..3b2f2dbd --- /dev/null +++ b/docs/api/base.rst @@ -0,0 +1,4 @@ +Base class +========== + +.. autoclass:: Base() diff --git a/docs/api/get.rst b/docs/api/get.rst new file mode 100644 index 00000000..85942724 --- /dev/null +++ b/docs/api/get.rst @@ -0,0 +1,4 @@ +Retrieving information +====================== + +.. autoclass:: Get(Base) diff --git a/docs/api/index.rst b/docs/api/index.rst new file mode 100644 index 00000000..77e6dede --- /dev/null +++ b/docs/api/index.rst @@ -0,0 +1,16 @@ +API +=== + +.. toctree:: + + base + get + listener + message_management + search + send + thread_control + thread_interraction + thread_options + + models diff --git a/docs/api/listener.rst b/docs/api/listener.rst new file mode 100644 index 00000000..ecee59d5 --- /dev/null +++ b/docs/api/listener.rst @@ -0,0 +1,4 @@ +Event Listening +=============== + +.. autoclass:: Listener(Base) diff --git a/docs/api/message_management.rst b/docs/api/message_management.rst new file mode 100644 index 00000000..3d0ec6e9 --- /dev/null +++ b/docs/api/message_management.rst @@ -0,0 +1,4 @@ +Management of Messages +====================== + +.. autoclass:: MessageManagement(Listener) diff --git a/docs/api/models.rst b/docs/api/models.rst new file mode 100644 index 00000000..d9632fa3 --- /dev/null +++ b/docs/api/models.rst @@ -0,0 +1,5 @@ +Various Data Models +=================== + +.. automodule:: fbchat.models + :undoc-members: diff --git a/docs/api/search.rst b/docs/api/search.rst new file mode 100644 index 00000000..17802a07 --- /dev/null +++ b/docs/api/search.rst @@ -0,0 +1,4 @@ +Searching for information +========================= + +.. autoclass:: Search(Base) diff --git a/docs/api/send.rst b/docs/api/send.rst new file mode 100644 index 00000000..d35df6f7 --- /dev/null +++ b/docs/api/send.rst @@ -0,0 +1,4 @@ +Sending messages +================ + +.. autoclass:: Send(Listener) diff --git a/docs/api/thread_control.rst b/docs/api/thread_control.rst new file mode 100644 index 00000000..3b8d4e28 --- /dev/null +++ b/docs/api/thread_control.rst @@ -0,0 +1,4 @@ +Controlling Threads +=================== + +.. autoclass:: ThreadControl(Listener) diff --git a/docs/api/thread_interraction.rst b/docs/api/thread_interraction.rst new file mode 100644 index 00000000..0dfd015f --- /dev/null +++ b/docs/api/thread_interraction.rst @@ -0,0 +1,4 @@ +Interraction with Threads +========================= + +.. autoclass:: ThreadInterraction(Listener) diff --git a/docs/api/thread_options.rst b/docs/api/thread_options.rst new file mode 100644 index 00000000..2997ea19 --- /dev/null +++ b/docs/api/thread_options.rst @@ -0,0 +1,4 @@ +Configuring Thread Options +========================== + +.. autoclass:: ThreadOptions(Base) From 8d9c95759c760cf550bf497f5260e46e726c8a8e Mon Sep 17 00:00:00 2001 From: Mads Marquart <madsmtm@gmail.com> Date: Wed, 11 Apr 2018 13:43:55 +0200 Subject: [PATCH 7/9] Various small changes --- docs/conf.py | 2 +- examples/basic_usage.py | 6 +++--- fbchat/__init__.py | 9 ++++++--- fbchat/base.py | 3 +++ fbchat/models.py | 39 ++++++++++++++++++++++++--------------- fbchat/thread_options.py | 4 ++-- 6 files changed, 39 insertions(+), 24 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index d80d11fb..ef43c82d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -21,7 +21,7 @@ import sys sys.path.insert(0, os.path.abspath('..')) -from fbchat import __copyright__, __author__, __version__, __description__ +from fbchat import __copyright__, __author__, __version__, __description__ # noqa # -- General configuration ------------------------------------------------ diff --git a/examples/basic_usage.py b/examples/basic_usage.py index 722e64c5..b645c936 100644 --- a/examples/basic_usage.py +++ b/examples/basic_usage.py @@ -7,10 +7,10 @@ client = Client('<email>', '<password>') # Display data about you -print(client, dict(client)) +print(client, vars(client)) # Send a message to yourself -m = client.send_text(client, 'Hi me!') +message = client.send_text(client, 'Hi me!') # Display data about the sent message -print(m, dict(m)) +print(message, vars(message)) diff --git a/fbchat/__init__.py b/fbchat/__init__.py index 515b2ed1..dc325956 100644 --- a/fbchat/__init__.py +++ b/fbchat/__init__.py @@ -1,9 +1,12 @@ # -*- coding: UTF-8 -*- -"""fbchat: Facebook Chat (Messenger) for Python. +"""Facebook Chat (Messenger) for Python -:copyright: (c) 2015 - 2018 by Taehoon Kim. -:license: BSD, see LICENSE.txt for more details. +Copyright: + (c) 2015 - 2018 by Taehoon Kim + +License: + BSD, see LICENSE.txt for more details """ from __future__ import unicode_literals diff --git a/fbchat/base.py b/fbchat/base.py index 7d148177..eab25204 100644 --- a/fbchat/base.py +++ b/fbchat/base.py @@ -62,3 +62,6 @@ def set_session(self, session): Args: session (dict): A dictionay containing the session """ + + def update(self): + """Updates the cache""" diff --git a/fbchat/models.py b/fbchat/models.py index 6b785cb5..2c50c288 100644 --- a/fbchat/models.py +++ b/fbchat/models.py @@ -11,14 +11,21 @@ def __repr__(self): return '{}.{}'.format(type(self).__name__, self.name) +class FacebookError(Exception): + """Thrown by fbchat when Facebook returns an error""" + + #: The error code that Facebook returned + fb_error_code = None + #: A localized error message that Facebook returned + fb_error_message = None + + class Thread(object): - """Represents a Facebook thread""" + """Represents a Facebook chat-thread""" #: The unique identifier of the thread id = None - #: The thread's Facebook url - url = None - #: A url to the thread's picture + #: A url to the thread's thumbnail/profile picture image = None #: The name of the thread name = None @@ -39,7 +46,7 @@ class Thread(object): class User(Thread): - """Represents a Facebook user""" + """Represents a user and the chat-thread the client has with the user""" #: The user's first name first_name = None @@ -54,10 +61,12 @@ class User(Thread): class Group(Thread): - """Represents a Facebook group-thread""" + """Represents a group-thread""" #: Unique list of `User`\s, denoting the group's admins admins = None + #: The group's custom title + title = None class Page(Thread): @@ -74,7 +83,7 @@ class Page(Thread): class Message(object): - """Represents a Facebook message""" + """Represents a message""" #: The unique identifier of the message id = None @@ -114,7 +123,7 @@ class Mention(object): class Attachment(object): - """""" + """Represents a Facebook attachment""" #: The attachment ID id = None @@ -123,7 +132,7 @@ class Attachment(object): class Sticker(Attachment): - """""" + """Represents a sticker""" #: The sticker-pack's ID pack = None @@ -136,7 +145,7 @@ class Sticker(Attachment): class AnimatedSticker(Sticker): - """""" + """Represents an animated sticker""" # If the sticker is animated, the following should be present #: URL to a medium spritemap @@ -152,7 +161,7 @@ class AnimatedSticker(Sticker): class File(Attachment): - """""" + """Represents a file-attachment""" #: Url where you can download the file url = None @@ -165,7 +174,7 @@ class File(Attachment): class Audio(File): - """""" + """Represents an audio-attachment""" #: Name of the file filename = None @@ -178,7 +187,7 @@ class Audio(File): class Image(File): - """""" + """Represents an image-attachment""" #: Width of original image width = None @@ -195,14 +204,14 @@ class Image(File): class AnimatedImage(Image): - """""" + """Represents an animated image-attachment""" #: URL to an animated preview of the image animated_preview = None class Video(File): - """""" + """Represents a video-attachment""" #: Width of original video width = None diff --git a/fbchat/thread_options.py b/fbchat/thread_options.py index 53790a17..9912a811 100644 --- a/fbchat/thread_options.py +++ b/fbchat/thread_options.py @@ -22,8 +22,8 @@ def ignore(self, thread): thread (`Thread`): Thread to ignore """ - def un_ignore(self, thread): - """Un-ignore a thread + def unignore(self, thread): + """Unignore a thread Args: thread (`Thread`): Thread to stop ignoring From 4fc6680da208ab3f2be9437c5894970c993b3c5e Mon Sep 17 00:00:00 2001 From: Mads Marquart <madsmtm@gmail.com> Date: Thu, 3 May 2018 14:13:24 +0200 Subject: [PATCH 8/9] Use Google Style for attribute documentation --- fbchat/models.py | 256 +++++++++++++++++------------------------------ 1 file changed, 91 insertions(+), 165 deletions(-) diff --git a/fbchat/models.py b/fbchat/models.py index 2c50c288..5c015914 100644 --- a/fbchat/models.py +++ b/fbchat/models.py @@ -12,221 +12,147 @@ def __repr__(self): class FacebookError(Exception): - """Thrown by fbchat when Facebook returns an error""" + """Thrown by fbchat when Facebook returns an error - #: The error code that Facebook returned - fb_error_code = None - #: A localized error message that Facebook returned - fb_error_message = None + Attributes: + fb_error_code (int): The error code that Facebook returned + fb_error_message: A localized error message that Facebook returned + """ class Thread(object): - """Represents a Facebook chat-thread""" - - #: The unique identifier of the thread - id = None - #: A url to the thread's thumbnail/profile picture - image = None - #: The name of the thread - name = None - #: Timestamp of last message - last_message_timestamp = None - #: Number of messages in the thread - message_count = None - #: A dict, containing `User`\s and `Page`\s, mapped to their nicknames - nicknames = None - #: Unique list of `User`\s and `Page`\s, denoting the thread's participants - participants = None - #: The thread `Colour` - colour = None - #: Alias of :attr:`colour` - color = None - #: The thread's default emoji - emoji = None + """Represents a Facebook chat-thread + + Attributes: + id (int): The unique identifier of the thread + image: A url to the thread's thumbnail/profile picture + name: The name of the thread + last_message_timestamp: Timestamp of last message + message_count: Number of messages in the thread + nicknames: A dict, containing `User`\s and `Page`\s, mapped to their + nicknames + participants: Unique list of `User`\s and `Page`\s, denoting the + thread's participants + colour (`Colour`): The thread colour + color: Alias of :attr:`colour` + emoji: The thread's default emoji + """ class User(Thread): - """Represents a user and the chat-thread the client has with the user""" - - #: The user's first name - first_name = None - #: The user's last name - last_name = None - #: Whether the user and the client are friends - is_friend = None - #: The user's gender - gender = None - #: From 0 to 1. How close the client is to the user - affinity = None + """Represents a user and the chat-thread the client has with the user + + Attributes: + first_name: The user's first name + last_name: The user's last name + is_friend (bool): Whether the user and the client are friends + gender: The user's gender + affinity (float): From 0 to 1. How close the client is to the user + """ class Group(Thread): - """Represents a group-thread""" + """Represents a group-thread - #: Unique list of `User`\s, denoting the group's admins - admins = None - #: The group's custom title - title = None + Attributes: + admins (list): Unique list of `User`\s, denoting the group's admins + title: The group's custom title + """ class Page(Thread): - """Represents a Facebook page""" + """Represents a Facebook page - #: The name of the page's location city - city = None - #: Amount of likes that the page has - likes = None - #: Some extra information about the page - sub_title = None - #: The page's category - category = None + Attributes: + city: The name of the page's location city + likes: Amount of likes that the page has + sub_title: Some extra information about the page + category: The page's category + """ class Message(object): - """Represents a message""" - - #: The unique identifier of the message - id = None - #: The text-contents - text = None - #: A list of `Mention`\s - mentions = None - #: `Size` of a sent emoji - size = None - #: `User` or `Page`, denoting the sender - author = None - #: Float unix timestamp of when the message was sent - timestamp = None - #: Whether the message is read - is_read = None - #: A dict with `User`\s, mapped to their reaction - reactions = None - #: A `Sticker` - sticker = None - #: A list of `File`\s - files = None - #: A list of `Image`s. Subset of :attr:`files` - images = None - #: A list of `Video`s. Subset of :attr:`files` - videos = None + """Represents a message + + Attributes: + id (int): The unique identifier of the message + text: The text-contents + mentions (list of `Mention`\s): + size (`Size`): The size of a sent emoji + author (`User` or `Page`): The person who sent the message + timestamp: Unix timestamp of when the message was sent + is_read: Whether the message is read + reactions (dict): A dict with `User`\s, mapped to their reaction + sticker (`Sticker` or ``None``): + files (list of `File`\s): + images (list of `Image`\s): Subset of :attr:`files` + videos (list of `Video`\s): Subset of :attr:`files` + """ class Mention(object): - """Represents a @mention""" + """Represents a @mention - #: `Thread` that the mention is pointing at - thread = None - #: The character where the mention starts - offset = None - #: The length of the mention - length = None + Attributes: + thread (`Thread`): Person that the mention is pointing at + offset (int): The character in the message where the mention starts + length (int): The length of the mention + """ class Attachment(object): - """Represents a Facebook attachment""" + """Represents a Facebook attachment - #: The attachment ID - id = None - #: URL to the attachment - url = None + Attributes: + id (int): The attachment ID + url: URL to download the attachment + """ class Sticker(Attachment): - """Represents a sticker""" + """Represents a sticker - #: The sticker-pack's ID - pack = None - #: Width of the sticker - width = None - #: Height of the sticker - height = None - #: The sticker's label/name - label = None + Attributes: + pack (`StickerPack`): The sticker's pack + width (int): Width of the sticker + height (int): Height of the sticker + name: The sticker's label/name + """ class AnimatedSticker(Sticker): - """Represents an animated sticker""" + """Todo: This""" + - # If the sticker is animated, the following should be present - #: URL to a medium spritemap - medium_sprite = None - #: URL to a large spritemap - large_sprite = None - #: The amount of frames present in the spritemap pr. row - frames_per_row = None - #: The amount of frames present in the spritemap pr. coloumn - frames_per_col = None - #: The frame rate the spritemap is intended to be played in - frame_rate = None +class StickerPack(object): + """Todo: This""" class File(Attachment): - """Represents a file-attachment""" + """Represents a file-attachment - #: Url where you can download the file - url = None - #: Size of the file in bytes - size = None - #: Name of the file - name = None - #: Whether Facebook determines that this file may be harmful - is_malicious = None + Attributes: + size (int): Size of the file in bytes + name: Name of the file + is_malicious (bool): True if Facebook determines that this file may be + harmful + """ class Audio(File): - """Represents an audio-attachment""" - - #: Name of the file - filename = None - #: Url of the audio file - url = None - #: Duration of the audioclip in milliseconds - duration = None - #: Audio type - audio_type = None + """Todo: This""" class Image(File): - """Represents an image-attachment""" - - #: Width of original image - width = None - #: Height of original image - height = None - #: URL to a thumbnail of the image - thumbnail = None - #: URL to a medium preview of the image - preview = None - #: URL to a large preview of the image - large_preview = None - #: The extension of the original image (eg. 'png') - original_extension = None + """Todo: This""" class AnimatedImage(Image): - """Represents an animated image-attachment""" - - #: URL to an animated preview of the image - animated_preview = None + """Todo: This""" class Video(File): - """Represents a video-attachment""" - - #: Width of original video - width = None - #: Height of original video - height = None - #: Length of video in milliseconds - duration = None - #: URL to very compressed preview video - preview = None - #: URL to a small preview image of the video - small_image = None - #: URL to a medium preview image of the video - medium_image = None - #: URL to a large preview image of the video - large_image = None + """Todo: This""" class Size(Enum): From 312133bae7d814ace32480b7f3c4bdee44940c5d Mon Sep 17 00:00:00 2001 From: Mads Marquart <madsmtm@gmail.com> Date: Thu, 3 May 2018 14:14:16 +0200 Subject: [PATCH 9/9] Various small changes - We don't provide < python2.7 support, so removed the custom creation of a logging nullhandler - Added `# noqa`, to prevent a linter from complaining about a few lines - Renamed `LICENSE.txt` to `LICENSE` --- LICENSE.txt => LICENSE | 0 fbchat/__init__.py | 30 +++++++++++------------------- 2 files changed, 11 insertions(+), 19 deletions(-) rename LICENSE.txt => LICENSE (100%) diff --git a/LICENSE.txt b/LICENSE similarity index 100% rename from LICENSE.txt rename to LICENSE diff --git a/fbchat/__init__.py b/fbchat/__init__.py index dc325956..c528993b 100644 --- a/fbchat/__init__.py +++ b/fbchat/__init__.py @@ -6,7 +6,7 @@ (c) 2015 - 2018 by Taehoon Kim License: - BSD, see LICENSE.txt for more details + BSD, see LICENSE for more details """ from __future__ import unicode_literals @@ -14,15 +14,15 @@ from .models import * -from .base import Base -from .get import Get -from .listener import Listener -from .message_management import MessageManagement -from .search import Search -from .send import Send -from .thread_control import ThreadControl -from .thread_interraction import ThreadInterraction -from .thread_options import ThreadOptions +from .base import Base # noqa +from .get import Get # noqa +from .listener import Listener # noqa +from .message_management import MessageManagement # noqa +from .search import Search # noqa +from .send import Send # noqa +from .thread_control import ThreadControl # noqa +from .thread_interraction import ThreadInterraction # noqa +from .thread_options import ThreadOptions # noqa from .client import Client @@ -50,12 +50,4 @@ 'Client' ] - -try: # Python 2.7+ - from logging import NullHandler -except ImportError: - class NullHandler(logging.Handler): - def emit(self, record): - pass - -logging.getLogger(__name__).addHandler(NullHandler()) +logging.getLogger(__name__).addHandler(logging.NullHandler())