From 8dacc37ba96b7328a6a6b8481c2f06cd110e1417 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sun, 28 May 2017 21:11:16 +0200 Subject: [PATCH] More documentation work, changed `addUsersToGroup` back to taking a list of user IDs Created new README, and finished `intro` --- README.rst | 122 ++-------------- docs/_static/find-group-id.png | Bin 0 -> 54873 bytes docs/api.rst | 10 +- docs/examples.rst | 28 ++-- docs/index.rst | 21 ++- docs/intro.rst | 246 +++++++++++++++++++++++++-------- docs/testing.rst | 3 +- docs/todo.rst | 12 +- examples/fetch.py | 50 +++++++ examples/get.py | 8 -- examples/interract.py | 55 ++++++++ examples/keepbot.py | 10 +- examples/send.py | 25 ---- fbchat/client.py | 173 +++++++++++++---------- fbchat/models.py | 8 +- tests.py | 9 +- 16 files changed, 470 insertions(+), 310 deletions(-) create mode 100644 docs/_static/find-group-id.png create mode 100644 examples/fetch.py delete mode 100644 examples/get.py create mode 100644 examples/interract.py delete mode 100644 examples/send.py diff --git a/README.rst b/README.rst index 2641595..89cd2d1 100644 --- a/README.rst +++ b/README.rst @@ -1,118 +1,26 @@ -====== -fbchat -====== +fbchat: Facebook Chat (Messenger) for Python +============================================ +.. image:: docs/_static/license.svg + :target: LICENSE.txt + :alt: License: BSD -Facebook Chat (`Messenger `__) for Python. This project was inspired by `facebook-chat-api `__. +.. image:: docs/_static/python-versions.svg + :target: https://pypi.python.org/pypi/fbchat + :alt: Supported python versions: 2.7, 3.4, 3.5 and 3.6 -**No XMPP or API key is needed**. Just use your EMAIL and PASSWORD. +Facebook Chat (`Messenger `__) for Python. +This project was inspired by `facebook-chat-api `__. +**No XMPP or API key is needed**. Just use your email and password. -Installation -============ +Go to `ReadTheDocs `__ to see the full documentation, +or jump right into the code by viewing the `examples`_ -Simple: +Installation: .. code-block:: console $ pip install fbchat - -Example Login -============= - -.. code-block:: python - - import fbchat - - client = fbchat.Client('YOUR_EMAIL', 'YOUR_PASSWORD') - -Sending a Message -================= - -.. code-block:: python - - friends = client.getUsers("FRIEND'S NAME") # return a list of names - friend = friends[0] - sent = client.send(friend.uid, "Your Message") - if sent: - print("Message sent successfully!") - # IMAGES - client.sendLocalImage(friend.uid,message='',image='') # send local image - imgurl = "http://i.imgur.com/LDQ2ITV.jpg" - client.sendRemoteImage(friend.uid,message='', image=imgurl) # send image from image url - - -Getting user info from user id -============================== - -.. code-block:: python - - friend1 = client.getUsers('')[0] - friend2 = client.getUsers('')[0] - friend1_info = client.getUserInfo(friend1.uid) # returns dict with details - both_info = client.getUserInfo(friend1.uid,friend2.uid) # query both together, returns list of dicts - friend1_name = friend1_info['name'] - - -Getting last messages sent -========================== - -.. code-block:: python - - last_messages = client.getThreadInfo(friend.uid, last_n=20) - last_messages.reverse() # messages come in reversed order - - for message in last_messages: - print(message.body) - - -Example Echobot -=============== - -.. code-block:: python - - import fbchat - #subclass fbchat.Client and override required methods - class EchoBot(fbchat.Client): - - def __init__(self,email, password, debug=True, user_agent=None): - fbchat.Client.__init__(self,email, password, debug, user_agent) - - def on_message(self, mid, author_id, author_name, message, metadata): - self.markAsDelivered(author_id, mid) #mark delivered - self.markAsRead(author_id) #mark read - - print("%s said: %s"%(author_id, message)) - - #if you are not the author, echo - if str(author_id) != str(self.uid): - self.send(author_id,message) - - bot = EchoBot("", "") - bot.listen() - - -Saving session -========================== - -.. code-block:: python - - session_cookies = client.setSession() - # save session_cookies - - -Loading session -========================== - -.. code-block:: python - - client = fbchat.Client(None, None, session_cookies=session_cookies) - # OR - client.setSession(session_cookies) - - -Authors -======= - -Taehoon Kim / `@carpedm20 `__ +© Copyright 2015 - 2017 by Taehoon Kim / `@carpedm20 `__ diff --git a/docs/_static/find-group-id.png b/docs/_static/find-group-id.png new file mode 100644 index 0000000000000000000000000000000000000000..e19010d972183077e606d9342e477b4f528e0e39 GIT binary patch literal 54873 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

HU#vE>gd@qD-vr=t84s?vJmDOFeBC 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 literal 0 HcmV?d00001 diff --git a/docs/api.rst b/docs/api.rst index 8fc3a70..1290f35 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -13,10 +13,13 @@ If you are looking for information on a specific function, class, or method, thi Client ------ -.. todo:: - Write introduction text about `Client`, and add documentation for all events +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_retries=5, session_cookies=None, logging_level=logging.INFO, set_default_events=True) +.. todo:: + Add documentation for all events + +.. autoclass:: Client(email, password, user_agent=None, max_retries=5, session_cookies=None, logging_level=logging.INFO) :members: .. automethod:: sendRemoteImage(image_url, message=None, thread_id=None, thread_type=ThreadType.USER) @@ -33,6 +36,7 @@ A good tip is to write ``from fbchat.models import *`` at the start of your sour .. automodule:: fbchat.models :members: + :undoc-members: .. _api_utils: diff --git a/docs/examples.rst b/docs/examples.rst index 1371017..50f0fa3 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -7,22 +7,20 @@ Examples These are a few examples on how to use `fbchat`. Remember to swap out `` and `` for your email and password -Sending messages ----------------- +Interacting with Threads +------------------------ -This will send one of each message type to the specified thread +This will interract with the thread in every way `fbchat` supports -.. literalinclude:: ../examples/send.py - :language: python +.. literalinclude:: ../examples/interract.py -Getting information -------------------- +Fetching Information +-------------------- This will show the different ways of fetching information about users and threads -.. literalinclude:: ../examples/get.py - :language: python +.. literalinclude:: ../examples/fetch.py Echobot @@ -31,22 +29,20 @@ Echobot This will reply to any message with the same message .. literalinclude:: ../examples/echobot.py - :language: python -Remove bot +Remove Bot ---------- This will remove a user from a group if they write the message `Remove me!` .. literalinclude:: ../examples/removebot.py - :language: python -"Keep it"-bot -------------- +"Prevent changes"-Bot +--------------------- -This will prevent chat color, emoji, nicknames and chat name from being changed. It will also prevent people from being added and removed +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 - :language: python diff --git a/docs/index.rst b/docs/index.rst index 5c16436..0f083a6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,4 +1,5 @@ .. 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 @@ -16,26 +17,36 @@ Release v\ |version|. (:ref:`install`) .. image:: /_static/license.svg :target: https://github.com/carpedm20/fbchat/blob/master/LICENSE.txt + :alt: License: BSD .. generated with: https://img.shields.io/badge/python-2.7%2C%203.4%2C%203.5%2C%203.6-blue.svg .. image:: /_static/python-versions.svg :target: https://pypi.python.org/pypi/fbchat + :alt: Supported python versions: 2.7, 3.4, 3.5 and 3.6 -Facebook Chat (`Messenger `__) for Python. This project was inspired by `facebook-chat-api `__. +Facebook Chat (`Messenger `_) for Python. +This project was inspired by `facebook-chat-api `_. **No XMPP or API key is needed**. Just use your email and password. Currently `fbchat` support Python 2.7, 3.4, 3.5 and 3.6: -`fbchat` works by emulating the browser. This means doing the exact same GET/POST requests and tricking Facebook into thinking we're accessing the website normally. -Because we're doing it this way, this API requires the credentials of a Facebook account. +`fbchat` works by emulating the browser. +This means doing the exact same GET/POST requests and tricking Facebook into thinking it's accessing the website normally. +Therefore, this API requires the credentials of a Facebook account. .. warning:: - We are not responsible if your account gets banned for spammy activities such as sending lots of messages to people you don't know, sending messages very quickly, sending spammy looking URLs, logging in and out very quickly... Be responsible Facebook citizens. + We are not responsible if your account gets banned for spammy activities, + such as sending lots of messages to people you don't know, sending messages very quickly, + sending spammy looking URLs, logging in and out very quickly... Be responsible Facebook citizens. .. note:: - Facebook now has an `official API `_ for chat bots, so if you're familiar with node.js, this might be what you're looking for. + Facebook now has an `official API `_ for chat bots, + so if you're familiar with node.js, this might be what you're looking for. + +If you're already familiar with the basics of how Facebook works internally, go to :ref:`examples` to see example usage of `fbchat` + Overview -------- diff --git a/docs/intro.rst b/docs/intro.rst index 7cd1ec4..3e58bad 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -1,20 +1,138 @@ .. highlight:: python +.. module:: fbchat .. _intro: Introduction ============ -.. todo:: - Make a general introduction to `fbchat` +`fbchat` uses your email and password to communicate with the Facebook server. +That means that you should always store your password in a seperate file, in case e.g. someone looks over your shoulder while you're writing code. +You should also make sure that the file's access control is appropriately restrictive .. _intro_logging_in: -Logging in +Logging In ---------- -.. todo:: - Write something about logging in, logging out, checking login, 2FA and the event `on2FACode`, here +Simply create an instance of :class:`Client`. If you have two factor authentication enabled, type the code in the terminal prompt +(If you want to supply the code in another fasion, overwrite :func:`Client.on2FACode`):: + + from fbchat import Client + from fbchat.models import * + client = Client('', '') + +Replace ```` and ```` with your email and password respectively + +.. note:: + For ease of use then most of the code snippets in this document will assume you've already completed the login process + Though the second line, ``from fbchat.models import *``, is not strictly neccesary here, later code snippets will assume you've done this + +If you want to change how verbose `fbchat` is, change the logging level (in :class:`Client`) + +Throughout your code, if you want to check whether you are still logged in, use :func:`Client.isLoggedIn`. +An example would be to login again if you've been logged out, using :func:`Client.login`:: + + if not client.isLoggedIn(): + client.login('', '') + +When you're done using the client, and want to securely logout, use :func:`Client.logout`:: + + client.logout() + + +.. _intro_threads: + +Threads +------- + +A thread can refer to two things: A Messenger group chat or a single Facebook user + +:class:`models.ThreadType` is an enumerator with two values: ``USER`` and ``GROUP``. +These will specify whether the thread is a single user chat or a group chat. +This is required for many of `fbchat`'s functions, since Facebook differetiates between these two internally + +Searching for group chats and finding their ID is not yet possible with `fbchat`, +but searching for users is possible via. :func:`Client.getUsers`. See :ref:`intro_fetching` + +You can get your own user ID by using :any:`Client.uid` + +Getting the ID of a group chat is fairly trivial though, since you only need to navigate to ``_, +click on the group you want to find the ID of, and then read the id from the address bar. +The URL will look something like this: ``https://www.facebook.com/messages/t/1234567890``, where ``1234567890`` would be the ID of the group. +An image to illustrate this is shown below: + +.. image:: /_static/find-group-id.png + :alt: An image illustrating how to find the ID of a group + +The same method can be applied to some user accounts, though if they've set a custom URL, then you'll just see that URL instead + +Here's an snippet showing the usage of thread IDs and thread types, where ```` and ```` +corresponds to the ID of a single user, and the ID of a group respectively:: + + client.sendMessage('', thread_id='', thread_type=ThreadType.USER) + client.sendMessage('', thread_id='', thread_type=ThreadType.GROUP) + +Some functions (e.g. :func:`Client.changeThreadColor`) don't require a thread type, so in these cases you just provide the thread ID:: + + client.changeThreadColor(ThreadColor.BILOBA_FLOWER, thread_id='') + client.changeThreadColor(ThreadColor.MESSENGER_BLUE, thread_id='') + + +.. _intro_message_ids: + +Message IDs +----------- + +Every message you send on Facebook has a unique ID, and every action you do in a thread, +like changing a nickname or adding a person, has a unique ID too. + +Some of `fbchat`'s functions require these ID's, like :func:`Client.reactToMessage`, +and some of then provide this ID, like :func:`Client.sendMessage`. +This snippet shows how to send a message, and then use the returned ID to react to that message with a 😍 emoji:: + + message_id = client.sendMessage('message', thread_id=thread_id, thread_type=thread_type) + client.reactToMessage(message_id, MessageReaction.LOVE) + + +.. _intro_interacting: + +Interacting with Threads +------------------------ + +`fbchat` provides multiple functions for interacting with threads + +Most functionality works on all threads, though some things, +like adding users to and removing users from a group chat, logically only works on group chats + +The simplest way of using `fbchat` is to send a message. +The following snippet will, as you've probably already figured out, send the message `test message` to your account:: + + message_id = client.sendMessage('test message', thread_id=client.uid, thread_type=ThreadType.USER) + +You can see a full example showing all the possible thread interactions with `fbchat` by going to :ref:`examples` + + +.. _intro_fetching: + +Fetching Information +-------------------- + +You can use `fbchat` to fetch basic information like user names, profile pictures, thread names and user IDs + +You can retrieve a user's ID with :func:`Client.getUsers`. +The following snippet will search for users by their name, take the first (and most likely) user, and then get their user ID from the result:: + + users = client.getUsers('') + user = users[0] + print("User's ID: {}".format(user.uid)) + print("User's name: {}".format(user.name)) + print("User's profile picture url: {}".format(user.photo)) + print("User's main url: {}".format(user.url)) + +Since this uses Facebook's search functions, you don't have to specify the whole name, first names will usually be enough + +You can see a full example showing all the possible ways to fetch information with `fbchat` by going to :ref:`examples` .. _intro_sessions: @@ -22,65 +140,81 @@ Logging in Sessions -------- -.. todo:: - Make an introduction to and show example usage of sessions +`fbchat` provides functions to retrieve and set the session cookies. +This will enable you to store the session cookies in a seperate file, so that you don't have to login each time you start your script. +Use :func:`Client.getSession` to retrieve the cookies:: + session_cookies = client.getSession() -.. _intro_sending: +Then you can use :func:`Client.setSession`:: -Sending messages ----------------- + client.setSession(session_cookies) -.. todo:: - Make an introduction to and show example usage of how you send information +Or you can set the ``session_cookies`` on your initial login. +(If the session cookies are invalid, your email and password will be used to login instead):: -.. literalinclude:: ../examples/send.py - :language: python + client = Client('', '', session_cookies=session_cookies) - -.. _intro_fetching: - -Fetching information --------------------- - -.. todo:: - Make an introduction to and show example usage of fetching information - -.. literalinclude:: ../examples/get.py - :language: python - - -.. _intro_thread_id: - -Thread ids ----------- - -.. todo:: - Make an introduction to and show example usage of thread ids - - -.. _intro_thread_type: - -Thread types ------------- - -.. todo:: - Make an introduction to and show example usage of thread types - - -.. _intro_message_ids: - -Message ids ------------ - -.. todo:: - Make an introduction to and show example usage of message ids +.. warning:: + You session cookies can be just as valueable as you password, so store them with equal care .. _intro_events: -Events ------- +Listening & Events +------------------ -.. todo:: - Make an introduction to and show example usage of the event system +To use the listening functions `fbchat` offers (like :func:`Client.listen`), +you have to define what should be executed when certain events happen. +By default, (most) events will just be a `logging.info` statement, +meaning it will simply print information to the console when an event happens + +.. note:: + You can identify the event methods by their `on` prefix, e.g. `onMessage` + +The event actions can be changed by subclassing the :class:`Client`, and then overwriting the event methods:: + + class CustomClient(Client): + def onMessage(self, mid, author_id, message, thread_id, thread_type, ts, metadata, msg, **kwargs): + # Do something with the message here + pass + + client = CustomClient('', '') + +**Notice:** The following snippet is as equally valid as the previous one:: + + class CustomClient(Client): + def onMessage(self, message, author_id, thread_id, thread_type, **kwargs): + # Do something with the message here + pass + + client = CustomClient('', '') + +The change was in the parameters that our `onMessage` method took: ``message`` and ``author_id`` got swapped, +and ``mid``, ``ts``, ``metadata`` and ``msg`` got removed, but the function still works, since we included ``**kwargs`` + +.. note:: + Therefore, for both backwards and forwards compatability, + the API actually requires that you include ``**kwargs`` as your final argument. + +View the :ref:`examples` to see some more examples illustrating the event system + + +.. _intro_submitting: + +Submitting Issues +----------------- + +If you're having trouble with some of the snippets shown here, or you think some of the functionality is broken, +please feel free to submit an issue on `Github `_. +One side note is that you should first login with ``logging_level`` set to ``logging.DEBUG``:: + + from fbchat import Client + import logging + client = Client('', '', logging_level=logging.DEBUG) + +Then you can submit the relevant parts of this log, and detailed steps on how to reproduce + +.. warning:: + Always remove your credentials from any debug information you may provide us. + Preferably, use a test account, in case you miss anything diff --git a/docs/testing.rst b/docs/testing.rst index 1921234..a3b2f13 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -1,10 +1,11 @@ .. highlight:: sh +.. module:: fbchat .. _testing: Testing ======= -To use these tests copy ``tests/data.json`` to ``tests/my_data.json`` or type the information manually in the terminal prompts. +To use the tests, copy ``tests/data.json`` to ``tests/my_data.json`` or type the information manually in the terminal prompts. - email: Your (or a test user's) email / phone number - password: Your (or a test user's) password diff --git a/docs/todo.rst b/docs/todo.rst index 8d40721..e2d773a 100644 --- a/docs/todo.rst +++ b/docs/todo.rst @@ -1,19 +1,23 @@ .. highlight:: python +.. module:: fbchat .. _todo: Todo ==== +This page will be periodically updated to show missing features and documentation -Functionality -------------- -- Implement Client.changeThreadEmoji -- Implement Client.changeNickname +Missing Functionality +--------------------- + - Implement Client.searchForThread - This will use the graphql request API - Implement Client.searchForMessage - This will use the graphql request API +- Implement chatting with pages + - This might require a new :class:`models.ThreadType`, something like ``ThreadType.PAGE`` +- Rework `User`, `Thread` and `Message` models, and rework fething methods, to make the whole process more streamlined Documentation diff --git a/examples/fetch.py b/examples/fetch.py new file mode 100644 index 0000000..13af24e --- /dev/null +++ b/examples/fetch.py @@ -0,0 +1,50 @@ +# -*- 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.getAllUsers() + +print('user IDs: {}'.format(user.uid for user in users)) +print("user's names: {}".format(user.name for user in users)) + + +# If we have a user id, we can use `getUserInfo` to fetch a `User` object +user = client.getUserInfo('') +# We can also query both mutiple users together, which returns list of `User` objects +users = client.getUserInfo('<1st user id>', '<2nd user id>', '<3rd user id>') + +print('User INFO: {}'.format(user)) +print("User's INFO: {}".format(users)) + + +# `getUsers` 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.getUsers('')[0] + +print('user ID: {}'.format(user.uid)) +print("user's name: {}".format(user.name)) + + +# Fetches a list of all threads you're currently chatting with +threads = client.getThreadList() +# Fetches the next 10 threads +threads += client.getThreadList(start=20, length=10) + +print("Thread's INFO: {}".format(threads)) + + +# Gets the last 10 messages sent to the thread +messages = client.getThreadInfo(last_n=10, thread_id='', thread_type=ThreadType) +# Since the message come in reversed order, reverse them +messages.reverse() + +# Prints the content of all the messages +for message in messages: + print(message.body) + + +# Here should be an example of `getUnread` diff --git a/examples/get.py b/examples/get.py deleted file mode 100644 index e253e5a..0000000 --- a/examples/get.py +++ /dev/null @@ -1,8 +0,0 @@ -# -*- coding: UTF-8 -*- - -from fbchat import Client -from fbchat.models import * - -client = Client("", "") - -# This should show example usage of getAllUsers, getUsers, getUserInfo, getThreadInfo, getThreadList and getUnread \ No newline at end of file diff --git a/examples/interract.py b/examples/interract.py new file mode 100644 index 0000000..d216a67 --- /dev/null +++ b/examples/interract.py @@ -0,0 +1,55 @@ +# -*- coding: UTF-8 -*- + +from fbchat import Client +from fbchat.models import * + +client = Client("", "") + +thread_id = '1234567890' +thread_type = ThreadType.GROUP + +# Will send a message to the thread +client.sendMessage('', thread_id=thread_id, thread_type=thread_type) + +# Will send the default `like` emoji +client.sendEmoji(emoji=None, size=EmojiSize.LARGE, thread_id=thread_id, thread_type=thread_type) + +# Will send the emoji `👍` +client.sendEmoji(emoji='👍', size=EmojiSize.LARGE, thread_id=thread_id, thread_type=thread_type) + +# Will send the image located at `` +client.sendLocalImage('', message='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='This is a remote image', thread_id=thread_id, thread_type=thread_type) + + +# 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) + + # Will add the user with ID `` to the thread + client.addUsersToGroup('', thread_id=thread_id) + + # 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) + + +# Will change the nickname of the user `` to `` +client.changeNickname('', '', thread_id=thread_id, thread_type=thread_type) + +# Will change the title of the thread to `` +client.changeThreadTitle('<title>', thread_id=thread_id, thread_type=thread_type) + +# Will set the typing status of the thread to `TYPING` +client.setTypingStatus(TypingStatus.TYPING, thread_id=thread_id, thread_type=thread_type) + +# Will change the thread color to `MESSENGER_BLUE` +client.changeThreadColor(ThreadColor.MESSENGER_BLUE, thread_id=thread_id) + +# Will change the thread emoji to `👍` +client.changeThreadEmoji('👍', thread_id=thread_id) + +# Will react to a message with a 😍 emoji +client.reactToMessage('<message id>', MessageReaction.LOVE) diff --git a/examples/keepbot.py b/examples/keepbot.py index 702216c..1189f7b 100644 --- a/examples/keepbot.py +++ b/examples/keepbot.py @@ -9,12 +9,12 @@ old_thread_id = '1234567890' # Change these to match your liking old_color = ThreadColor.MESSENGER_BLUE old_emoji = '👍' -old_title = 'Old school' +old_title = 'Old group chat name' old_nicknames = { - '12345678901': 'Old School user nr. 1', - '12345678902': 'Old School user nr. 2', - '12345678903': 'Old School user nr. 3', - '12345678904': 'Old School user nr. 4' + '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): diff --git a/examples/send.py b/examples/send.py deleted file mode 100644 index 3a9791e..0000000 --- a/examples/send.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: UTF-8 -*- - -from fbchat import Client -from fbchat.models import * - -client = Client("<email>", "<password>") - -# Change these to match your thread -thread_id = '1234567890' -thread_type = ThreadType.GROUP # Or ThreadType.USER - -# This will send a message to the thread -client.sendMessage('Hey there', thread_id=thread_id, thread_type=thread_type) - -# This will send the default emoji -client.sendEmoji(emoji=None, size=EmojiSize.LARGE, thread_id=thread_id, thread_type=thread_type) - -# This will send the emoji `👍` -client.sendEmoji(emoji='👍', size=EmojiSize.LARGE, thread_id=thread_id, thread_type=thread_type) - -# This will send the image called `image.png` -client.sendLocalImage('image.png', message='This is a local image', thread_id=thread_id, thread_type=thread_type) - -# This will send the image at the url `https://example.com/image.png` -client.sendRemoteImage('https://example.com/image.png', message='This is a remote image', thread_id=thread_id, thread_type=thread_type) diff --git a/fbchat/client.py b/fbchat/client.py index a8b17bd..14ae441 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -28,12 +28,46 @@ handler = logging.StreamHandler() log.addHandler(handler) + +#: This function needs the logger +def check_request(r, check_json=True): + if not r.ok: + log.warning('Error when sending request: Got {} response'.format(r.status_code)) + return None + + content = get_decoded(r) + + if content is None or len(content) == 0: + log.warning('Error when sending request: Got empty response') + return None + + if check_json: + j = json.loads(strip_to_json(content)) + if 'error' in j: + # 'errorDescription' is in the users own language! + log.warning('Error #{} when sending request: {}'.format(j['error'], j['errorDescription'])) + return None + return j + else: + return r + + class Client(object): """A client for the Facebook Chat (Messenger). See https://fbchat.readthedocs.io for complete documentation of the API. """ + 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 a `thread_id`. See :ref:`intro_threads` for more info. + + Note: Modifying this results in undefined behaviour + """ + def __init__(self, email, password, debug=False, info_log=False, user_agent=None, max_retries=5, session_cookies=None, logging_level=logging.INFO): """Initializes and logs in the client @@ -56,7 +90,6 @@ class Client(object): self.seq = "0" self.payloadDefault = {} self.client = 'mercury' - self.listening = False self.default_thread_id = None self.default_thread_type = None self.threads = [] @@ -359,19 +392,6 @@ class Client(object): r = self._cleanPost(ReqUrl.CHECKPOINT, data) return r - def _checkRequest(self, r): - if not r.ok: - log.warning('Error when sending request: Got {} response'.format(r.status_code)) - return None - - j = get_json(r) - if 'error' in j: - # 'errorDescription' is in the users own language! - log.warning('Error #{} when sending request: {}'.format(j['error'], j['errorDescription'])) - return None - - return j - def isLoggedIn(self): """ Sends a request to Facebook to check the login status @@ -469,8 +489,8 @@ class Client(object): 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_thread_id` - :param thread_type: See :ref:`intro_thread_type` + :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 @@ -497,7 +517,7 @@ class Client(object): return given_thread_id, given_thread_type """ - GET METHODS + FETCH METHODS """ def getAllUsers(self): @@ -559,8 +579,11 @@ class Client(object): def getUserInfo(self, *user_ids): """Get user info from id. Unordered. + .. todo:: + Make this return a list of User objects + :param user_ids: One or more user ID(s) to query - :return: A raw dataset containing user information + :return: (list of) raw user data """ def fbidStrip(_fbid): @@ -587,8 +610,8 @@ class Client(object): """Get the last messages in a thread :param last_n: Number of messages to retrieve - :param thread_id: User/Group ID to retrieve from. See :ref:`intro_thread_id` - :param thread_type: See :ref:`intro_thread_type` + :param thread_id: User/Group ID to retrieve from. See :ref:`intro_threads` + :param thread_type: See :ref:`intro_threads` :type last_n: int :type thread_type: models.ThreadType :return: Dictionaries, containing message data @@ -679,19 +702,15 @@ class Client(object): # 'last_action_timestamp': 0 } - r = self._post(ReqUrl.THREAD_SYNC, form) - if not r.ok or len(r.text) == 0: - return None + j = check_request(self._post(ReqUrl.THREAD_SYNC, form)) - j = get_json(r) - result = { + return { "message_counts": j['payload']['message_counts'], "unseen_threads": j['payload']['unseen_thread_ids'] } - return result """ - END GET METHODS + END FETCH METHODS """ """ @@ -741,10 +760,10 @@ class Client(object): return data def _doSendRequest(self, data): - """Sends the data to `SendURL`, and returns the message id""" + """Sends the data to `SendURL`, and returns the message id or None on failure""" r = self._post(ReqUrl.SEND, data) - j = self._checkRequest(r) + j = check_request(r) if j is None: return None @@ -782,10 +801,10 @@ class Client(object): Sends a message to a thread :param message: Message to send - :param thread_id: User/Group ID to send to. See :ref:`intro_thread_id` - :param thread_type: See :ref:`intro_thread_type` + :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 message + :return: :ref:`Message ID <intro_message_ids>` of the sent message or `None` on failure """ thread_id, thread_type = self._getThread(thread_id, thread_type) data = self._getSendData(thread_id, thread_type) @@ -802,13 +821,13 @@ class Client(object): """ Sends an emoji to a thread - :param emoji: The chosen emoji to send. If not specified, the thread's default emoji is sent + :param emoji: The chosen emoji to send. If not specified, the default `like` emoji is sent :param size: If not specified, a small emoji is sent - :param thread_id: User/Group ID to send to. See :ref:`intro_thread_id` - :param thread_type: See :ref:`intro_thread_type` + :param thread_id: User/Group ID to send to. See :ref:`intro_threads` + :param thread_type: See :ref:`intro_threads` :type size: models.EmojiSize :type thread_type: models.ThreadType - :return: :ref:`Message ID <intro_message_ids>` of the sent emoji + :return: :ref:`Message ID <intro_message_ids>` of the sent emoji or `None` on failure """ thread_id, thread_type = self._getThread(thread_id, thread_type) data = self._getSendData(thread_id, thread_type) @@ -844,14 +863,14 @@ class Client(object): def sendImage(self, image_id, message=None, thread_id=None, thread_type=ThreadType.USER): """ - Sends an already uploaded image to a thread. (Used by :any:`Client.sendRemoteImage` and :any:`Client.sendLocalImage`) + Sends an already uploaded image to a thread. (Used by :func:`Client.sendRemoteImage` and :func:`Client.sendLocalImage`) :param image_id: ID of an image that's already uploaded to Facebook :param message: Additional message - :param thread_id: User/Group ID to send to. See :ref:`intro_thread_id` - :param thread_type: See :ref:`intro_thread_type` + :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 + :return: :ref:`Message ID <intro_message_ids>` of the sent image or `None` on failure """ thread_id, thread_type = self._getThread(thread_id, thread_type) data = self._getSendData(thread_id, thread_type) @@ -873,10 +892,10 @@ class Client(object): :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_thread_id` - :param thread_type: See :ref:`intro_thread_type` + :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 + :return: :ref:`Message ID <intro_message_ids>` of the sent image or `None` on failure """ if recipient_id is not None: deprecation('sendRemoteImage(recipient_id)', deprecated_in='0.10.2', removed_in='0.15.0', details='Use sendRemoteImage(thread_id) instead') @@ -902,10 +921,10 @@ class Client(object): :param image_path: URL of an image to upload and send :param message: Additional message - :param thread_id: User/Group ID to send to. See :ref:`intro_thread_id` - :param thread_type: See :ref:`intro_thread_type` + :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 + :return: :ref:`Message ID <intro_message_ids>` of the sent image or `None` on failure """ if recipient_id is not None: deprecation('sendLocalImage(recipient_id)', deprecated_in='0.10.2', removed_in='0.15.0', details='Use sendLocalImage(thread_id) instead') @@ -922,14 +941,14 @@ class Client(object): 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) - def addUsersToGroup(self, *user_ids, thread_id=None): + def addUsersToGroup(self, user_ids, thread_id=None): """ Adds users to a group. - :param user_ids: User ids to add - :param thread_id: Group ID to add people to. See :ref:`intro_thread_id` - :type user_ids: list of positional arguments - :return: :ref:`Message ID <intro_message_ids>` of the sent "message" + :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 or `None` on failure """ thread_id, thread_type = self._getThread(thread_id, None) data = self._getSendData(thread_id, ThreadType.GROUP) @@ -937,9 +956,14 @@ class Client(object): 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) + print(user_ids) + for i, user_id in enumerate(user_ids): if user_id == self.uid: log.warning('Error when adding users: Cannot add self to group chat') @@ -955,8 +979,9 @@ class Client(object): Removes users from a group. :param user_id: User ID to remove - :param thread_id: Group ID to remove people from. See :ref:`intro_thread_id` - :return: :ref:`Message ID <intro_message_ids>` of the sent "message" + :param thread_id: Group ID to remove people from. See :ref:`intro_threads` + :return: True if the action was successful + :rtype: bool """ thread_id, thread_type = self._getThread(thread_id, None) @@ -966,7 +991,7 @@ class Client(object): "tid": thread_id } - j = self._checkRequest(self._post(ReqUrl.REMOVE_USER, data)) + j = check_request(self._post(ReqUrl.REMOVE_USER, data)) return False if j is None else True @@ -982,11 +1007,12 @@ class Client(object): def changeThreadTitle(self, title, thread_id=None, thread_type=ThreadType.USER): """ - Changes title of a thread + 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 chat title - :param thread_id: Group ID to change title of. See :ref:`intro_thread_id` - :param thread_type: See :ref:`intro_thread_type` + :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 :return: True if the action was successful :rtype: bool @@ -1018,8 +1044,8 @@ class Client(object): :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_thread_id` - :param thread_type: See :ref:`intro_thread_type` + :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 :return: True if the action was successful :rtype: bool @@ -1032,7 +1058,7 @@ class Client(object): 'thread_or_other_fbid': thread_id } - j = self._checkRequest(self._post(ReqUrl.THREAD_NICKNAME, data)) + j = check_request(self._post(ReqUrl.THREAD_NICKNAME, data)) return False if j is None else True @@ -1041,7 +1067,7 @@ class Client(object): Changes thread color :param color: New thread color - :param thread_id: User/Group ID to change color of. See :ref:`intro_thread_id` + :param thread_id: User/Group ID to change color of. See :ref:`intro_threads` :type color: models.ThreadColor :return: True if the action was successful :rtype: bool @@ -1053,7 +1079,7 @@ class Client(object): 'thread_or_other_fbid': thread_id } - j = self._checkRequest(self._post(ReqUrl.THREAD_COLOR, data)) + j = check_request(self._post(ReqUrl.THREAD_COLOR, data)) return False if j is None else True @@ -1064,7 +1090,7 @@ class Client(object): 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_thread_id` + :param thread_id: User/Group ID to change emoji of. See :ref:`intro_threads` :return: True if the action was successful :rtype: bool """ @@ -1075,7 +1101,7 @@ class Client(object): 'thread_or_other_fbid': thread_id } - j = self._checkRequest(self._post(ReqUrl.THREAD_EMOJI, data)) + j = check_request(self._post(ReqUrl.THREAD_EMOJI, data)) return False if j is None else True @@ -1110,7 +1136,7 @@ class Client(object): .replace('u%27', '%27')\ .replace('%5CU{}'.format(MessageReactionFix[reaction.value][0]), MessageReactionFix[reaction.value][1]) - j = self._checkRequest(self._post('{}/?{}'.format(ReqUrl.MESSAGE_REACTION, url_part))) + j = check_request(self._post('{}/?{}'.format(ReqUrl.MESSAGE_REACTION, url_part))) return False if j is None else True @@ -1119,8 +1145,8 @@ class Client(object): 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_thread_id` - :param thread_type: See :ref:`intro_thread_type` + :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 :return: True if the action was successful @@ -1135,7 +1161,7 @@ class Client(object): "source": "mercury-chat" } - j = self._checkRequest(self._post(ReqUrl.TYPING, data)) + j = check_request(self._post(ReqUrl.TYPING, data)) return False if j is None else True @@ -1195,11 +1221,7 @@ class Client(object): r = self._post(ReqUrl.CONNECT, data) return r.ok - def ping(self, sticky): - """ - .. todo:: - Documenting this - """ + def _ping(self, sticky): data = { 'channel': self.user_channel, 'clientid': self.client_id, @@ -1209,8 +1231,7 @@ class Client(object): 'sticky': sticky, 'viewer_uid': self.uid } - r = self._get(ReqUrl.PING, data) - return r.ok + return False if check_request(self._get(ReqUrl.PING, data), check_json=False) is None else True def _getSticky(self): """Call pull api to get sticky and pool parameter, @@ -1435,7 +1456,7 @@ class Client(object): :rtype: bool """ try: - if markAlive: self.ping(self.sticky) + if markAlive: self._ping(self.sticky) try: content = self._pullMessage(self.sticky, self.pool) if content: self._parseMessage(content) diff --git a/fbchat/models.py b/fbchat/models.py index a708a67..657cc4e 100644 --- a/fbchat/models.py +++ b/fbchat/models.py @@ -6,7 +6,7 @@ import enum class User(object): """Represents a Facebook User""" - #: The unique identifier of the user. Can be used a `thread_id`. See :ref:`intro_thread_id` for more info + #: The unique identifier of the user. Can be used a `thread_id`. See :ref:`intro_threads` for more info uid = None #: Currently always set to `user`. Might change in the future type = 'user' @@ -75,10 +75,12 @@ class User(object): } class Thread(object): - def __init__(self, **entries): + """Represents a thread. Currently just acts as a dict""" + def __init__(self, **entries): self.__dict__.update(entries) class Message(object): + """Represents a message. Currently just acts as a dict""" def __init__(self, **entries): self.__dict__.update(entries) @@ -89,7 +91,7 @@ class Enum(enum.Enum): return '{}.{}'.format(type(self).__name__, self.name) class ThreadType(Enum): - """Used to specify what type of Facebook thread is being used. See :ref:`intro_thread_type` for more info""" + """Used to specify what type of Facebook thread is being used. See :ref:`intro_threads` for more info""" USER = 1 GROUP = 2 diff --git a/tests.py b/tests.py index 1b83a56..001e178 100644 --- a/tests.py +++ b/tests.py @@ -15,6 +15,13 @@ import py_compile logging_level = logging.ERROR +""" + +Testing script for `fbchat`. +Full documentation on https://fbchat.readthedocs.io/ + +""" + class CustomClient(Client): def __init__(self, *args, **kwargs): self.got_qprimer = False @@ -168,8 +175,8 @@ class TestFbchat(unittest.TestCase): def test_setTypingStatus(self): self.assertTrue(client.sendMessage('Hi', thread_id=user_uid, thread_type=ThreadType.USER)) self.assertTrue(client.setTypingStatus(TypingStatus.TYPING, thread_id=user_uid, thread_type=ThreadType.USER)) - self.assertTrue(client.setTypingStatus(TypingStatus.TYPING, thread_id=group_uid, thread_type=ThreadType.GROUP)) self.assertTrue(client.setTypingStatus(TypingStatus.STOPPED, thread_id=user_uid, thread_type=ThreadType.USER)) + self.assertTrue(client.setTypingStatus(TypingStatus.TYPING, thread_id=group_uid, thread_type=ThreadType.GROUP)) self.assertTrue(client.setTypingStatus(TypingStatus.STOPPED, thread_id=group_uid, thread_type=ThreadType.GROUP))