Compare commits

..

23 Commits

Author SHA1 Message Date
Mads Marquart
856962af63 Bump version: 1.7.3 → 1.8.0 2019-08-28 10:58:46 +02:00
Mads Marquart
7c68a29181 Stop using Client.graphql_request internally 2019-07-25 23:32:17 +02:00
Mads Marquart
2f4e3f2bb1 Remove Client._generatePayload
Make Client._get and Client._post require a query input
2019-07-25 20:20:26 +02:00
Mads Marquart
0389b838bc Merge pull request #455 from carpedm20/add-spell-check
Add spell checking.

Use sphinxcontrib-spelling to fix documentation and docstring spelling errors.
2019-07-25 18:51:53 +02:00
Mads Marquart
441f53e382 Merge pull request #454 from carpedm20/google-style-docstrings
Google docstring style
2019-07-24 21:56:33 +02:00
Mads Marquart
83c45dcf40 Fix spelling / typesetting in various places 2019-07-24 16:18:15 +02:00
Mads Marquart
cc9d81a39e Fix spelling mistakes in documentation 2019-07-24 16:18:15 +02:00
Mads Marquart
edf14cfd84 Add and configure sphinxcontrib-spelling 2019-07-24 16:17:36 +02:00
Mads Marquart
ee79969eda Delete docs/robots.txt
Introduced in a2930b4, but I found out you could deprecate the doc url at /en/master/ using the ReadTheDocs web configuration
2019-07-24 16:15:31 +02:00
Mads Marquart
dbb20b1fdc Convert various directives to Google style sections 2019-07-24 13:45:33 +02:00
Mads Marquart
beee209249 Convert :return: / :rtype: roles to Returns sections 2019-07-24 13:45:33 +02:00
Mads Marquart
d6876ce13b Convert :raises: roles to Raises sections 2019-07-24 13:43:34 +02:00
Mads Marquart
ed05d16a31 Move :type: roles into the Args sections 2019-07-24 13:43:34 +02:00
Mads Marquart
3806f01d2f Convert :param: roles to Args sections 2019-07-24 13:43:30 +02:00
Mads Marquart
5b69ced1e8 Add ability to use Google style docstrings
Use and configure the `napoleon` Sphinx extension
2019-07-24 13:43:02 +02:00
Mads Marquart
6b07f1d8b9 Fix first line of docstrings
- Use the imperative sense
- Use trailing dot
- Omit leading newline
- Grammar / vocabulary fixes
2019-07-24 13:43:01 +02:00
Przemek
700cf14a50 Add fetchThreadImages (#434) 2019-07-24 13:40:00 +02:00
Mads Marquart
1b08243cd2 Fix TODO entries showing file paths of the build system 2019-07-24 00:33:55 +02:00
Mads Marquart
a0b978004c Bump version: 1.7.2 → 1.7.3 2019-07-20 17:09:03 +02:00
Mads Marquart
efc8776e70 Fix login check, close #446
Facebook changed something internally, so that the redirected url is no longer always "/home.php", but instead sometimes just "/"
2019-07-20 17:01:54 +02:00
Szczepan Wiśniowski
915f9a3782 Add heart reaction (#445) 2019-07-20 16:21:44 +02:00
Mads Marquart
e136d77ade Fix 2FA login error, closes #442, replaces #443 2019-07-20 16:00:32 +02:00
Mads Marquart
04aec15833 Fix documentation badge 2019-07-04 00:43:34 +02:00
36 changed files with 1331 additions and 1088 deletions

View File

@@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 1.7.2 current_version = 1.8.0
commit = True commit = True
tag = True tag = True

View File

@@ -21,8 +21,8 @@ Traceback (most recent call last):
File "[site-packages]/fbchat/client.py", line 78, in __init__ File "[site-packages]/fbchat/client.py", line 78, in __init__
self.login(email, password, max_tries) self.login(email, password, max_tries)
File "[site-packages]/fbchat/client.py", line 407, in login File "[site-packages]/fbchat/client.py", line 407, in login
raise FBchatUserError('Login failed. Check email/password. (Failed on url: {})'.format(login_url)) raise FBchatUserError('Login failed. Check email/password. (Failed on URL: {})'.format(login_url))
fbchat.models.FBchatUserError: Login failed. Check email/password. (Failed on url: https://m.facebook.com/login.php?login_attempt=1) fbchat.models.FBchatUserError: Login failed. Check email/password. (Failed on URL: https://m.facebook.com/login.php?login_attempt=1)
``` ```
## Environment information ## Environment information

View File

@@ -30,7 +30,7 @@ jobs:
script: black --check --verbose . script: black --check --verbose .
- stage: deploy - stage: deploy
name: Github Releases name: GitHub Releases
if: tag IS present if: tag IS present
install: skip install: skip
script: flit build script: flit build

View File

@@ -1,5 +1,5 @@
Contributing to fbchat Contributing to ``fbchat``
====================== ==========================
Thanks for reading this, all contributions are very much welcome! Thanks for reading this, all contributions are very much welcome!

View File

@@ -1,5 +1,5 @@
fbchat: Facebook Chat (Messenger) for Python ``fbchat``: Facebook Chat (Messenger) for Python
============================================ ================================================
.. image:: https://img.shields.io/badge/license-BSD-blue.svg .. image:: https://img.shields.io/badge/license-BSD-blue.svg
:target: https://github.com/carpedm20/fbchat/tree/master/LICENSE :target: https://github.com/carpedm20/fbchat/tree/master/LICENSE
@@ -9,7 +9,7 @@ fbchat: Facebook Chat (Messenger) for Python
:target: https://pypi.python.org/pypi/fbchat :target: https://pypi.python.org/pypi/fbchat
:alt: Supported python versions: 2.7, 3.4, 3.5, 3.6, 3.7 and pypy :alt: Supported python versions: 2.7, 3.4, 3.5, 3.6, 3.7 and pypy
.. image:: https://readthedocs.org/projects/fbchat/badge/?version=master .. image:: https://readthedocs.org/projects/fbchat/badge/?version=latest
:target: https://fbchat.readthedocs.io :target: https://fbchat.readthedocs.io
:alt: Documentation :alt: Documentation

View File

@@ -41,6 +41,8 @@ extensions = [
"sphinx.ext.intersphinx", "sphinx.ext.intersphinx",
"sphinx.ext.todo", "sphinx.ext.todo",
"sphinx.ext.viewcode", "sphinx.ext.viewcode",
"sphinx.ext.napoleon",
"sphinxcontrib.spelling",
] ]
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
@@ -93,8 +95,6 @@ html_theme_options = {
"show_related": False, "show_related": False,
} }
html_extra_path = ["robots.txt"]
# Custom sidebar templates, must be a dictionary that maps document names # Custom sidebar templates, must be a dictionary that maps document names
# to template names. # to template names.
# #
@@ -182,3 +182,27 @@ intersphinx_mapping = {"https://docs.python.org/": None}
# If true, `todo` and `todoList` produce output, else they produce nothing. # If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = True todo_include_todos = True
todo_link_only = True
# -- Options for napoleon extension ----------------------------------------------
# Use Google style docstrings
napoleon_google_docstring = True
napoleon_numpy_docstring = False
# napoleon_use_admonition_for_examples = False
# napoleon_use_admonition_for_notes = False
# napoleon_use_admonition_for_references = False
# -- Options for spelling extension ----------------------------------------------
spelling_word_list_filename = [
"spelling/names.txt",
"spelling/technical.txt",
"spelling/fixes.txt",
]
spelling_ignore_wiki_words = False
# spelling_ignore_acronyms = False
spelling_ignore_python_builtins = False
spelling_ignore_importable_modules = False

View File

@@ -30,8 +30,8 @@ This will show the different ways of fetching information about users and thread
.. literalinclude:: ../examples/fetch.py .. literalinclude:: ../examples/fetch.py
Echobot ``Echobot``
------- -----------
This will reply to any message with the same message This will reply to any message with the same message

View File

@@ -9,7 +9,7 @@ Version X broke my installation
We try to provide backwards compatibility where possible, but since we're not part of Facebook, 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 most of the things may be broken at any point in time
Downgrade to an earlier version of fbchat, run this command Downgrade to an earlier version of ``fbchat``, run this command
.. code-block:: sh .. code-block:: sh
@@ -28,7 +28,7 @@ Submitting Issues
----------------- -----------------
If you're having trouble with some of the snippets, or you think some of the functionality is broken, 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 <https://github.com/carpedm20/fbchat>`_. please feel free to submit an issue on `GitHub <https://github.com/carpedm20/fbchat>`_.
You should first login with ``logging_level`` set to ``logging.DEBUG``:: You should first login with ``logging_level`` set to ``logging.DEBUG``::
from fbchat import Client from fbchat import Client

View File

@@ -6,8 +6,8 @@
.. This documentation's layout is heavily inspired by requests' layout: https://requests.readthedocs.io .. 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 Some documentation is also partially copied from facebook-chat-api: https://github.com/Schmavery/facebook-chat-api
fbchat: Facebook Chat (Messenger) for Python ``fbchat``: Facebook Chat (Messenger) for Python
============================================ ================================================
Release v\ |version|. (:ref:`install`) Release v\ |version|. (:ref:`install`)
@@ -35,7 +35,7 @@ This means doing the exact same GET/POST requests and tricking Facebook into thi
Therefore, this API requires the credentials of a Facebook account. Therefore, this API requires the credentials of a Facebook account.
.. note:: .. note::
If you're having problems, please check the :ref:`faq`, before asking questions on Github If you're having problems, please check the :ref:`faq`, before asking questions on GitHub
.. warning:: .. warning::
We are not responsible if your account gets banned for spammy activities, We are not responsible if your account gets banned for spammy activities,
@@ -44,7 +44,7 @@ Therefore, this API requires the credentials of a Facebook account.
.. note:: .. note::
Facebook now has an `official API <https://developers.facebook.com/docs/messenger-platform>`_ for chat bots, 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. 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`` If you're already familiar with the basics of how Facebook works internally, go to :ref:`examples` to see example usage of ``fbchat``

View File

@@ -3,10 +3,10 @@
Installation Installation
============ ============
Pip Install fbchat Install using pip
------------------ -----------------
To install fbchat, run this command: To install ``fbchat``, run this command:
.. code-block:: sh .. code-block:: sh
@@ -19,7 +19,7 @@ can guide you through the process.
Get the Source Code Get the Source Code
------------------- -------------------
fbchat is developed on GitHub, where the code is ``fbchat`` is developed on GitHub, where the code is
`always available <https://github.com/carpedm20/fbchat>`_. `always available <https://github.com/carpedm20/fbchat>`_.
You can either clone the public repository: You can either clone the public repository:

View File

@@ -24,7 +24,7 @@ Replace ``<email>`` and ``<password>`` with your email and password respectively
.. note:: .. note::
For ease of use then most of the code snippets in this document will assume you've already completed the login process 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 Though the second line, ``from fbchat.models import *``, is not strictly necessary 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`) If you want to change how verbose ``fbchat`` is, change the logging level (in :class:`Client`)
@@ -125,8 +125,8 @@ The following snippet will search for users by their name, take the first (and m
user = users[0] user = users[0]
print("User's ID: {}".format(user.uid)) print("User's ID: {}".format(user.uid))
print("User's name: {}".format(user.name)) print("User's name: {}".format(user.name))
print("User's profile picture url: {}".format(user.photo)) print("User's profile picture URL: {}".format(user.photo))
print("User's main url: {}".format(user.url)) 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 Since this uses Facebook's search functions, you don't have to specify the whole name, first names will usually be enough
@@ -154,7 +154,7 @@ Or you can set the ``session_cookies`` on your initial login.
client = Client('<email>', '<password>', session_cookies=session_cookies) client = Client('<email>', '<password>', session_cookies=session_cookies)
.. warning:: .. warning::
You session cookies can be just as valueable as you password, so store them with equal care You session cookies can be just as valuable as you password, so store them with equal care
.. _intro_events: .. _intro_events:
@@ -192,7 +192,7 @@ The change was in the parameters that our `onMessage` method took: ``message_obj
and ``mid``, ``ts``, ``metadata`` and ``msg`` got removed, but the function still works, since we included ``**kwargs`` and ``mid``, ``ts``, ``metadata`` and ``msg`` got removed, but the function still works, since we included ``**kwargs``
.. note:: .. note::
Therefore, for both backwards and forwards compatability, Therefore, for both backwards and forwards compatibility,
the API actually requires that you include ``**kwargs`` as your final argument. 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

View File

@@ -1,2 +0,0 @@
User-agent: *
Disallow: /en/master/

3
docs/spelling/fixes.txt Normal file
View File

@@ -0,0 +1,3 @@
premade
todo
emoji

3
docs/spelling/names.txt Normal file
View File

@@ -0,0 +1,3 @@
Facebook
GraphQL
GitHub

View File

@@ -0,0 +1,14 @@
iterables
timestamp
metadata
spam
spammy
admin
admins
unsend
unsends
unmute
spritemap
online
inbox
subclassing

View File

@@ -9,11 +9,11 @@ This page will be periodically updated to show missing features and documentatio
Missing Functionality Missing Functionality
--------------------- ---------------------
- Implement Client.searchForMessage - Implement ``Client.searchForMessage``
- This will use the graphql request API - This will use the GraphQL request API
- Implement chatting with pages properly - Implement chatting with pages properly
- Write better FAQ - Write better FAQ
- Explain usage of graphql - Explain usage of GraphQL
Documentation Documentation

View File

@@ -1,5 +1,6 @@
# -*- coding: UTF-8 -*- # -*- coding: UTF-8 -*-
from itertools import islice
from fbchat import Client from fbchat import Client
from fbchat.models import * from fbchat.models import *
@@ -62,3 +63,9 @@ print("thread's type: {}".format(thread.type))
# Here should be an example of `getUnread` # Here should be an example of `getUnread`
# Print image url for 20 last images from thread.
images = client.fetchThreadImages("<thread id>")
for image in islice(image, 20):
print(image.large_preview_url)

View File

@@ -47,7 +47,7 @@ client.sendLocalImage(
thread_type=thread_type, thread_type=thread_type,
) )
# Will download the image at the url `<image url>`, and then send it # Will download the image at the URL `<image url>`, and then send it
client.sendRemoteImage( client.sendRemoteImage(
"<image url>", "<image url>",
message=Message(text="This is a remote image"), message=Message(text="This is a remote image"),

View File

@@ -13,7 +13,7 @@ from ._client import Client
from ._util import log # TODO: Remove this (from examples too) from ._util import log # TODO: Remove this (from examples too)
__title__ = "fbchat" __title__ = "fbchat"
__version__ = "1.7.2" __version__ = "1.8.0"
__description__ = "Facebook Chat (Messenger) for Python" __description__ = "Facebook Chat (Messenger) for Python"
__copyright__ = "Copyright 2015 - 2019 by Taehoon Kim" __copyright__ = "Copyright 2015 - 2019 by Taehoon Kim"

View File

@@ -7,7 +7,7 @@ from . import _util
@attr.s(cmp=False) @attr.s(cmp=False)
class Attachment(object): class Attachment(object):
"""Represents a Facebook attachment""" """Represents a Facebook attachment."""
#: The attachment ID #: The attachment ID
uid = attr.ib(None) uid = attr.ib(None)
@@ -15,12 +15,12 @@ class Attachment(object):
@attr.s(cmp=False) @attr.s(cmp=False)
class UnsentMessage(Attachment): class UnsentMessage(Attachment):
"""Represents an unsent message attachment""" """Represents an unsent message attachment."""
@attr.s(cmp=False) @attr.s(cmp=False)
class ShareAttachment(Attachment): class ShareAttachment(Attachment):
"""Represents a shared item (eg. URL) that has been sent as a Facebook attachment""" """Represents a shared item (e.g. URL) attachment."""
#: ID of the author of the shared post #: ID of the author of the shared post
author = attr.ib(None) author = attr.ib(None)

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ log = logging.getLogger("client")
class Enum(aenum.Enum): class Enum(aenum.Enum):
"""Used internally by fbchat to support enumerations""" """Used internally by ``fbchat`` to support enumerations"""
def __repr__(self): def __repr__(self):
# For documentation: # For documentation:

View File

@@ -3,7 +3,10 @@ from __future__ import unicode_literals
class FBchatException(Exception): class FBchatException(Exception):
"""Custom exception thrown by fbchat. All exceptions in the fbchat module inherits this""" """Custom exception thrown by ``fbchat``.
All exceptions in the ``fbchat`` module inherits this.
"""
class FBchatFacebookError(FBchatException): class FBchatFacebookError(FBchatException):
@@ -11,7 +14,7 @@ class FBchatFacebookError(FBchatException):
fb_error_code = None fb_error_code = None
#: The error message that Facebook returned (In the user's own language) #: The error message that Facebook returned (In the user's own language)
fb_error_message = None fb_error_message = None
#: The status code that was sent in the http response (eg. 404) (Usually only set if not successful, aka. not 200) #: The status code that was sent in the HTTP response (e.g. 404) (Usually only set if not successful, aka. not 200)
request_status_code = None request_status_code = None
def __init__( def __init__(
@@ -22,7 +25,7 @@ class FBchatFacebookError(FBchatException):
request_status_code=None, request_status_code=None,
): ):
super(FBchatFacebookError, self).__init__(message) super(FBchatFacebookError, self).__init__(message)
"""Thrown by fbchat when Facebook returns an error""" """Thrown by ``fbchat`` when Facebook returns an error"""
self.fb_error_code = str(fb_error_code) self.fb_error_code = str(fb_error_code)
self.fb_error_message = fb_error_message self.fb_error_message = fb_error_message
self.request_status_code = request_status_code self.request_status_code = request_status_code
@@ -54,4 +57,4 @@ class FBchatPleaseRefresh(FBchatFacebookError):
class FBchatUserError(FBchatException): class FBchatUserError(FBchatException):
"""Thrown by fbchat when wrong values are entered""" """Thrown by ``fbchat`` when wrong values are entered."""

View File

@@ -7,9 +7,9 @@ from ._attachment import Attachment
@attr.s(cmp=False) @attr.s(cmp=False)
class FileAttachment(Attachment): class FileAttachment(Attachment):
"""Represents a file that has been sent as a Facebook attachment""" """Represents a file that has been sent as a Facebook attachment."""
#: Url where you can download the file #: URL where you can download the file
url = attr.ib(None) url = attr.ib(None)
#: Size of the file in bytes #: Size of the file in bytes
size = attr.ib(None) size = attr.ib(None)
@@ -33,11 +33,11 @@ class FileAttachment(Attachment):
@attr.s(cmp=False) @attr.s(cmp=False)
class AudioAttachment(Attachment): class AudioAttachment(Attachment):
"""Represents an audio file that has been sent as a Facebook attachment""" """Represents an audio file that has been sent as a Facebook attachment."""
#: Name of the file #: Name of the file
filename = attr.ib(None) filename = attr.ib(None)
#: Url of the audio file #: URL of the audio file
url = attr.ib(None) url = attr.ib(None)
#: Duration of the audio clip in milliseconds #: Duration of the audio clip in milliseconds
duration = attr.ib(None) duration = attr.ib(None)
@@ -59,13 +59,13 @@ class AudioAttachment(Attachment):
@attr.s(cmp=False, init=False) @attr.s(cmp=False, init=False)
class ImageAttachment(Attachment): class ImageAttachment(Attachment):
"""Represents an image that has been sent as a Facebook attachment """Represents an image that has been sent as a Facebook attachment.
To retrieve the full image url, use: :func:`fbchat.Client.fetchImageUrl`, and pass To retrieve the full image URL, use: `Client.fetchImageUrl`, and pass it the id of
it the uid of the image attachment the image attachment.
""" """
#: The extension of the original image (eg. 'png') #: The extension of the original image (e.g. ``png``)
original_extension = attr.ib(None) original_extension = attr.ib(None)
#: Width of original image #: Width of original image
width = attr.ib(None, converter=lambda x: None if x is None else int(x)) width = attr.ib(None, converter=lambda x: None if x is None else int(x))
@@ -92,7 +92,7 @@ class ImageAttachment(Attachment):
#: Height of the large preview image #: Height of the large preview image
large_preview_height = attr.ib(None) large_preview_height = attr.ib(None)
#: URL to an animated preview of the image (eg. for gifs) #: URL to an animated preview of the image (e.g. for GIFs)
animated_preview_url = attr.ib(None) animated_preview_url = attr.ib(None)
#: Width of the animated preview image #: Width of the animated preview image
animated_preview_width = attr.ib(None) animated_preview_width = attr.ib(None)
@@ -155,10 +155,22 @@ class ImageAttachment(Attachment):
uid=data.get("legacy_attachment_id"), uid=data.get("legacy_attachment_id"),
) )
@classmethod
def _from_list(cls, data):
data = data["node"]
return cls(
width=data["original_dimensions"].get("x"),
height=data["original_dimensions"].get("y"),
thumbnail_url=data["image"].get("uri"),
large_preview=data["image2"],
preview=data["image1"],
uid=data["legacy_attachment_id"],
)
@attr.s(cmp=False, init=False) @attr.s(cmp=False, init=False)
class VideoAttachment(Attachment): class VideoAttachment(Attachment):
"""Represents a video that has been sent as a Facebook attachment""" """Represents a video that has been sent as a Facebook attachment."""
#: Size of the original video in bytes #: Size of the original video in bytes
size = attr.ib(None) size = attr.ib(None)
@@ -252,6 +264,18 @@ class VideoAttachment(Attachment):
uid=data["target"].get("video_id"), uid=data["target"].get("video_id"),
) )
@classmethod
def _from_list(cls, data):
data = data["node"]
return cls(
width=data["original_dimensions"].get("x"),
height=data["original_dimensions"].get("y"),
small_image=data["image"],
medium_image=data["image1"],
large_image=data["image2"],
uid=data["legacy_attachment_id"],
)
def graphql_to_attachment(data): def graphql_to_attachment(data):
_type = data["__typename"] _type = data["__typename"]

View File

@@ -8,11 +8,11 @@ from ._thread import ThreadType, Thread
@attr.s(cmp=False, init=False) @attr.s(cmp=False, init=False)
class Group(Thread): class Group(Thread):
"""Represents a Facebook group. Inherits `Thread`""" """Represents a Facebook group. Inherits `Thread`."""
#: Unique list (set) of the group thread's participant user IDs #: Unique list (set) of the group thread's participant user IDs
participants = attr.ib(factory=set, converter=lambda x: set() if x is None else x) participants = attr.ib(factory=set, converter=lambda x: set() if x is None else x)
#: A dict, containing user nicknames mapped to their IDs #: A dictionary, containing user nicknames mapped to their IDs
nicknames = attr.ib(factory=dict, converter=lambda x: {} if x is None else x) nicknames = attr.ib(factory=dict, converter=lambda x: {} if x is None else x)
#: A :class:`ThreadColor`. The groups's message color #: A :class:`ThreadColor`. The groups's message color
color = attr.ib(None) color = attr.ib(None)
@@ -107,7 +107,7 @@ class Group(Thread):
@attr.s(cmp=False, init=False) @attr.s(cmp=False, init=False)
class Room(Group): class Room(Group):
"""Deprecated. Use :class:`Group` instead""" """Deprecated. Use `Group` instead."""
# True is room is not discoverable # True is room is not discoverable
privacy_mode = attr.ib(None) privacy_mode = attr.ib(None)

View File

@@ -8,9 +8,9 @@ from . import _util
@attr.s(cmp=False) @attr.s(cmp=False)
class LocationAttachment(Attachment): class LocationAttachment(Attachment):
"""Represents a user location """Represents a user location.
Latitude and longitude OR address is provided by Facebook Latitude and longitude OR address is provided by Facebook.
""" """
#: Latitude of the location #: Latitude of the location
@@ -58,7 +58,7 @@ class LocationAttachment(Attachment):
@attr.s(cmp=False, init=False) @attr.s(cmp=False, init=False)
class LiveLocationAttachment(LocationAttachment): class LiveLocationAttachment(LocationAttachment):
"""Represents a live user location""" """Represents a live user location."""
#: Name of the location #: Name of the location
name = attr.ib(None) name = attr.ib(None)

View File

@@ -9,7 +9,7 @@ from ._core import Enum
class EmojiSize(Enum): class EmojiSize(Enum):
"""Used to specify the size of a sent emoji""" """Used to specify the size of a sent emoji."""
LARGE = "369239383222810" LARGE = "369239383222810"
MEDIUM = "369239343222814" MEDIUM = "369239343222814"
@@ -33,8 +33,9 @@ class EmojiSize(Enum):
class MessageReaction(Enum): class MessageReaction(Enum):
"""Used to specify a message reaction""" """Used to specify a message reaction."""
HEART = ""
LOVE = "😍" LOVE = "😍"
SMILE = "😆" SMILE = "😆"
WOW = "😮" WOW = "😮"
@@ -46,7 +47,7 @@ class MessageReaction(Enum):
@attr.s(cmp=False) @attr.s(cmp=False)
class Mention(object): class Mention(object):
"""Represents a @mention""" """Represents a ``@mention``."""
#: The thread ID the mention is pointing at #: The thread ID the mention is pointing at
thread_id = attr.ib() thread_id = attr.ib()
@@ -58,7 +59,7 @@ class Mention(object):
@attr.s(cmp=False) @attr.s(cmp=False)
class Message(object): class Message(object):
"""Represents a Facebook message""" """Represents a Facebook message."""
#: The actual message #: The actual message
text = attr.ib(None) text = attr.ib(None)
@@ -74,9 +75,9 @@ class Message(object):
timestamp = attr.ib(None, init=False) timestamp = attr.ib(None, init=False)
#: Whether the message is read #: Whether the message is read
is_read = attr.ib(None, init=False) is_read = attr.ib(None, init=False)
#: A list of pepole IDs who read the message, works only with :func:`fbchat.Client.fetchThreadMessages` #: A list of people IDs who read the message, works only with :func:`fbchat.Client.fetchThreadMessages`
read_by = attr.ib(factory=list, init=False) read_by = attr.ib(factory=list, init=False)
#: A dict with user's IDs as keys, and their :class:`MessageReaction` as values #: A dictionary with user's IDs as keys, and their :class:`MessageReaction` as values
reactions = attr.ib(factory=dict, init=False) reactions = attr.ib(factory=dict, init=False)
#: A :class:`Sticker` #: A :class:`Sticker`
sticker = attr.ib(None) sticker = attr.ib(None)
@@ -97,7 +98,7 @@ class Message(object):
def formatMentions(cls, text, *args, **kwargs): def formatMentions(cls, text, *args, **kwargs):
"""Like `str.format`, but takes tuples with a thread id and text instead. """Like `str.format`, but takes tuples with a thread id and text instead.
Returns a `Message` object, with the formatted string and relevant mentions. Return a `Message` object, with the formatted string and relevant mentions.
>>> Message.formatMentions("Hey {!r}! My name is {}", ("1234", "Peter"), ("4321", "Michael")) >>> Message.formatMentions("Hey {!r}! My name is {}", ("1234", "Peter"), ("4321", "Michael"))
<Message (None): "Hey 'Peter'! My name is Michael", mentions=[<Mention 1234: offset=4 length=7>, <Mention 4321: offset=24 length=7>] emoji_size=None attachments=[]> <Message (None): "Hey 'Peter'! My name is Michael", mentions=[<Mention 1234: offset=4 length=7>, <Mention 4321: offset=24 length=7>] emoji_size=None attachments=[]>

View File

@@ -8,9 +8,9 @@ from ._thread import ThreadType, Thread
@attr.s(cmp=False, init=False) @attr.s(cmp=False, init=False)
class Page(Thread): class Page(Thread):
"""Represents a Facebook page. Inherits `Thread`""" """Represents a Facebook page. Inherits `Thread`."""
#: The page's custom url #: The page's custom URL
url = attr.ib(None) url = attr.ib(None)
#: The name of the page's location city #: The name of the page's location city
city = attr.ib(None) city = attr.ib(None)

View File

@@ -14,11 +14,11 @@ class GuestStatus(Enum):
@attr.s(cmp=False) @attr.s(cmp=False)
class Plan(object): class Plan(object):
"""Represents a plan""" """Represents a plan."""
#: ID of the plan #: ID of the plan
uid = attr.ib(None, init=False) uid = attr.ib(None, init=False)
#: Plan time (unix time stamp), only precise down to the minute #: Plan time (timestamp), only precise down to the minute
time = attr.ib(converter=int) time = attr.ib(converter=int)
#: Plan title #: Plan title
title = attr.ib() title = attr.ib()
@@ -28,7 +28,7 @@ class Plan(object):
location_id = attr.ib(None, converter=lambda x: x or "") location_id = attr.ib(None, converter=lambda x: x or "")
#: ID of the plan creator #: ID of the plan creator
author_id = attr.ib(None, init=False) author_id = attr.ib(None, init=False)
#: Dict of `User` IDs mapped to their `GuestStatus` #: Dictionary of `User` IDs mapped to their `GuestStatus`
guests = attr.ib(None, init=False) guests = attr.ib(None, init=False)
@property @property

View File

@@ -6,7 +6,7 @@ import attr
@attr.s(cmp=False) @attr.s(cmp=False)
class Poll(object): class Poll(object):
"""Represents a poll""" """Represents a poll."""
#: Title of the poll #: Title of the poll
title = attr.ib() title = attr.ib()
@@ -29,7 +29,7 @@ class Poll(object):
@attr.s(cmp=False) @attr.s(cmp=False)
class PollOption(object): class PollOption(object):
"""Represents a poll option""" """Represents a poll option."""
#: Text of the poll option #: Text of the poll option
text = attr.ib() text = attr.ib()

View File

@@ -7,7 +7,7 @@ from ._attachment import Attachment
@attr.s(cmp=False) @attr.s(cmp=False)
class QuickReply(object): class QuickReply(object):
"""Represents a quick reply""" """Represents a quick reply."""
#: Payload of the quick reply #: Payload of the quick reply
payload = attr.ib(None) payload = attr.ib(None)
@@ -21,7 +21,7 @@ class QuickReply(object):
@attr.s(cmp=False, init=False) @attr.s(cmp=False, init=False)
class QuickReplyText(QuickReply): class QuickReplyText(QuickReply):
"""Represents a text quick reply""" """Represents a text quick reply."""
#: Title of the quick reply #: Title of the quick reply
title = attr.ib(None) title = attr.ib(None)
@@ -38,7 +38,7 @@ class QuickReplyText(QuickReply):
@attr.s(cmp=False, init=False) @attr.s(cmp=False, init=False)
class QuickReplyLocation(QuickReply): class QuickReplyLocation(QuickReply):
"""Represents a location quick reply (Doesn't work on mobile)""" """Represents a location quick reply (Doesn't work on mobile)."""
#: Type of the quick reply #: Type of the quick reply
_type = "location" _type = "location"
@@ -50,7 +50,7 @@ class QuickReplyLocation(QuickReply):
@attr.s(cmp=False, init=False) @attr.s(cmp=False, init=False)
class QuickReplyPhoneNumber(QuickReply): class QuickReplyPhoneNumber(QuickReply):
"""Represents a phone number quick reply (Doesn't work on mobile)""" """Represents a phone number quick reply (Doesn't work on mobile)."""
#: URL of the quick reply image (optional) #: URL of the quick reply image (optional)
image_url = attr.ib(None) image_url = attr.ib(None)
@@ -64,7 +64,7 @@ class QuickReplyPhoneNumber(QuickReply):
@attr.s(cmp=False, init=False) @attr.s(cmp=False, init=False)
class QuickReplyEmail(QuickReply): class QuickReplyEmail(QuickReply):
"""Represents an email quick reply (Doesn't work on mobile)""" """Represents an email quick reply (Doesn't work on mobile)."""
#: URL of the quick reply image (optional) #: URL of the quick reply image (optional)
image_url = attr.ib(None) image_url = attr.ib(None)

View File

@@ -24,6 +24,12 @@ def session_factory(user_agent=None):
return session return session
def is_home(url):
parts = _util.urlparse(url)
# Check the urls `/home.php` and `/`
return "home" in parts.path or "/" == parts.path
def _2fa_helper(session, code, r): def _2fa_helper(session, code, r):
soup = find_input_fields(r.text) soup = find_input_fields(r.text)
data = dict() data = dict()
@@ -39,7 +45,7 @@ def _2fa_helper(session, code, r):
r = session.post(url, data=data) r = session.post(url, data=data)
if "home" in r.url: if is_home(r.url):
return r return r
del data["approvals_code"] del data["approvals_code"]
@@ -52,7 +58,7 @@ def _2fa_helper(session, code, r):
# At this stage, we have dtsg, nh, name_action_selected, submit[Continue] # At this stage, we have dtsg, nh, name_action_selected, submit[Continue]
r = session.post(url, data=data) r = session.post(url, data=data)
if "home" in r.url: if is_home(r.url):
return r return r
del data["name_action_selected"] del data["name_action_selected"]
@@ -60,7 +66,7 @@ def _2fa_helper(session, code, r):
# At this stage, we have dtsg, nh, submit[Continue] # At this stage, we have dtsg, nh, submit[Continue]
r = session.post(url, data=data) r = session.post(url, data=data)
if "home" in r.url: if is_home(r.url):
return r return r
del data["submit[Continue]"] del data["submit[Continue]"]
@@ -69,7 +75,7 @@ def _2fa_helper(session, code, r):
# At this stage, we have dtsg, nh, submit[This was me] # At this stage, we have dtsg, nh, submit[This was me]
r = session.post(url, data=data) r = session.post(url, data=data)
if "home" in r.url: if is_home(r.url):
return r return r
del data["submit[This was me]"] del data["submit[This was me]"]
@@ -107,7 +113,7 @@ class State(object):
} }
@classmethod @classmethod
def login(cls, email, password, user_agent=None): def login(cls, email, password, on_2fa_callback, user_agent=None):
session = session_factory(user_agent=user_agent) session = session_factory(user_agent=user_agent)
soup = find_input_fields(session.get("https://m.facebook.com/").text) soup = find_input_fields(session.get("https://m.facebook.com/").text)
@@ -131,7 +137,7 @@ class State(object):
if "save-device" in r.url: if "save-device" in r.url:
r = session.get("https://m.facebook.com/login/save-device/cancel/") r = session.get("https://m.facebook.com/login/save-device/cancel/")
if "home" in r.url: if is_home(r.url):
return cls.from_session(session=session) return cls.from_session(session=session)
else: else:
raise _exception.FBchatUserError( raise _exception.FBchatUserError(
@@ -143,7 +149,7 @@ class State(object):
# Send a request to the login url, to see if we're directed to the home page # Send a request to the login url, to see if we're directed to the home page
url = "https://m.facebook.com/login.php?login_attempt=1" url = "https://m.facebook.com/login.php?login_attempt=1"
r = self._session.get(url, allow_redirects=False) r = self._session.get(url, allow_redirects=False)
return "Location" in r.headers and "home" in r.headers["Location"] return "Location" in r.headers and is_home(r.headers["Location"])
def logout(self): def logout(self):
logout_h = self._logout_h logout_h = self._logout_h

View File

@@ -7,7 +7,7 @@ from ._attachment import Attachment
@attr.s(cmp=False, init=False) @attr.s(cmp=False, init=False)
class Sticker(Attachment): class Sticker(Attachment):
"""Represents a Facebook sticker that has been sent to a thread as an attachment""" """Represents a Facebook sticker that has been sent to a thread as an attachment."""
#: The sticker-pack's ID #: The sticker-pack's ID
pack = attr.ib(None) pack = attr.ib(None)
@@ -21,7 +21,7 @@ class Sticker(Attachment):
large_sprite_image = attr.ib(None) large_sprite_image = attr.ib(None)
#: The amount of frames present in the spritemap pr. row #: The amount of frames present in the spritemap pr. row
frames_per_row = attr.ib(None) frames_per_row = attr.ib(None)
#: The amount of frames present in the spritemap pr. coloumn #: The amount of frames present in the spritemap pr. column
frames_per_col = attr.ib(None) frames_per_col = attr.ib(None)
#: The frame rate the spritemap is intended to be played in #: The frame rate the spritemap is intended to be played in
frame_rate = attr.ib(None) frame_rate = attr.ib(None)

View File

@@ -6,7 +6,10 @@ from ._core import Enum
class ThreadType(Enum): class ThreadType(Enum):
"""Used to specify what type of Facebook thread is being used. See :ref:`intro_threads` 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
@@ -24,7 +27,7 @@ class ThreadLocation(Enum):
class ThreadColor(Enum): class ThreadColor(Enum):
"""Used to specify a thread colors""" """Used to specify a thread colors."""
MESSENGER_BLUE = "#0084ff" MESSENGER_BLUE = "#0084ff"
VIKING = "#44bec7" VIKING = "#44bec7"
@@ -60,13 +63,13 @@ class ThreadColor(Enum):
@attr.s(cmp=False, init=False) @attr.s(cmp=False, init=False)
class Thread(object): class Thread(object):
"""Represents a Facebook thread""" """Represents a Facebook thread."""
#: The unique identifier of the thread. Can be used a ``thread_id``. See :ref:`intro_threads` for more info #: The unique identifier of the thread. Can be used a ``thread_id``. See :ref:`intro_threads` for more info
uid = attr.ib(converter=str) uid = attr.ib(converter=str)
#: Specifies the type of thread. Can be used a ``thread_type``. See :ref:`intro_threads` for more info #: Specifies the type of thread. Can be used a ``thread_type``. See :ref:`intro_threads` for more info
type = attr.ib() type = attr.ib()
#: A url to the thread's picture #: A URL to the thread's picture
photo = attr.ib(None) photo = attr.ib(None)
#: The name of the thread #: The name of the thread
name = attr.ib(None) name = attr.ib(None)

View File

@@ -38,7 +38,7 @@ GENDERS = {
class TypingStatus(Enum): class TypingStatus(Enum):
"""Used to specify whether the user is typing or has stopped typing""" """Used to specify whether the user is typing or has stopped typing."""
STOPPED = 0 STOPPED = 0
TYPING = 1 TYPING = 1
@@ -46,9 +46,9 @@ class TypingStatus(Enum):
@attr.s(cmp=False, init=False) @attr.s(cmp=False, init=False)
class User(Thread): class User(Thread):
"""Represents a Facebook user. Inherits `Thread`""" """Represents a Facebook user. Inherits `Thread`."""
#: The profile url #: The profile URL
url = attr.ib(None) url = attr.ib(None)
#: The users first name #: The users first name
first_name = attr.ib(None) first_name = attr.ib(None)

View File

@@ -56,6 +56,7 @@ test = [
] ]
docs = [ docs = [
"sphinx~=2.0", "sphinx~=2.0",
"sphinxcontrib-spelling~=4.0"
] ]
lint = [ lint = [
"black", "black",