diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index c32f920..0cd5c08 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -3,36 +3,40 @@ Contributing to ``fbchat`` Thanks for reading this, all contributions are very much welcome! -Please be aware that ``fbchat`` uses `Scemantic Versioning `__ +Please be aware that ``fbchat`` uses `Scemantic Versioning `__ quite rigorously! That means that if you're submitting a breaking change, it will probably take a while before it gets considered. -In that case, you can point your PR to the ``2.0.0-dev`` branch, where the API is being properly developed. -Otherwise, just point it to ``master``. - Development Environment ----------------------- -You can use `flit` to install the package as a symlink: +This project uses ``flit`` to configure development environments. You can install it using: -.. code-block:: +.. code-block:: sh + $ pip install flit + +And now you can install ``fbchat`` as a symlink: + +.. code-block:: sh + + $ git clone https://github.com/carpedm20/fbchat.git + $ cd fbchat $ # *nix: $ flit install --symlink $ # Windows: $ flit install --pth-file +This will also install required development tools like ``black``, ``pytest`` and ``sphinx``. + After that, you can ``import`` the module as normal. -Before committing, you should run ``black .`` in the main directory, to format your code. +Checklist +--------- -Testing Environment -------------------- +Once you're done with your work, please follow the steps below: -The tests use `pytest `__, and to work they need two Facebook accounts, and a group thread between these. -To set these up, you should export the following environment variables: - -``client1_email``, ``client1_password``, ``client2_email``, ``client2_password`` and ``group_id`` - -If you're not able to do this, consider simply running ``pytest -m offline``. - -And if you're adding new functionality, if possible, make sure to create a new test for it. +- Run ``black .`` to format your code. +- Run ``pytest`` to test your code. +- Run ``make -C docs html``, and view the generated docs, to verify that the docs still work. +- Run ``make -C docs spelling`` to check your spelling in docstrings. +- Create a pull request, and point it to ``master`` `here `__. diff --git a/README.rst b/README.rst index eb3cc5f..09c3540 100644 --- a/README.rst +++ b/README.rst @@ -1,17 +1,17 @@ -``fbchat``: Facebook Chat (Messenger) for Python -================================================ +``fbchat`` - Facebook Messenger for Python +========================================== -.. image:: https://badgen.net/pypi/license/fbchat - :target: https://github.com/carpedm20/fbchat/tree/master/LICENSE - :alt: License: BSD 3-Clause +.. image:: https://badgen.net/pypi/v/fbchat + :target: https://pypi.python.org/pypi/fbchat + :alt: Project version .. image:: https://badgen.net/badge/python/3.5,3.6,3.7,3.8,pypy?list=| :target: https://pypi.python.org/pypi/fbchat :alt: Supported python versions: 3.5, 3.6, 3.7, 3.8 and pypy -.. image:: https://badgen.net/pypi/v/fbchat - :target: https://pypi.python.org/pypi/fbchat - :alt: Project version +.. image:: https://badgen.net/pypi/license/fbchat + :target: https://github.com/carpedm20/fbchat/tree/master/LICENSE + :alt: License: BSD 3-Clause .. image:: https://readthedocs.org/projects/fbchat/badge/?version=stable :target: https://fbchat.readthedocs.io @@ -25,37 +25,88 @@ :target: https://github.com/ambv/black :alt: Code style -Facebook Chat (`Messenger `__) for Python. -This project was inspired by `facebook-chat-api `__. +A powerful and efficient library to interact with +`Facebook's Messenger `__, using just your email and password. -**No XMPP or API key is needed**. Just use your email and password. +This is *not* an official API, Facebook has that `over here `__ for chat bots. This library differs by using a normal Facebook account instead. -Go to `Read the Docs `__ to see the full documentation, -or jump right into the code by viewing the `examples `__ +``fbchat`` currently support: -Version warning: ----------------- +- Sending many types of messages, with files, stickers, mentions, etc. +- Fetching all messages, threads and images in threads. +- Searching for messages and threads. +- Creating groups, setting the group emoji, changing nicknames, creating polls, etc. +- Listening for, an reacting to messages and other events in real-time. +- Type hints, and it has a modern codebase (e.g. only Python 3.5 and upwards). +- ``async``/``await`` (COMING). + +Essentially, everything you need to make an amazing Facebook bot! + + +Version Warning +--------------- ``v2`` is currently being developed at the ``master`` branch and it's highly unstable. If you want to view the old ``v1``, go `here `__. Additionally, you can view the project's progress `here `__. -Installation: + +Caveats +------- + +``fbchat`` works by imitating what the browser does, and thereby tricking Facebook into thinking it's accessing the website normally. + +However, there's a catch! **Using this library may not comply with Facebook's Terms Of Service!**, so be responsible Facebook citizens! We are not responsible if your account gets banned! + +Additionally, **the APIs the library is calling is undocumented!** In theory, this means that your code could break tomorrow, without the slightest warning! +If this happens to you, please report it, so that we can fix it as soon as possible! + +.. inclusion-marker-intro-end +.. This message doesn't make sense in the docs at Read The Docs, so we exclude it + +With that out of the way, you may go to `Read The Docs `__ to see the full documentation! + +.. inclusion-marker-installation-start + + +Installation +------------ + +.. code-block:: + + $ pip install fbchat + +If you don't have `pip `_, `this guide `_ can guide you through the process. + +You can also install directly from source, provided you have ``pip>=19.0``: + +.. code-block:: + + $ pip install git+https://github.com/carpedm20/fbchat.git + +.. inclusion-marker-installation-end + + +Example Usage ------------- .. code-block:: - $ pip install fbchat + import getpass + import fbchat + session = fbchat.Session.login("", getpass.getpass()) + user = fbchat.User(session=session, id=session.user_id) + user.send_text("Test message!") -You can also install from source if you have ``pip>=19.0``: - -.. code-block:: - - $ git clone https://github.com/carpedm20/fbchat.git - $ pip install fbchat +More examples are available `here `__. Maintainer ---------- - Mads Marquart / `@madsmtm `__ -- Taehoon Kim / `@carpedm20 `__ + + +Acknowledgements +---------------- + +This project was originally inspired by `facebook-chat-api `__. diff --git a/docs/_static/find-group-id.png b/docs/_static/find-group-id.png index e19010d..b60f143 100644 Binary files a/docs/_static/find-group-id.png and b/docs/_static/find-group-id.png differ diff --git a/docs/api.rst b/docs/api.rst deleted file mode 100644 index 274bf24..0000000 --- a/docs/api.rst +++ /dev/null @@ -1,70 +0,0 @@ -.. module:: fbchat -.. _api: - -.. Note: we're using () to hide the __init__ method where relevant - -Full API -======== - -If you are looking for information on a specific function, class, or method, this part of the documentation is for you. - -Client ------- - -.. autoclass:: Client - -Threads -------- - -.. autoclass:: Thread() -.. autoclass:: Page() -.. autoclass:: User() -.. autoclass:: Group() - -Messages --------- - -.. autoclass:: Message -.. autoclass:: Mention -.. autoclass:: EmojiSize(Enum) - :undoc-members: - -Exceptions ----------- - -.. autoexception:: FBchatException() -.. autoexception:: FBchatFacebookError() - -Attachments ------------ - -.. autoclass:: Attachment() -.. autoclass:: ShareAttachment() -.. autoclass:: Sticker() -.. autoclass:: LocationAttachment() -.. autoclass:: LiveLocationAttachment() -.. autoclass:: FileAttachment() -.. autoclass:: AudioAttachment() -.. autoclass:: ImageAttachment() -.. autoclass:: VideoAttachment() -.. autoclass:: ImageAttachment() - -Miscellaneous -------------- - -.. autoclass:: ThreadLocation(Enum) - :undoc-members: -.. autoclass:: ActiveStatus() - -.. autoclass:: QuickReply -.. autoclass:: QuickReplyText -.. autoclass:: QuickReplyLocation -.. autoclass:: QuickReplyPhoneNumber -.. autoclass:: QuickReplyEmail - -.. autoclass:: Poll -.. autoclass:: PollOption - -.. autoclass:: Plan -.. autoclass:: GuestStatus(Enum) - :undoc-members: diff --git a/docs/api/attachments.rst b/docs/api/attachments.rst new file mode 100644 index 0000000..01841a4 --- /dev/null +++ b/docs/api/attachments.rst @@ -0,0 +1,13 @@ +Attachments +=========== + +.. autoclass:: Attachment() +.. autoclass:: ShareAttachment() +.. autoclass:: Sticker() +.. autoclass:: LocationAttachment() +.. autoclass:: LiveLocationAttachment() +.. autoclass:: FileAttachment() +.. autoclass:: AudioAttachment() +.. autoclass:: ImageAttachment() +.. autoclass:: VideoAttachment() +.. autoclass:: ImageAttachment() diff --git a/docs/api/client.rst b/docs/api/client.rst new file mode 100644 index 0000000..b392eb0 --- /dev/null +++ b/docs/api/client.rst @@ -0,0 +1,4 @@ +Client +====== + +.. autoclass:: Client diff --git a/docs/api/events.rst b/docs/api/events.rst new file mode 100644 index 0000000..233e9bd --- /dev/null +++ b/docs/api/events.rst @@ -0,0 +1,4 @@ +Events +====== + +.. autoclass:: Listener diff --git a/docs/api/exceptions.rst b/docs/api/exceptions.rst new file mode 100644 index 0000000..9219745 --- /dev/null +++ b/docs/api/exceptions.rst @@ -0,0 +1,8 @@ +Exceptions +========== + +.. autoexception:: FacebookError() +.. autoexception:: HTTPError() +.. autoexception:: ParseError() +.. autoexception:: ExternalError() +.. autoexception:: GraphQLError() diff --git a/docs/api/index.rst b/docs/api/index.rst new file mode 100644 index 0000000..0cc6ccf --- /dev/null +++ b/docs/api/index.rst @@ -0,0 +1,21 @@ +.. module:: fbchat + +.. Note: we're using () to hide the __init__ method where relevant + +Full API +======== + +If you are looking for information on a specific function, class, or method, this part of the documentation is for you. + +.. toctree:: + :maxdepth: 1 + + session + client + threads + thread_data + messages + exceptions + attachments + events + misc diff --git a/docs/api/messages.rst b/docs/api/messages.rst new file mode 100644 index 0000000..1f1cf53 --- /dev/null +++ b/docs/api/messages.rst @@ -0,0 +1,8 @@ +Messages +======== + +.. autoclass:: Message +.. autoclass:: Mention +.. autoclass:: EmojiSize(Enum) + :undoc-members: +.. autoclass:: MessageData() diff --git a/docs/api/misc.rst b/docs/api/misc.rst new file mode 100644 index 0000000..89430b3 --- /dev/null +++ b/docs/api/misc.rst @@ -0,0 +1,20 @@ +Miscellaneous +============= + +.. autoclass:: ThreadLocation(Enum) + :undoc-members: +.. autoclass:: ActiveStatus() + +.. autoclass:: QuickReply +.. autoclass:: QuickReplyText +.. autoclass:: QuickReplyLocation +.. autoclass:: QuickReplyPhoneNumber +.. autoclass:: QuickReplyEmail + +.. autoclass:: Poll +.. autoclass:: PollOption + +.. autoclass:: Plan +.. autoclass:: PlanData() +.. autoclass:: GuestStatus(Enum) + :undoc-members: diff --git a/docs/api/session.rst b/docs/api/session.rst new file mode 100644 index 0000000..09ab89a --- /dev/null +++ b/docs/api/session.rst @@ -0,0 +1,4 @@ +Session +======= + +.. autoclass:: Session() diff --git a/docs/api/thread_data.rst b/docs/api/thread_data.rst new file mode 100644 index 0000000..7ed8306 --- /dev/null +++ b/docs/api/thread_data.rst @@ -0,0 +1,6 @@ +Thread Data +=========== + +.. autoclass:: PageData() +.. autoclass:: UserData() +.. autoclass:: GroupData() diff --git a/docs/api/threads.rst b/docs/api/threads.rst new file mode 100644 index 0000000..79de8f2 --- /dev/null +++ b/docs/api/threads.rst @@ -0,0 +1,8 @@ +Threads +======= + +.. autoclass:: ThreadABC() +.. autoclass:: Thread +.. autoclass:: Page +.. autoclass:: User +.. autoclass:: Group diff --git a/docs/conf.py b/docs/conf.py index 4f95bb1..f942d18 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -11,13 +11,18 @@ import sys sys.path.insert(0, os.path.abspath("..")) +os.environ["_FBCHAT_DISABLE_FIX_MODULE_METADATA"] = "1" + import fbchat +del os.environ["_FBCHAT_DISABLE_FIX_MODULE_METADATA"] + # -- Project information ----------------------------------------------------- project = fbchat.__name__ -copyright = fbchat.__copyright__ -author = fbchat.__author__ +copyright = "Copyright 2015 - 2018 by Taehoon Kim and 2018 - 2020 by Mads Marquart" +author = "Taehoon Kim; Moreels Pieter-Jan; Mads Marquart" +description = fbchat.__doc__.split("\n")[0] # The short X.Y version version = fbchat.__version__ @@ -37,10 +42,10 @@ needs_sphinx = "2.0" extensions = [ "sphinx.ext.autodoc", "sphinx.ext.intersphinx", - "sphinx.ext.todo", "sphinx.ext.viewcode", "sphinx.ext.napoleon", "sphinxcontrib.spelling", + "sphinx_autodoc_typehints", ] # Add any paths that contain templates here, relative to this directory. @@ -113,7 +118,7 @@ html_show_sourcelink = False # A shorter title for the navigation bar. Default is the same as html_title. # -html_short_title = fbchat.__description__ +html_short_title = description # -- Options for HTMLHelp output --------------------------------------------- @@ -127,16 +132,14 @@ htmlhelp_basename = project + "doc" # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). -latex_documents = [(master_doc, project + ".tex", fbchat.__title__, author, "manual")] +latex_documents = [(master_doc, project + ".tex", project, author, "manual")] # -- Options for manual page output ------------------------------------------ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, project, fbchat.__title__, [x.strip() for x in author.split(";")], 1) -] +man_pages = [(master_doc, project, project, [x.strip() for x in author.split(";")], 1)] # -- Options for Texinfo output ---------------------------------------------- @@ -145,15 +148,7 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ( - master_doc, - project, - fbchat.__title__, - author, - project, - fbchat.__description__, - "Miscellaneous", - ) + (master_doc, project, project, author, project, description, "Miscellaneous",) ] @@ -167,7 +162,7 @@ epub_exclude_files = ["search.html"] # -- Options for autodoc extension --------------------------------------- -autoclass_content = "both" +autoclass_content = "class" autodoc_member_order = "bysource" autodoc_default_options = {"members": True} @@ -176,13 +171,6 @@ autodoc_default_options = {"members": True} # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = {"https://docs.python.org/": None} -# -- Options for todo extension ---------------------------------------------- - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = True - -todo_link_only = True - # -- Options for napoleon extension ---------------------------------------------- # Use Google style docstrings diff --git a/docs/faq.rst b/docs/faq.rst index 311ee06..28183b8 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -1,42 +1,23 @@ -.. _faq: +Frequently Asked Questions +========================== -FAQ -=== +The new version broke my application +------------------------------------ -Version X broke my installation -------------------------------- +``fbchat`` follows `Scemantic Versioning `__ quite rigorously! -We try to provide backwards compatibility where possible, but since we're not part of Facebook, -most of the things may be broken at any point in time +That means that breaking changes can *only* occur in major versions (e.g. ``v1.9.6`` -> ``v2.0.0``). -Downgrade to an earlier version of ``fbchat``, run this command +If you find that something breaks, and you didn't update to a new major version, then it is a bug, and we would be grateful if you reported it! + +In case you're stuck with an old codebase, you can downgrade to a previous version of ``fbchat``, e.g. version ``1.9.6``: .. code-block:: sh - $ pip install fbchat== - -Where you replace ```` with the version you want to use + $ pip install fbchat==1.9.6 Will you be supporting creating posts/events/pages and so on? ------------------------------------------------------------- -We won't be focusing on anything else than chat-related things. This API is called ``fbCHAT``, after all ;) - - -Submitting Issues ------------------ - -If you're having trouble with some of the snippets, or you think some of the functionality is broken, -please feel free to submit an issue on `GitHub `_. -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 +We won't be focusing on anything else than chat-related things. This library is called ``fbCHAT``, after all! diff --git a/docs/index.rst b/docs/index.rst index 7efdb5a..d5c1225 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,52 +1,22 @@ -.. 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 - contain the root `toctree` directive. +.. See README.rst for explanation of these markers -.. This documentation's layout is heavily inspired by requests' layout: https://requests.readthedocs.io - Some documentation is also partially copied from facebook-chat-api: https://github.com/Schmavery/facebook-chat-api +.. include:: ../README.rst + :end-before: inclusion-marker-intro-end -``fbchat``: Facebook Chat (Messenger) for Python -================================================ +With that said, let's get started! -Release v\ |version|. (:ref:`install`) - -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 3.5, 3.6, 3.7 and 3.8: - -``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. - -.. note:: - If you're having problems, please check the :ref:`faq`, before asking questions on GitHub - -.. 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. - -.. 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. - -If you're already familiar with the basics of how Facebook works internally, go to :ref:`examples` to see example usage of ``fbchat`` +.. include:: ../README.rst + :start-after: inclusion-marker-installation-start + :end-before: inclusion-marker-installation-end -Overview --------- +Documentation Overview +---------------------- .. toctree:: :maxdepth: 2 - install intro examples - testing - api - todo faq + api/index diff --git a/docs/install.rst b/docs/install.rst deleted file mode 100644 index c8a1102..0000000 --- a/docs/install.rst +++ /dev/null @@ -1,43 +0,0 @@ -.. _install: - -Installation -============ - -Install using pip ------------------ - -To install ``fbchat``, run this command: - -.. code-block:: sh - - $ pip install fbchat - -If you don't have `pip `_ installed, -`this Python installation guide `_ -can guide you through the process. - -Get the Source Code -------------------- - -``fbchat`` is developed on GitHub, where the code is -`always available `_. - -You can either clone the public repository: - -.. code-block:: sh - - $ git clone git://github.com/carpedm20/fbchat.git - -Or, download a `tarball `_: - -.. code-block:: sh - - $ curl -OL https://github.com/carpedm20/fbchat/tarball/master - # optionally, zipball is also available (for Windows users). - -Once you have a copy of the source, you can embed it in your own Python -package, or install it into your site-packages easily: - -.. code-block:: sh - - $ python setup.py install diff --git a/docs/intro.rst b/docs/intro.rst index 46fa972..0e90837 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -1,194 +1,150 @@ -.. _intro: - Introduction ============ -``fbchat`` uses your email and password to communicate with the Facebook server. -That means that you should always store your password in a separate 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 +Welcome, this page will guide you through the basic concepts of using ``fbchat``. +The hardest, and most error prone part is logging in, and managing your login session, so that is what we will look at first. -.. _intro_logging_in: Logging In ---------- -Simply create an instance of `Client`. If you have two factor authentication enabled, type the code in the terminal prompt -(If you want to supply the code in another fashion, overwrite `Client.on_2fa_code`):: +Everything in ``fbchat`` starts with getting an instance of `Session`. Currently there are two ways of doing that, `Session.login` and `Session.from_cookies`. - from fbchat import Client - from fbchat.models import * - client = Client('', '') +The follow example will prompt you for you password, and use it to login:: -Replace ```` and ```` with your email and password respectively + import getpass + import fbchat + session = fbchat.Session.login("", getpass.getpass()) + # If your account requires a two factor authentication code: + session = fbchat.Session.login( + "", + getpass.getpass(), + lambda: getpass.getpass("2FA code"), + ) -.. 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 necessary here, later code snippets will assume you've done this +However, **this is not something you should do often!** Logging in/out all the time *will* get your Facebook account locked! -If you want to change how verbose ``fbchat`` is, change the logging level (in `Client`) +Instead, you should start by using `Session.login`, and then store the cookies with `Session.get_cookies`, so that they can be used instead the next time your application starts. -Throughout your code, if you want to check whether you are still logged in, use `Client.is_logged_in`. -An example would be to login again if you've been logged out, using `Client.login`:: +Usability-wise, this is also better, since you won't have to re-type your password every time you want to login. - if not client.is_logged_in(): - client.login('', '') +The following, quite lengthy, yet very import example, illustrates a way to do this: -When you're done using the client, and want to securely logout, use `Client.logout`:: +.. literalinclude:: ../examples/session_handling.py - client.logout() +Assuming you have successfully completed the above, congratulations! Using ``fbchat`` should be mostly trouble free from now on! -.. _intro_threads: +Understanding Thread Ids +------------------------ -Threads -------- +At the core of any thread is its unique identifier, its ID. -A thread can refer to two things: A Messenger group chat (`Group`) or a single Facebook user (`User`). +A thread basically just means "something I can chat with", but more precisely, it can refer to a few things: +- A Messenger group thread (`Group`) +- The conversation between you and a single Facebook user (`User`) +- The conversation between you and a Facebook Page (`Page`) -Searching for group chats and finding their ID can be done via. `Client.search_for_groups`, -and searching for users is possible via. `Client.search_for_users`. See :ref:`intro_fetching` +You can get your own user ID with `Session.user_id`. -You can get your own user ID by using `Session.user_id` +Getting the ID of a specific group thread is fairly trivial, you only need to login 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.messenger.com/t/1234567890``, where ``1234567890`` would be the ID of the group. -Getting the ID of a group chat is fairly trivial otherwise, 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: +The same method can be applied to some user accounts, though if they have set a custom URL, then you will have to use a different method. + +An image to illustrate the process 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 +Once you have an ID, you can use it to create a `Group` or a `User` instance, which will allow you to do all sorts of things. To do this, you need a valid, logged in session:: -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:: + group = fbchat.Group(session=session, id="") + # Or for user threads + user = fbchat.User(session=session, id="") - user.send(Message(text='')) - group.send(Message(text='')) +Just like threads, every message, poll, plan, attachment, action etc. you send or do on Facebook has a unique ID. -Some functions don't require a thread type, so in these cases you just provide the thread ID:: +Below is an example of using such a message ID to get a `Message` instance:: - thread = fbchat.Thread(session=session, id="") - thread.set_color("#a695c7") - thread.set_color("#67b868") + # Provide the thread the message was created in, and it's ID + message = fbchat.Message(thread=user, 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. - -This snippet shows how to send a message, and then use the returned ID to react to that message with a 😍 emoji:: - - message = thread.send_text("A message!") - message.react("😍") - - -.. _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:: - - user = User(session=session, id=session.user_id) - message_id = user.send(Message(text='test message')) - -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 +Managing these ids yourself quickly becomes very cumbersome! Luckily, there are other, easier ways of getting `Group`/`User` instances. -You can retrieve a user's ID with `Client.search_for_users`. -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:: +You would start by creating a `Client` instance, which is basically just a helper on top of `Session`, that will allow you to do various things:: - users = client.search_for_users('') - user = users[0] - print("User's ID: {}".format(user.id)) - print("User's name: {}".format(user.name)) - print("User's profile picture URL: {}".format(user.photo)) - print("User's main URL: {}".format(user.url)) + client = fbchat.Client(session=session) -Since this uses Facebook's search functions, you don't have to specify the whole name, first names will usually be enough +Now, you could search for threads using `Client.search_for_threads`, or fetch a list of them using `Client.fetch_threads`:: -You can see a full example showing all the possible ways to fetch information with ``fbchat`` by going to :ref:`examples` + # Fetch the 5 most likely search results + # Uses Facebook's search functions, you don't have to specify the whole name, first names will usually be enough + threads = list(client.search_for_threads("", limit=5)) + # Fetch the 5 most recent threads in your account + threads = list(client.fetch_threads(limit=5)) + +Note the `list` statements; this is because the methods actually return `generators `__. If you don't know what that means, don't worry, it is just something you can use to make your code faster later. + +The examples above will actually fetch `UserData`/`GroupData`, which are subclasses of `User`/`Group`. These model have extra properties, so you could for example print the names and ids of the fetched threads like this:: + + for thread in threads: + print(f"{thread.id}: {thread.name}") + +Once you have a thread, you can use that to fetch the messages therein:: + + for message in thread.fetch_messages(limit=20): + print(message.text) -.. _intro_sessions: +Interacting with Threads +------------------------ -Sessions --------- +Once you have a `User`/`Group` instance, you can do things on them as described in `ThreadABC`, since they are subclasses of that. -``fbchat`` provides functions to retrieve and set the session cookies. -This will enable you to store the session cookies in a separate file, so that you don't have to login each time you start your script. -Use `Client.get_gession` to retrieve the cookies:: +Some functionality, like adding users to a `Group`, or blocking a `User`, logically only works the relevant threads, so see the full API documentation for that. - session_cookies = client.get_gession() +With that out of the way, let's see some examples! -Then you can use `Client.set_gession`:: +The simplest way of interracting with a thread is by sending a message:: - client.set_gession(session_cookies) + # Send a message to the user + message = user.send_text("test message") -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):: +There are many types of messages you can send, see the full API documentation for more. - client = Client('', '', session_cookies=session_cookies) +Notice how we held on to the sent message? The return type i a `Message` instance, so you can interract with it afterwards:: -.. warning:: - You session cookies can be just as valuable as you password, so store them with equal care + # React to the message with the 😍 emoji + message.react("😍") +Besides sending messages, you can also interract with threads in other ways. An example is to change the thread color:: + + # Will change the thread color to the default blue + thread.set_color("#0084ff") -.. _intro_events: Listening & Events ------------------ -To use the listening functions ``fbchat`` offers (like `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 +Now, we are finally at the point we have all been waiting for: Creating an automatic Facebook bot! -.. note:: - You can identify the event methods by their ``on`` prefix, e.g. ``on_message`` +To get started, you create your methods that will handle your events:: -The event actions can be changed by subclassing the `Client`, and then overwriting the event methods:: + def on_message(event): + print(f"Message from {event.author.id}: {event.message.text}") - class CustomClient(Client): - def on_message(self, mid, author_id, message_object, thread, ts, metadata, msg, **kwargs): - # Do something with message_object here - pass +And then you create a listener object, and start handling the incoming events:: - client = CustomClient('', '') + listener = fbchat.Listener.connect(session, False, False) -**Notice:** The following snippet is as equally valid as the previous one:: + for event in listener.listen(): + if isinstance(event, fbchat.MessageEvent): + on_message(event) - class CustomClient(Client): - def on_message(self, message_object, author_id, thread, **kwargs): - # Do something with message_object here - pass - - client = CustomClient('', '') - -The change was in the parameters that our ``on_message`` method took: ``message_object`` 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 compatibility, - 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 +View the :ref:`examples` to see some more examples illustrating the event system. diff --git a/docs/spelling/technical.txt b/docs/spelling/technical.txt index 2595ac9..9453d88 100644 --- a/docs/spelling/technical.txt +++ b/docs/spelling/technical.txt @@ -1,4 +1,6 @@ iterables +iterable +mimetype timestamp metadata spam @@ -12,3 +14,4 @@ spritemap online inbox subclassing +codebase diff --git a/docs/testing.rst b/docs/testing.rst deleted file mode 100644 index 16dbd57..0000000 --- a/docs/testing.rst +++ /dev/null @@ -1,25 +0,0 @@ -.. _testing: - -Testing -======= - -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 -- group_thread_id: A test group that will be used to test group functionality -- user_thread_id: A person that will be used to test kick/add functionality (This user should be in the group) - -Please remember to test all supported python versions. -If you've made any changes to the 2FA functionality, test it with a 2FA enabled account. - -If you only want to execute specific tests, pass the function names in the command line (not including the ``test_`` prefix). Example: - -.. code-block:: sh - - $ python tests.py sendMessage sessions sendEmoji - -.. warning:: - - Do not execute the full set of tests in too quick succession. This can get your account temporarily blocked for spam! - (You should execute the script at max about 10 times a day) diff --git a/docs/todo.rst b/docs/todo.rst deleted file mode 100644 index 18d063d..0000000 --- a/docs/todo.rst +++ /dev/null @@ -1,22 +0,0 @@ -.. _todo: - -Todo -==== - -This page will be periodically updated to show missing features and documentation - - -Missing Functionality ---------------------- - -- Implement ``Client.search_for_message`` - - This will use the GraphQL request API -- Implement chatting with pages properly -- Write better FAQ -- Explain usage of GraphQL - - -Documentation -------------- - -.. todolist:: diff --git a/examples/session_handling.py b/examples/session_handling.py new file mode 100644 index 0000000..d82a0dd --- /dev/null +++ b/examples/session_handling.py @@ -0,0 +1,42 @@ +# TODO: Consider adding Session.from_file and Session.to_file, +# which would make this example a lot easier! + +import atexit +import json +import getpass +import fbchat + + +def load_cookies(filename): + try: + # Load cookies from file + with open(filename) as f: + return json.load(f) + except FileNotFoundError: + return # No cookies yet + + +def save_cookies(filename, cookies): + with open(filename, "w") as f: + json.dump(f, cookies) + + +def load_session(cookies): + if not cookies: + return + try: + return fbchat.Session.from_cookies(cookies) + except fbchat.FacebookError: + return # Failed loading from cookies + + +cookies = load_cookies("session.json") +session = load_session(cookies) +if not session: + # Session could not be loaded, login instead! + session = fbchat.Session.login("", getpass.getpass()) + +# Save session cookies to file when the program exits +atexit.register(lambda: save_cookies("session.json", session.get_cookies())) + +# Do stuff with session here diff --git a/fbchat/__init__.py b/fbchat/__init__.py index 93f300d..3d7c390 100644 --- a/fbchat/__init__.py +++ b/fbchat/__init__.py @@ -1,7 +1,11 @@ -"""Facebook Chat (Messenger) for Python +"""Facebook Messenger for Python. -:copyright: (c) 2015 - 2019 by Taehoon Kim -:license: BSD 3-Clause, see LICENSE for more details. +Copyright: + (c) 2015 - 2018 by Taehoon Kim + (c) 2018 - 2020 by Mads Marquart + +License: + BSD 3-Clause, see LICENSE for more details. """ import logging as _logging @@ -84,47 +88,12 @@ from ._mqtt import Listener from ._client import Client -__title__ = "fbchat" __version__ = "1.9.6" -__description__ = "Facebook Chat (Messenger) for Python" - -__copyright__ = "Copyright 2015 - 2019 by Taehoon Kim" -__license__ = "BSD 3-Clause" - -__author__ = "Taehoon Kim; Moreels Pieter-Jan; Mads Marquart" -__email__ = "carpedm20@gmail.com" __all__ = ("Session", "Listener", "Client") -# Everything below is taken from the excellent trio project: +from . import _fix_module_metadata -def fixup_module_metadata(namespace): - def fix_one(qualname, name, obj): - mod = getattr(obj, "__module__", None) - if mod is not None and mod.startswith("fbchat."): - obj.__module__ = "fbchat" - # Modules, unlike everything else in Python, put fully-qualitied - # names into their __name__ attribute. We check for "." to avoid - # rewriting these. - if hasattr(obj, "__name__") and "." not in obj.__name__: - obj.__name__ = name - obj.__qualname__ = qualname - if isinstance(obj, type): - # Fix methods - for attr_name, attr_value in obj.__dict__.items(): - fix_one(objname + "." + attr_name, attr_name, attr_value) - - for objname, obj in namespace.items(): - if not objname.startswith("_"): # ignore private attributes - fix_one(objname, objname, obj) - - -# Having the public path in .__module__ attributes is important for: -# - exception names in printed tracebacks -# - sphinx :show-inheritance: -# - deprecation warnings -# - pickle -# - probably other stuff -fixup_module_metadata(globals()) -del fixup_module_metadata +_fix_module_metadata.fixup_module_metadata(globals()) +del _fix_module_metadata diff --git a/fbchat/_client.py b/fbchat/_client.py index 0cc0de7..5f526aa 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -477,15 +477,12 @@ class Client: Args: threads: Threads to set as unread - at: Timestam to signal the read cursor at + at: Timestamp to signal the read cursor at """ return self._read_status(False, threads, at) def mark_as_seen(self): - """ - Todo: - Documenting this - """ + # TODO: Documenting this j = self.session._payload_post( "/ajax/mercury/mark_seen.php", {"seen_timestamp": _util.now()} ) diff --git a/fbchat/_fix_module_metadata.py b/fbchat/_fix_module_metadata.py new file mode 100644 index 0000000..d2faa38 --- /dev/null +++ b/fbchat/_fix_module_metadata.py @@ -0,0 +1,39 @@ +"""Everything in this module is taken from the excellent trio project. + +Having the public path in .__module__ attributes is important for: +- exception names in printed tracebacks +- ~sphinx :show-inheritance:~ +- deprecation warnings +- pickle +- probably other stuff +""" + +import os + + +def fixup_module_metadata(namespace): + def fix_one(qualname, name, obj): + mod = getattr(obj, "__module__", None) + if mod is not None and mod.startswith("fbchat."): + obj.__module__ = "fbchat" + # Modules, unlike everything else in Python, put fully-qualitied + # names into their __name__ attribute. We check for "." to avoid + # rewriting these. + if hasattr(obj, "__name__") and "." not in obj.__name__: + obj.__name__ = name + obj.__qualname__ = qualname + if isinstance(obj, type): + # Fix methods + for attr_name, attr_value in obj.__dict__.items(): + fix_one(objname + "." + attr_name, attr_name, attr_value) + + for objname, obj in namespace.items(): + if not objname.startswith("_"): # ignore private attributes + fix_one(objname, objname, obj) + + +# Allow disabling this when running Sphinx +# This is done so that Sphinx autodoc can detect the file's source +# TODO: Find a better way to detect when we're running Sphinx! +if os.environ.get("_FBCHAT_DISABLE_FIX_MODULE_METADATA") == "1": + fixup_module_metadata = lambda namespace: None diff --git a/fbchat/_page.py b/fbchat/_page.py index 18e8a25..ff89bfe 100644 --- a/fbchat/_page.py +++ b/fbchat/_page.py @@ -8,6 +8,8 @@ from . import _session, _plan, _thread class Page(_thread.ThreadABC): """Represents a Facebook page. Implements `ThreadABC`.""" + # TODO: Implement pages properly, the implementation is lacking in a lot of places! + #: The session to use when making requests. session = attr.ib(type=_session.Session) #: The unique identifier of the page. diff --git a/fbchat/_session.py b/fbchat/_session.py index 27f2f1f..a190813 100644 --- a/fbchat/_session.py +++ b/fbchat/_session.py @@ -329,6 +329,8 @@ class Session: raise _exception.ParseError("Missing payload", data=j) from e def _graphql_requests(self, *queries): + # TODO: Explain usage of GraphQL, probably in the docs + # Perhaps provide this API as public? data = { "method": "GET", "response_format": "json", diff --git a/fbchat/_thread.py b/fbchat/_thread.py index 4baf780..0d8a0f0 100644 --- a/fbchat/_thread.py +++ b/fbchat/_thread.py @@ -110,7 +110,7 @@ class ThreadABC(metaclass=abc.ABCMeta): reply_to_id: Optional message to reply to Returns: - :ref:`Message ID ` of the sent message + The sent message """ data = self._to_send_data() data["action_type"] = "ma-type:user-generated-message" @@ -139,7 +139,7 @@ class ThreadABC(metaclass=abc.ABCMeta): size: The size of the emoji Returns: - :ref:`Message ID ` of the sent message + The sent message """ data = self._to_send_data() data["action_type"] = "ma-type:user-generated-message" @@ -154,7 +154,7 @@ class ThreadABC(metaclass=abc.ABCMeta): sticker_id: ID of the sticker to send Returns: - :ref:`Message ID ` of the sent message + The sent message """ data = self._to_send_data() data["action_type"] = "ma-type:user-generated-message" @@ -420,7 +420,7 @@ class ThreadABC(metaclass=abc.ABCMeta): def set_color(self, color: str): """Change thread color. - The new color must be one of the following: + The new color must be one of the following:: "#0084ff", "#44bec7", "#ffc300", "#fa3c4c", "#d696bb", "#6699cc", "#13cf13", "#ff7e29", "#e68585", "#7646ff", "#20cef5", "#67b868", "#d4a88c", "#ff5ca1", diff --git a/pyproject.toml b/pyproject.toml index 5510324..fe4b5b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,7 +56,8 @@ test = [ ] docs = [ "sphinx~=2.0", - "sphinxcontrib-spelling~=4.0" + "sphinxcontrib-spelling~=4.0", + "sphinx-autodoc-typehints~=1.10", ] lint = [ "black",