Merge pull request #499 from carpedm20/session-in-models

Add ThreadABC helper, and move a bunch of methods out of Client
This commit is contained in:
Mads Marquart
2020-01-09 11:33:45 +01:00
26 changed files with 1011 additions and 1642 deletions

View File

@@ -17,8 +17,6 @@ Threads
------- -------
.. autoclass:: Thread() .. autoclass:: Thread()
.. autoclass:: ThreadType(Enum)
:undoc-members:
.. autoclass:: Page() .. autoclass:: Page()
.. autoclass:: User() .. autoclass:: User()
.. autoclass:: Group() .. autoclass:: Group()

View File

@@ -44,11 +44,7 @@ When you're done using the client, and want to securely logout, use `Client.logo
Threads 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 (`Group`) or a single Facebook user (`User`).
`ThreadType` is an enumerator with two values: ``USER`` and ``GROUP``.
These will specify whether the thread is a single user chat or a group chat.
This is required for many of ``fbchat``'s functions, since Facebook differentiates between these two internally
Searching for group chats and finding their ID can be done via. `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. `Client.search_for_users`. See :ref:`intro_fetching` and searching for users is possible via. `Client.search_for_users`. See :ref:`intro_fetching`
@@ -68,13 +64,14 @@ The same method can be applied to some user accounts, though if they've set a cu
Here's an snippet showing the usage of thread IDs and thread types, where ``<user id>`` and ``<group id>`` Here's an snippet showing the usage of thread IDs and thread types, where ``<user id>`` and ``<group id>``
corresponds to the ID of a single user, and the ID of a group respectively:: 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) user.send(Message(text='<message>'))
client.send(Message(text='<message>'), thread_id='<group id>', thread_type=ThreadType.GROUP) group.send(Message(text='<message>'))
Some functions (e.g. `Client.change_thread_color`) don't require a thread type, so in these cases you just provide the thread ID:: Some functions 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>') thread = fbchat.Thread(session=session, id="<user-or-group-id>")
client.change_thread_color(ThreadColor.MESSENGER_BLUE, thread_id='<group id>') thread.set_color(ThreadColor.BILOBA_FLOWER)
thread.set_color(ThreadColor.MESSENGER_BLUE)
.. _intro_message_ids: .. _intro_message_ids:
@@ -89,7 +86,7 @@ Some of ``fbchat``'s functions require these ID's, like `Client.react_to_message
and some of then provide this ID, like `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 = thread.send(Message(text='message'))
client.react_to_message(message_id, MessageReaction.LOVE) client.react_to_message(message_id, MessageReaction.LOVE)
@@ -106,7 +103,8 @@ like adding users to and removing users from a group chat, logically only works
The simplest way of using ``fbchat`` is to send a message. The simplest way of using ``fbchat`` is to send a message.
The following snippet will, as you've probably already figured out, send the message ``test message`` to your account:: The following snippet will, as you've probably already figured out, send the message ``test message`` to your account::
message_id = client.send(Message(text='test message'), thread_id=session.user_id, thread_type=ThreadType.USER) user = User(session=session, id=session.user_id)
message_id = user.send(Message(text='test message'))
You can see a full example showing all the possible thread interactions with ``fbchat`` by going to :ref:`examples` You can see a full example showing all the possible thread interactions with ``fbchat`` by going to :ref:`examples`
@@ -173,7 +171,7 @@ meaning it will simply print information to the console when an event happens
The event actions can be changed by subclassing the `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, ts, metadata, msg, **kwargs):
# Do something with message_object here # Do something with message_object here
pass pass
@@ -182,7 +180,7 @@ The event actions can be changed by subclassing the `Client`, and then overwriti
**Notice:** The following snippet is as equally valid as the previous one:: **Notice:** The following snippet is as equally valid as the previous one::
class CustomClient(Client): class CustomClient(Client):
def on_message(self, message_object, author_id, thread_id, thread_type, **kwargs): def on_message(self, message_object, author_id, thread, **kwargs):
# Do something with message_object here # Do something with message_object here
pass pass

View File

@@ -5,15 +5,11 @@ session = fbchat.Session.login("<email>", "<password>")
print("Own id: {}".format(sesion.user_id)) print("Own id: {}".format(sesion.user_id))
# Create helper client class # Create helper User class
client = fbchat.Client(session) user = fbchat.Thread(session=session, id=session.user_id)
# Send a message to yourself # Send a message to yourself
client.send( user.send(fbchat.Message(text="Hi me!"))
fbchat.Message(text="Hi me!"),
thread_id=session.user_id,
thread_type=fbchat.ThreadType.USER,
)
# Log the user out # Log the user out
session.logout() session.logout()

View File

@@ -2,15 +2,15 @@ import fbchat
# Subclass fbchat.Client and override required methods # Subclass fbchat.Client and override required methods
class EchoBot(fbchat.Client): class EchoBot(fbchat.Client):
def on_message(self, author_id, message_object, thread_id, thread_type, **kwargs): def on_message(self, author_id, message_object, thread, **kwargs):
self.mark_as_delivered(thread_id, message_object.id) self.mark_as_delivered(thread.id, message_object.id)
self.mark_as_read(thread_id) self.mark_as_read(thread.id)
print("{} from {} in {}".format(message_object, thread_id, thread_type.name)) print("{} from {} in {}".format(message_object, thread))
# If you're not the author, echo # If you're not the author, echo
if author_id != self.session.user_id: if author_id != self.session.user_id:
self.send(message_object, thread_id=thread_id, thread_type=thread_type) thread.send(message_object)
session = fbchat.Session.login("<email>", "<password>") session = fbchat.Session.login("<email>", "<password>")

View File

@@ -39,8 +39,13 @@ threads += client.fetch_thread_list(offset=20, limit=10)
print("Threads: {}".format(threads)) print("Threads: {}".format(threads))
# If we have a thread id, we can use `fetch_thread_info` to fetch a `Thread` object
thread = client.fetch_thread_info("<thread id>")["<thread id>"]
print("thread's name: {}".format(thread.name))
# Gets the last 10 messages sent to the thread # Gets the last 10 messages sent to the thread
messages = client.fetch_thread_messages(thread_id="<thread id>", limit=10) messages = thread.fetch_messages(limit=10)
# Since the message come in reversed order, reverse them # Since the message come in reversed order, reverse them
messages.reverse() messages.reverse()
@@ -49,22 +54,15 @@ for message in messages:
print(message.text) print(message.text)
# If we have a thread id, we can use `fetch_thread_info` to fetch a `Thread` object
thread = client.fetch_thread_info("<thread id>")["<thread id>"]
print("thread's name: {}".format(thread.name))
print("thread's type: {}".format(thread.type))
# `search_for_threads` searches works like `search_for_users`, but gives us a list of threads instead # `search_for_threads` searches works like `search_for_users`, but gives us a list of threads instead
thread = client.search_for_threads("<name of thread>")[0] thread = client.search_for_threads("<name of thread>")[0]
print("thread's name: {}".format(thread.name)) print("thread's name: {}".format(thread.name))
print("thread's type: {}".format(thread.type))
# Here should be an example of `getUnread` # Here should be an example of `getUnread`
# Print image url for 20 last images from thread. # Print image url for 20 last images from thread.
images = client.fetch_thread_images("<thread id>") images = thread.fetch_images()
for image in itertools.islice(image, 20): for image in itertools.islice(image, 20):
print(image.large_preview_url) print(image.large_preview_url)

View File

@@ -4,94 +4,64 @@ session = fbchat.Session.login("<email>", "<password>")
client = fbchat.Client(session) client = fbchat.Client(session)
thread_id = "1234567890" thread = User(session=session, id=session.user_id)
thread_type = fbchat.ThreadType.GROUP # thread = User(session=session, id="0987654321")
# thread = Group(session=session, id="1234567890")
# Will send a message to the thread # Will send a message to the thread
client.send( thread.send(fbchat.Message(text="<message>"))
fbchat.Message(text="<message>"), thread_id=thread_id, thread_type=thread_type
)
# Will send the default `like` emoji # Will send the default `like` emoji
client.send( thread.send(fbchat.Message(emoji_size=fbchat.EmojiSize.LARGE))
fbchat.Message(emoji_size=fbchat.EmojiSize.LARGE),
thread_id=thread_id,
thread_type=thread_type,
)
# Will send the emoji `👍` # Will send the emoji `👍`
client.send( thread.send(fbchat.Message(text="👍", emoji_size=fbchat.EmojiSize.LARGE))
fbchat.Message(text="👍", emoji_size=fbchat.EmojiSize.LARGE),
thread_id=thread_id,
thread_type=thread_type,
)
# Will send the sticker with ID `767334476626295` # Will send the sticker with ID `767334476626295`
client.send( thread.send(fbchat.Message(sticker=fbchat.Sticker("767334476626295")))
fbchat.Message(sticker=fbchat.Sticker("767334476626295")),
thread_id=thread_id,
thread_type=thread_type,
)
# Will send a message with a mention # Will send a message with a mention
client.send( thread.send(
fbchat.Message( fbchat.Message(
text="This is a @mention", text="This is a @mention",
mentions=[fbchat.Mention(thread_id, offset=10, length=8)], mentions=[fbchat.Mention(thread_id, offset=10, length=8)],
), )
thread_id=thread_id,
thread_type=thread_type,
) )
# Will send the image located at `<image path>` # Will send the image located at `<image path>`
client.send_local_image( thread.send_local_image(
"<image path>", "<image path>", message=fbchat.Message(text="This is a local image")
message=fbchat.Message(text="This is a local image"),
thread_id=thread_id,
thread_type=thread_type,
) )
# Will download the image at the URL `<image url>`, and then send it # Will download the image at the URL `<image url>`, and then send it
client.send_remote_image( thread.send_remote_image(
"<image url>", "<image url>", message=fbchat.Message(text="This is a remote image")
message=fbchat.Message(text="This is a remote image"),
thread_id=thread_id,
thread_type=thread_type,
) )
# Only do these actions if the thread is a group # Only do these actions if the thread is a group
if thread_type == fbchat.ThreadType.GROUP: if isinstance(thread, fbchat.Group):
# Will remove the user with ID `<user id>` from the thread # Will remove the user with ID `<user id>` from the group
client.remove_user_from_group("<user id>", thread_id=thread_id) thread.remove_participant("<user id>")
# Will add the users with IDs `<1st user id>`, `<2nd user id>` and `<3th user id>` to the group
# Will add the user with ID `<user id>` to the thread thread.add_participants(["<1st user id>", "<2nd user id>", "<3rd user id>"])
client.add_users_to_group("<user id>", thread_id=thread_id) # Will change the title of the group to `<title>`
thread.change_title("<title>")
# Will add the users with IDs `<1st user id>`, `<2nd user id>` and `<3th user id>` to the thread
client.add_users_to_group(
["<1st user id>", "<2nd user id>", "<3rd user id>"], thread_id=thread_id
)
# Will change the nickname of the user `<user_id>` to `<new nickname>` # Will change the nickname of the user `<user_id>` to `<new nickname>`
client.change_nickname( thread.set_nickname(fbchat.User(session=session, id="<user id>"), "<new nickname>")
"<new nickname>", "<user id>", thread_id=thread_id, thread_type=thread_type
)
# Will change the title of the thread to `<title>` # Will set the typing status of the thread
client.change_thread_title("<title>", thread_id=thread_id, thread_type=thread_type) thread.start_typing()
# Will set the typing status of the thread to `TYPING`
client.set_typing_status(
fbchat.TypingStatus.TYPING, thread_id=thread_id, thread_type=thread_type
)
# Will change the thread color to `MESSENGER_BLUE` # Will change the thread color to `MESSENGER_BLUE`
client.change_thread_color(fbchat.ThreadColor.MESSENGER_BLUE, thread_id=thread_id) thread.set_color(fbchat.ThreadColor.MESSENGER_BLUE)
# Will change the thread emoji to `👍` # Will change the thread emoji to `👍`
client.change_thread_emoji("👍", thread_id=thread_id) thread.set_emoji("👍")
# Will react to a message with a 😍 emoji # message = fbchat.Message(session=session, id="<message id>")
client.react_to_message("<message id>", fbchat.MessageReaction.LOVE) #
# # Will react to a message with a 😍 emoji
# message.react(fbchat.MessageReaction.LOVE)

View File

@@ -16,50 +16,48 @@ old_nicknames = {
class KeepBot(fbchat.Client): class KeepBot(fbchat.Client):
def on_color_change(self, author_id, new_color, thread_id, thread_type, **kwargs): def on_color_change(self, author_id, new_color, thread, **kwargs):
if old_thread_id == thread_id and old_color != new_color: if old_thread_id == thread.id and old_color != new_color:
print( print(
"{} changed the thread color. It will be changed back".format(author_id) "{} changed the thread color. It will be changed back".format(author_id)
) )
self.change_thread_color(old_color, thread_id=thread_id) thread.set_color(old_color)
def on_emoji_change(self, author_id, new_emoji, thread_id, thread_type, **kwargs): def on_emoji_change(self, author_id, new_emoji, thread, **kwargs):
if old_thread_id == thread_id and new_emoji != old_emoji: if old_thread_id == thread.id and new_emoji != old_emoji:
print( print(
"{} changed the thread emoji. It will be changed back".format(author_id) "{} changed the thread emoji. It will be changed back".format(author_id)
) )
self.change_thread_emoji(old_emoji, thread_id=thread_id) thread.set_emoji(old_emoji)
def on_people_added(self, added_ids, author_id, thread_id, **kwargs): def on_people_added(self, added_ids, author_id, thread, **kwargs):
if old_thread_id == thread_id and author_id != self.session.user_id: if old_thread_id == thread.id and author_id != self.session.user_id:
print("{} got added. They will be removed".format(added_ids)) print("{} got added. They will be removed".format(added_ids))
for added_id in added_ids: for added_id in added_ids:
self.remove_user_from_group(added_id, thread_id=thread_id) thread.remove_participant(added_id)
def on_person_removed(self, removed_id, author_id, thread_id, **kwargs): def on_person_removed(self, removed_id, author_id, thread, **kwargs):
# No point in trying to add ourself # No point in trying to add ourself
if ( if (
old_thread_id == thread_id old_thread_id == thread.id
and removed_id != self.session.user_id and removed_id != self.session.user_id
and author_id != self.session.user_id and author_id != self.session.user_id
): ):
print("{} got removed. They will be re-added".format(removed_id)) print("{} got removed. They will be re-added".format(removed_id))
self.add_users_to_group(removed_id, thread_id=thread_id) thread.add_participants(removed_id)
def on_title_change(self, author_id, new_title, thread_id, thread_type, **kwargs): def on_title_change(self, author_id, new_title, thread, **kwargs):
if old_thread_id == thread_id and old_title != new_title: if old_thread_id == thread.id and old_title != new_title:
print( print(
"{} changed the thread title. It will be changed back".format(author_id) "{} changed the thread title. It will be changed back".format(author_id)
) )
self.change_thread_title( thread.set_title(old_title)
old_title, thread_id=thread_id, thread_type=thread_type
)
def on_nickname_change( def on_nickname_change(
self, author_id, changed_for, new_nickname, thread_id, thread_type, **kwargs self, author_id, changed_for, new_nickname, thread, **kwargs
): ):
if ( if (
old_thread_id == thread_id old_thread_id == thread.id
and changed_for in old_nicknames and changed_for in old_nicknames
and old_nicknames[changed_for] != new_nickname and old_nicknames[changed_for] != new_nickname
): ):
@@ -68,11 +66,8 @@ class KeepBot(fbchat.Client):
author_id, changed_for author_id, changed_for
) )
) )
self.change_nickname( thread.set_nickname(
old_nicknames[changed_for], changed_for, old_nicknames[changed_for],
changed_for,
thread_id=thread_id,
thread_type=thread_type,
) )

View File

@@ -2,21 +2,17 @@ import fbchat
class RemoveBot(fbchat.Client): class RemoveBot(fbchat.Client):
def on_message(self, author_id, message_object, thread_id, thread_type, **kwargs): def on_message(self, author_id, message_object, thread, **kwargs):
# We can only kick people from group chats, so no need to try if it's a user chat # We can only kick people from group chats, so no need to try if it's a user chat
if ( if message_object.text == "Remove me!" and isinstance(thread, fbchat.Group):
message_object.text == "Remove me!" print("{} will be removed from {}".format(author_id, thread))
and thread_type == fbchat.ThreadType.GROUP thread.remove_participant(author_id)
):
print("{} will be removed from {}".format(author_id, thread_id))
self.remove_user_from_group(author_id, thread_id=thread_id)
else: else:
# Sends the data to the inherited on_message, so that we can still see when a message is recieved # Sends the data to the inherited on_message, so that we can still see when a message is recieved
super(RemoveBot, self).on_message( super(RemoveBot, self).on_message(
author_id=author_id, author_id=author_id,
message_object=message_object, message_object=message_object,
thread_id=thread_id, thread=thread,
thread_type=thread_type,
**kwargs, **kwargs,
) )

View File

@@ -14,7 +14,7 @@ from . import _core, _util
from ._core import Image from ._core import Image
from ._exception import FBchatException, FBchatFacebookError from ._exception import FBchatException, FBchatFacebookError
from ._session import Session from ._session import Session
from ._thread import ThreadType, ThreadLocation, ThreadColor, Thread from ._thread import ThreadLocation, ThreadColor, ThreadABC, Thread
from ._user import TypingStatus, User, ActiveStatus from ._user import TypingStatus, User, ActiveStatus
from ._group import Group from ._group import Group
from ._page import Page from ._page import Page

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,17 @@
import attr import attr
from ._core import attrs_default, Image from ._core import attrs_default, Image
from . import _util, _plan from . import _util, _session, _plan, _thread, _user
from ._thread import ThreadType, Thread from typing import Sequence, Iterable
@attrs_default @attrs_default
class Group(Thread): class Group(_thread.ThreadABC):
"""Represents a Facebook group. Inherits `Thread`.""" """Represents a Facebook group. Implements `ThreadABC`."""
type = ThreadType.GROUP
#: The session to use when making requests.
session = attr.ib(type=_session.Session)
#: The group's unique identifier.
id = attr.ib(converter=str)
#: The group's picture #: The group's picture
photo = attr.ib(None) photo = attr.ib(None)
#: The name of the group #: The name of the group
@@ -37,8 +39,120 @@ class Group(Thread):
# Link for joining group # Link for joining group
join_link = attr.ib(None) join_link = attr.ib(None)
def add_participants(self, user_ids: Iterable[str]):
"""Add users to the group.
Args:
user_ids: One or more user IDs to add
"""
data = self._to_send_data()
data["action_type"] = "ma-type:log-message"
data["log_message_type"] = "log:subscribe"
for i, user_id in enumerate(user_ids):
if user_id == self.session.user_id:
raise ValueError(
"Error when adding users: Cannot add self to group thread"
)
else:
data[
"log_message_data[added_participants][{}]".format(i)
] = "fbid:{}".format(user_id)
return self.session._do_send_request(data)
def remove_participant(self, user_id: str):
"""Remove user from the group.
Args:
user_id: User ID to remove
"""
data = {"uid": user_id, "tid": self.id}
j = self._payload_post("/chat/remove_participants/", data)
def _admin_status(self, user_ids: Iterable[str], status: bool):
data = {"add": admin, "thread_fbid": self.id}
for i, user_id in enumerate(user_ids):
data["admin_ids[{}]".format(i)] = str(user_id)
j = self.session._payload_post("/messaging/save_admins/?dpr=1", data)
def add_admins(self, user_ids: Iterable[str]):
"""Set specified users as group admins.
Args:
user_ids: One or more user IDs to set admin
"""
self._admin_status(user_ids, True)
def remove_admins(self, user_ids: Iterable[str]):
"""Remove admin status from specified users.
Args:
user_ids: One or more user IDs to remove admin
"""
self._admin_status(user_ids, False)
def set_title(self, title: str):
"""Change title of the group.
Args:
title: New title
"""
data = {"thread_name": title, "thread_id": self.id}
j = self.session._payload_post("/messaging/set_thread_name/?dpr=1", data)
def set_image(self, image_id: str):
"""Change the group image from an image id.
Args:
image_id: ID of uploaded image
"""
data = {"thread_image_id": image_id, "thread_id": self.id}
j = self.session._payload_post("/messaging/set_thread_image/?dpr=1", data)
def set_approval_mode(self, require_admin_approval: bool):
"""Change the group's approval mode.
Args:
require_admin_approval: True or False
"""
data = {"set_mode": int(require_admin_approval), "thread_fbid": thread_id}
j = self.session._payload_post("/messaging/set_approval_mode/?dpr=1", data)
def _users_approval(self, user_ids: Iterable[str], approve: bool):
data = {
"client_mutation_id": "0",
"actor_id": self.session.user_id,
"thread_fbid": self.id,
"user_ids": list(user_ids),
"response": "ACCEPT" if approve else "DENY",
"surface": "ADMIN_MODEL_APPROVAL_CENTER",
}
(j,) = self.session._graphql_requests(
_graphql.from_doc_id("1574519202665847", {"data": data})
)
def accept_users(self, user_ids: Iterable[str]):
"""Accept users to the group from the group's approval.
Args:
user_ids: One or more user IDs to accept
"""
self._users_approval(user_ids, True)
def deny_users(self, user_ids: Iterable[str]):
"""Deny users from joining the group.
Args:
user_ids: One or more user IDs to deny
"""
self._users_approval(user_ids, False)
@classmethod @classmethod
def _from_graphql(cls, data): def _from_graphql(cls, session, data):
if data.get("image") is None: if data.get("image") is None:
data["image"] = {} data["image"] = {}
c_info = cls._parse_customization_info(data) c_info = cls._parse_customization_info(data)
@@ -52,6 +166,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(
session=session,
id=data["thread_key"]["thread_fbid"], id=data["thread_key"]["thread_fbid"],
participants=set( participants=set(
[ [
@@ -82,3 +197,32 @@ class Group(Thread):
def _to_send_data(self): def _to_send_data(self):
return {"thread_fbid": self.id} return {"thread_fbid": self.id}
@attrs_default
class NewGroup(_thread.ThreadABC):
"""Helper class to create new groups.
TODO: Complete this!
Construct this class with the desired users, and call a method like `wave`, to...
"""
#: The session to use when making requests.
session = attr.ib(type=_session.Session)
#: The users that should be added to the group.
_users = attr.ib(type=Sequence[_user.User])
@property
def id(self):
raise NotImplementedError(
"The method you called is not supported on NewGroup objects."
" Please use the supported methods to create the group, before attempting"
" to call the method."
)
def _to_send_data(self) -> dict:
return {
"specific_to_list[{}]".format(i): "fbid:{}".format(user.id)
for i, user in enumerate(self._users)
}

View File

@@ -2,7 +2,8 @@ import attr
import json import json
from string import Formatter from string import Formatter
from ._core import log, attrs_default, Enum from ._core import log, attrs_default, Enum
from . import _util, _attachment, _location, _file, _quick_reply, _sticker from . import _util, _session, _attachment, _location, _file, _quick_reply, _sticker
from typing import Optional
class EmojiSize(Enum): class EmojiSize(Enum):
@@ -78,14 +79,18 @@ class Mention:
class Message: class Message:
"""Represents a Facebook message.""" """Represents a Facebook message."""
# TODO: Make these fields required!
#: The session to use when making requests.
session = attr.ib(None, type=_session.Session)
#: The message ID
id = attr.ib(None, converter=str)
#: The actual message #: The actual message
text = attr.ib(None) text = attr.ib(None)
#: A list of `Mention` objects #: A list of `Mention` objects
mentions = attr.ib(factory=list) mentions = attr.ib(factory=list)
#: A `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
id = attr.ib(None)
#: ID of the sender #: ID of the sender
author = attr.ib(None) author = attr.ib(None)
#: Datetime of when the message was sent #: Datetime of when the message was sent
@@ -111,6 +116,38 @@ class Message:
#: Whether the message was forwarded #: Whether the message was forwarded
forwarded = attr.ib(False) forwarded = attr.ib(False)
def unsend(self):
"""Unsend the message (removes it for everyone)."""
data = {"message_id": self.id}
j = self.session._payload_post("/messaging/unsend_message/?dpr=1", data)
def react(self, reaction: Optional[MessageReaction]):
"""React to the message, or removes reaction.
Args:
reaction: Reaction emoji to use, if None removes reaction
"""
data = {
"action": "ADD_REACTION" if reaction else "REMOVE_REACTION",
"client_mutation_id": "1",
"actor_id": self.session.user_id,
"message_id": self.id,
"reaction": reaction.value if reaction else None,
}
data = {"doc_id": 1491398900900362, "variables": json.dumps({"data": data})}
j = self.session._payload_post("/webgraphql/mutation", data)
_util.handle_graphql_errors(j)
@classmethod
def from_fetch(cls, thread, message_id: str) -> "Message":
"""Fetch `Message` object from the given message id.
Args:
message_id: Message ID to fetch from
"""
message_info = thread._forced_fetch(message_id).get("message")
return Message._from_graphql(thread.session, message_info)
@classmethod @classmethod
def format_mentions(cls, text, *args, **kwargs): def format_mentions(cls, text, *args, **kwargs):
"""Like `str.format`, but takes tuples with a thread id and text instead. """Like `str.format`, but takes tuples with a thread id and text instead.
@@ -224,7 +261,7 @@ class Message:
return [] return []
@classmethod @classmethod
def _from_graphql(cls, data, read_receipts=None): def _from_graphql(cls, session, 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:
@@ -250,12 +287,13 @@ class Message:
replied_to = cls._from_graphql(data["replied_to_message"]["message"]) replied_to = cls._from_graphql(data["replied_to_message"]["message"])
return cls( return cls(
session=session,
id=str(data["message_id"]),
text=data["message"].get("text"), text=data["message"].get("text"),
mentions=[ mentions=[
Mention._from_range(m) for m in data["message"].get("ranges") or () Mention._from_range(m) for m in data["message"].get("ranges") or ()
], ],
emoji_size=EmojiSize._from_tags(tags), emoji_size=EmojiSize._from_tags(tags),
id=str(data["message_id"]),
author=str(data["message_sender"]["id"]), author=str(data["message_sender"]["id"]),
created_at=created_at, created_at=created_at,
is_read=not data["unread"] if data.get("unread") is not None else None, is_read=not data["unread"] if data.get("unread") is not None else None,
@@ -278,7 +316,7 @@ class Message:
) )
@classmethod @classmethod
def _from_reply(cls, data, replied_to=None): def _from_reply(cls, session, data, replied_to=None):
tags = data["messageMetadata"].get("tags") tags = data["messageMetadata"].get("tags")
metadata = data.get("messageMetadata", {}) metadata = data.get("messageMetadata", {})
@@ -305,13 +343,14 @@ class Message:
) )
return cls( return cls(
session=session,
id=metadata.get("messageId"),
text=data.get("body"), text=data.get("body"),
mentions=[ mentions=[
Mention._from_prng(m) Mention._from_prng(m)
for m in _util.parse_json(data.get("data", {}).get("prng", "[]")) for m in _util.parse_json(data.get("data", {}).get("prng", "[]"))
], ],
emoji_size=EmojiSize._from_tags(tags), emoji_size=EmojiSize._from_tags(tags),
id=metadata.get("messageId"),
author=str(metadata.get("actorFbId")), author=str(metadata.get("actorFbId")),
created_at=_util.millis_to_datetime(metadata.get("timestamp")), created_at=_util.millis_to_datetime(metadata.get("timestamp")),
sticker=sticker, sticker=sticker,
@@ -324,7 +363,9 @@ class Message:
) )
@classmethod @classmethod
def _from_pull(cls, data, mid=None, tags=None, author=None, created_at=None): def _from_pull(
cls, session, data, mid=None, tags=None, author=None, created_at=None
):
mentions = [] mentions = []
if data.get("data") and data["data"].get("prng"): if data.get("data") and data["data"].get("prng"):
try: try:
@@ -371,10 +412,11 @@ class Message:
) )
return cls( return cls(
session=session,
id=mid,
text=data.get("body"), text=data.get("body"),
mentions=mentions, mentions=mentions,
emoji_size=EmojiSize._from_tags(tags), emoji_size=EmojiSize._from_tags(tags),
id=mid,
author=author, author=author,
created_at=created_at, created_at=created_at,
sticker=sticker, sticker=sticker,

View File

@@ -1,15 +1,16 @@
import attr import attr
from ._core import attrs_default, Image from ._core import attrs_default, Image
from . import _plan from . import _session, _plan, _thread
from ._thread import ThreadType, Thread
@attrs_default @attrs_default
class Page(Thread): class Page(_thread.ThreadABC):
"""Represents a Facebook page. Inherits `Thread`.""" """Represents a Facebook page. Implements `ThreadABC`."""
type = ThreadType.PAGE
#: The session to use when making requests.
session = attr.ib(type=_session.Session)
#: The unique identifier of the page.
id = attr.ib(converter=str)
#: The page's picture #: The page's picture
photo = attr.ib(None) photo = attr.ib(None)
#: The name of the page #: The name of the page
@@ -31,8 +32,11 @@ class Page(Thread):
#: The page's category #: The page's category
category = attr.ib(None) category = attr.ib(None)
def _to_send_data(self):
return {"other_user_fbid": self.id}
@classmethod @classmethod
def _from_graphql(cls, data): def _from_graphql(cls, session, data):
if data.get("profile_picture") is None: if data.get("profile_picture") is None:
data["profile_picture"] = {} data["profile_picture"] = {}
if data.get("city") is None: if data.get("city") is None:
@@ -42,6 +46,7 @@ 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(
session=session,
id=data["id"], id=data["id"],
url=data.get("url"), url=data.get("url"),
city=data.get("city").get("name"), city=data.get("city").get("name"),

View File

@@ -1,26 +1,9 @@
import abc
import attr import attr
import datetime
from ._core import attrs_default, Enum, Image from ._core import attrs_default, Enum, Image
from . import _util, _exception, _session
from typing import MutableMapping, Any, Iterable, Tuple
class ThreadType(Enum):
"""Used to specify what type of Facebook thread is being used.
See :ref:`intro_threads` for more info.
"""
USER = 1
GROUP = 2
PAGE = 3
def _to_class(self):
"""Convert this enum value to the corresponding class."""
from . import _user, _group, _page
return {
ThreadType.USER: _user.User,
ThreadType.GROUP: _group.Group,
ThreadType.PAGE: _page.Page,
}[self]
class ThreadLocation(Enum): class ThreadLocation(Enum):
@@ -67,17 +50,438 @@ class ThreadColor(Enum):
return cls._extend_if_invalid(value) return cls._extend_if_invalid(value)
@attrs_default class ThreadABC(metaclass=abc.ABCMeta):
class Thread: """Implemented by thread-like classes.
"""Represents a Facebook thread."""
#: The unique identifier of the thread. This is private to implement.
id = attr.ib(converter=str) """
#: Specifies the type of thread. Can be used a ``thread_type``. See :ref:`intro_threads` for more info
type = None @property
@abc.abstractmethod
def session(self) -> _session.Session:
"""The session to use when making requests."""
raise NotImplementedError
@property
@abc.abstractmethod
def id(self) -> str:
"""The unique identifier of the thread."""
raise NotImplementedError
@abc.abstractmethod
def _to_send_data(self) -> MutableMapping[str, str]:
raise NotImplementedError
def wave(self, first: bool = True) -> str:
"""Wave hello to the thread.
Args:
first: Whether to wave first or wave back
"""
data = self._to_send_data()
data["action_type"] = "ma-type:user-generated-message"
data["lightweight_action_attachment[lwa_state]"] = (
"INITIATED" if first else "RECIPROCATED"
)
data["lightweight_action_attachment[lwa_type]"] = "WAVE"
# TODO: This!
# if isinstance(self, _user.User):
# data["specific_to_list[0]"] = "fbid:{}".format(thread_id)
message_id, thread_id = self.session._do_send_request(data)
return message_id
def send(self, message) -> str:
"""Send message to the thread.
Args:
message (Message): Message to send
Returns:
:ref:`Message ID <intro_message_ids>` of the sent message
"""
data = self._to_send_data()
data.update(message._to_send_data())
return self.session._do_send_request(data)
def _send_location(self, current, latitude, longitude, message=None) -> str:
data = self._to_send_data()
if message is not None:
data.update(message._to_send_data())
data["action_type"] = "ma-type:user-generated-message"
data["location_attachment[coordinates][latitude]"] = latitude
data["location_attachment[coordinates][longitude]"] = longitude
data["location_attachment[is_current_location]"] = current
return self.session._do_send_request(data)
def send_location(self, latitude: float, longitude: float, message=None):
"""Send a given location to a thread as the user's current location.
Args:
latitude: The location latitude
longitude: The location longitude
message: Additional message
"""
self._send_location(
True, latitude=latitude, longitude=longitude, message=message,
)
def send_pinned_location(self, latitude: float, longitude: float, message=None):
"""Send a given location to a thread as a pinned location.
Args:
latitude: The location latitude
longitude: The location longitude
message: Additional message
"""
self._send_location(
False, latitude=latitude, longitude=longitude, message=message,
)
def send_files(self, files: Iterable[Tuple[str, str]], message):
"""Send files from file IDs to a thread.
`files` should be a list of tuples, with a file's ID and mimetype.
"""
data = self._to_send_data()
data.update(message._to_send_data())
data["action_type"] = "ma-type:user-generated-message"
data["has_attachment"] = True
for i, (file_id, mimetype) in enumerate(files):
data["{}s[{}]".format(_util.mimetype_to_key(mimetype), i)] = file_id
return self.session._do_send_request(data)
# TODO: This!
# def quick_reply(self, quick_reply, payload=None):
# """Reply to chosen quick reply.
#
# Args:
# quick_reply (QuickReply): Quick reply to reply to
# payload: Optional answer to the quick reply
# """
# if isinstance(quick_reply, QuickReplyText):
# new = QuickReplyText(
# 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):
# if not isinstance(payload, LocationAttachment):
# raise TypeError("Payload must be an instance of `LocationAttachment`")
# return self.send_location(payload)
# elif isinstance(quick_reply, QuickReplyEmail):
# new = QuickReplyEmail(
# payload=payload if payload else self.get_emails()[0],
# external_payload=quick_reply.payload,
# data=quick_reply.data,
# is_response=True,
# image_url=quick_reply.image_url,
# )
# return self.send(Message(text=payload, quick_replies=[new]))
# elif isinstance(quick_reply, QuickReplyPhoneNumber):
# new = QuickReplyPhoneNumber(
# payload=payload if payload else self.get_phone_numbers()[0],
# external_payload=quick_reply.payload,
# data=quick_reply.data,
# is_response=True,
# image_url=quick_reply.image_url,
# )
# return self.send(Message(text=payload, quick_replies=[new]))
def search_messages(
self, query: str, offset: int = 0, limit: int = 5
) -> Iterable[str]:
"""Find and get message IDs by query.
Args:
query: Text to search for
offset (int): Number of messages to skip
limit (int): Max. number of messages to retrieve
Returns:
typing.Iterable: Found Message IDs
"""
# TODO: Return proper searchable iterator
data = {
"query": query,
"snippetOffset": offset,
"snippetLimit": limit,
"identifier": "thread_fbid",
"thread_fbid": self.id,
}
j = self.session._payload_post("/ajax/mercury/search_snippets.php?dpr=1", data)
result = j["search_snippets"][query]
snippets = result[self.id]["snippets"] if result.get(self.id) else []
for snippet in snippets:
yield snippet["message_id"]
def fetch_messages(self, limit: int = 20, before: datetime.datetime = None):
"""Fetch messages in a thread, ordered by most recent.
Args:
limit: Max. number of messages to retrieve
before: The point from which to retrieve messages
Returns:
list: `Message` objects
"""
# TODO: Return proper searchable iterator
params = {
"id": self.id,
"message_limit": limit,
"load_messages": True,
"load_read_receipts": True,
"before": _util.datetime_to_millis(before) if before else None,
}
(j,) = self.session._graphql_requests(
_graphql.from_doc_id("1860982147341344", params)
)
if j.get("message_thread") is None:
raise FBchatException("Could not fetch thread {}: {}".format(self.id, j))
read_receipts = j["message_thread"]["read_receipts"]["nodes"]
messages = [
Message._from_graphql(self.session, message, read_receipts)
for message in j["message_thread"]["messages"]["nodes"]
]
messages.reverse()
return messages
def fetch_images(self):
"""Fetch images/videos posted in the thread."""
# TODO: Return proper searchable iterator
data = {"id": self.id, "first": 48}
(j,) = self.session._graphql_requests(
_graphql.from_query_id("515216185516880", data)
)
while True:
try:
i = j[self.id]["message_shared_media"]["edges"][0]
except IndexError:
if j[self.id]["message_shared_media"]["page_info"].get("has_next_page"):
data["after"] = j[self.id]["message_shared_media"]["page_info"].get(
"end_cursor"
)
(j,) = self.session._graphql_requests(
_graphql.from_query_id("515216185516880", data)
)
continue
else:
break
if i["node"].get("__typename") == "MessageImage":
yield ImageAttachment._from_list(i)
elif i["node"].get("__typename") == "MessageVideo":
yield VideoAttachment._from_list(i)
else:
yield Attachment(id=i["node"].get("legacy_attachment_id"))
del j[self.id]["message_shared_media"]["edges"][0]
def set_nickname(self, user_id: str, nickname: str):
"""Change the nickname of a user in the thread.
Args:
user_id: User that will have their nickname changed
nickname: New nickname
"""
data = {
"nickname": nickname,
"participant_id": user_id,
"thread_or_other_fbid": self.id,
}
j = self.session._payload_post(
"/messaging/save_thread_nickname/?source=thread_settings&dpr=1", data
)
def set_color(self, color: ThreadColor):
"""Change thread color.
Args:
color: New thread color
"""
data = {
"color_choice": color.value if color != ThreadColor.MESSENGER_BLUE else "",
"thread_or_other_fbid": self.id,
}
j = self.session._payload_post(
"/messaging/save_thread_color/?source=thread_settings&dpr=1", data
)
def set_emoji(self, emoji: str):
"""Change thread color.
Args:
emoji: New thread emoji
"""
data = {"emoji_choice": emoji, "thread_or_other_fbid": self.id}
# While changing the emoji, the Facebook web client actually sends multiple
# different requests, though only this one is required to make the change.
j = self.session._payload_post(
"/messaging/save_thread_emoji/?source=thread_settings&dpr=1", data
)
def forward_attachment(self, attachment_id):
"""Forward an attachment.
Args:
attachment_id: Attachment ID to forward
"""
data = {
"attachment_id": attachment_id,
"recipient_map[{}]".format(_util.generate_offline_threading_id()): self.id,
}
j = self.session._payload_post("/mercury/attachments/forward/", data)
if not j.get("success"):
raise _exception.FBchatFacebookError(
"Failed forwarding attachment: {}".format(j["error"]),
fb_error_message=j["error"],
)
def _set_typing(self, typing):
data = {
"typ": "1" if typing else "0",
"thread": self.id,
# TODO: This
# "to": self.id if isinstance(self, _user.User) else "",
"source": "mercury-chat",
}
j = self.session._payload_post("/ajax/messaging/typ.php", data)
def start_typing(self):
"""Set the current user to start typing in the thread."""
self._set_typing(True)
def stop_typing(self):
"""Set the current user to stop typing in the thread."""
self._set_typing(False)
def create_plan(
self,
name: str,
at: datetime.datetime,
location_name: str = None,
location_id: str = None,
):
"""Create a new plan.
# TODO: Arguments
Args:
title: Name of the new plan
at: When the plan is for
"""
data = {
"event_type": "EVENT",
"event_time": _util.datetime_to_seconds(at),
"title": name,
"thread_id": self.id,
"location_id": location_id or "",
"location_name": location or "",
"acontext": ACONTEXT,
}
j = self.session._payload_post("/ajax/eventreminder/create", data)
if "error" in j:
raise _exception.FBchatFacebookError(
"Failed creating plan: {}".format(j["error"]),
fb_error_message=j["error"],
)
def create_poll(self, question: str, options=Iterable[Tuple[str, bool]]):
"""Create poll in a thread.
# TODO: Arguments
"""
# We're using ordered dictionaries, because the Facebook endpoint that parses
# the POST parameters is badly implemented, and deals with ordering the options
# wrongly. If you can find a way to fix this for the endpoint, or if you find
# another endpoint, please do suggest it ;)
data = OrderedDict([("question_text", question), ("target_id", self.id)])
for i, (text, vote) in enumerate(options):
data["option_text_array[{}]".format(i)] = text
data["option_is_selected_array[{}]".format(i)] = str(int(vote))
j = self.session._payload_post(
"/messaging/group_polling/create_poll/?dpr=1", data
)
if j.get("status") != "success":
raise _exception.FBchatFacebookError(
"Failed creating poll: {}".format(j.get("errorTitle")),
fb_error_message=j.get("errorMessage"),
)
def mute(self, duration: datetime.timedelta = None):
"""Mute the thread.
Args:
duration: Time to mute, use ``None`` to mute forever
"""
if duration is None:
setting = "-1"
else:
setting = str(_util.timedelta_to_seconds(duration))
data = {"mute_settings": setting, "thread_fbid": self.id}
j = self.session._payload_post(
"/ajax/mercury/change_mute_thread.php?dpr=1", data
)
def unmute(self):
"""Unmute the thread."""
return self.mute(datetime.timedelta(0))
def _mute_reactions(self, mode: bool):
data = {"reactions_mute_mode": "1" if mode else "0", "thread_fbid": self.id}
j = self.session._payload_post(
"/ajax/mercury/change_reactions_mute_thread/?dpr=1", data
)
def mute_reactions(self):
"""Mute thread reactions."""
self._mute_reactions(True)
def unmute_reactions(self):
"""Unmute thread reactions."""
self._mute_reactions(False)
def _mute_mentions(self, mode: bool):
data = {"mentions_mute_mode": "1" if mode else "0", "thread_fbid": self.id}
j = self.session._payload_post(
"/ajax/mercury/change_mentions_mute_thread/?dpr=1", data
)
def mute_mentions(self):
"""Mute thread mentions."""
self._mute_mentions(True)
def unmute_mentions(self):
"""Unmute thread mentions."""
self._mute_mentions(False)
def mark_as_spam(self):
"""Mark the thread as spam, and delete it."""
data = {"id": self.id}
j = self.session._payload_post("/ajax/mercury/mark_spam.php?dpr=1", data)
def _forced_fetch(self, message_id: str) -> dict:
params = {
"thread_and_message_id": {"thread_id": self.id, "message_id": message_id}
}
(j,) = self.session._graphql_requests(
_graphql.from_doc_id("1768656253222505", params)
)
return j
@staticmethod @staticmethod
def _parse_customization_info(data): def _parse_customization_info(data: Any) -> MutableMapping[str, Any]:
if data is None or data.get("customization_info") is None: if data is None or data.get("customization_info") is None:
return {} return {}
info = data["customization_info"] info = data["customization_info"]
@@ -109,6 +513,24 @@ class Thread:
rtn["own_nickname"] = pc[1].get("nickname") rtn["own_nickname"] = pc[1].get("nickname")
return rtn return rtn
@attrs_default
class Thread(ThreadABC):
"""Represents a Facebook thread, where the actual type is unknown.
Implements parts of `ThreadABC`, call the method to figure out if your use case is
supported. Otherwise, you'll have to use an `User`/`Group`/`Page` object.
Note: This list may change in minor versions!
"""
#: The session to use when making requests.
session = attr.ib(type=_session.Session)
#: The unique identifier of the thread.
id = attr.ib(converter=str)
def _to_send_data(self): def _to_send_data(self):
# TODO: Only implement this in subclasses raise NotImplementedError(
return {"other_user_fbid": self.id} "The method you called is not supported on raw Thread objects."
" Please use an appropriate User/Group/Page object instead!"
)

View File

@@ -1,7 +1,6 @@
import attr import attr
from ._core import attrs_default, Enum, Image from ._core import attrs_default, Enum, Image
from . import _util, _plan from . import _util, _session, _plan, _thread
from ._thread import ThreadType, Thread
GENDERS = { GENDERS = {
@@ -42,11 +41,13 @@ class TypingStatus(Enum):
@attrs_default @attrs_default
class User(Thread): class User(_thread.ThreadABC):
"""Represents a Facebook user. Inherits `Thread`.""" """Represents a Facebook user. Implements `ThreadABC`."""
type = ThreadType.USER
#: The session to use when making requests.
session = attr.ib(type=_session.Session)
#: The user's unique identifier.
id = attr.ib(converter=str)
#: The user's picture #: The user's picture
photo = attr.ib(None) photo = attr.ib(None)
#: The name of the user #: The name of the user
@@ -78,8 +79,31 @@ class User(Thread):
#: The default emoji #: The default emoji
emoji = attr.ib(None) emoji = attr.ib(None)
def _to_send_data(self):
return {"other_user_fbid": self.id}
def confirm_friend_request(self):
"""Confirm a friend request, adding the user to your friend list."""
data = {"to_friend": self.id, "action": "confirm"}
j = self.session._payload_post("/ajax/add_friend/action.php?dpr=1", data)
def remove_friend(self):
"""Remove the user from the client's friend list."""
data = {"uid": self.id}
j = self.session._payload_post("/ajax/profile/removefriendconfirm.php", data)
def block(self):
"""Block messages from the user."""
data = {"fbid": self.id}
j = self.session._payload_post("/messaging/block_messages/?dpr=1", data)
def unblock(self):
"""Unblock a previously blocked user."""
data = {"fbid": self.id}
j = self.session._payload_post("/messaging/unblock_messages/?dpr=1", data)
@classmethod @classmethod
def _from_graphql(cls, data): def _from_graphql(cls, session, data):
if data.get("profile_picture") is None: if data.get("profile_picture") is None:
data["profile_picture"] = {} data["profile_picture"] = {}
c_info = cls._parse_customization_info(data) c_info = cls._parse_customization_info(data)
@@ -88,6 +112,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(
session=session,
id=data["id"], id=data["id"],
url=data.get("url"), url=data.get("url"),
first_name=data.get("first_name"), first_name=data.get("first_name"),
@@ -106,7 +131,7 @@ class User(Thread):
) )
@classmethod @classmethod
def _from_thread_fetch(cls, data): def _from_thread_fetch(cls, session, data):
if data.get("big_image_src") is None: if data.get("big_image_src") is None:
data["big_image_src"] = {} data["big_image_src"] = {}
c_info = cls._parse_customization_info(data) c_info = cls._parse_customization_info(data)
@@ -133,6 +158,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(
session=session,
id=user["id"], id=user["id"],
url=user.get("url"), url=user.get("url"),
name=user.get("name"), name=user.get("name"),
@@ -152,8 +178,9 @@ class User(Thread):
) )
@classmethod @classmethod
def _from_all_fetch(cls, data): def _from_all_fetch(cls, session, data):
return cls( return cls(
session=session,
id=data["id"], id=data["id"],
first_name=data.get("firstName"), first_name=data.get("firstName"),
url=data.get("uri"), url=data.get("uri"),

View File

@@ -3,19 +3,24 @@ import json
from utils import * from utils import *
from contextlib import contextmanager from contextlib import contextmanager
from fbchat import ThreadType, Message, Mention from fbchat import Message, Mention
@pytest.fixture(scope="session")
def session():
return object() # TODO: Add a mocked session
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def user(client2): def user(client2):
return {"id": client2.id, "type": ThreadType.USER} return {"id": client2.id, "type": None}
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def group(pytestconfig): def group(pytestconfig):
return { return {
"id": load_variable("group_id", pytestconfig.cache), "id": load_variable("group_id", pytestconfig.cache),
"type": ThreadType.GROUP, "type": None,
} }
@@ -24,11 +29,9 @@ def group(pytestconfig):
params=["user", "group", pytest.param("none", marks=[pytest.mark.xfail()])], params=["user", "group", pytest.param("none", marks=[pytest.mark.xfail()])],
) )
def thread(request, user, group): def thread(request, user, group):
return { return {"user": user, "group": group, "none": {"id": "0", "type": None},}[
"user": user, request.param
"group": group, ]
"none": {"id": "0", "type": ThreadType.GROUP},
}[request.param]
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
@@ -98,9 +101,7 @@ def compare(client, thread):
def inner(caught_event, **kwargs): def inner(caught_event, **kwargs):
d = { d = {
"author_id": client.id, "author_id": client.id,
"thread_id": client.id "thread_id": client.id if thread["type"] == None else thread["id"],
if thread["type"] == ThreadType.USER
else thread["id"],
"thread_type": thread["type"], "thread_type": thread["type"],
} }
d.update(kwargs) d.update(kwargs)

View File

@@ -1,7 +1,7 @@
import pytest import pytest
from os import path from os import path
from fbchat import ThreadType, Message, Mention, EmojiSize, Sticker from fbchat import Message, Mention, EmojiSize, Sticker
from utils import subset, STICKER_LIST, EMOJI_LIST from utils import subset, STICKER_LIST, EMOJI_LIST
pytestmark = pytest.mark.online pytestmark = pytest.mark.online
@@ -89,7 +89,6 @@ def test_fetch_info(client1, group):
assert info.name == "Mark Zuckerberg" assert info.name == "Mark Zuckerberg"
info = client1.fetch_group_info(group["id"])[group["id"]] info = client1.fetch_group_info(group["id"])[group["id"]]
assert info.type == ThreadType.GROUP
def test_fetch_image_url(client): def test_fetch_image_url(client):

View File

@@ -1,7 +1,7 @@
from fbchat._group import Group from fbchat._group import Group
def test_group_from_graphql(): def test_group_from_graphql(session):
data = { data = {
"name": "Group ABC", "name": "Group ABC",
"thread_key": {"thread_fbid": "11223344"}, "thread_key": {"thread_fbid": "11223344"},
@@ -26,6 +26,7 @@ def test_group_from_graphql():
"event_reminders": {"nodes": []}, "event_reminders": {"nodes": []},
} }
assert Group( assert Group(
session=session,
id="11223344", id="11223344",
photo=None, photo=None,
name="Group ABC", name="Group ABC",
@@ -40,4 +41,4 @@ def test_group_from_graphql():
approval_mode=False, approval_mode=False,
approval_requests=set(), approval_requests=set(),
join_link="", join_link="",
) == Group._from_graphql(data) ) == Group._from_graphql(session, data)

View File

@@ -2,7 +2,7 @@ import fbchat
from fbchat._page import Page from fbchat._page import Page
def test_page_from_graphql(): def test_page_from_graphql(session):
data = { data = {
"id": "123456", "id": "123456",
"name": "Some school", "name": "Some school",
@@ -12,10 +12,11 @@ def test_page_from_graphql():
"city": None, "city": None,
} }
assert Page( assert Page(
session=session,
id="123456", id="123456",
photo=fbchat.Image(url="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,
category="SCHOOL", category="SCHOOL",
) == Page._from_graphql(data) ) == Page._from_graphql(session, data)

View File

@@ -1,6 +1,6 @@
import pytest import pytest
from fbchat import Plan, FBchatFacebookError, ThreadType from fbchat import Plan, FBchatFacebookError
from utils import random_hex, subset from utils import random_hex, subset
from time import time from time import time
@@ -93,7 +93,7 @@ def test_on_plan_ended(client, thread, catch_event, compare):
x.wait(180) x.wait(180)
assert subset( assert subset(
x.res, x.res,
thread_id=client.id if thread["type"] == ThreadType.USER else thread["id"], thread_id=client.id if thread["type"] is None else thread["id"],
thread_type=thread["type"], thread_type=thread["type"],
) )

View File

@@ -1,6 +1,6 @@
import pytest import pytest
from fbchat import Poll, PollOption, ThreadType from fbchat import Poll, PollOption
from utils import random_hex, subset from utils import random_hex, subset
pytestmark = pytest.mark.online pytestmark = pytest.mark.online
@@ -49,12 +49,7 @@ def poll_data(request, client1, group, catch_event):
def test_create_poll(client1, group, catch_event, poll_data): def test_create_poll(client1, group, catch_event, poll_data):
event, poll, _ = poll_data event, poll, _ = poll_data
assert subset( assert subset(event, author_id=client1.id, thread=group)
event,
author_id=client1.id,
thread_id=group["id"],
thread_type=ThreadType.GROUP,
)
assert subset( assert subset(
vars(event["poll"]), title=poll.title, options_count=len(poll.options) vars(event["poll"]), title=poll.title, options_count=len(poll.options)
) )
@@ -88,12 +83,7 @@ def test_update_poll_vote(client1, group, catch_event, poll_data):
new_options=new_options, new_options=new_options,
) )
assert subset( assert subset(x.res, author_id=client1.id, thread=group)
x.res,
author_id=client1.id,
thread_id=group["id"],
thread_type=ThreadType.GROUP,
)
assert subset( assert subset(
vars(x.res["poll"]), title=poll.title, options_count=len(options + new_options) vars(x.res["poll"]), title=poll.title, options_count=len(options + new_options)
) )

View File

@@ -1,5 +1,4 @@
import pytest import pytest
from fbchat import ThreadType
pytestmark = pytest.mark.online pytestmark = pytest.mark.online
@@ -11,7 +10,6 @@ def test_search_for(client1):
u = users[0] u = users[0]
assert u.id == "4" assert u.id == "4"
assert u.type == ThreadType.USER
assert u.photo[:4] == "http" assert u.photo[:4] == "http"
assert u.url[:4] == "http" assert u.url[:4] == "http"
assert u.name == "Mark Zuckerberg" assert u.name == "Mark Zuckerberg"

View File

@@ -1,12 +1,6 @@
import pytest import pytest
import fbchat import fbchat
from fbchat._thread import ThreadType, ThreadColor, Thread from fbchat import ThreadColor, ThreadABC, Thread
def test_thread_type_to_class():
assert fbchat.User == ThreadType.USER._to_class()
assert fbchat.Group == ThreadType.GROUP._to_class()
assert fbchat.Page == ThreadType.PAGE._to_class()
def test_thread_color_from_graphql(): def test_thread_color_from_graphql():
@@ -19,8 +13,8 @@ def test_thread_color_from_graphql():
def test_thread_parse_customization_info_empty(): def test_thread_parse_customization_info_empty():
assert {} == Thread._parse_customization_info(None) assert {} == ThreadABC._parse_customization_info(None)
assert {} == Thread._parse_customization_info({"customization_info": None}) assert {} == ThreadABC._parse_customization_info({"customization_info": None})
def test_thread_parse_customization_info_group(): def test_thread_parse_customization_info_group():
@@ -43,7 +37,7 @@ def test_thread_parse_customization_info_group():
"color": ThreadColor.BRILLIANT_ROSE, "color": ThreadColor.BRILLIANT_ROSE,
"nicknames": {"123456789": "A", "987654321": "B"}, "nicknames": {"123456789": "A", "987654321": "B"},
} }
assert expected == Thread._parse_customization_info(data) assert expected == ThreadABC._parse_customization_info(data)
def test_thread_parse_customization_info_user(): def test_thread_parse_customization_info_user():
@@ -62,4 +56,9 @@ def test_thread_parse_customization_info_user():
# ... Other irrelevant fields # ... Other irrelevant fields
} }
expected = {"emoji": None, "color": None, "own_nickname": "A", "nickname": "B"} expected = {"emoji": None, "color": None, "own_nickname": "A", "nickname": "B"}
assert expected == Thread._parse_customization_info(data) assert expected == ThreadABC._parse_customization_info(data)
def test_thread_create_and_implements_thread_abc(session):
thread = Thread(session=session, id="123")
assert thread._parse_customization_info

View File

@@ -1,6 +1,6 @@
import pytest import pytest
from fbchat import Message, ThreadType, FBchatFacebookError, TypingStatus, ThreadColor from fbchat import Message, FBchatFacebookError, TypingStatus, ThreadColor
from utils import random_hex, subset from utils import random_hex, subset
from os import path from os import path
@@ -42,14 +42,8 @@ def test_remove_from_and_add_admins_to_group(client1, client2, group, catch_even
def test_change_title(client1, group, catch_event): def test_change_title(client1, group, catch_event):
title = random_hex() title = random_hex()
with catch_event("on_title_change") as x: with catch_event("on_title_change") as x:
client1.change_thread_title(title, group["id"], thread_type=ThreadType.GROUP) client1.change_thread_title(title, group["id"])
assert subset( assert subset(x.res, author_id=client1.id, new_title=title, thread=group)
x.res,
author_id=client1.id,
new_title=title,
thread_id=group["id"],
thread_type=ThreadType.GROUP,
)
def test_change_nickname(client, client_all, catch_event, compare): def test_change_nickname(client, client_all, catch_event, compare):

View File

@@ -4,7 +4,7 @@ import fbchat
from fbchat._user import User, ActiveStatus from fbchat._user import User, ActiveStatus
def test_user_from_graphql(): def test_user_from_graphql(session):
data = { data = {
"id": "1234", "id": "1234",
"name": "Abc Def Ghi", "name": "Abc Def Ghi",
@@ -17,6 +17,7 @@ def test_user_from_graphql():
"viewer_affinity": 0.4560002, "viewer_affinity": 0.4560002,
} }
assert User( assert User(
session=session,
id="1234", id="1234",
photo=fbchat.Image(url="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",
@@ -26,10 +27,10 @@ def test_user_from_graphql():
is_friend=True, is_friend=True,
gender="female_singular", gender="female_singular",
affinity=0.4560002, affinity=0.4560002,
) == User._from_graphql(data) ) == User._from_graphql(session, data)
def test_user_from_thread_fetch(): def test_user_from_thread_fetch(session):
data = { data = {
"thread_key": {"thread_fbid": None, "other_user_id": "1234"}, "thread_key": {"thread_fbid": None, "other_user_id": "1234"},
"name": None, "name": None,
@@ -138,6 +139,7 @@ def test_user_from_thread_fetch():
"delivery_receipts": ..., "delivery_receipts": ...,
} }
assert User( assert User(
session=session,
id="1234", id="1234",
photo=fbchat.Image(url="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",
@@ -152,10 +154,10 @@ def test_user_from_thread_fetch():
own_nickname="B", own_nickname="B",
color=None, color=None,
emoji=None, emoji=None,
) == User._from_thread_fetch(data) ) == User._from_thread_fetch(session, data)
def test_user_from_all_fetch(): def test_user_from_all_fetch(session):
data = { data = {
"id": "1234", "id": "1234",
"name": "Abc Def Ghi", "name": "Abc Def Ghi",
@@ -176,6 +178,7 @@ def test_user_from_all_fetch():
"is_blocked": False, "is_blocked": False,
} }
assert User( assert User(
session=session,
id="1234", id="1234",
photo=fbchat.Image(url="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",
@@ -183,7 +186,7 @@ def test_user_from_all_fetch():
first_name="Abc", first_name="Abc",
is_friend=True, is_friend=True,
gender="female_singular", gender="female_singular",
) == User._from_all_fetch(data) ) == User._from_all_fetch(session, data)
@pytest.mark.skip(reason="can't gather test data, the pulling is broken") @pytest.mark.skip(reason="can't gather test data, the pulling is broken")

View File

@@ -5,7 +5,7 @@ import pytest
from os import environ from os import environ
from random import randrange from random import randrange
from contextlib import contextmanager from contextlib import contextmanager
from fbchat import ThreadType, EmojiSize, FBchatFacebookError, Sticker, Client from fbchat import EmojiSize, FBchatFacebookError, Sticker, Client
log = logging.getLogger("fbchat.tests").addHandler(logging.NullHandler()) log = logging.getLogger("fbchat.tests").addHandler(logging.NullHandler())