More documentation work, changed addUsersToGroup back to taking a list of user IDs

Created new README, and finished `intro`
This commit is contained in:
Mads Marquart
2017-05-28 21:11:16 +02:00
parent 39eafa5a3e
commit 8dacc37ba9
16 changed files with 470 additions and 310 deletions

View File

@@ -1,118 +1,26 @@
====== fbchat: Facebook Chat (Messenger) for Python
fbchat ============================================
======
.. image:: docs/_static/license.svg
:target: LICENSE.txt
:alt: License: BSD
Facebook Chat (`Messenger <https://www.messenger.com/>`__) for Python. This project was inspired by `facebook-chat-api <https://github.com/Schmavery/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 <https://www.facebook.com/messages/>`__) for Python.
This project was inspired by `facebook-chat-api <https://github.com/Schmavery/facebook-chat-api>`__.
**No XMPP or API key is needed**. Just use your email and password.
Installation Go to `ReadTheDocs <https://fbchat.readthedocs.com>`__ to see the full documentation,
============ or jump right into the code by viewing the `examples`_
Simple: Installation:
.. code-block:: console .. code-block:: console
$ pip install fbchat $ pip install fbchat
© Copyright 2015 - 2017 by Taehoon Kim / `@carpedm20 <http://carpedm20.github.io/about/>`__
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='<message text>',image='<path/to/image/file>') # send local image
imgurl = "http://i.imgur.com/LDQ2ITV.jpg"
client.sendRemoteImage(friend.uid,message='<message text>', image=imgurl) # send image from image url
Getting user info from user id
==============================
.. code-block:: python
friend1 = client.getUsers('<friend name 1>')[0]
friend2 = client.getUsers('<friend name 2>')[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("<email>", "<password>")
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 <http://carpedm20.github.io/about/>`__

BIN
docs/_static/find-group-id.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View File

@@ -13,10 +13,13 @@ If you are looking for information on a specific function, class, or method, thi
Client Client
------ ------
.. todo:: This is the main class of `fbchat`, which contains all the methods you use to interract with Facebook.
Write introduction text about `Client`, and add documentation for all events 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: :members:
.. automethod:: sendRemoteImage(image_url, message=None, thread_id=None, thread_type=ThreadType.USER) .. 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 .. automodule:: fbchat.models
:members: :members:
:undoc-members:
.. _api_utils: .. _api_utils:

View File

@@ -7,22 +7,20 @@ Examples
These are a few examples on how to use `fbchat`. Remember to swap out `<email>` and `<password>` for your email and password These are a few examples on how to use `fbchat`. Remember to swap out `<email>` and `<password>` 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 .. literalinclude:: ../examples/interract.py
:language: python
Getting information Fetching Information
------------------- --------------------
This will show the different ways of fetching information about users and threads This will show the different ways of fetching information about users and threads
.. literalinclude:: ../examples/get.py .. literalinclude:: ../examples/fetch.py
:language: python
Echobot Echobot
@@ -31,22 +29,20 @@ Echobot
This will reply to any message with the same message This will reply to any message with the same message
.. literalinclude:: ../examples/echobot.py .. 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!` This will remove a user from a group if they write the message `Remove me!`
.. literalinclude:: ../examples/removebot.py .. 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 .. literalinclude:: ../examples/keepbot.py
:language: python

View File

@@ -1,4 +1,5 @@
.. highlight:: python .. highlight:: python
.. module:: fbchat
.. fbchat documentation master file, created by .. fbchat documentation master file, created by
sphinx-quickstart on Thu May 25 15:43:01 2017. sphinx-quickstart on Thu May 25 15:43:01 2017.
You can adapt this file completely to your liking, but it should at least 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 .. image:: /_static/license.svg
:target: https://github.com/carpedm20/fbchat/blob/master/LICENSE.txt :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 .. 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 .. image:: /_static/python-versions.svg
:target: https://pypi.python.org/pypi/fbchat :target: https://pypi.python.org/pypi/fbchat
:alt: Supported python versions: 2.7, 3.4, 3.5 and 3.6
Facebook Chat (`Messenger <https://www.facebook.com/messages/>`__) for Python. This project was inspired by `facebook-chat-api <https://github.com/Schmavery/facebook-chat-api>`__. Facebook Chat (`Messenger <https://www.facebook.com/messages/>`_) for Python.
This project was inspired by `facebook-chat-api <https://github.com/Schmavery/facebook-chat-api>`_.
**No XMPP or API key is needed**. Just use your email and password. **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: 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. `fbchat` works by emulating the browser.
Because we're doing it this way, this API requires the credentials of a Facebook account. 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:: .. 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:: .. note::
Facebook now has an `official API <https://developers.facebook.com/docs/messenger-platform>`_ 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 <https://developers.facebook.com/docs/messenger-platform>`_ 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 Overview
-------- --------

View File

@@ -1,20 +1,138 @@
.. highlight:: python .. highlight:: python
.. module:: fbchat
.. _intro: .. _intro:
Introduction Introduction
============ ============
.. todo:: `fbchat` uses your email and password to communicate with the Facebook server.
Make a general introduction to `fbchat` 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: .. _intro_logging_in:
Logging in Logging In
---------- ----------
.. todo:: Simply create an instance of :class:`Client`. If you have two factor authentication enabled, type the code in the terminal prompt
Write something about logging in, logging out, checking login, 2FA and the event `on2FACode`, here (If you want to supply the code in another fasion, overwrite :func:`Client.on2FACode`)::
from fbchat import Client
from fbchat.models import *
client = Client('<email>', '<password>')
Replace ``<email>`` and ``<password>`` 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('<email>', '<password>')
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 `<https://www.facebook.com/messages/>`_,
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 ``<user id>`` and ``<group id>``
corresponds to the ID of a single user, and the ID of a group respectively::
client.sendMessage('<message>', thread_id='<user id>', thread_type=ThreadType.USER)
client.sendMessage('<message>', thread_id='<group 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='<user id>')
client.changeThreadColor(ThreadColor.MESSENGER_BLUE, thread_id='<group 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('<name of user>')
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: .. _intro_sessions:
@@ -22,65 +140,81 @@ Logging in
Sessions Sessions
-------- --------
.. todo:: `fbchat` provides functions to retrieve and set the session cookies.
Make an introduction to and show example usage of sessions 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:: Or you can set the ``session_cookies`` on your initial login.
Make an introduction to and show example usage of how you send information (If the session cookies are invalid, your email and password will be used to login instead)::
.. literalinclude:: ../examples/send.py client = Client('<email>', '<password>', session_cookies=session_cookies)
:language: python
.. warning::
.. _intro_fetching: You session cookies can be just as valueable as you password, so store them with equal care
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
.. _intro_events: .. _intro_events:
Events Listening & Events
------ ------------------
.. todo:: To use the listening functions `fbchat` offers (like :func:`Client.listen`),
Make an introduction to and show example usage of the event system 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('<email>', '<password>')
**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('<email>', '<password>')
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 <https://github.com/carpedm20/fbchat>`_.
One side note is that you should first login with ``logging_level`` set to ``logging.DEBUG``::
from fbchat import Client
import logging
client = Client('<email>', '<password>', 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

View File

@@ -1,10 +1,11 @@
.. highlight:: sh .. highlight:: sh
.. module:: fbchat
.. _testing: .. _testing:
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 - email: Your (or a test user's) email / phone number
- password: Your (or a test user's) password - password: Your (or a test user's) password

View File

@@ -1,19 +1,23 @@
.. highlight:: python .. highlight:: python
.. module:: fbchat
.. _todo: .. _todo:
Todo Todo
==== ====
This page will be periodically updated to show missing features and documentation
Functionality
-------------
- Implement Client.changeThreadEmoji Missing Functionality
- Implement Client.changeNickname ---------------------
- Implement Client.searchForThread - Implement Client.searchForThread
- This will use the graphql request API - This will use the graphql request API
- Implement Client.searchForMessage - Implement Client.searchForMessage
- This will use the graphql request API - 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 Documentation

50
examples/fetch.py Normal file
View File

@@ -0,0 +1,50 @@
# -*- coding: UTF-8 -*-
from fbchat import Client
from fbchat.models import *
client = Client("<email>", "<password>")
# 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('<user id>')
# 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('<name of user>')[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 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`

View File

@@ -1,8 +0,0 @@
# -*- coding: UTF-8 -*-
from fbchat import Client
from fbchat.models import *
client = Client("<email>", "<password>")
# This should show example usage of getAllUsers, getUsers, getUserInfo, getThreadInfo, getThreadList and getUnread

55
examples/interract.py Normal file
View File

@@ -0,0 +1,55 @@
# -*- coding: UTF-8 -*-
from fbchat import Client
from fbchat.models import *
client = Client("<email>", "<password>")
thread_id = '1234567890'
thread_type = ThreadType.GROUP
# Will send a message to the thread
client.sendMessage('<message>', 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 `<image path>`
client.sendLocalImage('<image path>', message='This is a local image', thread_id=thread_id, thread_type=thread_type)
# Will download the image at the url `<image url>`, and then send it
client.sendRemoteImage('<image url>', 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 `<user id>` from the thread
client.removeUserFromGroup('<user id>', thread_id=thread_id)
# Will add the user with ID `<user id>` to the thread
client.addUsersToGroup('<user id>', 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 `<user_id>` to `<new nickname>`
client.changeNickname('<new nickname>', '<user id>', thread_id=thread_id, thread_type=thread_type)
# Will change the title of the thread to `<title>`
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)

View File

@@ -9,12 +9,12 @@ old_thread_id = '1234567890'
# Change these to match your liking # Change these to match your liking
old_color = ThreadColor.MESSENGER_BLUE old_color = ThreadColor.MESSENGER_BLUE
old_emoji = '👍' old_emoji = '👍'
old_title = 'Old school' old_title = 'Old group chat name'
old_nicknames = { old_nicknames = {
'12345678901': 'Old School user nr. 1', '12345678901': "User nr. 1's nickname",
'12345678902': 'Old School user nr. 2', '12345678902': "User nr. 2's nickname",
'12345678903': 'Old School user nr. 3', '12345678903': "User nr. 3's nickname",
'12345678904': 'Old School user nr. 4' '12345678904': "User nr. 4's nickname"
} }
class KeepBot(Client): class KeepBot(Client):

View File

@@ -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)

View File

@@ -28,12 +28,46 @@ handler = logging.StreamHandler()
log.addHandler(handler) 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): class Client(object):
"""A client for the Facebook Chat (Messenger). """A client for the Facebook Chat (Messenger).
See https://fbchat.readthedocs.io for complete documentation of the API. 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, def __init__(self, email, password, debug=False, info_log=False, user_agent=None, max_retries=5,
session_cookies=None, logging_level=logging.INFO): session_cookies=None, logging_level=logging.INFO):
"""Initializes and logs in the client """Initializes and logs in the client
@@ -56,7 +90,6 @@ class Client(object):
self.seq = "0" self.seq = "0"
self.payloadDefault = {} self.payloadDefault = {}
self.client = 'mercury' self.client = 'mercury'
self.listening = False
self.default_thread_id = None self.default_thread_id = None
self.default_thread_type = None self.default_thread_type = None
self.threads = [] self.threads = []
@@ -359,19 +392,6 @@ class Client(object):
r = self._cleanPost(ReqUrl.CHECKPOINT, data) r = self._cleanPost(ReqUrl.CHECKPOINT, data)
return r 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): def isLoggedIn(self):
""" """
Sends a request to Facebook to check the login status Sends a request to Facebook to check the login status
@@ -469,8 +489,8 @@ class Client(object):
def setDefaultThread(self, thread_id, thread_type): def setDefaultThread(self, thread_id, thread_type):
"""Sets default thread to send messages to """Sets default thread to send messages to
:param thread_id: User/Group ID to default to. See :ref:`intro_thread_id` :param thread_id: User/Group ID to default to. See :ref:`intro_threads`
:param thread_type: See :ref:`intro_thread_type` :param thread_type: See :ref:`intro_threads`
:type thread_type: models.ThreadType :type thread_type: models.ThreadType
""" """
self.default_thread_id = thread_id self.default_thread_id = thread_id
@@ -497,7 +517,7 @@ class Client(object):
return given_thread_id, given_thread_type return given_thread_id, given_thread_type
""" """
GET METHODS FETCH METHODS
""" """
def getAllUsers(self): def getAllUsers(self):
@@ -559,8 +579,11 @@ class Client(object):
def getUserInfo(self, *user_ids): def getUserInfo(self, *user_ids):
"""Get user info from id. Unordered. """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 :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): def fbidStrip(_fbid):
@@ -587,8 +610,8 @@ class Client(object):
"""Get the last messages in a thread """Get the last messages in a thread
:param last_n: Number of messages to retrieve :param last_n: Number of messages to retrieve
:param thread_id: User/Group ID to retrieve from. See :ref:`intro_thread_id` :param thread_id: User/Group ID to retrieve from. See :ref:`intro_threads`
:param thread_type: See :ref:`intro_thread_type` :param thread_type: See :ref:`intro_threads`
:type last_n: int :type last_n: int
:type thread_type: models.ThreadType :type thread_type: models.ThreadType
:return: Dictionaries, containing message data :return: Dictionaries, containing message data
@@ -679,19 +702,15 @@ class Client(object):
# 'last_action_timestamp': 0 # 'last_action_timestamp': 0
} }
r = self._post(ReqUrl.THREAD_SYNC, form) j = check_request(self._post(ReqUrl.THREAD_SYNC, form))
if not r.ok or len(r.text) == 0:
return None
j = get_json(r) return {
result = {
"message_counts": j['payload']['message_counts'], "message_counts": j['payload']['message_counts'],
"unseen_threads": j['payload']['unseen_thread_ids'] "unseen_threads": j['payload']['unseen_thread_ids']
} }
return result
""" """
END GET METHODS END FETCH METHODS
""" """
""" """
@@ -741,10 +760,10 @@ class Client(object):
return data return data
def _doSendRequest(self, 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) r = self._post(ReqUrl.SEND, data)
j = self._checkRequest(r) j = check_request(r)
if j is None: if j is None:
return None return None
@@ -782,10 +801,10 @@ class Client(object):
Sends a message to a thread Sends a message to a thread
:param message: Message to send :param message: Message to send
:param thread_id: User/Group ID to send to. See :ref:`intro_thread_id` :param thread_id: User/Group ID to send to. See :ref:`intro_threads`
:param thread_type: See :ref:`intro_thread_type` :param thread_type: See :ref:`intro_threads`
:type thread_type: models.ThreadType :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) thread_id, thread_type = self._getThread(thread_id, thread_type)
data = self._getSendData(thread_id, thread_type) data = self._getSendData(thread_id, thread_type)
@@ -802,13 +821,13 @@ class Client(object):
""" """
Sends an emoji to a thread 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 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_id: User/Group ID to send to. See :ref:`intro_threads`
:param thread_type: See :ref:`intro_thread_type` :param thread_type: See :ref:`intro_threads`
:type size: models.EmojiSize :type size: models.EmojiSize
:type thread_type: models.ThreadType :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) thread_id, thread_type = self._getThread(thread_id, thread_type)
data = self._getSendData(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): 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 image_id: ID of an image that's already uploaded to Facebook
:param message: Additional message :param message: Additional message
:param thread_id: User/Group ID to send to. See :ref:`intro_thread_id` :param thread_id: User/Group ID to send to. See :ref:`intro_threads`
:param thread_type: See :ref:`intro_thread_type` :param thread_type: See :ref:`intro_threads`
:type thread_type: models.ThreadType :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) thread_id, thread_type = self._getThread(thread_id, thread_type)
data = self._getSendData(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 image_url: URL of an image to upload and send
:param message: Additional message :param message: Additional message
:param thread_id: User/Group ID to send to. See :ref:`intro_thread_id` :param thread_id: User/Group ID to send to. See :ref:`intro_threads`
:param thread_type: See :ref:`intro_thread_type` :param thread_type: See :ref:`intro_threads`
:type thread_type: models.ThreadType :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: 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') 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 image_path: URL of an image to upload and send
:param message: Additional message :param message: Additional message
:param thread_id: User/Group ID to send to. See :ref:`intro_thread_id` :param thread_id: User/Group ID to send to. See :ref:`intro_threads`
:param thread_type: See :ref:`intro_thread_type` :param thread_type: See :ref:`intro_threads`
:type thread_type: models.ThreadType :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: 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') 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) 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) 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. Adds users to a group.
:param user_ids: User ids to add :param user_ids: One or more user ids to add
:param thread_id: Group ID to add people to. See :ref:`intro_thread_id` :param thread_id: Group ID to add people to. See :ref:`intro_threads`
:type user_ids: list of positional arguments :type user_ids: list
:return: :ref:`Message ID <intro_message_ids>` of the sent "message" :return: :ref:`Message ID <intro_message_ids>` of the executed action or `None` on failure
""" """
thread_id, thread_type = self._getThread(thread_id, None) thread_id, thread_type = self._getThread(thread_id, None)
data = self._getSendData(thread_id, ThreadType.GROUP) data = self._getSendData(thread_id, ThreadType.GROUP)
@@ -937,9 +956,14 @@ class Client(object):
data['action_type'] = 'ma-type:log-message' data['action_type'] = 'ma-type:log-message'
data['log_message_type'] = 'log:subscribe' data['log_message_type'] = 'log:subscribe'
if type(user_ids) is not list:
user_ids = [user_ids]
# Make list of users unique # Make list of users unique
user_ids = set(user_ids) user_ids = set(user_ids)
print(user_ids)
for i, user_id in enumerate(user_ids): for i, user_id in enumerate(user_ids):
if user_id == self.uid: if user_id == self.uid:
log.warning('Error when adding users: Cannot add self to group chat') log.warning('Error when adding users: Cannot add self to group chat')
@@ -955,8 +979,9 @@ class Client(object):
Removes users from a group. Removes users from a group.
:param user_id: User ID to remove :param user_id: User ID to remove
:param thread_id: Group ID to remove people from. See :ref:`intro_thread_id` :param thread_id: Group ID to remove people from. See :ref:`intro_threads`
:return: :ref:`Message ID <intro_message_ids>` of the sent "message" :return: True if the action was successful
:rtype: bool
""" """
thread_id, thread_type = self._getThread(thread_id, None) thread_id, thread_type = self._getThread(thread_id, None)
@@ -966,7 +991,7 @@ class Client(object):
"tid": thread_id "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 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): 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 title: New group chat title
:param thread_id: Group ID to change title of. See :ref:`intro_thread_id` :param thread_id: Group ID to change title of. See :ref:`intro_threads`
:param thread_type: See :ref:`intro_thread_type` :param thread_type: See :ref:`intro_threads`
:type thread_type: models.ThreadType :type thread_type: models.ThreadType
:return: True if the action was successful :return: True if the action was successful
:rtype: bool :rtype: bool
@@ -1018,8 +1044,8 @@ class Client(object):
:param nickname: New nickname :param nickname: New nickname
:param user_id: User that will have their nickname changed :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_id: User/Group ID to change color of. See :ref:`intro_threads`
:param thread_type: See :ref:`intro_thread_type` :param thread_type: See :ref:`intro_threads`
:type thread_type: models.ThreadType :type thread_type: models.ThreadType
:return: True if the action was successful :return: True if the action was successful
:rtype: bool :rtype: bool
@@ -1032,7 +1058,7 @@ class Client(object):
'thread_or_other_fbid': thread_id '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 return False if j is None else True
@@ -1041,7 +1067,7 @@ class Client(object):
Changes thread color Changes thread color
:param color: New 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 :type color: models.ThreadColor
:return: True if the action was successful :return: True if the action was successful
:rtype: bool :rtype: bool
@@ -1053,7 +1079,7 @@ class Client(object):
'thread_or_other_fbid': thread_id '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 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 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 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 :return: True if the action was successful
:rtype: bool :rtype: bool
""" """
@@ -1075,7 +1101,7 @@ class Client(object):
'thread_or_other_fbid': thread_id '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 return False if j is None else True
@@ -1110,7 +1136,7 @@ class Client(object):
.replace('u%27', '%27')\ .replace('u%27', '%27')\
.replace('%5CU{}'.format(MessageReactionFix[reaction.value][0]), MessageReactionFix[reaction.value][1]) .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 return False if j is None else True
@@ -1119,8 +1145,8 @@ class Client(object):
Sets users typing status in a thread Sets users typing status in a thread
:param status: Specify the typing status :param status: Specify the typing status
:param thread_id: User/Group ID to change status in. See :ref:`intro_thread_id` :param thread_id: User/Group ID to change status in. See :ref:`intro_threads`
:param thread_type: See :ref:`intro_thread_type` :param thread_type: See :ref:`intro_threads`
:type status: models.TypingStatus :type status: models.TypingStatus
:type thread_type: models.ThreadType :type thread_type: models.ThreadType
:return: True if the action was successful :return: True if the action was successful
@@ -1135,7 +1161,7 @@ class Client(object):
"source": "mercury-chat" "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 return False if j is None else True
@@ -1195,11 +1221,7 @@ class Client(object):
r = self._post(ReqUrl.CONNECT, data) r = self._post(ReqUrl.CONNECT, data)
return r.ok return r.ok
def ping(self, sticky): def _ping(self, sticky):
"""
.. todo::
Documenting this
"""
data = { data = {
'channel': self.user_channel, 'channel': self.user_channel,
'clientid': self.client_id, 'clientid': self.client_id,
@@ -1209,8 +1231,7 @@ class Client(object):
'sticky': sticky, 'sticky': sticky,
'viewer_uid': self.uid 'viewer_uid': self.uid
} }
r = self._get(ReqUrl.PING, data) return False if check_request(self._get(ReqUrl.PING, data), check_json=False) is None else True
return r.ok
def _getSticky(self): def _getSticky(self):
"""Call pull api to get sticky and pool parameter, """Call pull api to get sticky and pool parameter,
@@ -1435,7 +1456,7 @@ class Client(object):
:rtype: bool :rtype: bool
""" """
try: try:
if markAlive: self.ping(self.sticky) if markAlive: self._ping(self.sticky)
try: try:
content = self._pullMessage(self.sticky, self.pool) content = self._pullMessage(self.sticky, self.pool)
if content: self._parseMessage(content) if content: self._parseMessage(content)

View File

@@ -6,7 +6,7 @@ import enum
class User(object): class User(object):
"""Represents a Facebook User""" """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 uid = None
#: Currently always set to `user`. Might change in the future #: Currently always set to `user`. Might change in the future
type = 'user' type = 'user'
@@ -75,10 +75,12 @@ class User(object):
} }
class Thread(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) self.__dict__.update(entries)
class Message(object): class Message(object):
"""Represents a message. Currently just acts as a dict"""
def __init__(self, **entries): def __init__(self, **entries):
self.__dict__.update(entries) self.__dict__.update(entries)
@@ -89,7 +91,7 @@ class Enum(enum.Enum):
return '{}.{}'.format(type(self).__name__, self.name) return '{}.{}'.format(type(self).__name__, self.name)
class ThreadType(Enum): 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 USER = 1
GROUP = 2 GROUP = 2

View File

@@ -15,6 +15,13 @@ import py_compile
logging_level = logging.ERROR logging_level = logging.ERROR
"""
Testing script for `fbchat`.
Full documentation on https://fbchat.readthedocs.io/
"""
class CustomClient(Client): class CustomClient(Client):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.got_qprimer = False self.got_qprimer = False
@@ -168,8 +175,8 @@ class TestFbchat(unittest.TestCase):
def test_setTypingStatus(self): def test_setTypingStatus(self):
self.assertTrue(client.sendMessage('Hi', thread_id=user_uid, thread_type=ThreadType.USER)) 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=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.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)) self.assertTrue(client.setTypingStatus(TypingStatus.STOPPED, thread_id=group_uid, thread_type=ThreadType.GROUP))