@@ -13,8 +13,8 @@ You should also make sure that the file's access control is appropriately restri
|
|||||||
Logging In
|
Logging In
|
||||||
----------
|
----------
|
||||||
|
|
||||||
Simply create an instance of :class:`Client`. If you have two factor authentication enabled, type the code in the terminal prompt
|
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 :func:`Client.on_2fa_code`)::
|
(If you want to supply the code in another fashion, overwrite `Client.on_2fa_code`)::
|
||||||
|
|
||||||
from fbchat import Client
|
from fbchat import Client
|
||||||
from fbchat.models import *
|
from fbchat.models import *
|
||||||
@@ -26,15 +26,15 @@ Replace ``<email>`` and ``<password>`` with your email and password respectively
|
|||||||
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 necessary 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 `Client`)
|
||||||
|
|
||||||
Throughout your code, if you want to check whether you are still logged in, use :func:`Client.is_logged_in`.
|
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 :func:`Client.login`::
|
An example would be to login again if you've been logged out, using `Client.login`::
|
||||||
|
|
||||||
if not client.is_logged_in():
|
if not client.is_logged_in():
|
||||||
client.login('<email>', '<password>')
|
client.login('<email>', '<password>')
|
||||||
|
|
||||||
When you're done using the client, and want to securely logout, use :func:`Client.logout`::
|
When you're done using the client, and want to securely logout, use `Client.logout`::
|
||||||
|
|
||||||
client.logout()
|
client.logout()
|
||||||
|
|
||||||
@@ -46,14 +46,14 @@ Threads
|
|||||||
|
|
||||||
A thread can refer to two things: A Messenger group chat or a single Facebook user
|
A thread can refer to two things: A Messenger group chat or a single Facebook user
|
||||||
|
|
||||||
:class:`ThreadType` is an enumerator with two values: ``USER`` and ``GROUP``.
|
`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.
|
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 differentiates between these two internally
|
This is required for many of ``fbchat``'s functions, since Facebook differentiates between these two internally
|
||||||
|
|
||||||
Searching for group chats and finding their ID can be done via. :func:`Client.search_for_groups`,
|
Searching for group chats and finding their ID can be done via. `Client.search_for_groups`,
|
||||||
and searching for users is possible via. :func:`Client.search_for_users`. See :ref:`intro_fetching`
|
and searching for users is possible via. `Client.search_for_users`. See :ref:`intro_fetching`
|
||||||
|
|
||||||
You can get your own user ID by using :any:`Client.uid`
|
You can get your own user ID by using `Client.uid`
|
||||||
|
|
||||||
Getting the ID of a group chat is fairly trivial otherwise, since you only need to navigate to `<https://www.facebook.com/messages/>`_,
|
Getting the ID of a group chat is fairly trivial otherwise, 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.
|
click on the group you want to find the ID of, and then read the id from the address bar.
|
||||||
@@ -71,7 +71,7 @@ corresponds to the ID of a single user, and the ID of a group respectively::
|
|||||||
client.send(Message(text='<message>'), thread_id='<user id>', thread_type=ThreadType.USER)
|
client.send(Message(text='<message>'), thread_id='<user id>', thread_type=ThreadType.USER)
|
||||||
client.send(Message(text='<message>'), thread_id='<group id>', thread_type=ThreadType.GROUP)
|
client.send(Message(text='<message>'), thread_id='<group id>', thread_type=ThreadType.GROUP)
|
||||||
|
|
||||||
Some functions (e.g. :func:`Client.change_thread_color`) don't require a thread type, so in these cases you just provide the thread ID::
|
Some functions (e.g. `Client.change_thread_color`) don't require a thread type, so in these cases you just provide the thread ID::
|
||||||
|
|
||||||
client.change_thread_color(ThreadColor.BILOBA_FLOWER, thread_id='<user id>')
|
client.change_thread_color(ThreadColor.BILOBA_FLOWER, thread_id='<user id>')
|
||||||
client.change_thread_color(ThreadColor.MESSENGER_BLUE, thread_id='<group id>')
|
client.change_thread_color(ThreadColor.MESSENGER_BLUE, thread_id='<group id>')
|
||||||
@@ -85,8 +85,8 @@ Message IDs
|
|||||||
Every message you send on Facebook has a unique ID, and every action you do in a thread,
|
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.
|
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.react_to_message`,
|
Some of ``fbchat``'s functions require these ID's, like `Client.react_to_message`,
|
||||||
and some of then provide this ID, like :func:`Client.send`.
|
and some of then provide this ID, like `Client.send`.
|
||||||
This snippet shows how to send a message, and then use the returned ID to react to that message with a 😍 emoji::
|
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.send(Message(text='message'), thread_id=thread_id, thread_type=thread_type)
|
message_id = client.send(Message(text='message'), thread_id=thread_id, thread_type=thread_type)
|
||||||
@@ -118,7 +118,7 @@ Fetching Information
|
|||||||
|
|
||||||
You can use ``fbchat`` to fetch basic information like user names, profile pictures, thread names and user IDs
|
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.search_for_users`.
|
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::
|
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.search_for_users('<name of user>')
|
users = client.search_for_users('<name of user>')
|
||||||
@@ -140,11 +140,11 @@ Sessions
|
|||||||
|
|
||||||
``fbchat`` provides functions to retrieve and set the session cookies.
|
``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.
|
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 :func:`Client.get_gession` to retrieve the cookies::
|
Use `Client.get_gession` to retrieve the cookies::
|
||||||
|
|
||||||
session_cookies = client.get_gession()
|
session_cookies = client.get_gession()
|
||||||
|
|
||||||
Then you can use :func:`Client.set_gession`::
|
Then you can use `Client.set_gession`::
|
||||||
|
|
||||||
client.set_gession(session_cookies)
|
client.set_gession(session_cookies)
|
||||||
|
|
||||||
@@ -162,7 +162,7 @@ Or you can set the ``session_cookies`` on your initial login.
|
|||||||
Listening & Events
|
Listening & Events
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
To use the listening functions ``fbchat`` offers (like :func:`Client.listen`),
|
To use the listening functions ``fbchat`` offers (like `Client.listen`),
|
||||||
you have to define what should be executed when certain events happen.
|
you have to define what should be executed when certain events happen.
|
||||||
By default, (most) events will just be a `logging.info` statement,
|
By default, (most) events will just be a `logging.info` statement,
|
||||||
meaning it will simply print information to the console when an event happens
|
meaning it will simply print information to the console when an event happens
|
||||||
@@ -170,7 +170,7 @@ meaning it will simply print information to the console when an event happens
|
|||||||
.. note::
|
.. note::
|
||||||
You can identify the event methods by their ``on`` prefix, e.g. ``on_message``
|
You can identify the event methods by their ``on`` prefix, e.g. ``on_message``
|
||||||
|
|
||||||
The event actions can be changed by subclassing the :class:`Client`, and then overwriting the event methods::
|
The event actions can be changed by subclassing the `Client`, and then overwriting the event methods::
|
||||||
|
|
||||||
class CustomClient(Client):
|
class CustomClient(Client):
|
||||||
def on_message(self, mid, author_id, message_object, thread_id, thread_type, ts, metadata, msg, **kwargs):
|
def on_message(self, mid, author_id, message_object, thread_id, thread_type, ts, metadata, msg, **kwargs):
|
||||||
|
@@ -11,6 +11,7 @@ _logging.getLogger(__name__).addHandler(_logging.NullHandler())
|
|||||||
|
|
||||||
# The order of these is somewhat significant, e.g. User has to be imported after Thread!
|
# The order of these is somewhat significant, e.g. User has to be imported after Thread!
|
||||||
from . import _core, _util
|
from . import _core, _util
|
||||||
|
from ._core import Image
|
||||||
from ._exception import FBchatException, FBchatFacebookError
|
from ._exception import FBchatException, FBchatFacebookError
|
||||||
from ._thread import ThreadType, ThreadLocation, ThreadColor, Thread
|
from ._thread import ThreadType, ThreadLocation, ThreadColor, Thread
|
||||||
from ._user import TypingStatus, User, ActiveStatus
|
from ._user import TypingStatus, User, ActiveStatus
|
||||||
|
@@ -1,8 +1,9 @@
|
|||||||
import attr
|
import attr
|
||||||
|
from ._core import attrs_default, Image
|
||||||
from . import _util
|
from . import _util
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
@attrs_default
|
||||||
class Attachment:
|
class Attachment:
|
||||||
"""Represents a Facebook attachment."""
|
"""Represents a Facebook attachment."""
|
||||||
|
|
||||||
@@ -10,12 +11,12 @@ class Attachment:
|
|||||||
uid = attr.ib(None)
|
uid = attr.ib(None)
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
@attrs_default
|
||||||
class UnsentMessage(Attachment):
|
class UnsentMessage(Attachment):
|
||||||
"""Represents an unsent message attachment."""
|
"""Represents an unsent message attachment."""
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
@attrs_default
|
||||||
class ShareAttachment(Attachment):
|
class ShareAttachment(Attachment):
|
||||||
"""Represents a shared item (e.g. URL) attachment."""
|
"""Represents a shared item (e.g. URL) attachment."""
|
||||||
|
|
||||||
@@ -31,26 +32,30 @@ class ShareAttachment(Attachment):
|
|||||||
description = attr.ib(None)
|
description = attr.ib(None)
|
||||||
#: Name of the source
|
#: Name of the source
|
||||||
source = attr.ib(None)
|
source = attr.ib(None)
|
||||||
#: URL of the attachment image
|
#: The attached image
|
||||||
image_url = attr.ib(None)
|
image = attr.ib(None)
|
||||||
#: URL of the original image if Facebook uses ``safe_image``
|
#: URL of the original image if Facebook uses ``safe_image``
|
||||||
original_image_url = attr.ib(None)
|
original_image_url = attr.ib(None)
|
||||||
#: Width of the image
|
|
||||||
image_width = attr.ib(None)
|
|
||||||
#: Height of the image
|
|
||||||
image_height = attr.ib(None)
|
|
||||||
#: List of additional attachments
|
#: List of additional attachments
|
||||||
attachments = attr.ib(factory=list, converter=lambda x: [] if x is None else x)
|
attachments = attr.ib(factory=list)
|
||||||
|
|
||||||
# Put here for backwards compatibility, so that the init argument order is preserved
|
|
||||||
uid = attr.ib(None)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _from_graphql(cls, data):
|
def _from_graphql(cls, data):
|
||||||
from . import _file
|
from . import _file
|
||||||
|
|
||||||
|
image = None
|
||||||
|
original_image_url = None
|
||||||
|
media = data.get("media")
|
||||||
|
if media and media.get("image"):
|
||||||
|
image = Image._from_uri(media["image"])
|
||||||
|
original_image_url = (
|
||||||
|
_util.get_url_parameter(image.url, "url")
|
||||||
|
if "/safe_image.php" in image.url
|
||||||
|
else image.url
|
||||||
|
)
|
||||||
|
|
||||||
url = data.get("url")
|
url = data.get("url")
|
||||||
rtn = cls(
|
return cls(
|
||||||
uid=data.get("deduplication_key"),
|
uid=data.get("deduplication_key"),
|
||||||
author=data["target"]["actors"][0]["id"]
|
author=data["target"]["actors"][0]["id"]
|
||||||
if data["target"].get("actors")
|
if data["target"].get("actors")
|
||||||
@@ -64,20 +69,10 @@ class ShareAttachment(Attachment):
|
|||||||
if data.get("description")
|
if data.get("description")
|
||||||
else None,
|
else None,
|
||||||
source=data["source"].get("text") if data.get("source") else None,
|
source=data["source"].get("text") if data.get("source") else None,
|
||||||
|
image=image,
|
||||||
|
original_image_url=original_image_url,
|
||||||
attachments=[
|
attachments=[
|
||||||
_file.graphql_to_subattachment(attachment)
|
_file.graphql_to_subattachment(attachment)
|
||||||
for attachment in data.get("subattachments")
|
for attachment in data.get("subattachments")
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
media = data.get("media")
|
|
||||||
if media and media.get("image"):
|
|
||||||
image = media["image"]
|
|
||||||
rtn.image_url = image.get("uri")
|
|
||||||
rtn.original_image_url = (
|
|
||||||
_util.get_url_parameter(rtn.image_url, "url")
|
|
||||||
if "/safe_image.php" in rtn.image_url
|
|
||||||
else rtn.image_url
|
|
||||||
)
|
|
||||||
rtn.image_width = image.get("width")
|
|
||||||
rtn.image_height = image.get("height")
|
|
||||||
return rtn
|
|
||||||
|
@@ -38,9 +38,9 @@ ACONTEXT = {
|
|||||||
class Client:
|
class Client:
|
||||||
"""A client for the Facebook Chat (Messenger).
|
"""A client for the Facebook Chat (Messenger).
|
||||||
|
|
||||||
This is the main class of ``fbchat``, which contains all the methods you use to
|
This is the main class, which contains all the methods you use to interact with
|
||||||
interact with Facebook. You can extend this class, and overwrite the ``on`` methods,
|
Facebook. You can extend this class, and overwrite the ``on`` methods, to provide
|
||||||
to provide custom event handling (mainly useful while listening).
|
custom event handling (mainly useful while listening).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -215,7 +215,7 @@ class Client:
|
|||||||
limit: The max. amount of threads to fetch (default all threads)
|
limit: The max. amount of threads to fetch (default all threads)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list: :class:`Thread` objects
|
list: `Thread` objects
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
FBchatException: If request failed
|
FBchatException: If request failed
|
||||||
@@ -266,7 +266,7 @@ class Client:
|
|||||||
threads: Thread: List of threads to check for users
|
threads: Thread: List of threads to check for users
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list: :class:`User` objects
|
list: `User` objects
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
FBchatException: If request failed
|
FBchatException: If request failed
|
||||||
@@ -292,7 +292,7 @@ class Client:
|
|||||||
"""Fetch all users the client is currently chatting with.
|
"""Fetch all users the client is currently chatting with.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list: :class:`User` objects
|
list: `User` objects
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
FBchatException: If request failed
|
FBchatException: If request failed
|
||||||
@@ -317,7 +317,7 @@ class Client:
|
|||||||
limit: The max. amount of users to fetch
|
limit: The max. amount of users to fetch
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list: :class:`User` objects, ordered by relevance
|
list: `User` objects, ordered by relevance
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
FBchatException: If request failed
|
FBchatException: If request failed
|
||||||
@@ -334,7 +334,7 @@ class Client:
|
|||||||
name: Name of the page
|
name: Name of the page
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list: :class:`Page` objects, ordered by relevance
|
list: `Page` objects, ordered by relevance
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
FBchatException: If request failed
|
FBchatException: If request failed
|
||||||
@@ -352,7 +352,7 @@ class Client:
|
|||||||
limit: The max. amount of groups to fetch
|
limit: The max. amount of groups to fetch
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list: :class:`Group` objects, ordered by relevance
|
list: `Group` objects, ordered by relevance
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
FBchatException: If request failed
|
FBchatException: If request failed
|
||||||
@@ -370,7 +370,7 @@ class Client:
|
|||||||
limit: The max. amount of groups to fetch
|
limit: The max. amount of groups to fetch
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list: :class:`User`, :class:`Group` and :class:`Page` objects, ordered by relevance
|
list: `User`, `Group` and `Page` objects, ordered by relevance
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
FBchatException: If request failed
|
FBchatException: If request failed
|
||||||
@@ -441,7 +441,7 @@ class Client:
|
|||||||
thread_id: User/Group ID to search in. See :ref:`intro_threads`
|
thread_id: User/Group ID to search in. See :ref:`intro_threads`
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
typing.Iterable: Found :class:`Message` objects
|
typing.Iterable: Found `Message` objects
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
FBchatException: If request failed
|
FBchatException: If request failed
|
||||||
@@ -457,7 +457,7 @@ class Client:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
query: Text to search for
|
query: Text to search for
|
||||||
fetch_messages: Whether to fetch :class:`Message` objects or IDs only
|
fetch_messages: Whether to fetch `Message` objects or IDs only
|
||||||
thread_limit (int): Max. number of threads to retrieve
|
thread_limit (int): Max. number of threads to retrieve
|
||||||
message_limit (int): Max. number of messages to retrieve
|
message_limit (int): Max. number of messages to retrieve
|
||||||
|
|
||||||
@@ -531,7 +531,7 @@ class Client:
|
|||||||
user_ids: One or more user ID(s) to query
|
user_ids: One or more user ID(s) to query
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: :class:`User` objects, labeled by their ID
|
dict: `User` objects, labeled by their ID
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
FBchatException: If request failed
|
FBchatException: If request failed
|
||||||
@@ -556,7 +556,7 @@ class Client:
|
|||||||
page_ids: One or more page ID(s) to query
|
page_ids: One or more page ID(s) to query
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: :class:`Page` objects, labeled by their ID
|
dict: `Page` objects, labeled by their ID
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
FBchatException: If request failed
|
FBchatException: If request failed
|
||||||
@@ -578,7 +578,7 @@ class Client:
|
|||||||
group_ids: One or more group ID(s) to query
|
group_ids: One or more group ID(s) to query
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: :class:`Group` objects, labeled by their ID
|
dict: `Group` objects, labeled by their ID
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
FBchatException: If request failed
|
FBchatException: If request failed
|
||||||
@@ -603,7 +603,7 @@ class Client:
|
|||||||
thread_ids: One or more thread ID(s) to query
|
thread_ids: One or more thread ID(s) to query
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: :class:`Thread` objects, labeled by their ID
|
dict: `Thread` objects, labeled by their ID
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
FBchatException: If request failed
|
FBchatException: If request failed
|
||||||
@@ -669,7 +669,7 @@ class Client:
|
|||||||
before (datetime.datetime): The point from which to retrieve messages
|
before (datetime.datetime): The point from which to retrieve messages
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list: :class:`Message` objects
|
list: `Message` objects
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
FBchatException: If request failed
|
FBchatException: If request failed
|
||||||
@@ -686,22 +686,14 @@ class Client:
|
|||||||
if j.get("message_thread") is None:
|
if j.get("message_thread") is None:
|
||||||
raise FBchatException("Could not fetch thread {}: {}".format(thread_id, j))
|
raise FBchatException("Could not fetch thread {}: {}".format(thread_id, j))
|
||||||
|
|
||||||
|
read_receipts = j["message_thread"]["read_receipts"]["nodes"]
|
||||||
|
|
||||||
messages = [
|
messages = [
|
||||||
Message._from_graphql(message)
|
Message._from_graphql(message, read_receipts)
|
||||||
for message in j["message_thread"]["messages"]["nodes"]
|
for message in j["message_thread"]["messages"]["nodes"]
|
||||||
]
|
]
|
||||||
messages.reverse()
|
messages.reverse()
|
||||||
|
|
||||||
read_receipts = j["message_thread"]["read_receipts"]["nodes"]
|
|
||||||
|
|
||||||
for message in messages:
|
|
||||||
for receipt in read_receipts:
|
|
||||||
if (
|
|
||||||
_util.millis_to_datetime(int(receipt["watermark"]))
|
|
||||||
>= message.created_at
|
|
||||||
):
|
|
||||||
message.read_by.append(receipt["actor"]["id"])
|
|
||||||
|
|
||||||
return messages
|
return messages
|
||||||
|
|
||||||
def fetch_thread_list(
|
def fetch_thread_list(
|
||||||
@@ -715,7 +707,7 @@ class Client:
|
|||||||
before (datetime.datetime): The point from which to retrieve threads
|
before (datetime.datetime): The point from which to retrieve threads
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list: :class:`Thread` objects
|
list: `Thread` objects
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
FBchatException: If request failed
|
FBchatException: If request failed
|
||||||
@@ -814,7 +806,7 @@ class Client:
|
|||||||
thread_id: User/Group ID to get message info from. See :ref:`intro_threads`
|
thread_id: User/Group ID to get message info from. See :ref:`intro_threads`
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Message: :class:`Message` object
|
Message: `Message` object
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
FBchatException: If request failed
|
FBchatException: If request failed
|
||||||
@@ -845,7 +837,7 @@ class Client:
|
|||||||
plan_id: Plan ID to fetch from
|
plan_id: Plan ID to fetch from
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Plan: :class:`Plan` object
|
Plan: `Plan` object
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
FBchatException: If request failed
|
FBchatException: If request failed
|
||||||
@@ -901,7 +893,7 @@ class Client:
|
|||||||
thread_id: ID of the thread
|
thread_id: ID of the thread
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
typing.Iterable: :class:`ImageAttachment` or :class:`VideoAttachment`
|
typing.Iterable: `ImageAttachment` or `VideoAttachment`
|
||||||
"""
|
"""
|
||||||
data = {"id": thread_id, "first": 48}
|
data = {"id": thread_id, "first": 48}
|
||||||
thread_id = str(thread_id)
|
thread_id = str(thread_id)
|
||||||
@@ -964,7 +956,7 @@ class Client:
|
|||||||
Raises:
|
Raises:
|
||||||
FBchatException: If request failed
|
FBchatException: If request failed
|
||||||
"""
|
"""
|
||||||
thread = thread_type._to_class()(thread_id)
|
thread = thread_type._to_class()(uid=thread_id)
|
||||||
data = thread._to_send_data()
|
data = thread._to_send_data()
|
||||||
data.update(message._to_send_data())
|
data.update(message._to_send_data())
|
||||||
return self._do_send_request(data)
|
return self._do_send_request(data)
|
||||||
@@ -983,7 +975,7 @@ class Client:
|
|||||||
Raises:
|
Raises:
|
||||||
FBchatException: If request failed
|
FBchatException: If request failed
|
||||||
"""
|
"""
|
||||||
thread = thread_type._to_class()(thread_id)
|
thread = thread_type._to_class()(uid=thread_id)
|
||||||
data = thread._to_send_data()
|
data = thread._to_send_data()
|
||||||
data["action_type"] = "ma-type:user-generated-message"
|
data["action_type"] = "ma-type:user-generated-message"
|
||||||
data["lightweight_action_attachment[lwa_state]"] = (
|
data["lightweight_action_attachment[lwa_state]"] = (
|
||||||
@@ -1009,31 +1001,40 @@ class Client:
|
|||||||
Raises:
|
Raises:
|
||||||
FBchatException: If request failed
|
FBchatException: If request failed
|
||||||
"""
|
"""
|
||||||
quick_reply.is_response = True
|
|
||||||
if isinstance(quick_reply, QuickReplyText):
|
if isinstance(quick_reply, QuickReplyText):
|
||||||
return self.send(
|
new = QuickReplyText(
|
||||||
Message(text=quick_reply.title, quick_replies=[quick_reply])
|
payload=quick_reply.payload,
|
||||||
|
external_payload=quick_reply.external_payload,
|
||||||
|
data=quick_reply.data,
|
||||||
|
is_response=True,
|
||||||
|
title=quick_reply.title,
|
||||||
|
image_url=quick_reply.image_url,
|
||||||
)
|
)
|
||||||
|
return self.send(Message(text=quick_reply.title, quick_replies=[new]))
|
||||||
elif isinstance(quick_reply, QuickReplyLocation):
|
elif isinstance(quick_reply, QuickReplyLocation):
|
||||||
if not isinstance(payload, LocationAttachment):
|
if not isinstance(payload, LocationAttachment):
|
||||||
raise TypeError(
|
raise TypeError("Payload must be an instance of `LocationAttachment`")
|
||||||
"Payload must be an instance of `fbchat.LocationAttachment`"
|
|
||||||
)
|
|
||||||
return self.send_location(
|
return self.send_location(
|
||||||
payload, thread_id=thread_id, thread_type=thread_type
|
payload, thread_id=thread_id, thread_type=thread_type
|
||||||
)
|
)
|
||||||
elif isinstance(quick_reply, QuickReplyEmail):
|
elif isinstance(quick_reply, QuickReplyEmail):
|
||||||
if not payload:
|
new = QuickReplyEmail(
|
||||||
payload = self.get_emails()[0]
|
payload=payload if payload else self.get_emails()[0],
|
||||||
quick_reply.external_payload = quick_reply.payload
|
external_payload=quick_reply.payload,
|
||||||
quick_reply.payload = payload
|
data=quick_reply.data,
|
||||||
return self.send(Message(text=payload, quick_replies=[quick_reply]))
|
is_response=True,
|
||||||
|
image_url=quick_reply.image_url,
|
||||||
|
)
|
||||||
|
return self.send(Message(text=payload, quick_replies=[new]))
|
||||||
elif isinstance(quick_reply, QuickReplyPhoneNumber):
|
elif isinstance(quick_reply, QuickReplyPhoneNumber):
|
||||||
if not payload:
|
new = QuickReplyPhoneNumber(
|
||||||
payload = self.get_phone_numbers()[0]
|
payload=payload if payload else self.get_phone_numbers()[0],
|
||||||
quick_reply.external_payload = quick_reply.payload
|
external_payload=quick_reply.payload,
|
||||||
quick_reply.payload = payload
|
data=quick_reply.data,
|
||||||
return self.send(Message(text=payload, quick_replies=[quick_reply]))
|
is_response=True,
|
||||||
|
image_url=quick_reply.image_url,
|
||||||
|
)
|
||||||
|
return self.send(Message(text=payload, quick_replies=[new]))
|
||||||
|
|
||||||
def unsend(self, mid):
|
def unsend(self, mid):
|
||||||
"""Unsend message by it's ID (removes it for everyone).
|
"""Unsend message by it's ID (removes it for everyone).
|
||||||
@@ -1047,7 +1048,7 @@ class Client:
|
|||||||
def _send_location(
|
def _send_location(
|
||||||
self, location, current=True, message=None, thread_id=None, thread_type=None
|
self, location, current=True, message=None, thread_id=None, thread_type=None
|
||||||
):
|
):
|
||||||
thread = thread_type._to_class()(thread_id)
|
thread = thread_type._to_class()(uid=thread_id)
|
||||||
data = thread._to_send_data()
|
data = thread._to_send_data()
|
||||||
if message is not None:
|
if message is not None:
|
||||||
data.update(message._to_send_data())
|
data.update(message._to_send_data())
|
||||||
@@ -1115,7 +1116,7 @@ class Client:
|
|||||||
|
|
||||||
`files` should be a list of tuples, with a file's ID and mimetype.
|
`files` should be a list of tuples, with a file's ID and mimetype.
|
||||||
"""
|
"""
|
||||||
thread = thread_type._to_class()(thread_id)
|
thread = thread_type._to_class()(uid=thread_id)
|
||||||
data = thread._to_send_data()
|
data = thread._to_send_data()
|
||||||
data.update(self._old_message(message)._to_send_data())
|
data.update(self._old_message(message)._to_send_data())
|
||||||
data["action_type"] = "ma-type:user-generated-message"
|
data["action_type"] = "ma-type:user-generated-message"
|
||||||
@@ -1281,7 +1282,7 @@ class Client:
|
|||||||
Raises:
|
Raises:
|
||||||
FBchatException: If request failed
|
FBchatException: If request failed
|
||||||
"""
|
"""
|
||||||
data = Group(thread_id)._to_send_data()
|
data = Group(uid=thread_id)._to_send_data()
|
||||||
|
|
||||||
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"
|
||||||
@@ -2533,9 +2534,8 @@ class Client:
|
|||||||
i = d["deltaMessageReply"]
|
i = d["deltaMessageReply"]
|
||||||
metadata = i["message"]["messageMetadata"]
|
metadata = i["message"]["messageMetadata"]
|
||||||
thread_id, thread_type = get_thread_id_and_thread_type(metadata)
|
thread_id, thread_type = get_thread_id_and_thread_type(metadata)
|
||||||
message = Message._from_reply(i["message"])
|
replied_to = Message._from_reply(i["repliedToMessage"])
|
||||||
message.replied_to = Message._from_reply(i["repliedToMessage"])
|
message = Message._from_reply(i["message"], replied_to)
|
||||||
message.reply_to_id = message.replied_to.uid
|
|
||||||
self.on_message(
|
self.on_message(
|
||||||
mid=message.uid,
|
mid=message.uid,
|
||||||
author_id=message.author,
|
author_id=message.author,
|
||||||
@@ -3653,7 +3653,7 @@ class Client:
|
|||||||
"""Called when the client is listening and client receives information about friend active status.
|
"""Called when the client is listening and client receives information about friend active status.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
statuses (dict): Dictionary with user IDs as keys and :class:`ActiveStatus` as values
|
statuses (dict): Dictionary with user IDs as keys and `ActiveStatus` as values
|
||||||
msg: A full set of the data received
|
msg: A full set of the data received
|
||||||
"""
|
"""
|
||||||
log.debug("Buddylist overlay received: {}".format(statuses))
|
log.debug("Buddylist overlay received: {}".format(statuses))
|
||||||
|
@@ -1,11 +1,19 @@
|
|||||||
|
import sys
|
||||||
|
import attr
|
||||||
import logging
|
import logging
|
||||||
import aenum
|
import aenum
|
||||||
|
|
||||||
log = logging.getLogger("fbchat")
|
log = logging.getLogger("fbchat")
|
||||||
|
|
||||||
|
# Enable kw_only if the python version supports it
|
||||||
|
kw_only = sys.version_info[:2] > (3, 5)
|
||||||
|
|
||||||
|
#: Default attrs settings for classes
|
||||||
|
attrs_default = attr.s(slots=True, kw_only=kw_only)
|
||||||
|
|
||||||
|
|
||||||
class Enum(aenum.Enum):
|
class Enum(aenum.Enum):
|
||||||
"""Used internally by ``fbchat`` to support enumerations"""
|
"""Used internally to support enumerations"""
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
# For documentation:
|
# For documentation:
|
||||||
@@ -21,3 +29,46 @@ class Enum(aenum.Enum):
|
|||||||
)
|
)
|
||||||
aenum.extend_enum(cls, "UNKNOWN_{}".format(value).upper(), value)
|
aenum.extend_enum(cls, "UNKNOWN_{}".format(value).upper(), value)
|
||||||
return cls(value)
|
return cls(value)
|
||||||
|
|
||||||
|
|
||||||
|
# Frozen, so that it can be used in sets
|
||||||
|
@attr.s(frozen=True, slots=True, kw_only=kw_only)
|
||||||
|
class Image:
|
||||||
|
#: URL to the image
|
||||||
|
url = attr.ib(type=str)
|
||||||
|
#: Width of the image
|
||||||
|
width = attr.ib(None, type=int)
|
||||||
|
#: Height of the image
|
||||||
|
height = attr.ib(None, type=int)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_uri(cls, data):
|
||||||
|
return cls(
|
||||||
|
url=data["uri"],
|
||||||
|
width=int(data["width"]) if data.get("width") else None,
|
||||||
|
height=int(data["height"]) if data.get("height") else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_url(cls, data):
|
||||||
|
return cls(
|
||||||
|
url=data["url"],
|
||||||
|
width=int(data["width"]) if data.get("width") else None,
|
||||||
|
height=int(data["height"]) if data.get("height") else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_uri_or_none(cls, data):
|
||||||
|
if data is None:
|
||||||
|
return None
|
||||||
|
if data.get("uri") is None:
|
||||||
|
return None
|
||||||
|
return cls._from_uri(data)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_url_or_none(cls, data):
|
||||||
|
if data is None:
|
||||||
|
return None
|
||||||
|
if data.get("url") is None:
|
||||||
|
return None
|
||||||
|
return cls._from_url(data)
|
||||||
|
@@ -1,32 +1,32 @@
|
|||||||
|
import attr
|
||||||
|
|
||||||
|
# Not frozen, since that doesn't work in PyPy
|
||||||
|
attrs_exception = attr.s(slots=True, auto_exc=True)
|
||||||
|
|
||||||
|
|
||||||
|
@attrs_exception
|
||||||
class FBchatException(Exception):
|
class FBchatException(Exception):
|
||||||
"""Custom exception thrown by ``fbchat``.
|
"""Custom exception thrown by ``fbchat``.
|
||||||
|
|
||||||
All exceptions in the ``fbchat`` module inherits this.
|
All exceptions in the module inherits this.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
message = attr.ib()
|
||||||
|
|
||||||
|
|
||||||
|
@attrs_exception
|
||||||
class FBchatFacebookError(FBchatException):
|
class FBchatFacebookError(FBchatException):
|
||||||
|
"""Raised when Facebook returns an error."""
|
||||||
|
|
||||||
#: The error code that Facebook returned
|
#: The error code that Facebook returned
|
||||||
fb_error_code = None
|
fb_error_code = attr.ib(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 = attr.ib(None)
|
||||||
#: The status code that was sent in the HTTP response (e.g. 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 = attr.ib(None)
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
message,
|
|
||||||
fb_error_code=None,
|
|
||||||
fb_error_message=None,
|
|
||||||
request_status_code=None,
|
|
||||||
):
|
|
||||||
super(FBchatFacebookError, self).__init__(message)
|
|
||||||
"""Thrown by ``fbchat`` when Facebook returns an error"""
|
|
||||||
self.fb_error_code = str(fb_error_code)
|
|
||||||
self.fb_error_message = fb_error_message
|
|
||||||
self.request_status_code = request_status_code
|
|
||||||
|
|
||||||
|
|
||||||
|
@attrs_exception
|
||||||
class FBchatInvalidParameters(FBchatFacebookError):
|
class FBchatInvalidParameters(FBchatFacebookError):
|
||||||
"""Raised by Facebook if:
|
"""Raised by Facebook if:
|
||||||
|
|
||||||
@@ -36,17 +36,19 @@ class FBchatInvalidParameters(FBchatFacebookError):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@attrs_exception
|
||||||
class FBchatNotLoggedIn(FBchatFacebookError):
|
class FBchatNotLoggedIn(FBchatFacebookError):
|
||||||
"""Raised by Facebook if the client has been logged out."""
|
"""Raised by Facebook if the client has been logged out."""
|
||||||
|
|
||||||
fb_error_code = "1357001"
|
fb_error_code = attr.ib("1357001")
|
||||||
|
|
||||||
|
|
||||||
|
@attrs_exception
|
||||||
class FBchatPleaseRefresh(FBchatFacebookError):
|
class FBchatPleaseRefresh(FBchatFacebookError):
|
||||||
"""Raised by Facebook if the client has been inactive for too long.
|
"""Raised by Facebook if the client has been inactive for too long.
|
||||||
|
|
||||||
This error usually happens after 1-2 days of inactivity.
|
This error usually happens after 1-2 days of inactivity.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
fb_error_code = "1357004"
|
fb_error_code = attr.ib("1357004")
|
||||||
fb_error_message = "Please try closing and re-opening your browser window."
|
fb_error_message = attr.ib("Please try closing and re-opening your browser window.")
|
||||||
|
202
fbchat/_file.py
202
fbchat/_file.py
@@ -1,9 +1,10 @@
|
|||||||
import attr
|
import attr
|
||||||
|
from ._core import attrs_default, Image
|
||||||
from . import _util
|
from . import _util
|
||||||
from ._attachment import Attachment
|
from ._attachment import Attachment
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
@attrs_default
|
||||||
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."""
|
||||||
|
|
||||||
@@ -16,20 +17,18 @@ class FileAttachment(Attachment):
|
|||||||
#: Whether Facebook determines that this file may be harmful
|
#: Whether Facebook determines that this file may be harmful
|
||||||
is_malicious = attr.ib(None)
|
is_malicious = attr.ib(None)
|
||||||
|
|
||||||
# Put here for backwards compatibility, so that the init argument order is preserved
|
|
||||||
uid = attr.ib(None)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _from_graphql(cls, data):
|
def _from_graphql(cls, data, size=None):
|
||||||
return cls(
|
return cls(
|
||||||
url=data.get("url"),
|
url=data.get("url"),
|
||||||
|
size=size,
|
||||||
name=data.get("filename"),
|
name=data.get("filename"),
|
||||||
is_malicious=data.get("is_malicious"),
|
is_malicious=data.get("is_malicious"),
|
||||||
uid=data.get("message_file_fbid"),
|
uid=data.get("message_file_fbid"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
@attrs_default
|
||||||
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."""
|
||||||
|
|
||||||
@@ -42,9 +41,6 @@ class AudioAttachment(Attachment):
|
|||||||
#: Audio type
|
#: Audio type
|
||||||
audio_type = attr.ib(None)
|
audio_type = attr.ib(None)
|
||||||
|
|
||||||
# Put here for backwards compatibility, so that the init argument order is preserved
|
|
||||||
uid = attr.ib(None)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _from_graphql(cls, data):
|
def _from_graphql(cls, data):
|
||||||
return cls(
|
return cls(
|
||||||
@@ -55,7 +51,7 @@ class AudioAttachment(Attachment):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@attr.s(init=False)
|
@attrs_default
|
||||||
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.
|
||||||
|
|
||||||
@@ -69,104 +65,49 @@ class ImageAttachment(Attachment):
|
|||||||
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))
|
||||||
#: Height of original image
|
#: Height of original image
|
||||||
height = attr.ib(None, converter=lambda x: None if x is None else int(x))
|
height = attr.ib(None, converter=lambda x: None if x is None else int(x))
|
||||||
|
|
||||||
#: Whether the image is animated
|
#: Whether the image is animated
|
||||||
is_animated = attr.ib(None)
|
is_animated = attr.ib(None)
|
||||||
|
#: A set, containing variously sized / various types of previews of the image
|
||||||
#: URL to a thumbnail of the image
|
previews = attr.ib(factory=set)
|
||||||
thumbnail_url = attr.ib(None)
|
|
||||||
|
|
||||||
#: URL to a medium preview of the image
|
|
||||||
preview_url = attr.ib(None)
|
|
||||||
#: Width of the medium preview image
|
|
||||||
preview_width = attr.ib(None)
|
|
||||||
#: Height of the medium preview image
|
|
||||||
preview_height = attr.ib(None)
|
|
||||||
|
|
||||||
#: URL to a large preview of the image
|
|
||||||
large_preview_url = attr.ib(None)
|
|
||||||
#: Width of the large preview image
|
|
||||||
large_preview_width = attr.ib(None)
|
|
||||||
#: Height of the large preview image
|
|
||||||
large_preview_height = attr.ib(None)
|
|
||||||
|
|
||||||
#: URL to an animated preview of the image (e.g. for GIFs)
|
|
||||||
animated_preview_url = attr.ib(None)
|
|
||||||
#: Width of the animated preview image
|
|
||||||
animated_preview_width = attr.ib(None)
|
|
||||||
#: Height of the animated preview image
|
|
||||||
animated_preview_height = attr.ib(None)
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
original_extension=None,
|
|
||||||
width=None,
|
|
||||||
height=None,
|
|
||||||
is_animated=None,
|
|
||||||
thumbnail_url=None,
|
|
||||||
preview=None,
|
|
||||||
large_preview=None,
|
|
||||||
animated_preview=None,
|
|
||||||
**kwargs
|
|
||||||
):
|
|
||||||
super(ImageAttachment, self).__init__(**kwargs)
|
|
||||||
self.original_extension = original_extension
|
|
||||||
if width is not None:
|
|
||||||
width = int(width)
|
|
||||||
self.width = width
|
|
||||||
if height is not None:
|
|
||||||
height = int(height)
|
|
||||||
self.height = height
|
|
||||||
self.is_animated = is_animated
|
|
||||||
self.thumbnail_url = thumbnail_url
|
|
||||||
|
|
||||||
if preview is None:
|
|
||||||
preview = {}
|
|
||||||
self.preview_url = preview.get("uri")
|
|
||||||
self.preview_width = preview.get("width")
|
|
||||||
self.preview_height = preview.get("height")
|
|
||||||
|
|
||||||
if large_preview is None:
|
|
||||||
large_preview = {}
|
|
||||||
self.large_preview_url = large_preview.get("uri")
|
|
||||||
self.large_preview_width = large_preview.get("width")
|
|
||||||
self.large_preview_height = large_preview.get("height")
|
|
||||||
|
|
||||||
if animated_preview is None:
|
|
||||||
animated_preview = {}
|
|
||||||
self.animated_preview_url = animated_preview.get("uri")
|
|
||||||
self.animated_preview_width = animated_preview.get("width")
|
|
||||||
self.animated_preview_height = animated_preview.get("height")
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _from_graphql(cls, data):
|
def _from_graphql(cls, data):
|
||||||
|
previews = {
|
||||||
|
Image._from_uri_or_none(data.get("thumbnail")),
|
||||||
|
Image._from_uri_or_none(data.get("preview") or data.get("preview_image")),
|
||||||
|
Image._from_uri_or_none(data.get("large_preview")),
|
||||||
|
Image._from_uri_or_none(data.get("animated_image")),
|
||||||
|
}
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
original_extension=data.get("original_extension")
|
original_extension=data.get("original_extension")
|
||||||
or (data["filename"].split("-")[0] if data.get("filename") else None),
|
or (data["filename"].split("-")[0] if data.get("filename") else None),
|
||||||
width=data.get("original_dimensions", {}).get("width"),
|
width=data.get("original_dimensions", {}).get("width"),
|
||||||
height=data.get("original_dimensions", {}).get("height"),
|
height=data.get("original_dimensions", {}).get("height"),
|
||||||
is_animated=data["__typename"] == "MessageAnimatedImage",
|
is_animated=data["__typename"] == "MessageAnimatedImage",
|
||||||
thumbnail_url=data.get("thumbnail", {}).get("uri"),
|
previews={p for p in previews if p},
|
||||||
preview=data.get("preview") or data.get("preview_image"),
|
|
||||||
large_preview=data.get("large_preview"),
|
|
||||||
animated_preview=data.get("animated_image"),
|
|
||||||
uid=data.get("legacy_attachment_id"),
|
uid=data.get("legacy_attachment_id"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _from_list(cls, data):
|
def _from_list(cls, data):
|
||||||
data = data["node"]
|
data = data["node"]
|
||||||
|
|
||||||
|
previews = {
|
||||||
|
Image._from_uri_or_none(data["image"]),
|
||||||
|
Image._from_uri(data["image1"]),
|
||||||
|
Image._from_uri(data["image2"]),
|
||||||
|
}
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
width=data["original_dimensions"].get("x"),
|
width=data["original_dimensions"].get("x"),
|
||||||
height=data["original_dimensions"].get("y"),
|
height=data["original_dimensions"].get("y"),
|
||||||
thumbnail_url=data["image"].get("uri"),
|
previews={p for p in previews if p},
|
||||||
large_preview=data["image2"],
|
|
||||||
preview=data["image1"],
|
|
||||||
uid=data["legacy_attachment_id"],
|
uid=data["legacy_attachment_id"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@attr.s(init=False)
|
@attrs_default
|
||||||
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."""
|
||||||
|
|
||||||
@@ -180,111 +121,66 @@ class VideoAttachment(Attachment):
|
|||||||
duration = attr.ib(None)
|
duration = attr.ib(None)
|
||||||
#: URL to very compressed preview video
|
#: URL to very compressed preview video
|
||||||
preview_url = attr.ib(None)
|
preview_url = attr.ib(None)
|
||||||
|
#: A set, containing variously sized previews of the video
|
||||||
#: URL to a small preview image of the video
|
previews = attr.ib(factory=set)
|
||||||
small_image_url = attr.ib(None)
|
|
||||||
#: Width of the small preview image
|
|
||||||
small_image_width = attr.ib(None)
|
|
||||||
#: Height of the small preview image
|
|
||||||
small_image_height = attr.ib(None)
|
|
||||||
|
|
||||||
#: URL to a medium preview image of the video
|
|
||||||
medium_image_url = attr.ib(None)
|
|
||||||
#: Width of the medium preview image
|
|
||||||
medium_image_width = attr.ib(None)
|
|
||||||
#: Height of the medium preview image
|
|
||||||
medium_image_height = attr.ib(None)
|
|
||||||
|
|
||||||
#: URL to a large preview image of the video
|
|
||||||
large_image_url = attr.ib(None)
|
|
||||||
#: Width of the large preview image
|
|
||||||
large_image_width = attr.ib(None)
|
|
||||||
#: Height of the large preview image
|
|
||||||
large_image_height = attr.ib(None)
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
size=None,
|
|
||||||
width=None,
|
|
||||||
height=None,
|
|
||||||
duration=None,
|
|
||||||
preview_url=None,
|
|
||||||
small_image=None,
|
|
||||||
medium_image=None,
|
|
||||||
large_image=None,
|
|
||||||
**kwargs
|
|
||||||
):
|
|
||||||
super(VideoAttachment, self).__init__(**kwargs)
|
|
||||||
self.size = size
|
|
||||||
self.width = width
|
|
||||||
self.height = height
|
|
||||||
self.duration = duration
|
|
||||||
self.preview_url = preview_url
|
|
||||||
|
|
||||||
if small_image is None:
|
|
||||||
small_image = {}
|
|
||||||
self.small_image_url = small_image.get("uri")
|
|
||||||
self.small_image_width = small_image.get("width")
|
|
||||||
self.small_image_height = small_image.get("height")
|
|
||||||
|
|
||||||
if medium_image is None:
|
|
||||||
medium_image = {}
|
|
||||||
self.medium_image_url = medium_image.get("uri")
|
|
||||||
self.medium_image_width = medium_image.get("width")
|
|
||||||
self.medium_image_height = medium_image.get("height")
|
|
||||||
|
|
||||||
if large_image is None:
|
|
||||||
large_image = {}
|
|
||||||
self.large_image_url = large_image.get("uri")
|
|
||||||
self.large_image_width = large_image.get("width")
|
|
||||||
self.large_image_height = large_image.get("height")
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _from_graphql(cls, data):
|
def _from_graphql(cls, data, size=None):
|
||||||
|
previews = {
|
||||||
|
Image._from_uri_or_none(data.get("chat_image")),
|
||||||
|
Image._from_uri_or_none(data.get("inbox_image")),
|
||||||
|
Image._from_uri_or_none(data.get("large_image")),
|
||||||
|
}
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
|
size=size,
|
||||||
width=data.get("original_dimensions", {}).get("width"),
|
width=data.get("original_dimensions", {}).get("width"),
|
||||||
height=data.get("original_dimensions", {}).get("height"),
|
height=data.get("original_dimensions", {}).get("height"),
|
||||||
duration=_util.millis_to_timedelta(data.get("playable_duration_in_ms")),
|
duration=_util.millis_to_timedelta(data.get("playable_duration_in_ms")),
|
||||||
preview_url=data.get("playable_url"),
|
preview_url=data.get("playable_url"),
|
||||||
small_image=data.get("chat_image"),
|
previews={p for p in previews if p},
|
||||||
medium_image=data.get("inbox_image"),
|
|
||||||
large_image=data.get("large_image"),
|
|
||||||
uid=data.get("legacy_attachment_id"),
|
uid=data.get("legacy_attachment_id"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _from_subattachment(cls, data):
|
def _from_subattachment(cls, data):
|
||||||
media = data["media"]
|
media = data["media"]
|
||||||
|
image = Image._from_uri_or_none(media.get("image"))
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
duration=_util.millis_to_timedelta(media.get("playable_duration_in_ms")),
|
duration=_util.millis_to_timedelta(media.get("playable_duration_in_ms")),
|
||||||
preview_url=media.get("playable_url"),
|
preview_url=media.get("playable_url"),
|
||||||
medium_image=media.get("image"),
|
previews={image} if image else {},
|
||||||
uid=data["target"].get("video_id"),
|
uid=data["target"].get("video_id"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _from_list(cls, data):
|
def _from_list(cls, data):
|
||||||
data = data["node"]
|
data = data["node"]
|
||||||
|
previews = {
|
||||||
|
Image._from_uri(data["image"]),
|
||||||
|
Image._from_uri(data["image1"]),
|
||||||
|
Image._from_uri(data["image2"]),
|
||||||
|
}
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
width=data["original_dimensions"].get("x"),
|
width=data["original_dimensions"].get("x"),
|
||||||
height=data["original_dimensions"].get("y"),
|
height=data["original_dimensions"].get("y"),
|
||||||
small_image=data["image"],
|
previews=previews,
|
||||||
medium_image=data["image1"],
|
|
||||||
large_image=data["image2"],
|
|
||||||
uid=data["legacy_attachment_id"],
|
uid=data["legacy_attachment_id"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def graphql_to_attachment(data):
|
def graphql_to_attachment(data, size=None):
|
||||||
_type = data["__typename"]
|
_type = data["__typename"]
|
||||||
if _type in ["MessageImage", "MessageAnimatedImage"]:
|
if _type in ["MessageImage", "MessageAnimatedImage"]:
|
||||||
return ImageAttachment._from_graphql(data)
|
return ImageAttachment._from_graphql(data)
|
||||||
elif _type == "MessageVideo":
|
elif _type == "MessageVideo":
|
||||||
return VideoAttachment._from_graphql(data)
|
return VideoAttachment._from_graphql(data, size=size)
|
||||||
elif _type == "MessageAudio":
|
elif _type == "MessageAudio":
|
||||||
return AudioAttachment._from_graphql(data)
|
return AudioAttachment._from_graphql(data)
|
||||||
elif _type == "MessageFile":
|
elif _type == "MessageFile":
|
||||||
return FileAttachment._from_graphql(data)
|
return FileAttachment._from_graphql(data, size=size)
|
||||||
|
|
||||||
return Attachment(uid=data.get("legacy_attachment_id"))
|
return Attachment(uid=data.get("legacy_attachment_id"))
|
||||||
|
|
||||||
|
@@ -1,30 +1,29 @@
|
|||||||
import attr
|
import attr
|
||||||
|
from ._core import attrs_default, Image
|
||||||
from . import _util, _plan
|
from . import _util, _plan
|
||||||
from ._thread import ThreadType, Thread
|
from ._thread import ThreadType, Thread
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
@attrs_default
|
||||||
class Group(Thread):
|
class Group(Thread):
|
||||||
"""Represents a Facebook group. Inherits `Thread`."""
|
"""Represents a Facebook group. Inherits `Thread`."""
|
||||||
|
|
||||||
type = ThreadType.GROUP
|
type = ThreadType.GROUP
|
||||||
|
|
||||||
#: 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)
|
||||||
#: A dictionary, 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)
|
||||||
#: A :class:`ThreadColor`. The groups's message color
|
#: A `ThreadColor`. The groups's message color
|
||||||
color = attr.ib(None)
|
color = attr.ib(None)
|
||||||
#: The groups's default emoji
|
#: The groups's default emoji
|
||||||
emoji = attr.ib(None)
|
emoji = attr.ib(None)
|
||||||
# Set containing user IDs of thread admins
|
# Set containing user IDs of thread admins
|
||||||
admins = attr.ib(factory=set, converter=lambda x: set() if x is None else x)
|
admins = attr.ib(factory=set)
|
||||||
# True if users need approval to join
|
# True if users need approval to join
|
||||||
approval_mode = attr.ib(None)
|
approval_mode = attr.ib(None)
|
||||||
# Set containing user IDs requesting to join
|
# Set containing user IDs requesting to join
|
||||||
approval_requests = attr.ib(
|
approval_requests = attr.ib(factory=set)
|
||||||
factory=set, converter=lambda x: set() if x is None else x
|
|
||||||
)
|
|
||||||
# Link for joining group
|
# Link for joining group
|
||||||
join_link = attr.ib(None)
|
join_link = attr.ib(None)
|
||||||
|
|
||||||
@@ -43,7 +42,7 @@ class Group(Thread):
|
|||||||
plan = _plan.Plan._from_graphql(data["event_reminders"]["nodes"][0])
|
plan = _plan.Plan._from_graphql(data["event_reminders"]["nodes"][0])
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
data["thread_key"]["thread_fbid"],
|
uid=data["thread_key"]["thread_fbid"],
|
||||||
participants=set(
|
participants=set(
|
||||||
[
|
[
|
||||||
node["messaging_actor"]["id"]
|
node["messaging_actor"]["id"]
|
||||||
@@ -64,7 +63,7 @@ class Group(Thread):
|
|||||||
if data.get("group_approval_queue")
|
if data.get("group_approval_queue")
|
||||||
else None,
|
else None,
|
||||||
join_link=data["joinable_mode"].get("link"),
|
join_link=data["joinable_mode"].get("link"),
|
||||||
photo=data["image"].get("uri"),
|
photo=Image._from_uri_or_none(data["image"]),
|
||||||
name=data.get("name"),
|
name=data.get("name"),
|
||||||
message_count=data.get("messages_count"),
|
message_count=data.get("messages_count"),
|
||||||
last_active=last_active,
|
last_active=last_active,
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
import attr
|
import attr
|
||||||
|
from ._core import attrs_default, Image
|
||||||
from ._attachment import Attachment
|
from ._attachment import Attachment
|
||||||
from . import _util
|
from . import _util
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
@attrs_default
|
||||||
class LocationAttachment(Attachment):
|
class LocationAttachment(Attachment):
|
||||||
"""Represents a user location.
|
"""Represents a user location.
|
||||||
|
|
||||||
@@ -14,20 +15,13 @@ class LocationAttachment(Attachment):
|
|||||||
latitude = attr.ib(None)
|
latitude = attr.ib(None)
|
||||||
#: Longitude of the location
|
#: Longitude of the location
|
||||||
longitude = attr.ib(None)
|
longitude = attr.ib(None)
|
||||||
#: URL of image showing the map of the location
|
#: Image showing the map of the location
|
||||||
image_url = attr.ib(None, init=False)
|
image = attr.ib(None)
|
||||||
#: Width of the image
|
|
||||||
image_width = attr.ib(None, init=False)
|
|
||||||
#: Height of the image
|
|
||||||
image_height = attr.ib(None, init=False)
|
|
||||||
#: URL to Bing maps with the location
|
#: URL to Bing maps with the location
|
||||||
url = attr.ib(None, init=False)
|
url = attr.ib(None)
|
||||||
# Address of the location
|
# Address of the location
|
||||||
address = attr.ib(None)
|
address = attr.ib(None)
|
||||||
|
|
||||||
# Put here for backwards compatibility, so that the init argument order is preserved
|
|
||||||
uid = attr.ib(None)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _from_graphql(cls, data):
|
def _from_graphql(cls, data):
|
||||||
url = data.get("url")
|
url = data.get("url")
|
||||||
@@ -37,23 +31,20 @@ class LocationAttachment(Attachment):
|
|||||||
address = None
|
address = None
|
||||||
except ValueError:
|
except ValueError:
|
||||||
latitude, longitude = None, None
|
latitude, longitude = None, None
|
||||||
rtn = cls(
|
|
||||||
|
return cls(
|
||||||
uid=int(data["deduplication_key"]),
|
uid=int(data["deduplication_key"]),
|
||||||
latitude=latitude,
|
latitude=latitude,
|
||||||
longitude=longitude,
|
longitude=longitude,
|
||||||
|
image=Image._from_uri_or_none(data["media"].get("image"))
|
||||||
|
if data.get("media")
|
||||||
|
else None,
|
||||||
|
url=url,
|
||||||
address=address,
|
address=address,
|
||||||
)
|
)
|
||||||
media = data.get("media")
|
|
||||||
if media and media.get("image"):
|
|
||||||
image = media["image"]
|
|
||||||
rtn.image_url = image.get("uri")
|
|
||||||
rtn.image_width = image.get("width")
|
|
||||||
rtn.image_height = image.get("height")
|
|
||||||
rtn.url = url
|
|
||||||
return rtn
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
@attrs_default
|
||||||
class LiveLocationAttachment(LocationAttachment):
|
class LiveLocationAttachment(LocationAttachment):
|
||||||
"""Represents a live user location."""
|
"""Represents a live user location."""
|
||||||
|
|
||||||
@@ -82,7 +73,13 @@ class LiveLocationAttachment(LocationAttachment):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def _from_graphql(cls, data):
|
def _from_graphql(cls, data):
|
||||||
target = data["target"]
|
target = data["target"]
|
||||||
rtn = cls(
|
|
||||||
|
image = None
|
||||||
|
media = data.get("media")
|
||||||
|
if media and media.get("image"):
|
||||||
|
image = Image._from_uri(media["image"])
|
||||||
|
|
||||||
|
return cls(
|
||||||
uid=int(target["live_location_id"]),
|
uid=int(target["live_location_id"]),
|
||||||
latitude=target["coordinate"]["latitude"]
|
latitude=target["coordinate"]["latitude"]
|
||||||
if target.get("coordinate")
|
if target.get("coordinate")
|
||||||
@@ -90,15 +87,9 @@ class LiveLocationAttachment(LocationAttachment):
|
|||||||
longitude=target["coordinate"]["longitude"]
|
longitude=target["coordinate"]["longitude"]
|
||||||
if target.get("coordinate")
|
if target.get("coordinate")
|
||||||
else None,
|
else None,
|
||||||
|
image=image,
|
||||||
|
url=data.get("url"),
|
||||||
name=data["title_with_entities"]["text"],
|
name=data["title_with_entities"]["text"],
|
||||||
expires_at=_util.seconds_to_datetime(target.get("expiration_time")),
|
expires_at=_util.seconds_to_datetime(target.get("expiration_time")),
|
||||||
is_expired=target.get("is_expired"),
|
is_expired=target.get("is_expired"),
|
||||||
)
|
)
|
||||||
media = data.get("media")
|
|
||||||
if media and media.get("image"):
|
|
||||||
image = media["image"]
|
|
||||||
rtn.image_url = image.get("uri")
|
|
||||||
rtn.image_width = image.get("width")
|
|
||||||
rtn.image_height = image.get("height")
|
|
||||||
rtn.url = data.get("url")
|
|
||||||
return rtn
|
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import attr
|
import attr
|
||||||
import json
|
import json
|
||||||
from string import Formatter
|
from string import Formatter
|
||||||
from ._core import log, Enum
|
from ._core import log, attrs_default, Enum
|
||||||
from . import _util, _attachment, _location, _file, _quick_reply, _sticker
|
from . import _util, _attachment, _location, _file, _quick_reply, _sticker
|
||||||
|
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ class MessageReaction(Enum):
|
|||||||
NO = "👎"
|
NO = "👎"
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
@attrs_default
|
||||||
class Mention:
|
class Mention:
|
||||||
"""Represents a ``@mention``."""
|
"""Represents a ``@mention``."""
|
||||||
|
|
||||||
@@ -53,43 +53,63 @@ class Mention:
|
|||||||
#: The length of the mention
|
#: The length of the mention
|
||||||
length = attr.ib(10)
|
length = attr.ib(10)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_range(cls, data):
|
||||||
|
return cls(
|
||||||
|
thread_id=data.get("entity", {}).get("id"),
|
||||||
|
offset=data.get("offset"),
|
||||||
|
length=data.get("length"),
|
||||||
|
)
|
||||||
|
|
||||||
@attr.s
|
@classmethod
|
||||||
|
def _from_prng(cls, data):
|
||||||
|
return cls(thread_id=data.get("i"), offset=data.get("o"), length=data.get("l"))
|
||||||
|
|
||||||
|
def _to_send_data(self, i):
|
||||||
|
return {
|
||||||
|
"profile_xmd[{}][id]".format(i): self.thread_id,
|
||||||
|
"profile_xmd[{}][offset]".format(i): self.offset,
|
||||||
|
"profile_xmd[{}][length]".format(i): self.length,
|
||||||
|
"profile_xmd[{}][type]".format(i): "p",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@attrs_default
|
||||||
class Message:
|
class Message:
|
||||||
"""Represents a Facebook message."""
|
"""Represents a Facebook message."""
|
||||||
|
|
||||||
#: The actual message
|
#: The actual message
|
||||||
text = attr.ib(None)
|
text = attr.ib(None)
|
||||||
#: A list of :class:`Mention` objects
|
#: A list of `Mention` objects
|
||||||
mentions = attr.ib(factory=list, converter=lambda x: [] if x is None else x)
|
mentions = attr.ib(factory=list)
|
||||||
#: A :class:`EmojiSize`. Size of a sent emoji
|
#: A `EmojiSize`. Size of a sent emoji
|
||||||
emoji_size = attr.ib(None)
|
emoji_size = attr.ib(None)
|
||||||
#: The message ID
|
#: The message ID
|
||||||
uid = attr.ib(None, init=False)
|
uid = attr.ib(None)
|
||||||
#: ID of the sender
|
#: ID of the sender
|
||||||
author = attr.ib(None, init=False)
|
author = attr.ib(None)
|
||||||
#: Datetime of when the message was sent
|
#: Datetime of when the message was sent
|
||||||
created_at = attr.ib(None, init=False)
|
created_at = attr.ib(None)
|
||||||
#: Whether the message is read
|
#: Whether the message is read
|
||||||
is_read = attr.ib(None, init=False)
|
is_read = attr.ib(None)
|
||||||
#: A list of people IDs who read the message, works only with :func:`fbchat.Client.fetch_thread_messages`
|
#: A list of people IDs who read the message, works only with `Client.fetch_thread_messages`
|
||||||
read_by = attr.ib(factory=list, init=False)
|
read_by = attr.ib(factory=list)
|
||||||
#: A dictionary with user's IDs as keys, and their :class:`MessageReaction` as values
|
#: A dictionary with user's IDs as keys, and their `MessageReaction` as values
|
||||||
reactions = attr.ib(factory=dict, init=False)
|
reactions = attr.ib(factory=dict)
|
||||||
#: A :class:`Sticker`
|
#: A `Sticker`
|
||||||
sticker = attr.ib(None)
|
sticker = attr.ib(None)
|
||||||
#: A list of attachments
|
#: A list of attachments
|
||||||
attachments = attr.ib(factory=list, converter=lambda x: [] if x is None else x)
|
attachments = attr.ib(factory=list)
|
||||||
#: A list of :class:`QuickReply`
|
#: A list of `QuickReply`
|
||||||
quick_replies = attr.ib(factory=list, converter=lambda x: [] if x is None else x)
|
quick_replies = attr.ib(factory=list)
|
||||||
#: Whether the message is unsent (deleted for everyone)
|
#: Whether the message is unsent (deleted for everyone)
|
||||||
unsent = attr.ib(False, init=False)
|
unsent = attr.ib(False)
|
||||||
#: Message ID you want to reply to
|
#: Message ID you want to reply to
|
||||||
reply_to_id = attr.ib(None)
|
reply_to_id = attr.ib(None)
|
||||||
#: Replied message
|
#: Replied message
|
||||||
replied_to = attr.ib(None, init=False)
|
replied_to = attr.ib(None)
|
||||||
#: Whether the message was forwarded
|
#: Whether the message was forwarded
|
||||||
forwarded = attr.ib(False, init=False)
|
forwarded = attr.ib(False)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def format_mentions(cls, text, *args, **kwargs):
|
def format_mentions(cls, text, *args, **kwargs):
|
||||||
@@ -139,8 +159,7 @@ class Message:
|
|||||||
)
|
)
|
||||||
offset += len(name)
|
offset += len(name)
|
||||||
|
|
||||||
message = cls(text=result, mentions=mentions)
|
return cls(text=result, mentions=mentions)
|
||||||
return message
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_forwarded_from_tags(tags):
|
def _get_forwarded_from_tags(tags):
|
||||||
@@ -158,10 +177,7 @@ class Message:
|
|||||||
data["body"] = self.text
|
data["body"] = self.text
|
||||||
|
|
||||||
for i, mention in enumerate(self.mentions):
|
for i, mention in enumerate(self.mentions):
|
||||||
data["profile_xmd[{}][id]".format(i)] = mention.thread_id
|
data.update(mention._to_send_data(i))
|
||||||
data["profile_xmd[{}][offset]".format(i)] = mention.offset
|
|
||||||
data["profile_xmd[{}][length]".format(i)] = mention.length
|
|
||||||
data["profile_xmd[{}][type]".format(i)] = "p"
|
|
||||||
|
|
||||||
if self.emoji_size:
|
if self.emoji_size:
|
||||||
if self.text:
|
if self.text:
|
||||||
@@ -197,99 +213,82 @@ class Message:
|
|||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_quick_replies(data):
|
||||||
|
if data:
|
||||||
|
data = json.loads(data).get("quick_replies")
|
||||||
|
if isinstance(data, list):
|
||||||
|
return [_quick_reply.graphql_to_quick_reply(q) for q in data]
|
||||||
|
elif isinstance(data, dict):
|
||||||
|
return [_quick_reply.graphql_to_quick_reply(data, is_response=True)]
|
||||||
|
return []
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _from_graphql(cls, data):
|
def _from_graphql(cls, data, read_receipts=None):
|
||||||
if data.get("message_sender") is None:
|
if data.get("message_sender") is None:
|
||||||
data["message_sender"] = {}
|
data["message_sender"] = {}
|
||||||
if data.get("message") is None:
|
if data.get("message") is None:
|
||||||
data["message"] = {}
|
data["message"] = {}
|
||||||
tags = data.get("tags_list")
|
tags = data.get("tags_list")
|
||||||
rtn = cls(
|
|
||||||
text=data["message"].get("text"),
|
created_at = _util.millis_to_datetime(int(data.get("timestamp_precise")))
|
||||||
mentions=[
|
|
||||||
Mention(
|
attachments = [
|
||||||
m.get("entity", {}).get("id"),
|
|
||||||
offset=m.get("offset"),
|
|
||||||
length=m.get("length"),
|
|
||||||
)
|
|
||||||
for m in data["message"].get("ranges") or ()
|
|
||||||
],
|
|
||||||
emoji_size=EmojiSize._from_tags(tags),
|
|
||||||
sticker=_sticker.Sticker._from_graphql(data.get("sticker")),
|
|
||||||
)
|
|
||||||
rtn.forwarded = cls._get_forwarded_from_tags(tags)
|
|
||||||
rtn.uid = str(data["message_id"])
|
|
||||||
rtn.author = str(data["message_sender"]["id"])
|
|
||||||
rtn.created_at = _util.millis_to_datetime(int(data.get("timestamp_precise")))
|
|
||||||
rtn.unsent = False
|
|
||||||
if data.get("unread") is not None:
|
|
||||||
rtn.is_read = not data["unread"]
|
|
||||||
rtn.reactions = {
|
|
||||||
str(r["user"]["id"]): MessageReaction._extend_if_invalid(r["reaction"])
|
|
||||||
for r in data["message_reactions"]
|
|
||||||
}
|
|
||||||
if data.get("blob_attachments") is not None:
|
|
||||||
rtn.attachments = [
|
|
||||||
_file.graphql_to_attachment(attachment)
|
_file.graphql_to_attachment(attachment)
|
||||||
for attachment in data["blob_attachments"]
|
for attachment in data["blob_attachments"] or ()
|
||||||
]
|
|
||||||
if data.get("platform_xmd_encoded"):
|
|
||||||
quick_replies = json.loads(data["platform_xmd_encoded"]).get(
|
|
||||||
"quick_replies"
|
|
||||||
)
|
|
||||||
if isinstance(quick_replies, list):
|
|
||||||
rtn.quick_replies = [
|
|
||||||
_quick_reply.graphql_to_quick_reply(q) for q in quick_replies
|
|
||||||
]
|
|
||||||
elif isinstance(quick_replies, dict):
|
|
||||||
rtn.quick_replies = [
|
|
||||||
_quick_reply.graphql_to_quick_reply(quick_replies, is_response=True)
|
|
||||||
]
|
]
|
||||||
|
unsent = False
|
||||||
if data.get("extensible_attachment") is not None:
|
if data.get("extensible_attachment") is not None:
|
||||||
attachment = graphql_to_extensible_attachment(data["extensible_attachment"])
|
attachment = graphql_to_extensible_attachment(data["extensible_attachment"])
|
||||||
if isinstance(attachment, _attachment.UnsentMessage):
|
if isinstance(attachment, _attachment.UnsentMessage):
|
||||||
rtn.unsent = True
|
unsent = True
|
||||||
elif attachment:
|
elif attachment:
|
||||||
rtn.attachments.append(attachment)
|
attachments.append(attachment)
|
||||||
if data.get("replied_to_message") is not None:
|
|
||||||
rtn.replied_to = cls._from_graphql(data["replied_to_message"]["message"])
|
|
||||||
rtn.reply_to_id = rtn.replied_to.uid
|
|
||||||
return rtn
|
|
||||||
|
|
||||||
@classmethod
|
replied_to = None
|
||||||
def _from_reply(cls, data):
|
if data.get("replied_to_message"):
|
||||||
tags = data["messageMetadata"].get("tags")
|
replied_to = cls._from_graphql(data["replied_to_message"]["message"])
|
||||||
rtn = cls(
|
|
||||||
text=data.get("body"),
|
return cls(
|
||||||
|
text=data["message"].get("text"),
|
||||||
mentions=[
|
mentions=[
|
||||||
Mention(m.get("i"), offset=m.get("o"), length=m.get("l"))
|
Mention._from_range(m) for m in data["message"].get("ranges") or ()
|
||||||
for m in json.loads(data.get("data", {}).get("prng", "[]"))
|
|
||||||
],
|
],
|
||||||
emoji_size=EmojiSize._from_tags(tags),
|
emoji_size=EmojiSize._from_tags(tags),
|
||||||
|
uid=str(data["message_id"]),
|
||||||
|
author=str(data["message_sender"]["id"]),
|
||||||
|
created_at=created_at,
|
||||||
|
is_read=not data["unread"] if data.get("unread") is not None else None,
|
||||||
|
read_by=[
|
||||||
|
receipt["actor"]["id"]
|
||||||
|
for receipt in read_receipts or ()
|
||||||
|
if _util.millis_to_datetime(int(receipt["watermark"])) >= created_at
|
||||||
|
],
|
||||||
|
reactions={
|
||||||
|
str(r["user"]["id"]): MessageReaction._extend_if_invalid(r["reaction"])
|
||||||
|
for r in data["message_reactions"]
|
||||||
|
},
|
||||||
|
sticker=_sticker.Sticker._from_graphql(data.get("sticker")),
|
||||||
|
attachments=attachments,
|
||||||
|
quick_replies=cls._parse_quick_replies(data.get("platform_xmd_encoded")),
|
||||||
|
unsent=unsent,
|
||||||
|
reply_to_id=replied_to.uid if replied_to else None,
|
||||||
|
replied_to=replied_to,
|
||||||
|
forwarded=cls._get_forwarded_from_tags(tags),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_reply(cls, data, replied_to=None):
|
||||||
|
tags = data["messageMetadata"].get("tags")
|
||||||
metadata = data.get("messageMetadata", {})
|
metadata = data.get("messageMetadata", {})
|
||||||
rtn.forwarded = cls._get_forwarded_from_tags(tags)
|
|
||||||
rtn.uid = metadata.get("messageId")
|
attachments = []
|
||||||
rtn.author = str(metadata.get("actorFbId"))
|
unsent = False
|
||||||
rtn.created_at = _util.millis_to_datetime(metadata.get("timestamp"))
|
sticker = None
|
||||||
rtn.unsent = False
|
for attachment in data.get("attachments") or ():
|
||||||
if data.get("data", {}).get("platform_xmd"):
|
|
||||||
quick_replies = json.loads(data["data"]["platform_xmd"]).get(
|
|
||||||
"quick_replies"
|
|
||||||
)
|
|
||||||
if isinstance(quick_replies, list):
|
|
||||||
rtn.quick_replies = [
|
|
||||||
_quick_reply.graphql_to_quick_reply(q) for q in quick_replies
|
|
||||||
]
|
|
||||||
elif isinstance(quick_replies, dict):
|
|
||||||
rtn.quick_replies = [
|
|
||||||
_quick_reply.graphql_to_quick_reply(quick_replies, is_response=True)
|
|
||||||
]
|
|
||||||
if data.get("attachments") is not None:
|
|
||||||
for attachment in data["attachments"]:
|
|
||||||
attachment = json.loads(attachment["mercuryJSON"])
|
attachment = json.loads(attachment["mercuryJSON"])
|
||||||
if attachment.get("blob_attachment"):
|
if attachment.get("blob_attachment"):
|
||||||
rtn.attachments.append(
|
attachments.append(
|
||||||
_file.graphql_to_attachment(attachment["blob_attachment"])
|
_file.graphql_to_attachment(attachment["blob_attachment"])
|
||||||
)
|
)
|
||||||
if attachment.get("extensible_attachment"):
|
if attachment.get("extensible_attachment"):
|
||||||
@@ -297,57 +296,61 @@ class Message:
|
|||||||
attachment["extensible_attachment"]
|
attachment["extensible_attachment"]
|
||||||
)
|
)
|
||||||
if isinstance(extensible_attachment, _attachment.UnsentMessage):
|
if isinstance(extensible_attachment, _attachment.UnsentMessage):
|
||||||
rtn.unsent = True
|
unsent = True
|
||||||
else:
|
else:
|
||||||
rtn.attachments.append(extensible_attachment)
|
attachments.append(extensible_attachment)
|
||||||
if attachment.get("sticker_attachment"):
|
if attachment.get("sticker_attachment"):
|
||||||
rtn.sticker = _sticker.Sticker._from_graphql(
|
sticker = _sticker.Sticker._from_graphql(
|
||||||
attachment["sticker_attachment"]
|
attachment["sticker_attachment"]
|
||||||
)
|
)
|
||||||
return rtn
|
|
||||||
|
return cls(
|
||||||
|
text=data.get("body"),
|
||||||
|
mentions=[
|
||||||
|
Mention._from_prng(m)
|
||||||
|
for m in _util.parse_json(data.get("data", {}).get("prng", "[]"))
|
||||||
|
],
|
||||||
|
emoji_size=EmojiSize._from_tags(tags),
|
||||||
|
uid=metadata.get("messageId"),
|
||||||
|
author=str(metadata.get("actorFbId")),
|
||||||
|
created_at=_util.millis_to_datetime(metadata.get("timestamp")),
|
||||||
|
sticker=sticker,
|
||||||
|
attachments=attachments,
|
||||||
|
quick_replies=cls._parse_quick_replies(data.get("platform_xmd_encoded")),
|
||||||
|
unsent=unsent,
|
||||||
|
reply_to_id=replied_to.uid if replied_to else None,
|
||||||
|
replied_to=replied_to,
|
||||||
|
forwarded=cls._get_forwarded_from_tags(tags),
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _from_pull(cls, data, mid=None, tags=None, author=None, created_at=None):
|
def _from_pull(cls, data, mid=None, tags=None, author=None, created_at=None):
|
||||||
rtn = cls(text=data.get("body"))
|
mentions = []
|
||||||
rtn.uid = mid
|
|
||||||
rtn.author = author
|
|
||||||
rtn.created_at = created_at
|
|
||||||
|
|
||||||
if data.get("data") and data["data"].get("prng"):
|
if data.get("data") and data["data"].get("prng"):
|
||||||
try:
|
try:
|
||||||
rtn.mentions = [
|
mentions = [
|
||||||
Mention(
|
Mention._from_prng(m)
|
||||||
str(mention.get("i")),
|
for m in _util.parse_json(data["data"]["prng"])
|
||||||
offset=mention.get("o"),
|
|
||||||
length=mention.get("l"),
|
|
||||||
)
|
|
||||||
for mention in _util.parse_json(data["data"]["prng"])
|
|
||||||
]
|
]
|
||||||
except Exception:
|
except Exception:
|
||||||
log.exception("An exception occured while reading attachments")
|
log.exception("An exception occured while reading attachments")
|
||||||
|
|
||||||
if data.get("attachments"):
|
attachments = []
|
||||||
|
unsent = False
|
||||||
|
sticker = None
|
||||||
try:
|
try:
|
||||||
for a in data["attachments"]:
|
for a in data.get("attachments") or ():
|
||||||
mercury = a["mercury"]
|
mercury = a["mercury"]
|
||||||
if mercury.get("blob_attachment"):
|
if mercury.get("blob_attachment"):
|
||||||
image_metadata = a.get("imageMetadata", {})
|
image_metadata = a.get("imageMetadata", {})
|
||||||
attach_type = mercury["blob_attachment"]["__typename"]
|
attach_type = mercury["blob_attachment"]["__typename"]
|
||||||
attachment = _file.graphql_to_attachment(
|
attachment = _file.graphql_to_attachment(
|
||||||
mercury["blob_attachment"]
|
mercury["blob_attachment"], a["fileSize"]
|
||||||
)
|
)
|
||||||
|
attachments.append(attachment)
|
||||||
if attach_type in [
|
|
||||||
"MessageFile",
|
|
||||||
"MessageVideo",
|
|
||||||
"MessageAudio",
|
|
||||||
]:
|
|
||||||
# TODO: Add more data here for audio files
|
|
||||||
attachment.size = int(a["fileSize"])
|
|
||||||
rtn.attachments.append(attachment)
|
|
||||||
|
|
||||||
elif mercury.get("sticker_attachment"):
|
elif mercury.get("sticker_attachment"):
|
||||||
rtn.sticker = _sticker.Sticker._from_graphql(
|
sticker = _sticker.Sticker._from_graphql(
|
||||||
mercury["sticker_attachment"]
|
mercury["sticker_attachment"]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -356,9 +359,9 @@ class Message:
|
|||||||
mercury["extensible_attachment"]
|
mercury["extensible_attachment"]
|
||||||
)
|
)
|
||||||
if isinstance(attachment, _attachment.UnsentMessage):
|
if isinstance(attachment, _attachment.UnsentMessage):
|
||||||
rtn.unsent = True
|
unsent = True
|
||||||
elif attachment:
|
elif attachment:
|
||||||
rtn.attachments.append(attachment)
|
attachments.append(attachment)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
log.exception(
|
log.exception(
|
||||||
@@ -367,9 +370,18 @@ class Message:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
rtn.emoji_size = EmojiSize._from_tags(tags)
|
return cls(
|
||||||
rtn.forwarded = cls._get_forwarded_from_tags(tags)
|
text=data.get("body"),
|
||||||
return rtn
|
mentions=mentions,
|
||||||
|
emoji_size=EmojiSize._from_tags(tags),
|
||||||
|
uid=mid,
|
||||||
|
author=author,
|
||||||
|
created_at=created_at,
|
||||||
|
sticker=sticker,
|
||||||
|
attachments=attachments,
|
||||||
|
unsent=unsent,
|
||||||
|
forwarded=cls._get_forwarded_from_tags(tags),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def graphql_to_extensible_attachment(data):
|
def graphql_to_extensible_attachment(data):
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
import attr
|
import attr
|
||||||
|
from ._core import attrs_default, Image
|
||||||
from . import _plan
|
from . import _plan
|
||||||
from ._thread import ThreadType, Thread
|
from ._thread import ThreadType, Thread
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
@attrs_default
|
||||||
class Page(Thread):
|
class Page(Thread):
|
||||||
"""Represents a Facebook page. Inherits `Thread`."""
|
"""Represents a Facebook page. Inherits `Thread`."""
|
||||||
|
|
||||||
@@ -31,11 +32,11 @@ class Page(Thread):
|
|||||||
plan = _plan.Plan._from_graphql(data["event_reminders"]["nodes"][0])
|
plan = _plan.Plan._from_graphql(data["event_reminders"]["nodes"][0])
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
data["id"],
|
uid=data["id"],
|
||||||
url=data.get("url"),
|
url=data.get("url"),
|
||||||
city=data.get("city").get("name"),
|
city=data.get("city").get("name"),
|
||||||
category=data.get("category_type"),
|
category=data.get("category_type"),
|
||||||
photo=data["profile_picture"].get("uri"),
|
photo=Image._from_uri(data["profile_picture"]),
|
||||||
name=data.get("name"),
|
name=data.get("name"),
|
||||||
message_count=data.get("messages_count"),
|
message_count=data.get("messages_count"),
|
||||||
plan=plan,
|
plan=plan,
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import attr
|
import attr
|
||||||
import json
|
import json
|
||||||
from ._core import Enum
|
from ._core import attrs_default, Enum
|
||||||
from . import _util
|
from . import _util
|
||||||
|
|
||||||
|
|
||||||
@@ -10,24 +10,24 @@ class GuestStatus(Enum):
|
|||||||
DECLINED = 3
|
DECLINED = 3
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
@attrs_default
|
||||||
class Plan:
|
class Plan:
|
||||||
"""Represents a plan."""
|
"""Represents a plan."""
|
||||||
|
|
||||||
#: ID of the plan
|
|
||||||
uid = attr.ib(None, init=False)
|
|
||||||
#: Plan time (datetime), only precise down to the minute
|
#: Plan time (datetime), only precise down to the minute
|
||||||
time = attr.ib()
|
time = attr.ib()
|
||||||
#: Plan title
|
#: Plan title
|
||||||
title = attr.ib()
|
title = attr.ib()
|
||||||
|
#: ID of the plan
|
||||||
|
uid = attr.ib(None)
|
||||||
#: Plan location name
|
#: Plan location name
|
||||||
location = attr.ib(None, converter=lambda x: x or "")
|
location = attr.ib(None, converter=lambda x: x or "")
|
||||||
#: Plan location ID
|
#: Plan location ID
|
||||||
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)
|
||||||
#: Dictionary 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)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def going(self):
|
def going(self):
|
||||||
@@ -58,44 +58,41 @@ class Plan:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _from_pull(cls, data):
|
def _from_pull(cls, data):
|
||||||
rtn = cls(
|
return cls(
|
||||||
|
uid=data.get("event_id"),
|
||||||
time=_util.seconds_to_datetime(int(data.get("event_time"))),
|
time=_util.seconds_to_datetime(int(data.get("event_time"))),
|
||||||
title=data.get("event_title"),
|
title=data.get("event_title"),
|
||||||
location=data.get("event_location_name"),
|
location=data.get("event_location_name"),
|
||||||
location_id=data.get("event_location_id"),
|
location_id=data.get("event_location_id"),
|
||||||
)
|
author_id=data.get("event_creator_id"),
|
||||||
rtn.uid = data.get("event_id")
|
guests={
|
||||||
rtn.author_id = data.get("event_creator_id")
|
|
||||||
rtn.guests = {
|
|
||||||
x["node"]["id"]: GuestStatus[x["guest_list_state"]]
|
x["node"]["id"]: GuestStatus[x["guest_list_state"]]
|
||||||
for x in json.loads(data["guest_state_list"])
|
for x in json.loads(data["guest_state_list"])
|
||||||
}
|
},
|
||||||
return rtn
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _from_fetch(cls, data):
|
def _from_fetch(cls, data):
|
||||||
rtn = cls(
|
return cls(
|
||||||
|
uid=data.get("oid"),
|
||||||
time=_util.seconds_to_datetime(data.get("event_time")),
|
time=_util.seconds_to_datetime(data.get("event_time")),
|
||||||
title=data.get("title"),
|
title=data.get("title"),
|
||||||
location=data.get("location_name"),
|
location=data.get("location_name"),
|
||||||
location_id=str(data["location_id"]) if data.get("location_id") else None,
|
location_id=str(data["location_id"]) if data.get("location_id") else None,
|
||||||
|
author_id=data.get("creator_id"),
|
||||||
|
guests={id_: GuestStatus[s] for id_, s in data["event_members"].items()},
|
||||||
)
|
)
|
||||||
rtn.uid = data.get("oid")
|
|
||||||
rtn.author_id = data.get("creator_id")
|
|
||||||
rtn.guests = {id_: GuestStatus[s] for id_, s in data["event_members"].items()}
|
|
||||||
return rtn
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _from_graphql(cls, data):
|
def _from_graphql(cls, data):
|
||||||
rtn = cls(
|
return cls(
|
||||||
|
uid=data.get("id"),
|
||||||
time=_util.seconds_to_datetime(data.get("time")),
|
time=_util.seconds_to_datetime(data.get("time")),
|
||||||
title=data.get("event_title"),
|
title=data.get("event_title"),
|
||||||
location=data.get("location_name"),
|
location=data.get("location_name"),
|
||||||
)
|
author_id=data["lightweight_event_creator"].get("id"),
|
||||||
rtn.uid = data.get("id")
|
guests={
|
||||||
rtn.author_id = data["lightweight_event_creator"].get("id")
|
|
||||||
rtn.guests = {
|
|
||||||
x["node"]["id"]: GuestStatus[x["guest_list_state"]]
|
x["node"]["id"]: GuestStatus[x["guest_list_state"]]
|
||||||
for x in data["event_reminder_members"]["edges"]
|
for x in data["event_reminder_members"]["edges"]
|
||||||
}
|
},
|
||||||
return rtn
|
)
|
||||||
|
@@ -1,13 +1,14 @@
|
|||||||
import attr
|
import attr
|
||||||
|
from ._core import attrs_default
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
@attrs_default
|
||||||
class Poll:
|
class Poll:
|
||||||
"""Represents a poll."""
|
"""Represents a poll."""
|
||||||
|
|
||||||
#: Title of the poll
|
#: Title of the poll
|
||||||
title = attr.ib()
|
title = attr.ib()
|
||||||
#: List of :class:`PollOption`, can be fetched with :func:`fbchat.Client.fetch_poll_options`
|
#: List of `PollOption`, can be fetched with `Client.fetch_poll_options`
|
||||||
options = attr.ib()
|
options = attr.ib()
|
||||||
#: Options count
|
#: Options count
|
||||||
options_count = attr.ib(None)
|
options_count = attr.ib(None)
|
||||||
@@ -24,7 +25,7 @@ class Poll:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
@attrs_default
|
||||||
class PollOption:
|
class PollOption:
|
||||||
"""Represents a poll option."""
|
"""Represents a poll option."""
|
||||||
|
|
||||||
|
@@ -1,22 +1,23 @@
|
|||||||
import attr
|
import attr
|
||||||
|
from ._core import attrs_default
|
||||||
from ._attachment import Attachment
|
from ._attachment import Attachment
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
@attrs_default
|
||||||
class QuickReply:
|
class QuickReply:
|
||||||
"""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)
|
||||||
#: External payload for responses
|
#: External payload for responses
|
||||||
external_payload = attr.ib(None, init=False)
|
external_payload = attr.ib(None)
|
||||||
#: Additional data
|
#: Additional data
|
||||||
data = attr.ib(None)
|
data = attr.ib(None)
|
||||||
#: Whether it's a response for a quick reply
|
#: Whether it's a response for a quick reply
|
||||||
is_response = attr.ib(False)
|
is_response = attr.ib(False)
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
@attrs_default
|
||||||
class QuickReplyText(QuickReply):
|
class QuickReplyText(QuickReply):
|
||||||
"""Represents a text quick reply."""
|
"""Represents a text quick reply."""
|
||||||
|
|
||||||
@@ -28,7 +29,7 @@ class QuickReplyText(QuickReply):
|
|||||||
_type = "text"
|
_type = "text"
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
@attrs_default
|
||||||
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)."""
|
||||||
|
|
||||||
@@ -36,7 +37,7 @@ class QuickReplyLocation(QuickReply):
|
|||||||
_type = "location"
|
_type = "location"
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
@attrs_default
|
||||||
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)."""
|
||||||
|
|
||||||
@@ -46,7 +47,7 @@ class QuickReplyPhoneNumber(QuickReply):
|
|||||||
_type = "user_phone_number"
|
_type = "user_phone_number"
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
@attrs_default
|
||||||
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)."""
|
||||||
|
|
||||||
|
@@ -5,7 +5,7 @@ import requests
|
|||||||
import random
|
import random
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
from ._core import log
|
from ._core import log, attrs_default
|
||||||
from . import _graphql, _util, _exception
|
from . import _graphql, _util, _exception
|
||||||
|
|
||||||
FB_DTSG_REGEX = re.compile(r'name="fb_dtsg" value="(.*?)"')
|
FB_DTSG_REGEX = re.compile(r'name="fb_dtsg" value="(.*?)"')
|
||||||
@@ -98,7 +98,7 @@ def _2fa_helper(session, code, r):
|
|||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
||||||
@attr.s(slots=True) # TODO i Python 3: Add kw_only=True
|
@attrs_default
|
||||||
class State:
|
class State:
|
||||||
"""Stores and manages state required for most Facebook requests."""
|
"""Stores and manages state required for most Facebook requests."""
|
||||||
|
|
||||||
|
@@ -1,8 +1,9 @@
|
|||||||
import attr
|
import attr
|
||||||
|
from ._core import attrs_default, Image
|
||||||
from ._attachment import Attachment
|
from ._attachment import Attachment
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
@attrs_default
|
||||||
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."""
|
||||||
|
|
||||||
@@ -23,12 +24,8 @@ class Sticker(Attachment):
|
|||||||
#: 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)
|
||||||
|
|
||||||
#: URL to the sticker's image
|
#: The sticker's image
|
||||||
url = attr.ib(None)
|
image = attr.ib(None)
|
||||||
#: Width of the sticker
|
|
||||||
width = attr.ib(None)
|
|
||||||
#: Height of the sticker
|
|
||||||
height = attr.ib(None)
|
|
||||||
#: The sticker's label/name
|
#: The sticker's label/name
|
||||||
label = attr.ib(None)
|
label = attr.ib(None)
|
||||||
|
|
||||||
@@ -36,19 +33,20 @@ class Sticker(Attachment):
|
|||||||
def _from_graphql(cls, data):
|
def _from_graphql(cls, data):
|
||||||
if not data:
|
if not data:
|
||||||
return None
|
return None
|
||||||
self = cls(uid=data["id"])
|
|
||||||
if data.get("pack"):
|
return cls(
|
||||||
self.pack = data["pack"].get("id")
|
uid=data["id"],
|
||||||
if data.get("sprite_image"):
|
pack=data["pack"].get("id") if data.get("pack") else None,
|
||||||
self.is_animated = True
|
is_animated=bool(data.get("sprite_image")),
|
||||||
self.medium_sprite_image = data["sprite_image"].get("uri")
|
medium_sprite_image=data["sprite_image"].get("uri")
|
||||||
self.large_sprite_image = data["sprite_image_2x"].get("uri")
|
if data.get("sprite_image")
|
||||||
self.frames_per_row = data.get("frames_per_row")
|
else None,
|
||||||
self.frames_per_col = data.get("frames_per_column")
|
large_sprite_image=data["sprite_image_2x"].get("uri")
|
||||||
self.frame_rate = data.get("frame_rate")
|
if data.get("sprite_image_2x")
|
||||||
self.url = data.get("url")
|
else None,
|
||||||
self.width = data.get("width")
|
frames_per_row=data.get("frames_per_row"),
|
||||||
self.height = data.get("height")
|
frames_per_col=data.get("frames_per_column"),
|
||||||
if data.get("label"):
|
frame_rate=data.get("frame_rate"),
|
||||||
self.label = data["label"]
|
image=Image._from_url_or_none(data),
|
||||||
return self
|
label=data["label"] if data.get("label") else None,
|
||||||
|
)
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import attr
|
import attr
|
||||||
from ._core import Enum
|
from ._core import attrs_default, Enum, Image
|
||||||
|
|
||||||
|
|
||||||
class ThreadType(Enum):
|
class ThreadType(Enum):
|
||||||
@@ -67,7 +67,7 @@ class ThreadColor(Enum):
|
|||||||
return cls._extend_if_invalid(value)
|
return cls._extend_if_invalid(value)
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
@attrs_default
|
||||||
class Thread:
|
class Thread:
|
||||||
"""Represents a Facebook thread."""
|
"""Represents a Facebook thread."""
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ class Thread:
|
|||||||
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 = None
|
type = None
|
||||||
#: A URL to the thread's picture
|
#: 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)
|
||||||
@@ -83,7 +83,7 @@ class Thread:
|
|||||||
last_active = attr.ib(None)
|
last_active = attr.ib(None)
|
||||||
#: Number of messages in the thread
|
#: Number of messages in the thread
|
||||||
message_count = attr.ib(None)
|
message_count = attr.ib(None)
|
||||||
#: Set :class:`Plan`
|
#: Set `Plan`
|
||||||
plan = attr.ib(None)
|
plan = attr.ib(None)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import attr
|
import attr
|
||||||
from ._core import Enum
|
from ._core import attrs_default, Enum, Image
|
||||||
from . import _util, _plan
|
from . import _util, _plan
|
||||||
from ._thread import ThreadType, Thread
|
from ._thread import ThreadType, Thread
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ class TypingStatus(Enum):
|
|||||||
TYPING = 1
|
TYPING = 1
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
@attrs_default
|
||||||
class User(Thread):
|
class User(Thread):
|
||||||
"""Represents a Facebook user. Inherits `Thread`."""
|
"""Represents a Facebook user. Inherits `Thread`."""
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ class User(Thread):
|
|||||||
nickname = attr.ib(None)
|
nickname = attr.ib(None)
|
||||||
#: The clients nickname, as seen by the user
|
#: The clients nickname, as seen by the user
|
||||||
own_nickname = attr.ib(None)
|
own_nickname = attr.ib(None)
|
||||||
#: A :class:`ThreadColor`. The message color
|
#: A `ThreadColor`. The message color
|
||||||
color = attr.ib(None)
|
color = attr.ib(None)
|
||||||
#: The default emoji
|
#: The default emoji
|
||||||
emoji = attr.ib(None)
|
emoji = attr.ib(None)
|
||||||
@@ -78,7 +78,7 @@ class User(Thread):
|
|||||||
plan = _plan.Plan._from_graphql(data["event_reminders"]["nodes"][0])
|
plan = _plan.Plan._from_graphql(data["event_reminders"]["nodes"][0])
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
data["id"],
|
uid=data["id"],
|
||||||
url=data.get("url"),
|
url=data.get("url"),
|
||||||
first_name=data.get("first_name"),
|
first_name=data.get("first_name"),
|
||||||
last_name=data.get("last_name"),
|
last_name=data.get("last_name"),
|
||||||
@@ -89,7 +89,7 @@ class User(Thread):
|
|||||||
color=c_info.get("color"),
|
color=c_info.get("color"),
|
||||||
emoji=c_info.get("emoji"),
|
emoji=c_info.get("emoji"),
|
||||||
own_nickname=c_info.get("own_nickname"),
|
own_nickname=c_info.get("own_nickname"),
|
||||||
photo=data["profile_picture"].get("uri"),
|
photo=Image._from_uri(data["profile_picture"]),
|
||||||
name=data.get("name"),
|
name=data.get("name"),
|
||||||
message_count=data.get("messages_count"),
|
message_count=data.get("messages_count"),
|
||||||
plan=plan,
|
plan=plan,
|
||||||
@@ -123,7 +123,7 @@ class User(Thread):
|
|||||||
plan = _plan.Plan._from_graphql(data["event_reminders"]["nodes"][0])
|
plan = _plan.Plan._from_graphql(data["event_reminders"]["nodes"][0])
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
user["id"],
|
uid=user["id"],
|
||||||
url=user.get("url"),
|
url=user.get("url"),
|
||||||
name=user.get("name"),
|
name=user.get("name"),
|
||||||
first_name=first_name,
|
first_name=first_name,
|
||||||
@@ -135,7 +135,7 @@ class User(Thread):
|
|||||||
color=c_info.get("color"),
|
color=c_info.get("color"),
|
||||||
emoji=c_info.get("emoji"),
|
emoji=c_info.get("emoji"),
|
||||||
own_nickname=c_info.get("own_nickname"),
|
own_nickname=c_info.get("own_nickname"),
|
||||||
photo=user["big_image_src"].get("uri"),
|
photo=Image._from_uri(user["big_image_src"]),
|
||||||
message_count=data.get("messages_count"),
|
message_count=data.get("messages_count"),
|
||||||
last_active=last_active,
|
last_active=last_active,
|
||||||
plan=plan,
|
plan=plan,
|
||||||
@@ -144,10 +144,10 @@ class User(Thread):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def _from_all_fetch(cls, data):
|
def _from_all_fetch(cls, data):
|
||||||
return cls(
|
return cls(
|
||||||
data["id"],
|
uid=data["id"],
|
||||||
first_name=data.get("firstName"),
|
first_name=data.get("firstName"),
|
||||||
url=data.get("uri"),
|
url=data.get("uri"),
|
||||||
photo=data.get("thumbSrc"),
|
photo=Image(url=data.get("thumbSrc")),
|
||||||
name=data.get("name"),
|
name=data.get("name"),
|
||||||
is_friend=data.get("is_friend"),
|
is_friend=data.get("is_friend"),
|
||||||
gender=GENDERS.get(data.get("gender")),
|
gender=GENDERS.get(data.get("gender")),
|
||||||
|
@@ -14,7 +14,7 @@ maintainer-email = "madsmtm@gmail.com"
|
|||||||
home-page = "https://github.com/carpedm20/fbchat/"
|
home-page = "https://github.com/carpedm20/fbchat/"
|
||||||
requires = [
|
requires = [
|
||||||
"aenum~=2.0",
|
"aenum~=2.0",
|
||||||
"attrs>=18.2",
|
"attrs>=19.1",
|
||||||
"requests~=2.19",
|
"requests~=2.19",
|
||||||
"beautifulsoup4~=4.0",
|
"beautifulsoup4~=4.0",
|
||||||
]
|
]
|
||||||
|
@@ -72,10 +72,8 @@ def test_share_from_graphql_link():
|
|||||||
title="a.com",
|
title="a.com",
|
||||||
description="",
|
description="",
|
||||||
source="a.com",
|
source="a.com",
|
||||||
image_url=None,
|
image=None,
|
||||||
original_image_url=None,
|
original_image_url=None,
|
||||||
image_width=None,
|
|
||||||
image_height=None,
|
|
||||||
attachments=[],
|
attachments=[],
|
||||||
uid="ee.mid.$xyz",
|
uid="ee.mid.$xyz",
|
||||||
) == ShareAttachment._from_graphql(data)
|
) == ShareAttachment._from_graphql(data)
|
||||||
@@ -125,10 +123,10 @@ def test_share_from_graphql_link_with_image():
|
|||||||
" Share photos and videos, send messages and get updates."
|
" Share photos and videos, send messages and get updates."
|
||||||
),
|
),
|
||||||
source=None,
|
source=None,
|
||||||
image_url="https://www.facebook.com/rsrc.php/v3/x.png",
|
image=fbchat.Image(
|
||||||
|
url="https://www.facebook.com/rsrc.php/v3/x.png", width=325, height=325
|
||||||
|
),
|
||||||
original_image_url="https://www.facebook.com/rsrc.php/v3/x.png",
|
original_image_url="https://www.facebook.com/rsrc.php/v3/x.png",
|
||||||
image_width=325,
|
|
||||||
image_height=325,
|
|
||||||
attachments=[],
|
attachments=[],
|
||||||
uid="deadbeef123",
|
uid="deadbeef123",
|
||||||
) == ShareAttachment._from_graphql(data)
|
) == ShareAttachment._from_graphql(data)
|
||||||
@@ -187,14 +185,14 @@ def test_share_from_graphql_video():
|
|||||||
" Subscribe to the official Rick As..."
|
" Subscribe to the official Rick As..."
|
||||||
),
|
),
|
||||||
source="youtube.com",
|
source="youtube.com",
|
||||||
image_url=(
|
image=fbchat.Image(
|
||||||
"https://external-arn2-1.xx.fbcdn.net/safe_image.php?d=xyz123"
|
url="https://external-arn2-1.xx.fbcdn.net/safe_image.php?d=xyz123"
|
||||||
"&w=960&h=540&url=https%3A%2F%2Fi.ytimg.com%2Fvi%2FdQw4w9WgXcQ"
|
"&w=960&h=540&url=https%3A%2F%2Fi.ytimg.com%2Fvi%2FdQw4w9WgXcQ"
|
||||||
"%2Fmaxresdefault.jpg&sx=0&sy=0&sw=1280&sh=720&_nc_hash=abc123"
|
"%2Fmaxresdefault.jpg&sx=0&sy=0&sw=1280&sh=720&_nc_hash=abc123",
|
||||||
|
width=960,
|
||||||
|
height=540,
|
||||||
),
|
),
|
||||||
original_image_url="https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg",
|
original_image_url="https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg",
|
||||||
image_width=960,
|
|
||||||
image_height=540,
|
|
||||||
attachments=[],
|
attachments=[],
|
||||||
uid="ee.mid.$gAAT4Sw1WSGhzQ9uRWVtEpZHZ8ZPV",
|
uid="ee.mid.$gAAT4Sw1WSGhzQ9uRWVtEpZHZ8ZPV",
|
||||||
) == ShareAttachment._from_graphql(data)
|
) == ShareAttachment._from_graphql(data)
|
||||||
@@ -310,10 +308,12 @@ def test_share_with_image_subattachment():
|
|||||||
title="",
|
title="",
|
||||||
description="Abc",
|
description="Abc",
|
||||||
source="Def",
|
source="Def",
|
||||||
image_url="https://scontent-arn2-1.xx.fbcdn.net/v/t1.0-9/1.jpg",
|
image=fbchat.Image(
|
||||||
|
url="https://scontent-arn2-1.xx.fbcdn.net/v/t1.0-9/1.jpg",
|
||||||
|
width=720,
|
||||||
|
height=960,
|
||||||
|
),
|
||||||
original_image_url="https://scontent-arn2-1.xx.fbcdn.net/v/t1.0-9/1.jpg",
|
original_image_url="https://scontent-arn2-1.xx.fbcdn.net/v/t1.0-9/1.jpg",
|
||||||
image_width=720,
|
|
||||||
image_height=960,
|
|
||||||
attachments=[None],
|
attachments=[None],
|
||||||
uid="deadbeef123",
|
uid="deadbeef123",
|
||||||
) == ShareAttachment._from_graphql(data)
|
) == ShareAttachment._from_graphql(data)
|
||||||
@@ -436,19 +436,23 @@ def test_share_with_video_subattachment():
|
|||||||
title="",
|
title="",
|
||||||
description="Abc",
|
description="Abc",
|
||||||
source="Def",
|
source="Def",
|
||||||
image_url="https://scontent-arn2-1.xx.fbcdn.net/v/t15.5256-10/p180x540/1.jpg",
|
image=fbchat.Image(
|
||||||
|
url="https://scontent-arn2-1.xx.fbcdn.net/v/t15.5256-10/p180x540/1.jpg",
|
||||||
|
width=960,
|
||||||
|
height=540,
|
||||||
|
),
|
||||||
original_image_url="https://scontent-arn2-1.xx.fbcdn.net/v/t15.5256-10/p180x540/1.jpg",
|
original_image_url="https://scontent-arn2-1.xx.fbcdn.net/v/t15.5256-10/p180x540/1.jpg",
|
||||||
image_width=960,
|
|
||||||
image_height=540,
|
|
||||||
attachments=[
|
attachments=[
|
||||||
fbchat.VideoAttachment(
|
fbchat.VideoAttachment(
|
||||||
uid="2222",
|
uid="2222",
|
||||||
duration=datetime.timedelta(seconds=24, microseconds=469000),
|
duration=datetime.timedelta(seconds=24, microseconds=469000),
|
||||||
preview_url="https://video-arn2-1.xx.fbcdn.net/v/t42.9040-2/vid.mp4",
|
preview_url="https://video-arn2-1.xx.fbcdn.net/v/t42.9040-2/vid.mp4",
|
||||||
medium_image={
|
previews={
|
||||||
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t15.5256-10/p180x540/1.jpg",
|
fbchat.Image(
|
||||||
"width": 960,
|
url="https://scontent-arn2-1.xx.fbcdn.net/v/t15.5256-10/p180x540/1.jpg",
|
||||||
"height": 540,
|
width=960,
|
||||||
|
height=540,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
@@ -33,16 +33,18 @@ def test_imageattachment_from_list():
|
|||||||
uid="1234",
|
uid="1234",
|
||||||
width=2833,
|
width=2833,
|
||||||
height=1367,
|
height=1367,
|
||||||
thumbnail_url="https://scontent-arn2-1.xx.fbcdn.net/v/s261x260/1.jpg",
|
previews={
|
||||||
preview={
|
fbchat.Image(url="https://scontent-arn2-1.xx.fbcdn.net/v/s261x260/1.jpg"),
|
||||||
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/2.jpg",
|
fbchat.Image(
|
||||||
"width": 960,
|
url="https://scontent-arn2-1.xx.fbcdn.net/v/2.jpg",
|
||||||
"height": 463,
|
width=960,
|
||||||
},
|
height=463,
|
||||||
large_preview={
|
),
|
||||||
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/s2048x2048/3.jpg",
|
fbchat.Image(
|
||||||
"width": 2048,
|
url="https://scontent-arn2-1.xx.fbcdn.net/v/s2048x2048/3.jpg",
|
||||||
"height": 988,
|
width=2048,
|
||||||
|
height=988,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
) == ImageAttachment._from_list({"node": data})
|
) == ImageAttachment._from_list({"node": data})
|
||||||
|
|
||||||
@@ -71,18 +73,20 @@ def test_videoattachment_from_list():
|
|||||||
uid="1234",
|
uid="1234",
|
||||||
width=640,
|
width=640,
|
||||||
height=368,
|
height=368,
|
||||||
small_image={
|
previews={
|
||||||
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t15.3394-10/p261x260/1.jpg"
|
fbchat.Image(
|
||||||
},
|
url="https://scontent-arn2-1.xx.fbcdn.net/v/t15.3394-10/p261x260/1.jpg"
|
||||||
medium_image={
|
),
|
||||||
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t15.3394-10/2.jpg",
|
fbchat.Image(
|
||||||
"width": 640,
|
url="https://scontent-arn2-1.xx.fbcdn.net/v/t15.3394-10/2.jpg",
|
||||||
"height": 368,
|
width=640,
|
||||||
},
|
height=368,
|
||||||
large_image={
|
),
|
||||||
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t15.3394-10/3.jpg",
|
fbchat.Image(
|
||||||
"width": 640,
|
url="https://scontent-arn2-1.xx.fbcdn.net/v/t15.3394-10/3.jpg",
|
||||||
"height": 368,
|
width=640,
|
||||||
|
height=368,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
) == VideoAttachment._from_list({"node": data})
|
) == VideoAttachment._from_list({"node": data})
|
||||||
|
|
||||||
@@ -152,11 +156,11 @@ def test_graphql_to_attachment_image1():
|
|||||||
"width": 128,
|
"width": 128,
|
||||||
},
|
},
|
||||||
"large_preview": {
|
"large_preview": {
|
||||||
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/2.png",
|
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/1.png",
|
||||||
"height": 128,
|
"height": 128,
|
||||||
"width": 128,
|
"width": 128,
|
||||||
},
|
},
|
||||||
"thumbnail": {"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/p50x50/3.png"},
|
"thumbnail": {"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/p50x50/2.png"},
|
||||||
"photo_encodings": [],
|
"photo_encodings": [],
|
||||||
"legacy_attachment_id": "1234",
|
"legacy_attachment_id": "1234",
|
||||||
"original_dimensions": {"x": 128, "y": 128},
|
"original_dimensions": {"x": 128, "y": 128},
|
||||||
@@ -170,16 +174,13 @@ def test_graphql_to_attachment_image1():
|
|||||||
width=None,
|
width=None,
|
||||||
height=None,
|
height=None,
|
||||||
is_animated=False,
|
is_animated=False,
|
||||||
thumbnail_url="https://scontent-arn2-1.xx.fbcdn.net/v/p50x50/3.png",
|
previews={
|
||||||
preview={
|
fbchat.Image(url="https://scontent-arn2-1.xx.fbcdn.net/v/p50x50/2.png"),
|
||||||
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/1.png",
|
fbchat.Image(
|
||||||
"width": 128,
|
url="https://scontent-arn2-1.xx.fbcdn.net/v/1.png",
|
||||||
"height": 128,
|
width=128,
|
||||||
},
|
height=128,
|
||||||
large_preview={
|
),
|
||||||
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/2.png",
|
|
||||||
"width": 128,
|
|
||||||
"height": 128,
|
|
||||||
},
|
},
|
||||||
) == graphql_to_attachment(data)
|
) == graphql_to_attachment(data)
|
||||||
|
|
||||||
@@ -209,11 +210,8 @@ def test_graphql_to_attachment_image2():
|
|||||||
width=None,
|
width=None,
|
||||||
height=None,
|
height=None,
|
||||||
is_animated=True,
|
is_animated=True,
|
||||||
preview={"uri": "https://cdn.fbsbx.com/v/1.gif", "width": 128, "height": 128},
|
previews={
|
||||||
animated_preview={
|
fbchat.Image(url="https://cdn.fbsbx.com/v/1.gif", width=128, height=128)
|
||||||
"uri": "https://cdn.fbsbx.com/v/1.gif",
|
|
||||||
"width": 128,
|
|
||||||
"height": 128,
|
|
||||||
},
|
},
|
||||||
) == graphql_to_attachment(data)
|
) == graphql_to_attachment(data)
|
||||||
|
|
||||||
@@ -251,20 +249,22 @@ def test_graphql_to_attachment_video():
|
|||||||
height=None,
|
height=None,
|
||||||
duration=datetime.timedelta(seconds=6),
|
duration=datetime.timedelta(seconds=6),
|
||||||
preview_url="https://video-arn2-1.xx.fbcdn.net/v/video-4321.mp4",
|
preview_url="https://video-arn2-1.xx.fbcdn.net/v/video-4321.mp4",
|
||||||
small_image={
|
previews={
|
||||||
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/s168x128/1.jpg",
|
fbchat.Image(
|
||||||
"width": 168,
|
url="https://scontent-arn2-1.xx.fbcdn.net/v/s168x128/1.jpg",
|
||||||
"height": 96,
|
width=168,
|
||||||
},
|
height=96,
|
||||||
medium_image={
|
),
|
||||||
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/p261x260/3.jpg",
|
fbchat.Image(
|
||||||
"width": 452,
|
url="https://scontent-arn2-1.xx.fbcdn.net/v/p261x260/3.jpg",
|
||||||
"height": 260,
|
width=452,
|
||||||
},
|
height=260,
|
||||||
large_image={
|
),
|
||||||
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/2.jpg",
|
fbchat.Image(
|
||||||
"width": 640,
|
url="https://scontent-arn2-1.xx.fbcdn.net/v/2.jpg",
|
||||||
"height": 368,
|
width=640,
|
||||||
|
height=368,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
) == graphql_to_attachment(data)
|
) == graphql_to_attachment(data)
|
||||||
|
|
||||||
@@ -350,9 +350,11 @@ def test_graphql_to_subattachment_video():
|
|||||||
uid="1234",
|
uid="1234",
|
||||||
duration=datetime.timedelta(seconds=24, microseconds=469000),
|
duration=datetime.timedelta(seconds=24, microseconds=469000),
|
||||||
preview_url="https://video-arn2-1.xx.fbcdn.net/v/t42.9040-2/vid.mp4",
|
preview_url="https://video-arn2-1.xx.fbcdn.net/v/t42.9040-2/vid.mp4",
|
||||||
medium_image={
|
previews={
|
||||||
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t15.5256-10/p180x540/1.jpg",
|
fbchat.Image(
|
||||||
"width": 960,
|
url="https://scontent-arn2-1.xx.fbcdn.net/v/t15.5256-10/p180x540/1.jpg",
|
||||||
"height": 540,
|
width=960,
|
||||||
|
height=540,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
) == graphql_to_subattachment(data)
|
) == graphql_to_subattachment(data)
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
import datetime
|
import datetime
|
||||||
|
import fbchat
|
||||||
from fbchat._location import LocationAttachment, LiveLocationAttachment
|
from fbchat._location import LocationAttachment, LiveLocationAttachment
|
||||||
|
|
||||||
|
|
||||||
@@ -33,12 +34,17 @@ def test_location_attachment_from_graphql():
|
|||||||
"target": {"__typename": "MessageLocation"},
|
"target": {"__typename": "MessageLocation"},
|
||||||
"subattachments": [],
|
"subattachments": [],
|
||||||
}
|
}
|
||||||
expected = LocationAttachment(latitude=55.4, longitude=12.4322, uid=400828513928715)
|
assert LocationAttachment(
|
||||||
expected.image_url = "https://external-arn2-1.xx.fbcdn.net/static_map.php?v=1020&osm_provider=2&size=545x280&zoom=15&markers=55.40000000%2C12.43220000&language=en"
|
uid=400828513928715,
|
||||||
expected.image_width = 545
|
latitude=55.4,
|
||||||
expected.image_height = 280
|
longitude=12.4322,
|
||||||
expected.url = "https://l.facebook.com/l.php?u=https%3A%2F%2Fwww.bing.com%2Fmaps%2Fdefault.aspx%3Fv%3D2%26pc%3DFACEBK%26mid%3D8100%26where1%3D55.4%252C%2B12.4322%26FORM%3DFBKPL1%26mkt%3Den-GB&h=a&s=1"
|
image=fbchat.Image(
|
||||||
assert expected == LocationAttachment._from_graphql(data)
|
url="https://external-arn2-1.xx.fbcdn.net/static_map.php?v=1020&osm_provider=2&size=545x280&zoom=15&markers=55.40000000%2C12.43220000&language=en",
|
||||||
|
width=545,
|
||||||
|
height=280,
|
||||||
|
),
|
||||||
|
url="https://l.facebook.com/l.php?u=https%3A%2F%2Fwww.bing.com%2Fmaps%2Fdefault.aspx%3Fv%3D2%26pc%3DFACEBK%26mid%3D8100%26where1%3D55.4%252C%2B12.4322%26FORM%3DFBKPL1%26mkt%3Den-GB&h=a&s=1",
|
||||||
|
) == LocationAttachment._from_graphql(data)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip(reason="need to gather test data")
|
@pytest.mark.skip(reason="need to gather test data")
|
||||||
@@ -73,16 +79,15 @@ def test_live_location_from_graphql_expired():
|
|||||||
},
|
},
|
||||||
"subattachments": [],
|
"subattachments": [],
|
||||||
}
|
}
|
||||||
expected = LiveLocationAttachment(
|
assert LiveLocationAttachment(
|
||||||
uid=2254535444791641,
|
uid=2254535444791641,
|
||||||
name="Location-sharing ended",
|
name="Location-sharing ended",
|
||||||
expires_at=datetime.datetime(
|
expires_at=datetime.datetime(
|
||||||
2019, 1, 4, 18, 25, 45, tzinfo=datetime.timezone.utc
|
2019, 1, 4, 18, 25, 45, tzinfo=datetime.timezone.utc
|
||||||
),
|
),
|
||||||
is_expired=True,
|
is_expired=True,
|
||||||
)
|
url="https://www.facebook.com/",
|
||||||
expected.url = "https://www.facebook.com/"
|
) == LiveLocationAttachment._from_graphql(data)
|
||||||
assert expected == LiveLocationAttachment._from_graphql(data)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip(reason="need to gather test data")
|
@pytest.mark.skip(reason="need to gather test data")
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import fbchat
|
||||||
from fbchat._page import Page
|
from fbchat._page import Page
|
||||||
|
|
||||||
|
|
||||||
@@ -12,7 +13,7 @@ def test_page_from_graphql():
|
|||||||
}
|
}
|
||||||
assert Page(
|
assert Page(
|
||||||
uid="123456",
|
uid="123456",
|
||||||
photo="https://scontent-arn2-1.xx.fbcdn.net/v/...",
|
photo=fbchat.Image(url="https://scontent-arn2-1.xx.fbcdn.net/v/..."),
|
||||||
name="Some school",
|
name="Some school",
|
||||||
url="https://www.facebook.com/some-school/",
|
url="https://www.facebook.com/some-school/",
|
||||||
city=None,
|
city=None,
|
||||||
|
@@ -3,13 +3,16 @@ from fbchat._plan import GuestStatus, Plan
|
|||||||
|
|
||||||
|
|
||||||
def test_plan_properties():
|
def test_plan_properties():
|
||||||
plan = Plan(time=..., title=...)
|
plan = Plan(
|
||||||
plan.guests = {
|
time=...,
|
||||||
|
title=...,
|
||||||
|
guests={
|
||||||
"1234": GuestStatus.INVITED,
|
"1234": GuestStatus.INVITED,
|
||||||
"2345": GuestStatus.INVITED,
|
"2345": GuestStatus.INVITED,
|
||||||
"3456": GuestStatus.GOING,
|
"3456": GuestStatus.GOING,
|
||||||
"4567": GuestStatus.DECLINED,
|
"4567": GuestStatus.DECLINED,
|
||||||
}
|
},
|
||||||
|
)
|
||||||
assert set(plan.invited) == {"1234", "2345"}
|
assert set(plan.invited) == {"1234", "2345"}
|
||||||
assert plan.going == ["3456"]
|
assert plan.going == ["3456"]
|
||||||
assert plan.declined == ["4567"]
|
assert plan.declined == ["4567"]
|
||||||
@@ -32,19 +35,18 @@ def test_plan_from_pull():
|
|||||||
'{"guest_list_state":"GOING","node":{"id":"4567"}}]'
|
'{"guest_list_state":"GOING","node":{"id":"4567"}}]'
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
plan = Plan(
|
assert Plan(
|
||||||
|
uid="1111",
|
||||||
time=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc),
|
time=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc),
|
||||||
title="abc",
|
title="abc",
|
||||||
)
|
author_id="1234",
|
||||||
plan.uid = "1111"
|
guests={
|
||||||
plan.author_id = "1234"
|
|
||||||
plan.guests = {
|
|
||||||
"1234": GuestStatus.INVITED,
|
"1234": GuestStatus.INVITED,
|
||||||
"2356": GuestStatus.INVITED,
|
"2356": GuestStatus.INVITED,
|
||||||
"3456": GuestStatus.DECLINED,
|
"3456": GuestStatus.DECLINED,
|
||||||
"4567": GuestStatus.GOING,
|
"4567": GuestStatus.GOING,
|
||||||
}
|
},
|
||||||
assert plan == Plan._from_pull(data)
|
) == Plan._from_pull(data)
|
||||||
|
|
||||||
|
|
||||||
def test_plan_from_fetch():
|
def test_plan_from_fetch():
|
||||||
@@ -90,21 +92,20 @@ def test_plan_from_fetch():
|
|||||||
"4567": "GOING",
|
"4567": "GOING",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
plan = Plan(
|
assert Plan(
|
||||||
|
uid=1111,
|
||||||
time=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc),
|
time=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc),
|
||||||
title="abc",
|
title="abc",
|
||||||
location="",
|
location="",
|
||||||
location_id="",
|
location_id="",
|
||||||
)
|
author_id=1234,
|
||||||
plan.uid = 1111
|
guests={
|
||||||
plan.author_id = 1234
|
|
||||||
plan.guests = {
|
|
||||||
"1234": GuestStatus.INVITED,
|
"1234": GuestStatus.INVITED,
|
||||||
"2356": GuestStatus.INVITED,
|
"2356": GuestStatus.INVITED,
|
||||||
"3456": GuestStatus.DECLINED,
|
"3456": GuestStatus.DECLINED,
|
||||||
"4567": GuestStatus.GOING,
|
"4567": GuestStatus.GOING,
|
||||||
}
|
},
|
||||||
assert plan == Plan._from_fetch(data)
|
) == Plan._from_fetch(data)
|
||||||
|
|
||||||
|
|
||||||
def test_plan_from_graphql():
|
def test_plan_from_graphql():
|
||||||
@@ -133,18 +134,17 @@ def test_plan_from_graphql():
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
plan = Plan(
|
assert Plan(
|
||||||
time=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc),
|
time=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc),
|
||||||
title="abc",
|
title="abc",
|
||||||
location="",
|
location="",
|
||||||
location_id="",
|
location_id="",
|
||||||
)
|
uid="1111",
|
||||||
plan.uid = "1111"
|
author_id="1234",
|
||||||
plan.author_id = "1234"
|
guests={
|
||||||
plan.guests = {
|
|
||||||
"1234": GuestStatus.INVITED,
|
"1234": GuestStatus.INVITED,
|
||||||
"2356": GuestStatus.INVITED,
|
"2356": GuestStatus.INVITED,
|
||||||
"3456": GuestStatus.DECLINED,
|
"3456": GuestStatus.DECLINED,
|
||||||
"4567": GuestStatus.GOING,
|
"4567": GuestStatus.GOING,
|
||||||
}
|
},
|
||||||
assert plan == Plan._from_graphql(data)
|
) == Plan._from_graphql(data)
|
||||||
|
@@ -10,12 +10,12 @@ pytestmark = pytest.mark.online
|
|||||||
@pytest.fixture(
|
@pytest.fixture(
|
||||||
scope="module",
|
scope="module",
|
||||||
params=[
|
params=[
|
||||||
Plan(int(time()) + 100, random_hex()),
|
Plan(time=int(time()) + 100, title=random_hex()),
|
||||||
pytest.param(
|
pytest.param(
|
||||||
Plan(int(time()), random_hex()),
|
Plan(time=int(time()), title=random_hex()),
|
||||||
marks=[pytest.mark.xfail(raises=FBchatFacebookError)],
|
marks=[pytest.mark.xfail(raises=FBchatFacebookError)],
|
||||||
),
|
),
|
||||||
pytest.param(Plan(0, None), marks=[pytest.mark.xfail()]),
|
pytest.param(Plan(time=0, title=None), marks=[pytest.mark.xfail()]),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def plan_data(request, client, user, thread, catch_event, compare):
|
def plan_data(request, client, user, thread, catch_event, compare):
|
||||||
|
@@ -13,26 +13,26 @@ pytestmark = pytest.mark.online
|
|||||||
Poll(
|
Poll(
|
||||||
title=random_hex(),
|
title=random_hex(),
|
||||||
options=[
|
options=[
|
||||||
PollOption(random_hex(), vote=True),
|
PollOption(text=random_hex(), vote=True),
|
||||||
PollOption(random_hex(), vote=True),
|
PollOption(text=random_hex(), vote=True),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Poll(
|
Poll(
|
||||||
title=random_hex(),
|
title=random_hex(),
|
||||||
options=[
|
options=[
|
||||||
PollOption(random_hex(), vote=False),
|
PollOption(text=random_hex(), vote=False),
|
||||||
PollOption(random_hex(), vote=False),
|
PollOption(text=random_hex(), vote=False),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Poll(
|
Poll(
|
||||||
title=random_hex(),
|
title=random_hex(),
|
||||||
options=[
|
options=[
|
||||||
PollOption(random_hex(), vote=True),
|
PollOption(text=random_hex(), vote=True),
|
||||||
PollOption(random_hex(), vote=True),
|
PollOption(text=random_hex(), vote=True),
|
||||||
PollOption(random_hex(), vote=False),
|
PollOption(text=random_hex(), vote=False),
|
||||||
PollOption(random_hex(), vote=False),
|
PollOption(text=random_hex(), vote=False),
|
||||||
PollOption(random_hex()),
|
PollOption(text=random_hex()),
|
||||||
PollOption(random_hex()),
|
PollOption(text=random_hex()),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
pytest.param(
|
pytest.param(
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
import fbchat
|
||||||
from fbchat._sticker import Sticker
|
from fbchat._sticker import Sticker
|
||||||
|
|
||||||
|
|
||||||
@@ -15,14 +16,14 @@ def test_from_graphql_normal():
|
|||||||
uid="369239383222810",
|
uid="369239383222810",
|
||||||
pack="227877430692340",
|
pack="227877430692340",
|
||||||
is_animated=False,
|
is_animated=False,
|
||||||
medium_sprite_image=None,
|
frames_per_row=1,
|
||||||
large_sprite_image=None,
|
frames_per_col=1,
|
||||||
frames_per_row=None,
|
frame_rate=83,
|
||||||
frames_per_col=None,
|
image=fbchat.Image(
|
||||||
frame_rate=None,
|
|
||||||
url="https://scontent-arn2-1.xx.fbcdn.net/v/redacted.png",
|
url="https://scontent-arn2-1.xx.fbcdn.net/v/redacted.png",
|
||||||
width=274,
|
width=274,
|
||||||
height=274,
|
height=274,
|
||||||
|
),
|
||||||
label="Like, thumbs up",
|
label="Like, thumbs up",
|
||||||
) == Sticker._from_graphql(
|
) == Sticker._from_graphql(
|
||||||
{
|
{
|
||||||
@@ -54,9 +55,11 @@ def test_from_graphql_animated():
|
|||||||
frames_per_row=2,
|
frames_per_row=2,
|
||||||
frames_per_col=2,
|
frames_per_col=2,
|
||||||
frame_rate=142,
|
frame_rate=142,
|
||||||
|
image=fbchat.Image(
|
||||||
url="https://scontent-arn2-1.fbcdn.net/v/redacted1.png",
|
url="https://scontent-arn2-1.fbcdn.net/v/redacted1.png",
|
||||||
width=240,
|
width=240,
|
||||||
height=293,
|
height=293,
|
||||||
|
),
|
||||||
label="Love, cat with heart",
|
label="Love, cat with heart",
|
||||||
) == Sticker._from_graphql(
|
) == Sticker._from_graphql(
|
||||||
{
|
{
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
import datetime
|
import datetime
|
||||||
|
import fbchat
|
||||||
from fbchat._user import User, ActiveStatus
|
from fbchat._user import User, ActiveStatus
|
||||||
|
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@ def test_user_from_graphql():
|
|||||||
}
|
}
|
||||||
assert User(
|
assert User(
|
||||||
uid="1234",
|
uid="1234",
|
||||||
photo="https://scontent-arn2-1.xx.fbcdn.net/v/...",
|
photo=fbchat.Image(url="https://scontent-arn2-1.xx.fbcdn.net/v/..."),
|
||||||
name="Abc Def Ghi",
|
name="Abc Def Ghi",
|
||||||
url="https://www.facebook.com/profile.php?id=1234",
|
url="https://www.facebook.com/profile.php?id=1234",
|
||||||
first_name="Abc",
|
first_name="Abc",
|
||||||
@@ -137,7 +138,7 @@ def test_user_from_thread_fetch():
|
|||||||
}
|
}
|
||||||
assert User(
|
assert User(
|
||||||
uid="1234",
|
uid="1234",
|
||||||
photo="https://scontent-arn2-1.xx.fbcdn.net/v/...",
|
photo=fbchat.Image(url="https://scontent-arn2-1.xx.fbcdn.net/v/..."),
|
||||||
name="Abc Def Ghi",
|
name="Abc Def Ghi",
|
||||||
last_active=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc),
|
last_active=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc),
|
||||||
message_count=1111,
|
message_count=1111,
|
||||||
@@ -175,7 +176,7 @@ def test_user_from_all_fetch():
|
|||||||
}
|
}
|
||||||
assert User(
|
assert User(
|
||||||
uid="1234",
|
uid="1234",
|
||||||
photo="https://scontent-arn2-1.xx.fbcdn.net/v/...",
|
photo=fbchat.Image(url="https://scontent-arn2-1.xx.fbcdn.net/v/..."),
|
||||||
name="Abc Def Ghi",
|
name="Abc Def Ghi",
|
||||||
url="https://www.facebook.com/profile.php?id=1234",
|
url="https://www.facebook.com/profile.php?id=1234",
|
||||||
first_name="Abc",
|
first_name="Abc",
|
||||||
|
@@ -22,9 +22,13 @@ EMOJI_LIST = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
STICKER_LIST = [
|
STICKER_LIST = [
|
||||||
Sticker("767334476626295"),
|
Sticker(uid="767334476626295"),
|
||||||
pytest.param(Sticker("0"), marks=[pytest.mark.xfail(raises=FBchatFacebookError)]),
|
pytest.param(
|
||||||
pytest.param(Sticker(None), marks=[pytest.mark.xfail(raises=FBchatFacebookError)]),
|
Sticker(uid="0"), marks=[pytest.mark.xfail(raises=FBchatFacebookError)]
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
Sticker(uid=None), marks=[pytest.mark.xfail(raises=FBchatFacebookError)]
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
TEXT_LIST = [
|
TEXT_LIST = [
|
||||||
|
Reference in New Issue
Block a user