From 0531a9e4826de6fec133ab3bd04a159cd4514d0d Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Wed, 8 Jan 2020 23:07:13 +0100 Subject: [PATCH 01/14] Add session attribute to Group/User/Page/Thread --- fbchat/_client.py | 35 +++++++++++++++++++++++------------ fbchat/_group.py | 9 +++++++-- fbchat/_page.py | 9 +++++++-- fbchat/_thread.py | 3 +++ fbchat/_user.py | 15 +++++++++++---- tests/conftest.py | 5 +++++ tests/test_group.py | 5 +++-- tests/test_page.py | 5 +++-- tests/test_user.py | 15 +++++++++------ 9 files changed, 71 insertions(+), 30 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index e5fa3d8..2f0b78a 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -61,6 +61,10 @@ class Client: self._buddylist = dict() self._session = session + @property + def session(self): + return self._session + """ INTERNAL REQUEST METHODS """ @@ -214,7 +218,7 @@ class Client: if data["id"] in ["0", 0]: # Skip invalid users continue - users.append(User._from_all_fetch(data)) + users.append(User._from_all_fetch(self.session, data)) return users def search_for_users(self, name, limit=10): @@ -233,7 +237,9 @@ class Client: params = {"search": name, "limit": limit} (j,) = self.graphql_requests(_graphql.from_query(_graphql.SEARCH_USER, params)) - return [User._from_graphql(node) for node in j[name]["users"]["nodes"]] + return [ + User._from_graphql(self.session, node) for node in j[name]["users"]["nodes"] + ] def search_for_pages(self, name, limit=10): """Find and get pages by their name. @@ -250,7 +256,9 @@ class Client: params = {"search": name, "limit": limit} (j,) = self.graphql_requests(_graphql.from_query(_graphql.SEARCH_PAGE, params)) - return [Page._from_graphql(node) for node in j[name]["pages"]["nodes"]] + return [ + Page._from_graphql(self.session, node) for node in j[name]["pages"]["nodes"] + ] def search_for_groups(self, name, limit=10): """Find and get group threads by their name. @@ -268,7 +276,10 @@ class Client: params = {"search": name, "limit": limit} (j,) = self.graphql_requests(_graphql.from_query(_graphql.SEARCH_GROUP, params)) - return [Group._from_graphql(node) for node in j["viewer"]["groups"]["nodes"]] + return [ + Group._from_graphql(self.session, node) + for node in j["viewer"]["groups"]["nodes"] + ] def search_for_threads(self, name, limit=10): """Find and get threads by their name. @@ -291,12 +302,12 @@ class Client: rtn = [] for node in j[name]["threads"]["nodes"]: if node["__typename"] == "User": - rtn.append(User._from_graphql(node)) + rtn.append(User._from_graphql(self.session, node)) elif node["__typename"] == "MessageThread": # MessageThread => Group thread - rtn.append(Group._from_graphql(node)) + rtn.append(Group._from_graphql(self.session, node)) elif node["__typename"] == "Page": - rtn.append(Page._from_graphql(node)) + rtn.append(Page._from_graphql(self.session, node)) elif node["__typename"] == "Group": # We don't handle Facebook "Groups" pass @@ -551,16 +562,16 @@ class Client: entry = entry["message_thread"] if entry.get("thread_type") == "GROUP": _id = entry["thread_key"]["thread_fbid"] - rtn[_id] = Group._from_graphql(entry) + rtn[_id] = Group._from_graphql(self.session, entry) elif entry.get("thread_type") == "ONE_TO_ONE": _id = entry["thread_key"]["other_user_id"] if pages_and_users.get(_id) is None: raise FBchatException("Could not fetch thread {}".format(_id)) entry.update(pages_and_users[_id]) if entry["type"] == ThreadType.USER: - rtn[_id] = User._from_graphql(entry) + rtn[_id] = User._from_graphql(self.session, entry) else: - rtn[_id] = Page._from_graphql(entry) + rtn[_id] = Page._from_graphql(self.session, entry) else: raise FBchatException( "{} had an unknown thread type: {}".format(thread_ids[i], entry) @@ -641,9 +652,9 @@ class Client: for node in j["viewer"]["message_threads"]["nodes"]: _type = node.get("thread_type") if _type == "GROUP": - rtn.append(Group._from_graphql(node)) + rtn.append(Group._from_graphql(self.session, node)) elif _type == "ONE_TO_ONE": - rtn.append(User._from_thread_fetch(node)) + rtn.append(User._from_thread_fetch(self.session, node)) else: raise FBchatException( "Unknown thread type: {}, with data: {}".format(_type, node) diff --git a/fbchat/_group.py b/fbchat/_group.py index 8412f5d..e0c347a 100644 --- a/fbchat/_group.py +++ b/fbchat/_group.py @@ -1,6 +1,6 @@ import attr from ._core import attrs_default, Image -from . import _util, _plan +from . import _util, _session, _plan from ._thread import ThreadType, Thread @@ -10,6 +10,10 @@ class Group(Thread): 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 photo = attr.ib(None) #: The name of the group @@ -38,7 +42,7 @@ class Group(Thread): join_link = attr.ib(None) @classmethod - def _from_graphql(cls, data): + def _from_graphql(cls, session, data): if data.get("image") is None: data["image"] = {} c_info = cls._parse_customization_info(data) @@ -52,6 +56,7 @@ class Group(Thread): plan = _plan.Plan._from_graphql(data["event_reminders"]["nodes"][0]) return cls( + session=session, id=data["thread_key"]["thread_fbid"], participants=set( [ diff --git a/fbchat/_page.py b/fbchat/_page.py index cc434bd..b2601a4 100644 --- a/fbchat/_page.py +++ b/fbchat/_page.py @@ -1,6 +1,6 @@ import attr from ._core import attrs_default, Image -from . import _plan +from . import _session, _plan from ._thread import ThreadType, Thread @@ -10,6 +10,10 @@ class Page(Thread): 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 photo = attr.ib(None) #: The name of the page @@ -32,7 +36,7 @@ class Page(Thread): category = attr.ib(None) @classmethod - def _from_graphql(cls, data): + def _from_graphql(cls, session, data): if data.get("profile_picture") is None: data["profile_picture"] = {} if data.get("city") is None: @@ -42,6 +46,7 @@ class Page(Thread): plan = _plan.Plan._from_graphql(data["event_reminders"]["nodes"][0]) return cls( + session=session, id=data["id"], url=data.get("url"), city=data.get("city").get("name"), diff --git a/fbchat/_thread.py b/fbchat/_thread.py index 4f4bd74..7c3796e 100644 --- a/fbchat/_thread.py +++ b/fbchat/_thread.py @@ -1,5 +1,6 @@ import attr from ._core import attrs_default, Enum, Image +from . import _session class ThreadType(Enum): @@ -71,6 +72,8 @@ class ThreadColor(Enum): class Thread: """Represents a Facebook thread.""" + #: The session to use when making requests. + session = attr.ib(type=_session.Session) #: The unique identifier of the thread. id = attr.ib(converter=str) #: Specifies the type of thread. Can be used a ``thread_type``. See :ref:`intro_threads` for more info diff --git a/fbchat/_user.py b/fbchat/_user.py index e4fe483..da35652 100644 --- a/fbchat/_user.py +++ b/fbchat/_user.py @@ -1,6 +1,6 @@ import attr from ._core import attrs_default, Enum, Image -from . import _util, _plan +from . import _util, _session, _plan from ._thread import ThreadType, Thread @@ -47,6 +47,10 @@ class User(Thread): 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 photo = attr.ib(None) #: The name of the user @@ -79,7 +83,7 @@ class User(Thread): emoji = attr.ib(None) @classmethod - def _from_graphql(cls, data): + def _from_graphql(cls, session, data): if data.get("profile_picture") is None: data["profile_picture"] = {} c_info = cls._parse_customization_info(data) @@ -88,6 +92,7 @@ class User(Thread): plan = _plan.Plan._from_graphql(data["event_reminders"]["nodes"][0]) return cls( + session=session, id=data["id"], url=data.get("url"), first_name=data.get("first_name"), @@ -106,7 +111,7 @@ class User(Thread): ) @classmethod - def _from_thread_fetch(cls, data): + def _from_thread_fetch(cls, session, data): if data.get("big_image_src") is None: data["big_image_src"] = {} c_info = cls._parse_customization_info(data) @@ -133,6 +138,7 @@ class User(Thread): plan = _plan.Plan._from_graphql(data["event_reminders"]["nodes"][0]) return cls( + session=session, id=user["id"], url=user.get("url"), name=user.get("name"), @@ -152,8 +158,9 @@ class User(Thread): ) @classmethod - def _from_all_fetch(cls, data): + def _from_all_fetch(cls, session, data): return cls( + session=session, id=data["id"], first_name=data.get("firstName"), url=data.get("uri"), diff --git a/tests/conftest.py b/tests/conftest.py index 122d148..d3978b0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,6 +6,11 @@ from contextlib import contextmanager from fbchat import ThreadType, Message, Mention +@pytest.fixture(scope="session") +def session(): + return object() # TODO: Add a mocked session + + @pytest.fixture(scope="session") def user(client2): return {"id": client2.id, "type": ThreadType.USER} diff --git a/tests/test_group.py b/tests/test_group.py index d1c69eb..c014e1d 100644 --- a/tests/test_group.py +++ b/tests/test_group.py @@ -1,7 +1,7 @@ from fbchat._group import Group -def test_group_from_graphql(): +def test_group_from_graphql(session): data = { "name": "Group ABC", "thread_key": {"thread_fbid": "11223344"}, @@ -26,6 +26,7 @@ def test_group_from_graphql(): "event_reminders": {"nodes": []}, } assert Group( + session=session, id="11223344", photo=None, name="Group ABC", @@ -40,4 +41,4 @@ def test_group_from_graphql(): approval_mode=False, approval_requests=set(), join_link="", - ) == Group._from_graphql(data) + ) == Group._from_graphql(session, data) diff --git a/tests/test_page.py b/tests/test_page.py index 0ff6051..2bcacdf 100644 --- a/tests/test_page.py +++ b/tests/test_page.py @@ -2,7 +2,7 @@ import fbchat from fbchat._page import Page -def test_page_from_graphql(): +def test_page_from_graphql(session): data = { "id": "123456", "name": "Some school", @@ -12,10 +12,11 @@ def test_page_from_graphql(): "city": None, } assert Page( + session=session, id="123456", photo=fbchat.Image(url="https://scontent-arn2-1.xx.fbcdn.net/v/..."), name="Some school", url="https://www.facebook.com/some-school/", city=None, category="SCHOOL", - ) == Page._from_graphql(data) + ) == Page._from_graphql(session, data) diff --git a/tests/test_user.py b/tests/test_user.py index a3a7319..a154bcd 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -4,7 +4,7 @@ import fbchat from fbchat._user import User, ActiveStatus -def test_user_from_graphql(): +def test_user_from_graphql(session): data = { "id": "1234", "name": "Abc Def Ghi", @@ -17,6 +17,7 @@ def test_user_from_graphql(): "viewer_affinity": 0.4560002, } assert User( + session=session, id="1234", photo=fbchat.Image(url="https://scontent-arn2-1.xx.fbcdn.net/v/..."), name="Abc Def Ghi", @@ -25,10 +26,10 @@ def test_user_from_graphql(): last_name="Ghi", is_friend=True, gender="female_singular", - ) == User._from_graphql(data) + ) == User._from_graphql(session, data) -def test_user_from_thread_fetch(): +def test_user_from_thread_fetch(session): data = { "thread_key": {"thread_fbid": None, "other_user_id": "1234"}, "name": None, @@ -137,6 +138,7 @@ def test_user_from_thread_fetch(): "delivery_receipts": ..., } assert User( + session=session, id="1234", photo=fbchat.Image(url="https://scontent-arn2-1.xx.fbcdn.net/v/..."), name="Abc Def Ghi", @@ -151,10 +153,10 @@ def test_user_from_thread_fetch(): own_nickname="B", color=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 = { "id": "1234", "name": "Abc Def Ghi", @@ -175,6 +177,7 @@ def test_user_from_all_fetch(): "is_blocked": False, } assert User( + session=session, id="1234", photo=fbchat.Image(url="https://scontent-arn2-1.xx.fbcdn.net/v/..."), name="Abc Def Ghi", @@ -182,7 +185,7 @@ def test_user_from_all_fetch(): first_name="Abc", is_friend=True, 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") From a26554b4d6cf70b7f5b3afb5c6f7bd9b34fb02cc Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Wed, 8 Jan 2020 23:23:19 +0100 Subject: [PATCH 02/14] Move user-related functions to User model --- fbchat/_client.py | 57 ----------------------------------------------- fbchat/_user.py | 20 +++++++++++++++++ 2 files changed, 20 insertions(+), 57 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index 2f0b78a..8c6b564 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -1693,63 +1693,6 @@ class Client: "/ajax/mercury/mark_seen.php", {"seen_timestamp": _util.now()} ) - def friend_connect(self, friend_id): - """ - Todo: - Documenting this - """ - data = {"to_friend": friend_id, "action": "confirm"} - - j = self._payload_post("/ajax/add_friend/action.php?dpr=1", data) - - def remove_friend(self, friend_id=None): - """Remove a specified friend from the client's friend list. - - Args: - friend_id: The ID of the friend that you want to remove - - Returns: - True - - Raises: - FBchatException: If request failed - """ - data = {"uid": friend_id} - j = self._payload_post("/ajax/profile/removefriendconfirm.php", data) - return True - - def block_user(self, user_id): - """Block messages from a specified user. - - Args: - user_id: The ID of the user that you want to block - - Returns: - True - - Raises: - FBchatException: If request failed - """ - data = {"fbid": user_id} - j = self._payload_post("/messaging/block_messages/?dpr=1", data) - return True - - def unblock_user(self, user_id): - """Unblock a previously blocked user. - - Args: - user_id: The ID of the user that you want to unblock - - Returns: - Whether the request was successful - - Raises: - FBchatException: If request failed - """ - data = {"fbid": user_id} - j = self._payload_post("/messaging/unblock_messages/?dpr=1", data) - return True - def move_threads(self, location, thread_ids): """Move threads to specified location. diff --git a/fbchat/_user.py b/fbchat/_user.py index da35652..1224fbe 100644 --- a/fbchat/_user.py +++ b/fbchat/_user.py @@ -82,6 +82,26 @@ class User(Thread): #: The default emoji emoji = attr.ib(None) + 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 def _from_graphql(cls, session, data): if data.get("profile_picture") is None: From 64f55a572e76f5474c7fbe68859f487d82a0ae5b Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Wed, 8 Jan 2020 23:32:45 +0100 Subject: [PATCH 03/14] Move group-related functions to Group model --- fbchat/_client.py | 171 ---------------------------------------------- fbchat/_group.py | 113 ++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 171 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index 8c6b564..3cbe7bd 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -1191,177 +1191,6 @@ class Client: ) return thread_id - def add_users_to_group(self, user_ids, thread_id=None): - """Add users to a group. - - Args: - user_ids (list): One or more user IDs to add - thread_id: Group ID to add people to. See :ref:`intro_threads` - - Raises: - FBchatException: If request failed - """ - data = Group(id=thread_id)._to_send_data() - - data["action_type"] = "ma-type:log-message" - data["log_message_type"] = "log:subscribe" - - user_ids = _util.require_list(user_ids) - - 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._do_send_request(data) - - def remove_user_from_group(self, user_id, thread_id=None): - """Remove user from a group. - - Args: - user_id: User ID to remove - thread_id: Group ID to remove people from. See :ref:`intro_threads` - - Raises: - FBchatException: If request failed - """ - data = {"uid": user_id, "tid": thread_id} - j = self._payload_post("/chat/remove_participants/", data) - - def _admin_status(self, admin_ids, admin, thread_id=None): - data = {"add": admin, "thread_fbid": thread_id} - - admin_ids = _util.require_list(admin_ids) - - for i, admin_id in enumerate(admin_ids): - data["admin_ids[{}]".format(i)] = str(admin_id) - - j = self._payload_post("/messaging/save_admins/?dpr=1", data) - - def add_group_admins(self, admin_ids, thread_id=None): - """Set specified users as group admins. - - Args: - admin_ids: One or more user IDs to set admin - thread_id: Group ID to remove people from. See :ref:`intro_threads` - - Raises: - FBchatException: If request failed - """ - self._admin_status(admin_ids, True, thread_id) - - def remove_group_admins(self, admin_ids, thread_id=None): - """Remove admin status from specified users. - - Args: - admin_ids: One or more user IDs to remove admin - thread_id: Group ID to remove people from. See :ref:`intro_threads` - - Raises: - FBchatException: If request failed - """ - self._admin_status(admin_ids, False, thread_id) - - def change_group_approval_mode(self, require_admin_approval, thread_id=None): - """Change group's approval mode. - - Args: - require_admin_approval: True or False - thread_id: Group ID to remove people from. See :ref:`intro_threads` - - Raises: - FBchatException: If request failed - """ - data = {"set_mode": int(require_admin_approval), "thread_fbid": thread_id} - j = self._payload_post("/messaging/set_approval_mode/?dpr=1", data) - - def _users_approval(self, user_ids, approve, thread_id=None): - user_ids = _util.require_list(user_ids) - - data = { - "client_mutation_id": "0", - "actor_id": self._session.user_id, - "thread_fbid": thread_id, - "user_ids": user_ids, - "response": "ACCEPT" if approve else "DENY", - "surface": "ADMIN_MODEL_APPROVAL_CENTER", - } - (j,) = self.graphql_requests( - _graphql.from_doc_id("1574519202665847", {"data": data}) - ) - - def accept_users_to_group(self, user_ids, thread_id=None): - """Accept users to the group from the group's approval. - - Args: - user_ids: One or more user IDs to accept - thread_id: Group ID to accept users to. See :ref:`intro_threads` - - Raises: - FBchatException: If request failed - """ - self._users_approval(user_ids, True, thread_id) - - def deny_users_from_group(self, user_ids, thread_id=None): - """Deny users from joining the group. - - Args: - user_ids: One or more user IDs to deny - thread_id: Group ID to deny users from. See :ref:`intro_threads` - - Raises: - FBchatException: If request failed - """ - self._users_approval(user_ids, False, thread_id) - - def _change_group_image(self, image_id, thread_id=None): - """Change a thread image from an image id. - - Args: - image_id: ID of uploaded image - thread_id: User/Group ID to change image. See :ref:`intro_threads` - - Raises: - FBchatException: If request failed - """ - data = {"thread_image_id": image_id, "thread_id": thread_id} - - j = self._payload_post("/messaging/set_thread_image/?dpr=1", data) - return image_id - - def change_group_image_remote(self, image_url, thread_id=None): - """Change a thread image from a URL. - - Args: - image_url: URL of an image to upload and change - thread_id: User/Group ID to change image. See :ref:`intro_threads` - - Raises: - FBchatException: If request failed - """ - ((image_id, mimetype),) = self._upload(_util.get_files_from_urls([image_url])) - return self._change_group_image(image_id, thread_id) - - def change_group_image_local(self, image_path, thread_id=None): - """Change a thread image from a local path. - - Args: - image_path: Path of an image to upload and change - thread_id: User/Group ID to change image. See :ref:`intro_threads` - - Raises: - FBchatException: If request failed - """ - with _util.get_files_from_paths([image_path]) as files: - ((image_id, mimetype),) = self._upload(files) - - return self._change_group_image(image_id, thread_id) - def change_thread_title(self, title, thread_id=None, thread_type=ThreadType.USER): """Change title of a thread. diff --git a/fbchat/_group.py b/fbchat/_group.py index e0c347a..e418a44 100644 --- a/fbchat/_group.py +++ b/fbchat/_group.py @@ -2,6 +2,7 @@ import attr from ._core import attrs_default, Image from . import _util, _session, _plan from ._thread import ThreadType, Thread +from typing import Iterable @attrs_default @@ -41,6 +42,118 @@ class Group(Thread): # Link for joining group 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 def _from_graphql(cls, session, data): if data.get("image") is None: From 4199439e07f0505227f8c115c71053f7bb2f1193 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Wed, 8 Jan 2020 23:52:14 +0100 Subject: [PATCH 04/14] Remove Thread.type --- fbchat/_client.py | 10 +++++----- fbchat/_group.py | 4 +--- fbchat/_page.py | 4 +--- fbchat/_thread.py | 2 -- fbchat/_user.py | 4 +--- tests/test_fetch.py | 1 - tests/test_search.py | 1 - 7 files changed, 8 insertions(+), 18 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index 3cbe7bd..ff2d043 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -186,10 +186,10 @@ class Client: users = [] users_to_fetch = [] # It's more efficient to fetch all users in one request for thread in threads: - if thread.type == ThreadType.USER: + if isinstance(thread, User): if thread.id not in [user.id for user in users]: users.append(thread) - elif thread.type == ThreadType.GROUP: + elif isinstance(thread, Group): for user_id in thread.participants: if ( user_id not in [user.id for user in users] @@ -458,7 +458,7 @@ class Client: threads = self.fetch_thread_info(*user_ids) users = {} for id_, thread in threads.items(): - if thread.type == ThreadType.USER: + if isinstance(thread, User): users[id_] = thread else: raise ValueError("Thread {} was not a user".format(thread)) @@ -483,7 +483,7 @@ class Client: threads = self.fetch_thread_info(*page_ids) pages = {} for id_, thread in threads.items(): - if thread.type == ThreadType.PAGE: + if isinstance(thread, Page): pages[id_] = thread else: raise ValueError("Thread {} was not a page".format(thread)) @@ -505,7 +505,7 @@ class Client: threads = self.fetch_thread_info(*group_ids) groups = {} for id_, thread in threads.items(): - if thread.type == ThreadType.GROUP: + if isinstance(thread, Group): groups[id_] = thread else: raise ValueError("Thread {} was not a group".format(thread)) diff --git a/fbchat/_group.py b/fbchat/_group.py index e418a44..d206259 100644 --- a/fbchat/_group.py +++ b/fbchat/_group.py @@ -1,7 +1,7 @@ import attr from ._core import attrs_default, Image from . import _util, _session, _plan -from ._thread import ThreadType, Thread +from ._thread import Thread from typing import Iterable @@ -9,8 +9,6 @@ from typing import Iterable class Group(Thread): """Represents a Facebook group. Inherits `Thread`.""" - type = ThreadType.GROUP - #: The session to use when making requests. session = attr.ib(type=_session.Session) #: The group's unique identifier. diff --git a/fbchat/_page.py b/fbchat/_page.py index b2601a4..9310df4 100644 --- a/fbchat/_page.py +++ b/fbchat/_page.py @@ -1,15 +1,13 @@ import attr from ._core import attrs_default, Image from . import _session, _plan -from ._thread import ThreadType, Thread +from ._thread import Thread @attrs_default class Page(Thread): """Represents a Facebook page. Inherits `Thread`.""" - type = ThreadType.PAGE - #: The session to use when making requests. session = attr.ib(type=_session.Session) #: The unique identifier of the page. diff --git a/fbchat/_thread.py b/fbchat/_thread.py index 7c3796e..643a979 100644 --- a/fbchat/_thread.py +++ b/fbchat/_thread.py @@ -76,8 +76,6 @@ class Thread: session = attr.ib(type=_session.Session) #: The unique identifier of the thread. 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 @staticmethod def _parse_customization_info(data): diff --git a/fbchat/_user.py b/fbchat/_user.py index 1224fbe..93ea274 100644 --- a/fbchat/_user.py +++ b/fbchat/_user.py @@ -1,7 +1,7 @@ import attr from ._core import attrs_default, Enum, Image from . import _util, _session, _plan -from ._thread import ThreadType, Thread +from ._thread import Thread GENDERS = { @@ -45,8 +45,6 @@ class TypingStatus(Enum): class User(Thread): """Represents a Facebook user. Inherits `Thread`.""" - type = ThreadType.USER - #: The session to use when making requests. session = attr.ib(type=_session.Session) #: The user's unique identifier. diff --git a/tests/test_fetch.py b/tests/test_fetch.py index 59d222d..4ddd66a 100644 --- a/tests/test_fetch.py +++ b/tests/test_fetch.py @@ -89,7 +89,6 @@ def test_fetch_info(client1, group): assert info.name == "Mark Zuckerberg" info = client1.fetch_group_info(group["id"])[group["id"]] - assert info.type == ThreadType.GROUP def test_fetch_image_url(client): diff --git a/tests/test_search.py b/tests/test_search.py index ac59cfa..bd7b440 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -11,7 +11,6 @@ def test_search_for(client1): u = users[0] assert u.id == "4" - assert u.type == ThreadType.USER assert u.photo[:4] == "http" assert u.url[:4] == "http" assert u.name == "Mark Zuckerberg" From 152f20027a74fc20c4dde904476faaf7534d9747 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Thu, 9 Jan 2020 00:35:43 +0100 Subject: [PATCH 05/14] Add ThreadABC helper, that'll contain functions that threads can call --- fbchat/__init__.py | 2 +- fbchat/_client.py | 8 ++---- fbchat/_group.py | 7 +++-- fbchat/_page.py | 10 ++++--- fbchat/_thread.py | 62 +++++++++++++++++++++++++++++++++++++------- fbchat/_user.py | 10 ++++--- tests/test_thread.py | 15 +++++++---- 7 files changed, 80 insertions(+), 34 deletions(-) diff --git a/fbchat/__init__.py b/fbchat/__init__.py index 4f88896..01b27c6 100644 --- a/fbchat/__init__.py +++ b/fbchat/__init__.py @@ -14,7 +14,7 @@ from . import _core, _util from ._core import Image from ._exception import FBchatException, FBchatFacebookError from ._session import Session -from ._thread import ThreadType, ThreadLocation, ThreadColor, Thread +from ._thread import ThreadType, ThreadLocation, ThreadColor, ThreadABC, Thread from ._user import TypingStatus, User, ActiveStatus from ._group import Group from ._page import Page diff --git a/fbchat/_client.py b/fbchat/_client.py index ff2d043..2a01d97 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -108,11 +108,6 @@ class Client: FETCH METHODS """ - def _forced_fetch(self, thread_id, mid): - params = {"thread_and_message_id": {"thread_id": thread_id, "message_id": mid}} - (j,) = self.graphql_requests(_graphql.from_doc_id("1768656253222505", params)) - return j - def fetch_threads(self, thread_location, before=None, after=None, limit=None): """Fetch all threads in ``thread_location``. @@ -1820,7 +1815,8 @@ class Client: self.on_unknown_messsage_type(msg=m) else: thread_id = str(delta["threadKey"]["threadFbId"]) - fetch_info = self._forced_fetch(thread_id, mid) + thread = Thread(session=self.session, id=thread_id) + fetch_info = thread._forced_fetch(mid) fetch_data = fetch_info["message"] author_id = fetch_data["message_sender"]["id"] at = _util.millis_to_datetime(int(fetch_data["timestamp_precise"])) diff --git a/fbchat/_group.py b/fbchat/_group.py index d206259..d72eb04 100644 --- a/fbchat/_group.py +++ b/fbchat/_group.py @@ -1,13 +1,12 @@ import attr from ._core import attrs_default, Image -from . import _util, _session, _plan -from ._thread import Thread +from . import _util, _session, _plan, _thread from typing import Iterable @attrs_default -class Group(Thread): - """Represents a Facebook group. Inherits `Thread`.""" +class Group(_thread.ThreadABC): + """Represents a Facebook group. Implements `ThreadABC`.""" #: The session to use when making requests. session = attr.ib(type=_session.Session) diff --git a/fbchat/_page.py b/fbchat/_page.py index 9310df4..32d97a1 100644 --- a/fbchat/_page.py +++ b/fbchat/_page.py @@ -1,12 +1,11 @@ import attr from ._core import attrs_default, Image -from . import _session, _plan -from ._thread import Thread +from . import _session, _plan, _thread @attrs_default -class Page(Thread): - """Represents a Facebook page. Inherits `Thread`.""" +class Page(_thread.ThreadABC): + """Represents a Facebook page. Implements `ThreadABC`.""" #: The session to use when making requests. session = attr.ib(type=_session.Session) @@ -33,6 +32,9 @@ class Page(Thread): #: The page's category category = attr.ib(None) + def _to_send_data(self): + return {"other_user_fbid": self.id} + @classmethod def _from_graphql(cls, session, data): if data.get("profile_picture") is None: diff --git a/fbchat/_thread.py b/fbchat/_thread.py index 643a979..87cd1cb 100644 --- a/fbchat/_thread.py +++ b/fbchat/_thread.py @@ -1,6 +1,8 @@ +import abc import attr from ._core import attrs_default, Enum, Image from . import _session +from typing import MutableMapping, Any class ThreadType(Enum): @@ -68,17 +70,39 @@ class ThreadColor(Enum): return cls._extend_if_invalid(value) -@attrs_default -class Thread: - """Represents a Facebook thread.""" +class ThreadABC(metaclass=abc.ABCMeta): + """Implemented by thread-like classes. - #: The session to use when making requests. - session = attr.ib(type=_session.Session) - #: The unique identifier of the thread. - id = attr.ib(converter=str) + This is private to implement. + """ + + @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 _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 - 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: return {} info = data["customization_info"] @@ -110,6 +134,24 @@ class Thread: rtn["own_nickname"] = pc[1].get("nickname") 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): - # TODO: Only implement this in subclasses - return {"other_user_fbid": self.id} + raise NotImplementedError( + "The method you called is not supported on raw Thread objects." + " Please use an appropriate User/Group/Page object instead!" + ) diff --git a/fbchat/_user.py b/fbchat/_user.py index 93ea274..d1afd60 100644 --- a/fbchat/_user.py +++ b/fbchat/_user.py @@ -1,7 +1,6 @@ import attr from ._core import attrs_default, Enum, Image -from . import _util, _session, _plan -from ._thread import Thread +from . import _util, _session, _plan, _thread GENDERS = { @@ -42,8 +41,8 @@ class TypingStatus(Enum): @attrs_default -class User(Thread): - """Represents a Facebook user. Inherits `Thread`.""" +class User(_thread.ThreadABC): + """Represents a Facebook user. Implements `ThreadABC`.""" #: The session to use when making requests. session = attr.ib(type=_session.Session) @@ -80,6 +79,9 @@ class User(Thread): #: The default emoji 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"} diff --git a/tests/test_thread.py b/tests/test_thread.py index baea7ad..53cf7b5 100644 --- a/tests/test_thread.py +++ b/tests/test_thread.py @@ -1,6 +1,6 @@ import pytest import fbchat -from fbchat._thread import ThreadType, ThreadColor, Thread +from fbchat import ThreadType, ThreadColor, ThreadABC, Thread def test_thread_type_to_class(): @@ -19,8 +19,8 @@ def test_thread_color_from_graphql(): def test_thread_parse_customization_info_empty(): - assert {} == Thread._parse_customization_info(None) - assert {} == Thread._parse_customization_info({"customization_info": None}) + assert {} == ThreadABC._parse_customization_info(None) + assert {} == ThreadABC._parse_customization_info({"customization_info": None}) def test_thread_parse_customization_info_group(): @@ -43,7 +43,7 @@ def test_thread_parse_customization_info_group(): "color": ThreadColor.BRILLIANT_ROSE, "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(): @@ -62,4 +62,9 @@ def test_thread_parse_customization_info_user(): # ... Other irrelevant fields } 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 From aeca4865ae8a3419c024129f6d03bffc9e4d80a8 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Thu, 9 Jan 2020 00:10:39 +0100 Subject: [PATCH 06/14] Add unfinished NewGroup helper class --- fbchat/_client.py | 28 ---------------------------- fbchat/_group.py | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 30 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index 2a01d97..3328d7d 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -1158,34 +1158,6 @@ class Client: fb_error_message=j["error"], ) - def create_group(self, message, user_ids): - """Create a group with the given user ids. - - Args: - message: The initial message - user_ids: A list of users to create the group with. - - Returns: - ID of the new group - - Raises: - FBchatException: If request failed - """ - data = self._old_message(message)._to_send_data() - - if len(user_ids) < 2: - raise ValueError("Error when creating group: Not enough participants") - - for i, user_id in enumerate(user_ids + [self._session.user_id]): - data["specific_to_list[{}]".format(i)] = "fbid:{}".format(user_id) - - message_id, thread_id = self._do_send_request(data, get_thread_id=True) - if not thread_id: - raise FBchatException( - "Error when creating group: No thread_id could be found" - ) - return thread_id - def change_thread_title(self, title, thread_id=None, thread_type=ThreadType.USER): """Change title of a thread. diff --git a/fbchat/_group.py b/fbchat/_group.py index d72eb04..ddaadc9 100644 --- a/fbchat/_group.py +++ b/fbchat/_group.py @@ -1,7 +1,7 @@ import attr from ._core import attrs_default, Image -from . import _util, _session, _plan, _thread -from typing import Iterable +from . import _util, _session, _plan, _thread, _user +from typing import Sequence, Iterable @attrs_default @@ -197,3 +197,32 @@ class Group(_thread.ThreadABC): def _to_send_data(self): 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) + } From 13aa1f5e5a019854d1d2687d89daf19dedba6185 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Thu, 9 Jan 2020 00:25:40 +0100 Subject: [PATCH 07/14] Move send methods to ThreadABC --- fbchat/_client.py | 281 ---------------------------------------------- fbchat/_thread.py | 125 ++++++++++++++++++++- 2 files changed, 123 insertions(+), 283 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index 3328d7d..2314aa0 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -845,111 +845,6 @@ class Client: SEND METHODS """ - def _old_message(self, message): - return message if isinstance(message, Message) else Message(text=message) - - def _do_send_request(self, data, get_thread_id=False): - """Send the data to `SendURL`, and returns the message ID or None on failure.""" - mid, thread_id = self._session._do_send_request(data) - if get_thread_id: - return mid, thread_id - else: - return mid - - def send(self, message, thread_id=None, thread_type=ThreadType.USER): - """Send message to a thread. - - Args: - message (Message): Message to send - thread_id: User/Group ID to send to. See :ref:`intro_threads` - thread_type (ThreadType): See :ref:`intro_threads` - - Returns: - :ref:`Message ID ` of the sent message - - Raises: - FBchatException: If request failed - """ - thread = thread_type._to_class()(id=thread_id) - data = thread._to_send_data() - data.update(message._to_send_data()) - return self._do_send_request(data) - - def wave(self, wave_first=True, thread_id=None, thread_type=None): - """Wave hello to a thread. - - Args: - wave_first: Whether to wave first or wave back - thread_id: User/Group ID to send to. See :ref:`intro_threads` - thread_type (ThreadType): See :ref:`intro_threads` - - Returns: - :ref:`Message ID ` of the sent message - - Raises: - FBchatException: If request failed - """ - thread = thread_type._to_class()(id=thread_id) - data = thread._to_send_data() - data["action_type"] = "ma-type:user-generated-message" - data["lightweight_action_attachment[lwa_state]"] = ( - "INITIATED" if wave_first else "RECIPROCATED" - ) - data["lightweight_action_attachment[lwa_type]"] = "WAVE" - if thread_type == ThreadType.USER: - data["specific_to_list[0]"] = "fbid:{}".format(thread_id) - return self._do_send_request(data) - - def quick_reply(self, quick_reply, payload=None, thread_id=None, thread_type=None): - """Reply to chosen quick reply. - - Args: - quick_reply (QuickReply): Quick reply to reply to - payload: Optional answer to the quick reply - thread_id: User/Group ID to send to. See :ref:`intro_threads` - thread_type (ThreadType): See :ref:`intro_threads` - - Returns: - :ref:`Message ID ` of the sent message - - Raises: - FBchatException: If request failed - """ - 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, thread_id=thread_id, thread_type=thread_type - ) - 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 unsend(self, mid): """Unsend message by it's ID (removes it for everyone). @@ -959,182 +854,6 @@ class Client: data = {"message_id": mid} j = self._payload_post("/messaging/unsend_message/?dpr=1", data) - def _send_location( - self, location, current=True, message=None, thread_id=None, thread_type=None - ): - thread = thread_type._to_class()(id=thread_id) - data = thread._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]"] = location.latitude - data["location_attachment[coordinates][longitude]"] = location.longitude - data["location_attachment[is_current_location]"] = current - return self._do_send_request(data) - - def send_location(self, location, message=None, thread_id=None, thread_type=None): - """Send a given location to a thread as the user's current location. - - Args: - location (LocationAttachment): Location to send - message (Message): Additional message - thread_id: User/Group ID to send to. See :ref:`intro_threads` - thread_type (ThreadType): See :ref:`intro_threads` - - Returns: - :ref:`Message ID ` of the sent message - - Raises: - FBchatException: If request failed - """ - self._send_location( - location=location, - current=True, - message=message, - thread_id=thread_id, - thread_type=thread_type, - ) - - def send_pinned_location( - self, location, message=None, thread_id=None, thread_type=None - ): - """Send a given location to a thread as a pinned location. - - Args: - location (LocationAttachment): Location to send - message (Message): Additional message - thread_id: User/Group ID to send to. See :ref:`intro_threads` - thread_type (ThreadType): See :ref:`intro_threads` - - Returns: - :ref:`Message ID ` of the sent message - - Raises: - FBchatException: If request failed - """ - self._send_location( - location=location, - current=False, - message=message, - thread_id=thread_id, - thread_type=thread_type, - ) - - def _upload(self, files, voice_clip=False): - return self._session._upload(files, voice_clip=voice_clip) - - def _send_files( - self, files, message=None, thread_id=None, thread_type=ThreadType.USER - ): - """Send files from file IDs to a thread. - - `files` should be a list of tuples, with a file's ID and mimetype. - """ - thread = thread_type._to_class()(id=thread_id) - data = thread._to_send_data() - data.update(self._old_message(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._do_send_request(data) - - def send_remote_files( - self, file_urls, message=None, thread_id=None, thread_type=ThreadType.USER - ): - """Send files from URLs to a thread. - - Args: - file_urls: URLs of files to upload and send - message: Additional message - thread_id: User/Group ID to send to. See :ref:`intro_threads` - thread_type (ThreadType): See :ref:`intro_threads` - - Returns: - :ref:`Message ID ` of the sent files - - Raises: - FBchatException: If request failed - """ - file_urls = _util.require_list(file_urls) - files = self._upload(_util.get_files_from_urls(file_urls)) - return self._send_files( - files=files, message=message, thread_id=thread_id, thread_type=thread_type - ) - - def send_local_files( - self, file_paths, message=None, thread_id=None, thread_type=ThreadType.USER - ): - """Send local files to a thread. - - Args: - file_paths: Paths of files to upload and send - message: Additional message - thread_id: User/Group ID to send to. See :ref:`intro_threads` - thread_type (ThreadType): See :ref:`intro_threads` - - Returns: - :ref:`Message ID ` of the sent files - - Raises: - FBchatException: If request failed - """ - file_paths = _util.require_list(file_paths) - with _util.get_files_from_paths(file_paths) as x: - files = self._upload(x) - return self._send_files( - files=files, message=message, thread_id=thread_id, thread_type=thread_type - ) - - def send_remote_voice_clips( - self, clip_urls, message=None, thread_id=None, thread_type=ThreadType.USER - ): - """Send voice clips from URLs to a thread. - - Args: - clip_urls: URLs of clips to upload and send - message: Additional message - thread_id: User/Group ID to send to. See :ref:`intro_threads` - thread_type (ThreadType): See :ref:`intro_threads` - - Returns: - :ref:`Message ID ` of the sent files - - Raises: - FBchatException: If request failed - """ - clip_urls = _util.require_list(clip_urls) - files = self._upload(_util.get_files_from_urls(clip_urls), voice_clip=True) - return self._send_files( - files=files, message=message, thread_id=thread_id, thread_type=thread_type - ) - - def send_local_voice_clips( - self, clip_paths, message=None, thread_id=None, thread_type=ThreadType.USER - ): - """Send local voice clips to a thread. - - Args: - clip_paths: Paths of clips to upload and send - message: Additional message - thread_id: User/Group ID to send to. See :ref:`intro_threads` - thread_type (ThreadType): See :ref:`intro_threads` - - Returns: - :ref:`Message ID ` of the sent files - - Raises: - FBchatException: If request failed - """ - clip_paths = _util.require_list(clip_paths) - with _util.get_files_from_paths(clip_paths) as x: - files = self._upload(x, voice_clip=True) - return self._send_files( - files=files, message=message, thread_id=thread_id, thread_type=thread_type - ) - def forward_attachment(self, attachment_id, thread_id=None): """Forward an attachment. diff --git a/fbchat/_thread.py b/fbchat/_thread.py index 87cd1cb..5afaa93 100644 --- a/fbchat/_thread.py +++ b/fbchat/_thread.py @@ -1,8 +1,8 @@ import abc import attr from ._core import attrs_default, Enum, Image -from . import _session -from typing import MutableMapping, Any +from . import _util, _session +from typing import MutableMapping, Any, Iterable, Tuple class ThreadType(Enum): @@ -92,6 +92,127 @@ class ThreadABC(metaclass=abc.ABCMeta): 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 thread_type == ThreadType.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 ` 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 _forced_fetch(self, message_id: str) -> dict: params = { "thread_and_message_id": {"thread_id": self.id, "message_id": message_id} From f3b1d10d8556843254ed31be158dc1990984ee8e Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Thu, 9 Jan 2020 00:29:03 +0100 Subject: [PATCH 08/14] Move fetch methods to ThreadABC --- fbchat/_client.py | 126 ---------------------------------------------- fbchat/_thread.py | 93 ++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 126 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index 2314aa0..e446027 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -313,59 +313,6 @@ class Client: return rtn - def search_for_message_ids(self, query, offset=0, limit=5, thread_id=None): - """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 - thread_id: User/Group ID to search in. See :ref:`intro_threads` - - Returns: - typing.Iterable: Found Message IDs - - Raises: - FBchatException: If request failed - """ - data = { - "query": query, - "snippetOffset": offset, - "snippetLimit": limit, - "identifier": "thread_fbid", - "thread_fbid": thread_id, - } - j = self._payload_post("/ajax/mercury/search_snippets.php?dpr=1", data) - - result = j["search_snippets"][query] - snippets = result[thread_id]["snippets"] if result.get(thread_id) else [] - for snippet in snippets: - yield snippet["message_id"] - - def search_for_messages(self, query, offset=0, limit=5, thread_id=None): - """Find and get `Message` objects by query. - - Warning: - This method sends request for every found message ID. - - Args: - query: Text to search for - offset (int): Number of messages to skip - limit (int): Max. number of messages to retrieve - thread_id: User/Group ID to search in. See :ref:`intro_threads` - - Returns: - typing.Iterable: Found `Message` objects - - Raises: - FBchatException: If request failed - """ - message_ids = self.search_for_message_ids( - query, offset=offset, limit=limit, thread_id=thread_id - ) - for mid in message_ids: - yield self.fetch_message_info(mid, thread_id) - def search(self, query, fetch_messages=False, thread_limit=5, message_limit=5): """Search for messages in all threads. @@ -574,42 +521,6 @@ class Client: return rtn - def fetch_thread_messages(self, thread_id=None, limit=20, before=None): - """Fetch messages in a thread, ordered by most recent. - - Args: - thread_id: User/Group ID to get messages from. See :ref:`intro_threads` - limit (int): Max. number of messages to retrieve - before (datetime.datetime): The point from which to retrieve messages - - Returns: - list: `Message` objects - - Raises: - FBchatException: If request failed - """ - params = { - "id": thread_id, - "message_limit": limit, - "load_messages": True, - "load_read_receipts": True, - "before": _util.datetime_to_millis(before) if before else None, - } - (j,) = self.graphql_requests(_graphql.from_doc_id("1860982147341344", params)) - - if j.get("message_thread") is None: - raise FBchatException("Could not fetch thread {}: {}".format(thread_id, j)) - - read_receipts = j["message_thread"]["read_receipts"]["nodes"] - - messages = [ - Message._from_graphql(message, read_receipts) - for message in j["message_thread"]["messages"]["nodes"] - ] - messages.reverse() - - return messages - def fetch_thread_list( self, limit=20, thread_location=ThreadLocation.INBOX, before=None ): @@ -800,43 +711,6 @@ class Client: """ return self._buddylist.get(str(user_id)) - def fetch_thread_images(self, thread_id=None): - """Fetch images posted in thread. - - Args: - thread_id: ID of the thread - - Returns: - typing.Iterable: `ImageAttachment` or `VideoAttachment` - """ - data = {"id": thread_id, "first": 48} - thread_id = str(thread_id) - (j,) = self.graphql_requests(_graphql.from_query_id("515216185516880", data)) - while True: - try: - i = j[thread_id]["message_shared_media"]["edges"][0] - except IndexError: - if j[thread_id]["message_shared_media"]["page_info"].get( - "has_next_page" - ): - data["after"] = j[thread_id]["message_shared_media"][ - "page_info" - ].get("end_cursor") - (j,) = self.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[thread_id]["message_shared_media"]["edges"][0] - """ END FETCH METHODS """ diff --git a/fbchat/_thread.py b/fbchat/_thread.py index 5afaa93..86cef6d 100644 --- a/fbchat/_thread.py +++ b/fbchat/_thread.py @@ -213,6 +213,99 @@ class ThreadABC(metaclass=abc.ABCMeta): # ) # 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(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 _forced_fetch(self, message_id: str) -> dict: params = { "thread_and_message_id": {"thread_id": self.id, "message_id": message_id} From bd2b39c27a8dea9388f6dbc541caf17f2f43e837 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Thu, 9 Jan 2020 01:13:17 +0100 Subject: [PATCH 09/14] Add thread actions to ThreadABC --- fbchat/_client.py | 193 ---------------------------------------------- fbchat/_thread.py | 141 ++++++++++++++++++++++++++++++++- 2 files changed, 140 insertions(+), 194 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index e446027..c517e3f 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -728,112 +728,6 @@ class Client: data = {"message_id": mid} j = self._payload_post("/messaging/unsend_message/?dpr=1", data) - def forward_attachment(self, attachment_id, thread_id=None): - """Forward an attachment. - - Args: - attachment_id: Attachment ID to forward - thread_id: User/Group ID to send to. See :ref:`intro_threads` - - Raises: - FBchatException: If request failed - """ - data = { - "attachment_id": attachment_id, - "recipient_map[{}]".format( - _util.generate_offline_threading_id() - ): thread_id, - } - j = self._payload_post("/mercury/attachments/forward/", data) - if not j.get("success"): - raise FBchatFacebookError( - "Failed forwarding attachment: {}".format(j["error"]), - fb_error_message=j["error"], - ) - - def change_thread_title(self, title, thread_id=None, thread_type=ThreadType.USER): - """Change title of a thread. - - If this is executed on a user thread, this will change the nickname of that - user, effectively changing the title. - - Args: - title: New group thread title - thread_id: Group ID to change title of. See :ref:`intro_threads` - thread_type (ThreadType): See :ref:`intro_threads` - - Raises: - FBchatException: If request failed - """ - if thread_type == ThreadType.USER: - # The thread is a user, so we change the user's nickname - return self.change_nickname( - title, thread_id, thread_id=thread_id, thread_type=thread_type - ) - - data = {"thread_name": title, "thread_id": thread_id} - j = self._payload_post("/messaging/set_thread_name/?dpr=1", data) - - def change_nickname( - self, nickname, user_id, thread_id=None, thread_type=ThreadType.USER - ): - """Change the nickname of a user in a thread. - - Args: - nickname: New nickname - user_id: User that will have their nickname changed - thread_id: User/Group ID to change color of. See :ref:`intro_threads` - thread_type (ThreadType): See :ref:`intro_threads` - - Raises: - FBchatException: If request failed - """ - data = { - "nickname": nickname, - "participant_id": user_id, - "thread_or_other_fbid": thread_id, - } - j = self._payload_post( - "/messaging/save_thread_nickname/?source=thread_settings&dpr=1", data - ) - - def change_thread_color(self, color, thread_id=None): - """Change thread color. - - Args: - color (ThreadColor): New thread color - thread_id: User/Group ID to change color of. See :ref:`intro_threads` - - Raises: - FBchatException: If request failed - """ - data = { - "color_choice": color.value if color != ThreadColor.MESSENGER_BLUE else "", - "thread_or_other_fbid": thread_id, - } - j = self._payload_post( - "/messaging/save_thread_color/?source=thread_settings&dpr=1", data - ) - - def change_thread_emoji(self, emoji, thread_id=None): - """Change thread color. - - Note: - While changing the emoji, the Facebook web client actually sends multiple - different requests, though only this one is required to make the change. - - Args: - color: New thread emoji - thread_id: User/Group ID to change emoji of. See :ref:`intro_threads` - - Raises: - FBchatException: If request failed - """ - data = {"emoji_choice": emoji, "thread_or_other_fbid": thread_id} - j = self._payload_post( - "/messaging/save_thread_emoji/?source=thread_settings&dpr=1", data - ) - def react_to_message(self, message_id, reaction): """React to a message, or removes reaction. @@ -855,32 +749,6 @@ class Client: j = self._payload_post("/webgraphql/mutation", data) _util.handle_graphql_errors(j) - def create_plan(self, plan, thread_id=None): - """Set a plan. - - Args: - plan (Plan): Plan to set - thread_id: User/Group ID to send plan to. See :ref:`intro_threads` - - Raises: - FBchatException: If request failed - """ - data = { - "event_type": "EVENT", - "event_time": _util.datetime_to_seconds(plan.time), - "title": plan.title, - "thread_id": thread_id, - "location_id": plan.location_id or "", - "location_name": plan.location or "", - "acontext": ACONTEXT, - } - j = self._payload_post("/ajax/eventreminder/create", data) - if "error" in j: - raise FBchatFacebookError( - "Failed creating plan: {}".format(j["error"]), - fb_error_message=j["error"], - ) - def edit_plan(self, plan, new_plan): """Edit a plan. @@ -931,33 +799,6 @@ class Client: } j = self._payload_post("/ajax/eventreminder/rsvp", data) - def create_poll(self, poll, thread_id=None): - """Create poll in a group thread. - - Args: - poll (Poll): Poll to create - thread_id: User/Group ID to create poll in. See :ref:`intro_threads` - - Raises: - FBchatException: If request failed - """ - # 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", poll.title), ("target_id", thread_id)]) - - for i, option in enumerate(poll.options): - data["option_text_array[{}]".format(i)] = option.text - data["option_is_selected_array[{}]".format(i)] = str(int(option.vote)) - - j = self._payload_post("/messaging/group_polling/create_poll/?dpr=1", data) - if j.get("status") != "success": - raise FBchatFacebookError( - "Failed creating poll: {}".format(j.get("errorTitle")), - fb_error_message=j.get("errorMessage"), - ) - def update_poll_vote(self, poll_id, option_ids=[], new_options=[]): """Update a poll vote. @@ -986,25 +827,6 @@ class Client: fb_error_message=j.get("errorMessage"), ) - def set_typing_status(self, status, thread_id=None, thread_type=None): - """Set users typing status in a thread. - - Args: - status (TypingStatus): Specify the typing status - thread_id: User/Group ID to change status in. See :ref:`intro_threads` - thread_type (ThreadType): See :ref:`intro_threads` - - Raises: - FBchatException: If request failed - """ - data = { - "typ": status.value, - "thread": thread_id, - "to": thread_id if thread_type == ThreadType.USER else "", - "source": "mercury-chat", - } - j = self._payload_post("/ajax/messaging/typ.php", data) - """ END SEND METHODS """ @@ -1146,21 +968,6 @@ class Client: ) return True - def mark_as_spam(self, thread_id=None): - """Mark a thread as spam, and delete it. - - Args: - thread_id: User/Group ID to mark as spam. See :ref:`intro_threads` - - Returns: - True - - Raises: - FBchatException: If request failed - """ - j = self._payload_post("/ajax/mercury/mark_spam.php?dpr=1", {"id": thread_id}) - return True - def delete_messages(self, message_ids): """Delete specified messages. diff --git a/fbchat/_thread.py b/fbchat/_thread.py index 86cef6d..d303160 100644 --- a/fbchat/_thread.py +++ b/fbchat/_thread.py @@ -1,7 +1,8 @@ import abc import attr +import datetime from ._core import attrs_default, Enum, Image -from . import _util, _session +from . import _util, _exception, _session from typing import MutableMapping, Any, Iterable, Tuple @@ -306,6 +307,144 @@ class ThreadABC(metaclass=abc.ABCMeta): 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 thread_type == ThreadType.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 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} From 4dea10d5de0254d9148afa3dc0891aa600715af5 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Thu, 9 Jan 2020 00:33:49 +0100 Subject: [PATCH 10/14] Add thread mute settings to ThreadABC --- fbchat/_client.py | 60 ----------------------------------------------- fbchat/_thread.py | 47 +++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 60 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index c517e3f..7f396ab 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -987,66 +987,6 @@ class Client: j = self._payload_post("/ajax/mercury/delete_messages.php?dpr=1", data) return True - def mute_thread(self, mute_time=None, thread_id=None): - """Mute thread. - - Args: - mute_time (datetime.timedelta): Time to mute, use ``None`` to mute forever - thread_id: User/Group ID to mute. See :ref:`intro_threads` - """ - if mute_time is None: - mute_settings = -1 - else: - mute_settings = _util.timedelta_to_seconds(mute_time) - data = {"mute_settings": str(mute_settings), "thread_fbid": thread_id} - j = self._payload_post("/ajax/mercury/change_mute_thread.php?dpr=1", data) - - def unmute_thread(self, thread_id=None): - """Unmute thread. - - Args: - thread_id: User/Group ID to unmute. See :ref:`intro_threads` - """ - return self.mute_thread(datetime.timedelta(0), thread_id) - - def mute_thread_reactions(self, mute=True, thread_id=None): - """Mute thread reactions. - - Args: - mute: Boolean. True to mute, False to unmute - thread_id: User/Group ID to mute. See :ref:`intro_threads` - """ - data = {"reactions_mute_mode": int(mute), "thread_fbid": thread_id} - j = self._payload_post( - "/ajax/mercury/change_reactions_mute_thread/?dpr=1", data - ) - - def unmute_thread_reactions(self, thread_id=None): - """Unmute thread reactions. - - Args: - thread_id: User/Group ID to unmute. See :ref:`intro_threads` - """ - return self.mute_thread_reactions(False, thread_id) - - def mute_thread_mentions(self, mute=True, thread_id=None): - """Mute thread mentions. - - Args: - mute: Boolean. True to mute, False to unmute - thread_id: User/Group ID to mute. See :ref:`intro_threads` - """ - data = {"mentions_mute_mode": int(mute), "thread_fbid": thread_id} - j = self._payload_post("/ajax/mercury/change_mentions_mute_thread/?dpr=1", data) - - def unmute_thread_mentions(self, thread_id=None): - """Unmute thread mentions. - - Args: - thread_id: User/Group ID to unmute. See :ref:`intro_threads` - """ - return self.mute_thread_mentions(False, thread_id) - """ LISTEN METHODS """ diff --git a/fbchat/_thread.py b/fbchat/_thread.py index d303160..fcfb46b 100644 --- a/fbchat/_thread.py +++ b/fbchat/_thread.py @@ -440,6 +440,53 @@ class ThreadABC(metaclass=abc.ABCMeta): 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} From 53e4669fc1e304a619381dc4f9670e98e0bbbc3a Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Thu, 9 Jan 2020 00:34:48 +0100 Subject: [PATCH 11/14] Move fetch_message_info to Message --- fbchat/_client.py | 16 ---------------- fbchat/_message.py | 10 ++++++++++ 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index 7f396ab..0a97d74 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -623,22 +623,6 @@ class Client: raise FBchatException("Could not fetch image URL from: {}".format(j)) return url - def fetch_message_info(self, mid, thread_id=None): - """Fetch `Message` object from the given message id. - - Args: - mid: Message ID to fetch from - thread_id: User/Group ID to get message info from. See :ref:`intro_threads` - - Returns: - Message: `Message` object - - Raises: - FBchatException: If request failed - """ - message_info = self._forced_fetch(thread_id, mid).get("message") - return Message._from_graphql(message_info) - def fetch_poll_options(self, poll_id): """Fetch list of `PollOption` objects from the poll id. diff --git a/fbchat/_message.py b/fbchat/_message.py index da5bee4..c96f614 100644 --- a/fbchat/_message.py +++ b/fbchat/_message.py @@ -111,6 +111,16 @@ class Message: #: Whether the message was forwarded forwarded = attr.ib(False) + @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(message_info) + @classmethod def format_mentions(cls, text, *args, **kwargs): """Like `str.format`, but takes tuples with a thread id and text instead. From 6b4327fa691e878b7ed6082130591a4dc75deb7a Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Thu, 9 Jan 2020 00:51:47 +0100 Subject: [PATCH 12/14] Add Message.session --- fbchat/_client.py | 9 +++++++-- fbchat/_message.py | 29 +++++++++++++++++++---------- fbchat/_thread.py | 2 +- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index 0a97d74..eaac9ed 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -1524,8 +1524,12 @@ class Client: i = d["deltaMessageReply"] metadata = i["message"]["messageMetadata"] thread_id, thread_type = get_thread_id_and_thread_type(metadata) - replied_to = Message._from_reply(i["repliedToMessage"]) - message = Message._from_reply(i["message"], replied_to) + replied_to = Message._from_reply( + self.session, i["repliedToMessage"] + ) + message = Message._from_reply( + self.session, i["message"], replied_to + ) self.on_message( mid=message.id, author_id=message.author, @@ -1544,6 +1548,7 @@ class Client: mid=mid, author_id=author_id, message_object=Message._from_pull( + self.session, delta, mid=mid, tags=metadata.get("tags"), diff --git a/fbchat/_message.py b/fbchat/_message.py index c96f614..7936043 100644 --- a/fbchat/_message.py +++ b/fbchat/_message.py @@ -2,7 +2,7 @@ import attr import json from string import Formatter 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 class EmojiSize(Enum): @@ -78,14 +78,18 @@ class Mention: class 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) + #: The actual message text = attr.ib(None) #: A list of `Mention` objects mentions = attr.ib(factory=list) #: A `EmojiSize`. Size of a sent emoji emoji_size = attr.ib(None) - #: The message ID - id = attr.ib(None) #: ID of the sender author = attr.ib(None) #: Datetime of when the message was sent @@ -119,7 +123,7 @@ class Message: message_id: Message ID to fetch from """ message_info = thread._forced_fetch(message_id).get("message") - return Message._from_graphql(message_info) + return Message._from_graphql(thread.session, message_info) @classmethod def format_mentions(cls, text, *args, **kwargs): @@ -234,7 +238,7 @@ class Message: return [] @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: data["message_sender"] = {} if data.get("message") is None: @@ -260,12 +264,13 @@ class Message: replied_to = cls._from_graphql(data["replied_to_message"]["message"]) return cls( + session=session, + id=str(data["message_id"]), text=data["message"].get("text"), mentions=[ Mention._from_range(m) for m in data["message"].get("ranges") or () ], emoji_size=EmojiSize._from_tags(tags), - id=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, @@ -288,7 +293,7 @@ class Message: ) @classmethod - def _from_reply(cls, data, replied_to=None): + def _from_reply(cls, session, data, replied_to=None): tags = data["messageMetadata"].get("tags") metadata = data.get("messageMetadata", {}) @@ -315,13 +320,14 @@ class Message: ) return cls( + session=session, + id=metadata.get("messageId"), 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), - id=metadata.get("messageId"), author=str(metadata.get("actorFbId")), created_at=_util.millis_to_datetime(metadata.get("timestamp")), sticker=sticker, @@ -334,7 +340,9 @@ class Message: ) @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 = [] if data.get("data") and data["data"].get("prng"): try: @@ -381,10 +389,11 @@ class Message: ) return cls( + session=session, + id=mid, text=data.get("body"), mentions=mentions, emoji_size=EmojiSize._from_tags(tags), - id=mid, author=author, created_at=created_at, sticker=sticker, diff --git a/fbchat/_thread.py b/fbchat/_thread.py index fcfb46b..16d2324 100644 --- a/fbchat/_thread.py +++ b/fbchat/_thread.py @@ -270,7 +270,7 @@ class ThreadABC(metaclass=abc.ABCMeta): read_receipts = j["message_thread"]["read_receipts"]["nodes"] messages = [ - Message._from_graphql(message, read_receipts) + Message._from_graphql(self.session, message, read_receipts) for message in j["message_thread"]["messages"]["nodes"] ] messages.reverse() From ded6039b69f2e4ec05d6ce4229136e73cc612c23 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Thu, 9 Jan 2020 01:00:43 +0100 Subject: [PATCH 13/14] Add message-related functions to Message model --- fbchat/_client.py | 30 ------------------------------ fbchat/_message.py | 25 ++++++++++++++++++++++++- 2 files changed, 24 insertions(+), 31 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index eaac9ed..309aad2 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -703,36 +703,6 @@ class Client: SEND METHODS """ - def unsend(self, mid): - """Unsend message by it's ID (removes it for everyone). - - Args: - mid: :ref:`Message ID ` of the message to unsend - """ - data = {"message_id": mid} - j = self._payload_post("/messaging/unsend_message/?dpr=1", data) - - def react_to_message(self, message_id, reaction): - """React to a message, or removes reaction. - - Args: - message_id: :ref:`Message ID ` to react to - reaction (MessageReaction): Reaction emoji to use, if None removes reaction - - Raises: - FBchatException: If request failed - """ - data = { - "action": "ADD_REACTION" if reaction else "REMOVE_REACTION", - "client_mutation_id": "1", - "actor_id": self._session.user_id, - "message_id": str(message_id), - "reaction": reaction.value if reaction else None, - } - data = {"doc_id": 1491398900900362, "variables": json.dumps({"data": data})} - j = self._payload_post("/webgraphql/mutation", data) - _util.handle_graphql_errors(j) - def edit_plan(self, plan, new_plan): """Edit a plan. diff --git a/fbchat/_message.py b/fbchat/_message.py index 7936043..926a4a4 100644 --- a/fbchat/_message.py +++ b/fbchat/_message.py @@ -3,6 +3,7 @@ import json from string import Formatter from ._core import log, attrs_default, Enum from . import _util, _session, _attachment, _location, _file, _quick_reply, _sticker +from typing import Optional class EmojiSize(Enum): @@ -82,7 +83,7 @@ class Message: #: The session to use when making requests. session = attr.ib(None, type=_session.Session) #: The message ID - id = attr.ib(None) + id = attr.ib(None, converter=str) #: The actual message text = attr.ib(None) @@ -115,6 +116,28 @@ class Message: #: Whether the message was forwarded 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. From 2ec0be963515b50689d13df7cb5a2092cbb0ac39 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Thu, 9 Jan 2020 11:13:48 +0100 Subject: [PATCH 14/14] Remove ThreadType completely --- docs/api.rst | 2 - docs/intro.rst | 26 +- examples/basic_usage.py | 10 +- examples/echobot.py | 10 +- examples/fetch.py | 16 +- examples/interract.py | 88 ++--- examples/keepbot.py | 43 +-- examples/removebot.py | 14 +- fbchat/__init__.py | 2 +- fbchat/_client.py | 552 ++++++++---------------------- fbchat/_thread.py | 25 +- tests/conftest.py | 18 +- tests/test_fetch.py | 2 +- tests/test_plans.py | 4 +- tests/test_polls.py | 16 +- tests/test_search.py | 1 - tests/test_thread.py | 8 +- tests/test_thread_interraction.py | 12 +- tests/utils.py | 2 +- 19 files changed, 250 insertions(+), 601 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 83a9466..f1d8b35 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -17,8 +17,6 @@ Threads ------- .. autoclass:: Thread() -.. autoclass:: ThreadType(Enum) - :undoc-members: .. autoclass:: Page() .. autoclass:: User() .. autoclass:: Group() diff --git a/docs/intro.rst b/docs/intro.rst index 36ae036..6f8f849 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -44,11 +44,7 @@ When you're done using the client, and want to securely logout, use `Client.logo Threads ------- -A thread can refer to two things: A Messenger group chat or a single Facebook 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 +A thread can refer to two things: A Messenger group chat (`Group`) or a single Facebook user (`User`). 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` @@ -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 ```` and ```` corresponds to the ID of a single user, and the ID of a group respectively:: - client.send(Message(text=''), thread_id='', thread_type=ThreadType.USER) - client.send(Message(text=''), thread_id='', thread_type=ThreadType.GROUP) + user.send(Message(text='')) + group.send(Message(text='')) -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='') - client.change_thread_color(ThreadColor.MESSENGER_BLUE, thread_id='') + thread = fbchat.Thread(session=session, id="") + thread.set_color(ThreadColor.BILOBA_FLOWER) + thread.set_color(ThreadColor.MESSENGER_BLUE) .. _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`. 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) @@ -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 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` @@ -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:: 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 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:: 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 pass diff --git a/examples/basic_usage.py b/examples/basic_usage.py index f5c8900..19c6f92 100644 --- a/examples/basic_usage.py +++ b/examples/basic_usage.py @@ -5,15 +5,11 @@ session = fbchat.Session.login("", "") print("Own id: {}".format(sesion.user_id)) -# Create helper client class -client = fbchat.Client(session) +# Create helper User class +user = fbchat.Thread(session=session, id=session.user_id) # Send a message to yourself -client.send( - fbchat.Message(text="Hi me!"), - thread_id=session.user_id, - thread_type=fbchat.ThreadType.USER, -) +user.send(fbchat.Message(text="Hi me!")) # Log the user out session.logout() diff --git a/examples/echobot.py b/examples/echobot.py index 8e482e9..ec0d891 100644 --- a/examples/echobot.py +++ b/examples/echobot.py @@ -2,15 +2,15 @@ import fbchat # Subclass fbchat.Client and override required methods class EchoBot(fbchat.Client): - def on_message(self, author_id, message_object, thread_id, thread_type, **kwargs): - self.mark_as_delivered(thread_id, message_object.id) - self.mark_as_read(thread_id) + def on_message(self, author_id, message_object, thread, **kwargs): + self.mark_as_delivered(thread.id, message_object.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 author_id != session.user_id: - self.send(message_object, thread_id=thread_id, thread_type=thread_type) + thread.send(message_object) session = fbchat.Session.login("", "") diff --git a/examples/fetch.py b/examples/fetch.py index b1246f9..30989ea 100644 --- a/examples/fetch.py +++ b/examples/fetch.py @@ -39,8 +39,13 @@ threads += client.fetch_thread_list(offset=20, limit=10) 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("")[""] +print("thread's name: {}".format(thread.name)) + + # Gets the last 10 messages sent to the thread -messages = client.fetch_thread_messages(thread_id="", limit=10) +messages = thread.fetch_messages(limit=10) # Since the message come in reversed order, reverse them messages.reverse() @@ -49,22 +54,15 @@ for message in messages: 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("")[""] -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 thread = client.search_for_threads("")[0] print("thread's name: {}".format(thread.name)) -print("thread's type: {}".format(thread.type)) # Here should be an example of `getUnread` # Print image url for 20 last images from thread. -images = client.fetch_thread_images("") +images = thread.fetch_images() for image in itertools.islice(image, 20): print(image.large_preview_url) diff --git a/examples/interract.py b/examples/interract.py index 5b26108..136a20f 100644 --- a/examples/interract.py +++ b/examples/interract.py @@ -4,94 +4,64 @@ session = fbchat.Session.login("", "") client = fbchat.Client(session) -thread_id = "1234567890" -thread_type = fbchat.ThreadType.GROUP +thread = User(session=session, id=session.user_id) +# thread = User(session=session, id="0987654321") +# thread = Group(session=session, id="1234567890") # Will send a message to the thread -client.send( - fbchat.Message(text=""), thread_id=thread_id, thread_type=thread_type -) +thread.send(fbchat.Message(text="")) # Will send the default `like` emoji -client.send( - fbchat.Message(emoji_size=fbchat.EmojiSize.LARGE), - thread_id=thread_id, - thread_type=thread_type, -) +thread.send(fbchat.Message(emoji_size=fbchat.EmojiSize.LARGE)) # Will send the emoji `👍` -client.send( - fbchat.Message(text="👍", emoji_size=fbchat.EmojiSize.LARGE), - thread_id=thread_id, - thread_type=thread_type, -) +thread.send(fbchat.Message(text="👍", emoji_size=fbchat.EmojiSize.LARGE)) # Will send the sticker with ID `767334476626295` -client.send( - fbchat.Message(sticker=fbchat.Sticker("767334476626295")), - thread_id=thread_id, - thread_type=thread_type, -) +thread.send(fbchat.Message(sticker=fbchat.Sticker("767334476626295"))) # Will send a message with a mention -client.send( +thread.send( fbchat.Message( text="This is a @mention", mentions=[fbchat.Mention(thread_id, offset=10, length=8)], - ), - thread_id=thread_id, - thread_type=thread_type, + ) ) # Will send the image located at `` -client.send_local_image( - "", - message=fbchat.Message(text="This is a local image"), - thread_id=thread_id, - thread_type=thread_type, +thread.send_local_image( + "", message=fbchat.Message(text="This is a local image") ) # Will download the image at the URL ``, and then send it -client.send_remote_image( - "", - message=fbchat.Message(text="This is a remote image"), - thread_id=thread_id, - thread_type=thread_type, +thread.send_remote_image( + "", message=fbchat.Message(text="This is a remote image") ) # Only do these actions if the thread is a group -if thread_type == fbchat.ThreadType.GROUP: - # Will remove the user with ID `` from the thread - client.remove_user_from_group("", thread_id=thread_id) - - # Will add the user with ID `` to the thread - client.add_users_to_group("", thread_id=thread_id) - - # 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 - ) +if isinstance(thread, fbchat.Group): + # Will remove the user with ID `` from the group + thread.remove_participant("") + # Will add the users with IDs `<1st user id>`, `<2nd user id>` and `<3th user id>` to the group + thread.add_participants(["<1st user id>", "<2nd user id>", "<3rd user id>"]) + # Will change the title of the group to `` + thread.change_title("<title>") # Will change the nickname of the user `<user_id>` to `<new nickname>` -client.change_nickname( - "<new nickname>", "<user id>", thread_id=thread_id, thread_type=thread_type -) +thread.set_nickname(fbchat.User(session=session, id="<user id>"), "<new nickname>") -# Will change the title of the thread to `<title>` -client.change_thread_title("<title>", thread_id=thread_id, thread_type=thread_type) - -# 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 set the typing status of the thread +thread.start_typing() # 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 `👍` -client.change_thread_emoji("👍", thread_id=thread_id) +thread.set_emoji("👍") -# Will react to a message with a 😍 emoji -client.react_to_message("<message id>", fbchat.MessageReaction.LOVE) +# message = fbchat.Message(session=session, id="<message id>") +# +# # Will react to a message with a 😍 emoji +# message.react(fbchat.MessageReaction.LOVE) diff --git a/examples/keepbot.py b/examples/keepbot.py index 7ce8874..7531f38 100644 --- a/examples/keepbot.py +++ b/examples/keepbot.py @@ -16,50 +16,48 @@ old_nicknames = { class KeepBot(fbchat.Client): - def on_color_change(self, author_id, new_color, thread_id, thread_type, **kwargs): - if old_thread_id == thread_id and old_color != new_color: + def on_color_change(self, author_id, new_color, thread, **kwargs): + if old_thread_id == thread.id and old_color != new_color: print( "{} 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): - if old_thread_id == thread_id and new_emoji != old_emoji: + def on_emoji_change(self, author_id, new_emoji, thread, **kwargs): + if old_thread_id == thread.id and new_emoji != old_emoji: print( "{} 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): - if old_thread_id == thread_id and author_id != session.user_id: + def on_people_added(self, added_ids, author_id, thread, **kwargs): + if old_thread_id == thread.id and author_id != session.user_id: print("{} got added. They will be removed".format(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 if ( - old_thread_id == thread_id + old_thread_id == thread.id and removed_id != session.user_id and author_id != session.user_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): - if old_thread_id == thread_id and old_title != new_title: + def on_title_change(self, author_id, new_title, thread, **kwargs): + if old_thread_id == thread.id and old_title != new_title: print( "{} changed the thread title. It will be changed back".format(author_id) ) - self.change_thread_title( - old_title, thread_id=thread_id, thread_type=thread_type - ) + thread.set_title(old_title) 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 ( - old_thread_id == thread_id + old_thread_id == thread.id and changed_for in old_nicknames and old_nicknames[changed_for] != new_nickname ): @@ -68,11 +66,8 @@ class KeepBot(fbchat.Client): author_id, changed_for ) ) - self.change_nickname( - old_nicknames[changed_for], - changed_for, - thread_id=thread_id, - thread_type=thread_type, + thread.set_nickname( + changed_for, old_nicknames[changed_for], ) diff --git a/examples/removebot.py b/examples/removebot.py index 4bb6d57..984ed98 100644 --- a/examples/removebot.py +++ b/examples/removebot.py @@ -2,21 +2,17 @@ import fbchat 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 - if ( - message_object.text == "Remove me!" - and thread_type == fbchat.ThreadType.GROUP - ): - print("{} will be removed from {}".format(author_id, thread_id)) - self.remove_user_from_group(author_id, thread_id=thread_id) + if message_object.text == "Remove me!" and isinstance(thread, fbchat.Group): + print("{} will be removed from {}".format(author_id, thread)) + thread.remove_participant(author_id) else: # Sends the data to the inherited on_message, so that we can still see when a message is recieved super(RemoveBot, self).on_message( author_id=author_id, message_object=message_object, - thread_id=thread_id, - thread_type=thread_type, + thread=thread, **kwargs, ) diff --git a/fbchat/__init__.py b/fbchat/__init__.py index 01b27c6..b6a6548 100644 --- a/fbchat/__init__.py +++ b/fbchat/__init__.py @@ -14,7 +14,7 @@ from . import _core, _util from ._core import Image from ._exception import FBchatException, FBchatFacebookError from ._session import Session -from ._thread import ThreadType, ThreadLocation, ThreadColor, ThreadABC, Thread +from ._thread import ThreadLocation, ThreadColor, ThreadABC, Thread from ._user import TypingStatus, User, ActiveStatus from ._group import Group from ._page import Page diff --git a/fbchat/_client.py b/fbchat/_client.py index 309aad2..06e63df 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -8,7 +8,7 @@ from ._core import log from . import _util, _graphql, _session from ._exception import FBchatException, FBchatFacebookError -from ._thread import ThreadType, ThreadLocation, ThreadColor +from ._thread import ThreadLocation, ThreadColor from ._user import TypingStatus, User, ActiveStatus from ._group import Group from ._page import Page @@ -358,7 +358,6 @@ class Client: if k["type"] in ["user", "friend"]: entries[_id] = { "id": _id, - "type": ThreadType.USER, "url": k.get("uri"), "first_name": k.get("firstName"), "is_viewer_friend": k.get("is_friend"), @@ -369,7 +368,6 @@ class Client: elif k["type"] == "page": entries[_id] = { "id": _id, - "type": ThreadType.PAGE, "url": k.get("uri"), "profile_picture": {"uri": k.get("thumbSrc")}, "name": k.get("name"), @@ -510,7 +508,7 @@ class Client: if pages_and_users.get(_id) is None: raise FBchatException("Could not fetch thread {}".format(_id)) entry.update(pages_and_users[_id]) - if entry["type"] == ThreadType.USER: + if "first_name" in entry["type"]: rtn[_id] = User._from_graphql(self.session, entry) else: rtn[_id] = Page._from_graphql(self.session, entry) @@ -760,8 +758,6 @@ class Client: poll_id: ID of the poll to update vote option_ids: List of the option IDs to vote new_options: List of the new option names - thread_id: User/Group ID to change status in. See :ref:`intro_threads` - thread_type (ThreadType): See :ref:`intro_threads` Raises: FBchatException: If request failed @@ -981,17 +977,14 @@ class Client: return j def _parse_delta(self, m): - def get_thread_id_and_thread_type(msg_metadata): - """Return a tuple consisting of thread ID and thread type.""" - id_thread = None - type_thread = None - if "threadFbId" in msg_metadata["threadKey"]: - id_thread = str(msg_metadata["threadKey"]["threadFbId"]) - type_thread = ThreadType.GROUP - elif "otherUserFbId" in msg_metadata["threadKey"]: - id_thread = str(msg_metadata["threadKey"]["otherUserFbId"]) - type_thread = ThreadType.USER - return id_thread, type_thread + def get_thread(data): + if "threadFbId" in data["threadKey"]: + group_id = str(data["threadKey"]["threadFbId"]) + return Group(session=self.session, id=group_id) + elif "otherUserFbId" in data["threadKey"]: + user_id = str(data["threadKey"]["otherUserFbId"]) + return User(session=self.session, id=user_id) + return None delta = m["delta"] delta_type = delta.get("type") @@ -1006,12 +999,11 @@ class Client: # Added participants if "addedParticipants" in delta: added_ids = [str(x["userFbId"]) for x in delta["addedParticipants"]] - thread_id = str(metadata["threadKey"]["threadFbId"]) self.on_people_added( mid=mid, added_ids=added_ids, author_id=author_id, - thread_id=thread_id, + group=get_thread(metadata), at=at, msg=m, ) @@ -1019,12 +1011,11 @@ class Client: # Left/removed participants elif "leftParticipantFbId" in delta: removed_id = str(delta["leftParticipantFbId"]) - thread_id = str(metadata["threadKey"]["threadFbId"]) self.on_person_removed( mid=mid, removed_id=removed_id, author_id=author_id, - thread_id=thread_id, + group=get_thread(metadata), at=at, msg=m, ) @@ -1032,13 +1023,12 @@ class Client: # Color change elif delta_type == "change_thread_theme": new_color = ThreadColor._from_graphql(delta["untypedData"]["theme_color"]) - thread_id, thread_type = get_thread_id_and_thread_type(metadata) + thread = get_thread(metadata) self.on_color_change( mid=mid, author_id=author_id, new_color=new_color, - thread_id=thread_id, - thread_type=thread_type, + thread=get_thread(metadata), at=at, metadata=metadata, msg=m, @@ -1047,13 +1037,11 @@ class Client: # Emoji change elif delta_type == "change_thread_icon": new_emoji = delta["untypedData"]["thread_icon"] - thread_id, thread_type = get_thread_id_and_thread_type(metadata) self.on_emoji_change( mid=mid, author_id=author_id, new_emoji=new_emoji, - thread_id=thread_id, - thread_type=thread_type, + thread=get_thread(metadata), at=at, metadata=metadata, msg=m, @@ -1062,13 +1050,11 @@ class Client: # Thread title change elif delta_class == "ThreadName": new_title = delta["name"] - thread_id, thread_type = get_thread_id_and_thread_type(metadata) self.on_title_change( mid=mid, author_id=author_id, new_title=new_title, - thread_id=thread_id, - thread_type=thread_type, + thread=get_thread(metadata), at=at, metadata=metadata, msg=m, @@ -1080,9 +1066,8 @@ class Client: if mid is None: self.on_unknown_messsage_type(msg=m) else: - thread_id = str(delta["threadKey"]["threadFbId"]) - thread = Thread(session=self.session, id=thread_id) - fetch_info = thread._forced_fetch(mid) + group = get_thread(metadata) + fetch_info = group._forced_fetch(mid) fetch_data = fetch_info["message"] author_id = fetch_data["message_sender"]["id"] at = _util.millis_to_datetime(int(fetch_data["timestamp_precise"])) @@ -1098,8 +1083,7 @@ class Client: mid=mid, author_id=author_id, new_image=image_id, - thread_id=thread_id, - thread_type=ThreadType.GROUP, + group=group, at=at, msg=m, ) @@ -1108,14 +1092,12 @@ class Client: elif delta_type == "change_thread_nickname": changed_for = str(delta["untypedData"]["participant_id"]) new_nickname = delta["untypedData"]["nickname"] - thread_id, thread_type = get_thread_id_and_thread_type(metadata) self.on_nickname_change( mid=mid, author_id=author_id, changed_for=changed_for, new_nickname=new_nickname, - thread_id=thread_id, - thread_type=thread_type, + thread=get_thread(metadata), at=at, metadata=metadata, msg=m, @@ -1123,7 +1105,6 @@ class Client: # Admin added or removed in a group thread elif delta_type == "change_thread_admins": - thread_id, thread_type = get_thread_id_and_thread_type(metadata) target_id = delta["untypedData"]["TARGET_ID"] admin_event = delta["untypedData"]["ADMIN_EVENT"] if admin_event == "add_admin": @@ -1131,8 +1112,7 @@ class Client: mid=mid, added_id=target_id, author_id=author_id, - thread_id=thread_id, - thread_type=thread_type, + thread=get_thread(metadata), at=at, msg=m, ) @@ -1141,22 +1121,19 @@ class Client: mid=mid, removed_id=target_id, author_id=author_id, - thread_id=thread_id, - thread_type=thread_type, + thread=get_thread(metadata), at=at, msg=m, ) # Group approval mode change elif delta_type == "change_thread_approval_mode": - thread_id, thread_type = get_thread_id_and_thread_type(metadata) approval_mode = bool(int(delta["untypedData"]["APPROVAL_MODE"])) self.on_approval_mode_change( mid=mid, approval_mode=approval_mode, author_id=author_id, - thread_id=thread_id, - thread_type=thread_type, + thread=get_thread(metadata), at=at, msg=m, ) @@ -1168,12 +1145,10 @@ class Client: delta.get("actorFbId") or delta["threadKey"]["otherUserFbId"] ) at = _util.millis_to_datetime(int(delta["deliveredWatermarkTimestampMs"])) - thread_id, thread_type = get_thread_id_and_thread_type(delta) self.on_message_delivered( msg_ids=message_ids, delivered_for=delivered_for, - thread_id=thread_id, - thread_type=thread_type, + thread=get_thread(metadata), at=at, metadata=metadata, msg=m, @@ -1184,11 +1159,9 @@ class Client: seen_by = str(delta.get("actorFbId") or delta["threadKey"]["otherUserFbId"]) seen_at = _util.millis_to_datetime(int(delta["actionTimestampMs"])) at = _util.millis_to_datetime(int(delta["watermarkTimestampMs"])) - thread_id, thread_type = get_thread_id_and_thread_type(delta) self.on_message_seen( seen_by=seen_by, - thread_id=thread_id, - thread_type=thread_type, + thread=get_thread(metadata), seen_at=seen_at, at=at, metadata=metadata, @@ -1208,11 +1181,9 @@ class Client: threads = [] if "folders" not in delta: threads = [ - get_thread_id_and_thread_type({"threadKey": thr}) - for thr in delta.get("threadKeys") + get_thread({"threadKey": thr}) for thr in delta.get("threadKeys") ] - # thread_id, thread_type = get_thread_id_and_thread_type(delta) self.on_marked_seen( threads=threads, seen_at=seen_at, at=at, metadata=delta, msg=m ) @@ -1227,7 +1198,6 @@ class Client: leaderboard = delta["untypedData"].get("leaderboard") if leaderboard is not None: leaderboard = json.loads(leaderboard)["scores"] - thread_id, thread_type = get_thread_id_and_thread_type(metadata) self.on_game_played( mid=mid, author_id=author_id, @@ -1235,8 +1205,7 @@ class Client: game_name=game_name, score=score, leaderboard=leaderboard, - thread_id=thread_id, - thread_type=thread_type, + thread=get_thread(metadata), at=at, metadata=metadata, msg=m, @@ -1244,7 +1213,6 @@ class Client: # Group call started/ended elif delta_type == "rtc_call_log": - thread_id, thread_type = get_thread_id_and_thread_type(metadata) call_status = delta["untypedData"]["event"] call_duration = _util.seconds_to_timedelta( int(delta["untypedData"]["call_duration"]) @@ -1255,8 +1223,7 @@ class Client: mid=mid, caller_id=author_id, is_video_call=is_video_call, - thread_id=thread_id, - thread_type=thread_type, + thread=get_thread(metadata), at=at, metadata=metadata, msg=m, @@ -1267,8 +1234,7 @@ class Client: caller_id=author_id, is_video_call=is_video_call, call_duration=call_duration, - thread_id=thread_id, - thread_type=thread_type, + thread=get_thread(metadata), at=at, metadata=metadata, msg=m, @@ -1276,14 +1242,12 @@ class Client: # User joined to group call elif delta_type == "participant_joined_group_call": - thread_id, thread_type = get_thread_id_and_thread_type(metadata) is_video_call = bool(int(delta["untypedData"]["group_call_type"])) self.on_user_joined_call( mid=mid, joined_id=author_id, is_video_call=is_video_call, - thread_id=thread_id, - thread_type=thread_type, + thread=get_thread(metadata), at=at, metadata=metadata, msg=m, @@ -1291,7 +1255,6 @@ class Client: # Group poll event elif delta_type == "group_poll": - thread_id, thread_type = get_thread_id_and_thread_type(metadata) event_type = delta["untypedData"]["event_type"] poll_json = json.loads(delta["untypedData"]["question_json"]) poll = Poll._from_graphql(poll_json) @@ -1301,8 +1264,7 @@ class Client: mid=mid, poll=poll, author_id=author_id, - thread_id=thread_id, - thread_type=thread_type, + thread=get_thread(metadata), at=at, metadata=metadata, msg=m, @@ -1317,8 +1279,7 @@ class Client: added_options=added_options, removed_options=removed_options, author_id=author_id, - thread_id=thread_id, - thread_type=thread_type, + thread=get_thread(metadata), at=at, metadata=metadata, msg=m, @@ -1326,13 +1287,11 @@ class Client: # Plan created elif delta_type == "lightweight_event_create": - thread_id, thread_type = get_thread_id_and_thread_type(metadata) self.on_plan_created( mid=mid, plan=Plan._from_pull(delta["untypedData"]), author_id=author_id, - thread_id=thread_id, - thread_type=thread_type, + thread=get_thread(metadata), at=at, metadata=metadata, msg=m, @@ -1340,12 +1299,10 @@ class Client: # Plan ended elif delta_type == "lightweight_event_notify": - thread_id, thread_type = get_thread_id_and_thread_type(metadata) self.on_plan_ended( mid=mid, plan=Plan._from_pull(delta["untypedData"]), - thread_id=thread_id, - thread_type=thread_type, + thread=get_thread(metadata), at=at, metadata=metadata, msg=m, @@ -1353,13 +1310,11 @@ class Client: # Plan edited elif delta_type == "lightweight_event_update": - thread_id, thread_type = get_thread_id_and_thread_type(metadata) self.on_plan_edited( mid=mid, plan=Plan._from_pull(delta["untypedData"]), author_id=author_id, - thread_id=thread_id, - thread_type=thread_type, + thread=get_thread(metadata), at=at, metadata=metadata, msg=m, @@ -1367,13 +1322,11 @@ class Client: # Plan deleted elif delta_type == "lightweight_event_delete": - thread_id, thread_type = get_thread_id_and_thread_type(metadata) self.on_plan_deleted( mid=mid, plan=Plan._from_pull(delta["untypedData"]), author_id=author_id, - thread_id=thread_id, - thread_type=thread_type, + thread=get_thread(metadata), at=at, metadata=metadata, msg=m, @@ -1381,15 +1334,13 @@ class Client: # Plan participation change elif delta_type == "lightweight_event_rsvp": - thread_id, thread_type = get_thread_id_and_thread_type(metadata) take_part = delta["untypedData"]["guest_status"] == "GOING" self.on_plan_participation( mid=mid, plan=Plan._from_pull(delta["untypedData"]), take_part=take_part, author_id=author_id, - thread_id=thread_id, - thread_type=thread_type, + thread=get_thread(metadata), at=at, metadata=metadata, msg=m, @@ -1404,7 +1355,6 @@ class Client: # Message reaction if d.get("deltaMessageReaction"): i = d["deltaMessageReaction"] - thread_id, thread_type = get_thread_id_and_thread_type(i) mid = i["messageId"] author_id = str(i["userId"]) reaction = ( @@ -1416,8 +1366,7 @@ class Client: mid=mid, reaction=reaction, author_id=author_id, - thread_id=thread_id, - thread_type=thread_type, + thread=get_thread(metadata), at=at, msg=m, ) @@ -1425,8 +1374,7 @@ class Client: self.on_reaction_removed( mid=mid, author_id=author_id, - thread_id=thread_id, - thread_type=thread_type, + thread=get_thread(metadata), at=at, msg=m, ) @@ -1434,7 +1382,6 @@ class Client: # Viewer status change elif d.get("deltaChangeViewerStatus"): i = d["deltaChangeViewerStatus"] - thread_id, thread_type = get_thread_id_and_thread_type(i) author_id = str(i["actorFbid"]) reason = i["reason"] can_reply = i["canViewerReply"] @@ -1442,16 +1389,14 @@ class Client: if can_reply: self.on_unblock( author_id=author_id, - thread_id=thread_id, - thread_type=thread_type, + thread=get_thread(metadata), at=at, msg=m, ) else: self.on_block( author_id=author_id, - thread_id=thread_id, - thread_type=thread_type, + thread=get_thread(metadata), at=at, msg=m, ) @@ -1459,7 +1404,6 @@ class Client: # Live location info elif d.get("liveLocationData"): i = d["liveLocationData"] - thread_id, thread_type = get_thread_id_and_thread_type(i) for l in i["messageLiveLocations"]: mid = l["messageId"] author_id = str(l["senderId"]) @@ -1468,8 +1412,7 @@ class Client: mid=mid, location=location, author_id=author_id, - thread_id=thread_id, - thread_type=thread_type, + thread=get_thread(metadata), at=at, msg=m, ) @@ -1477,15 +1420,13 @@ class Client: # Message deletion elif d.get("deltaRecallMessageData"): i = d["deltaRecallMessageData"] - thread_id, thread_type = get_thread_id_and_thread_type(i) mid = i["messageID"] at = _util.millis_to_datetime(i["deletionTimestamp"]) author_id = str(i["senderID"]) self.on_message_unsent( mid=mid, author_id=author_id, - thread_id=thread_id, - thread_type=thread_type, + thread=get_thread(metadata), at=at, msg=m, ) @@ -1493,7 +1434,6 @@ class Client: elif d.get("deltaMessageReply"): i = d["deltaMessageReply"] metadata = i["message"]["messageMetadata"] - thread_id, thread_type = get_thread_id_and_thread_type(metadata) replied_to = Message._from_reply( self.session, i["repliedToMessage"] ) @@ -1504,8 +1444,7 @@ class Client: mid=message.id, author_id=message.author, message_object=message, - thread_id=thread_id, - thread_type=thread_type, + thread=get_thread(metadata), at=message.created_at, metadata=metadata, msg=m, @@ -1513,7 +1452,6 @@ class Client: # New message elif delta.get("class") == "NewMessage": - thread_id, thread_type = get_thread_id_and_thread_type(metadata) self.on_message( mid=mid, author_id=author_id, @@ -1525,8 +1463,7 @@ class Client: author=author_id, created_at=at, ), - thread_id=thread_id, - thread_type=thread_type, + thread=get_thread(metadata), at=at, metadata=metadata, msg=m, @@ -1574,21 +1511,16 @@ class Client: author_id = str(m.get("from")) thread_id = m.get("thread_fbid") if thread_id: - thread_type = ThreadType.GROUP - thread_id = str(thread_id) + thread = Group(session=self.session, id=str(thread_id)) else: - thread_type = ThreadType.USER - if author_id == self._session.user_id: + if author_id == self.session.user_id: thread_id = m.get("to") else: thread_id = author_id + thread = User(session=self.session, id=thread_id) typing_status = TypingStatus(m.get("st")) self.on_typing( - author_id=author_id, - status=typing_status, - thread_id=thread_id, - thread_type=thread_type, - msg=m, + author_id=author_id, status=typing_status, thread=thread, msg=m, ) # Delivered @@ -1742,8 +1674,7 @@ class Client: mid=None, author_id=None, message_object=None, - thread_id=None, - thread_type=ThreadType.USER, + thread=None, at=None, metadata=None, msg=None, @@ -1754,21 +1685,19 @@ class Client: mid: The message ID author_id: The ID of the author message_object (Message): The message (As a `Message` object) - thread_id: Thread ID that the message was sent to. See :ref:`intro_threads` - thread_type (ThreadType): Type of thread that the message was sent to. See :ref:`intro_threads` + thread: Thread that the message was sent to. See :ref:`intro_threads` at (datetime.datetime): When the message was sent metadata: Extra metadata about the message msg: A full set of the data received """ - log.info("{} from {} in {}".format(message_object, thread_id, thread_type.name)) + log.info("{} from {} in {}".format(message_object, author_id, thread)) def on_color_change( self, mid=None, author_id=None, new_color=None, - thread_id=None, - thread_type=ThreadType.USER, + thread=None, at=None, metadata=None, msg=None, @@ -1779,25 +1708,19 @@ class Client: mid: The action ID author_id: The ID of the person who changed the color new_color (ThreadColor): The new color - thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` - thread_type (ThreadType): Type of thread that the action was sent to. See :ref:`intro_threads` + thread: Thread that the action was sent to. See :ref:`intro_threads` at (datetime.datetime): When the action was executed metadata: Extra metadata about the action msg: A full set of the data received """ - log.info( - "Color change from {} in {} ({}): {}".format( - author_id, thread_id, thread_type.name, new_color - ) - ) + log.info("Color change from {} in {}: {}".format(author_id, thread, new_color)) def on_emoji_change( self, mid=None, author_id=None, new_emoji=None, - thread_id=None, - thread_type=ThreadType.USER, + thread=None, at=None, metadata=None, msg=None, @@ -1808,25 +1731,19 @@ class Client: mid: The action ID author_id: The ID of the person who changed the emoji new_emoji: The new emoji - thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` - thread_type (ThreadType): Type of thread that the action was sent to. See :ref:`intro_threads` + thread: Thread that the action was sent to. See :ref:`intro_threads` at (datetime.datetime): When the action was executed metadata: Extra metadata about the action msg: A full set of the data received """ - log.info( - "Emoji change from {} in {} ({}): {}".format( - author_id, thread_id, thread_type.name, new_emoji - ) - ) + log.info("Emoji change from {} in {}: {}".format(author_id, thread, new_emoji)) def on_title_change( self, mid=None, author_id=None, new_title=None, - thread_id=None, - thread_type=ThreadType.USER, + group=None, at=None, metadata=None, msg=None, @@ -1837,27 +1754,15 @@ class Client: mid: The action ID author_id: The ID of the person who changed the title new_title: The new title - thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` - thread_type (ThreadType): Type of thread that the action was sent to. See :ref:`intro_threads` + group: Group that the action was sent to. See :ref:`intro_threads` at (datetime.datetime): When the action was executed metadata: Extra metadata about the action msg: A full set of the data received """ - log.info( - "Title change from {} in {} ({}): {}".format( - author_id, thread_id, thread_type.name, new_title - ) - ) + log.info("Title change from {} in {}: {}".format(author_id, group, new_title)) def on_image_change( - self, - mid=None, - author_id=None, - new_image=None, - thread_id=None, - thread_type=ThreadType.GROUP, - at=None, - msg=None, + self, mid=None, author_id=None, new_image=None, group=None, at=None, msg=None, ): """Called when the client is listening, and somebody changes a thread's image. @@ -1865,12 +1770,11 @@ class Client: mid: The action ID author_id: The ID of the person who changed the image new_image: The ID of the new image - thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` - thread_type (ThreadType): Type of thread that the action was sent to. See :ref:`intro_threads` + group: Group that the action was sent to. See :ref:`intro_threads` at (datetime.datetime): When the action was executed msg: A full set of the data received """ - log.info("{} changed thread image in {}".format(author_id, thread_id)) + log.info("{} changed group image in {}".format(author_id, group)) def on_nickname_change( self, @@ -1878,8 +1782,7 @@ class Client: author_id=None, changed_for=None, new_nickname=None, - thread_id=None, - thread_type=ThreadType.USER, + thread=None, at=None, metadata=None, msg=None, @@ -1891,27 +1794,19 @@ class Client: author_id: The ID of the person who changed the nickname changed_for: The ID of the person whom got their nickname changed new_nickname: The new nickname - thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` - thread_type (ThreadType): Type of thread that the action was sent to. See :ref:`intro_threads` + thread: Thread that the action was sent to. See :ref:`intro_threads` at (datetime.datetime): When the action was executed metadata: Extra metadata about the action msg: A full set of the data received """ log.info( - "Nickname change from {} in {} ({}) for {}: {}".format( - author_id, thread_id, thread_type.name, changed_for, new_nickname + "Nickname change from {} in {} for {}: {}".format( + author_id, thread, changed_for, new_nickname ) ) def on_admin_added( - self, - mid=None, - added_id=None, - author_id=None, - thread_id=None, - thread_type=ThreadType.GROUP, - at=None, - msg=None, + self, mid=None, added_id=None, author_id=None, group=None, at=None, msg=None, ): """Called when the client is listening, and somebody adds an admin to a group. @@ -1919,21 +1814,14 @@ class Client: mid: The action ID added_id: The ID of the admin who got added author_id: The ID of the person who added the admins - thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` + group: Group that the action was sent to. See :ref:`intro_threads` at (datetime.datetime): When the action was executed msg: A full set of the data received """ - log.info("{} added admin: {} in {}".format(author_id, added_id, thread_id)) + log.info("{} added admin: {} in {}".format(author_id, added_id, group)) def on_admin_removed( - self, - mid=None, - removed_id=None, - author_id=None, - thread_id=None, - thread_type=ThreadType.GROUP, - at=None, - msg=None, + self, mid=None, removed_id=None, author_id=None, group=None, at=None, msg=None, ): """Called when the client is listening, and somebody is removed as an admin in a group. @@ -1941,19 +1829,18 @@ class Client: mid: The action ID removed_id: The ID of the admin who got removed author_id: The ID of the person who removed the admins - thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` + group: Group that the action was sent to. See :ref:`intro_threads` at (datetime.datetime): When the action was executed msg: A full set of the data received """ - log.info("{} removed admin: {} in {}".format(author_id, removed_id, thread_id)) + log.info("{} removed admin: {} in {}".format(author_id, removed_id, group)) def on_approval_mode_change( self, mid=None, approval_mode=None, author_id=None, - thread_id=None, - thread_type=ThreadType.GROUP, + group=None, at=None, msg=None, ): @@ -1963,48 +1850,35 @@ class Client: mid: The action ID approval_mode: True if approval mode is activated author_id: The ID of the person who changed approval mode - thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` + group: Group that the action was sent to. See :ref:`intro_threads` at (datetime.datetime): When the action was executed msg: A full set of the data received """ if approval_mode: - log.info("{} activated approval mode in {}".format(author_id, thread_id)) + log.info("{} activated approval mode in {}".format(author_id, group)) else: - log.info("{} disabled approval mode in {}".format(author_id, thread_id)) + log.info("{} disabled approval mode in {}".format(author_id, group)) def on_message_seen( - self, - seen_by=None, - thread_id=None, - thread_type=ThreadType.USER, - seen_at=None, - at=None, - metadata=None, - msg=None, + self, seen_by=None, thread=None, seen_at=None, at=None, metadata=None, msg=None, ): """Called when the client is listening, and somebody marks a message as seen. Args: seen_by: The ID of the person who marked the message as seen - thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` - thread_type (ThreadType): Type of thread that the action was sent to. See :ref:`intro_threads` + thread: Thread that the action was sent to. See :ref:`intro_threads` seen_at (datetime.datetime): When the person saw the message at (datetime.datetime): When the action was executed metadata: Extra metadata about the action msg: A full set of the data received """ - log.info( - "Messages seen by {} in {} ({}) at {}".format( - seen_by, thread_id, thread_type.name, seen_at - ) - ) + log.info("Messages seen by {} in {} at {}".format(seen_by, thread, seen_at)) def on_message_delivered( self, msg_ids=None, delivered_for=None, - thread_id=None, - thread_type=ThreadType.USER, + thread=None, at=None, metadata=None, msg=None, @@ -2014,15 +1888,14 @@ class Client: Args: msg_ids: The messages that are marked as delivered delivered_for: The person that marked the messages as delivered - thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` - thread_type (ThreadType): Type of thread that the action was sent to. See :ref:`intro_threads` + thread: Thread that the action was sent to. See :ref:`intro_threads` at (datetime.datetime): When the action was executed metadata: Extra metadata about the action msg: A full set of the data received """ log.info( - "Messages {} delivered to {} in {} ({}) at {}".format( - msg_ids, delivered_for, thread_id, thread_type.name, at + "Messages {} delivered to {} in {} at {}".format( + msg_ids, delivered_for, thread, at ) ) @@ -2039,45 +1912,28 @@ class Client: metadata: Extra metadata about the action msg: A full set of the data received """ - log.info( - "Marked messages as seen in threads {} at {}".format( - [(x[0], x[1].name) for x in threads], seen_at - ) - ) + log.info("Marked messages as seen in threads {} at {}".format(threads, seen_at)) def on_message_unsent( - self, - mid=None, - author_id=None, - thread_id=None, - thread_type=None, - at=None, - msg=None, + self, mid=None, author_id=None, thread=None, at=None, msg=None, ): """Called when the client is listening, and someone unsends (deletes for everyone) a message. Args: mid: ID of the unsent message author_id: The ID of the person who unsent the message - thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` - thread_type (ThreadType): Type of thread that the action was sent to. See :ref:`intro_threads` + thread: Thread that the action was sent to. See :ref:`intro_threads` at (datetime.datetime): When the action was executed msg: A full set of the data received """ log.info( - "{} unsent the message {} in {} ({}) at {}".format( - author_id, repr(mid), thread_id, thread_type.name, at + "{} unsent the message {} in {} at {}".format( + author_id, repr(mid), thread, at ) ) def on_people_added( - self, - mid=None, - added_ids=None, - author_id=None, - thread_id=None, - at=None, - msg=None, + self, mid=None, added_ids=None, author_id=None, group=None, at=None, msg=None, ): """Called when the client is listening, and somebody adds people to a group thread. @@ -2085,22 +1941,14 @@ class Client: mid: The action ID added_ids: The IDs of the people who got added author_id: The ID of the person who added the people - thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` + group: Group that the action was sent to. See :ref:`intro_threads` at (datetime.datetime): When the action was executed msg: A full set of the data received """ - log.info( - "{} added: {} in {}".format(author_id, ", ".join(added_ids), thread_id) - ) + log.info("{} added: {} in {}".format(author_id, ", ".join(added_ids), group)) def on_person_removed( - self, - mid=None, - removed_id=None, - author_id=None, - thread_id=None, - at=None, - msg=None, + self, mid=None, removed_id=None, author_id=None, group=None, at=None, msg=None, ): """Called when the client is listening, and somebody removes a person from a group thread. @@ -2108,11 +1956,11 @@ class Client: mid: The action ID removed_id: The ID of the person who got removed author_id: The ID of the person who removed the person - thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` + group: Group that the action was sent to. See :ref:`intro_threads` at (datetime.datetime): When the action was executed msg: A full set of the data received """ - log.info("{} removed: {} in {}".format(author_id, removed_id, thread_id)) + log.info("{} removed: {} in {}".format(author_id, removed_id, group)) def on_friend_request(self, from_id=None, msg=None): """Called when the client is listening, and somebody sends a friend request. @@ -2136,16 +1984,13 @@ class Client: """ log.info("Inbox event: {}, {}, {}".format(unseen, unread, recent_unread)) - def on_typing( - self, author_id=None, status=None, thread_id=None, thread_type=None, msg=None - ): + def on_typing(self, author_id=None, status=None, thread=None, msg=None): """Called when the client is listening, and somebody starts or stops typing into a chat. Args: author_id: The ID of the person who sent the action status (TypingStatus): The typing status - thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` - thread_type (ThreadType): Type of thread that the action was sent to. See :ref:`intro_threads` + thread: Thread that the action was sent to. See :ref:`intro_threads` msg: A full set of the data received """ pass @@ -2158,8 +2003,7 @@ class Client: game_name=None, score=None, leaderboard=None, - thread_id=None, - thread_type=None, + thread=None, at=None, metadata=None, msg=None, @@ -2173,27 +2017,15 @@ class Client: game_name: Name of the game score: Score obtained in the game leaderboard: Actual leader board of the game in the thread - thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` - thread_type (ThreadType): Type of thread that the action was sent to. See :ref:`intro_threads` + thread: Thread that the action was sent to. See :ref:`intro_threads` at (datetime.datetime): When the action was executed metadata: Extra metadata about the action msg: A full set of the data received """ - log.info( - '{} played "{}" in {} ({})'.format( - author_id, game_name, thread_id, thread_type.name - ) - ) + log.info('{} played "{}" in {}'.format(author_id, game_name, thread)) def on_reaction_added( - self, - mid=None, - reaction=None, - author_id=None, - thread_id=None, - thread_type=None, - at=None, - msg=None, + self, mid=None, reaction=None, author_id=None, thread=None, at=None, msg=None, ): """Called when the client is listening, and somebody reacts to a message. @@ -2202,83 +2034,56 @@ class Client: reaction (MessageReaction): Reaction add_reaction: Whether user added or removed reaction author_id: The ID of the person who reacted to the message - thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` - thread_type (ThreadType): Type of thread that the action was sent to. See :ref:`intro_threads` + thread: Thread that the action was sent to. See :ref:`intro_threads` at (datetime.datetime): When the action was executed msg: A full set of the data received """ log.info( - "{} reacted to message {} with {} in {} ({})".format( - author_id, mid, reaction.name, thread_id, thread_type.name + "{} reacted to message {} with {} in {}".format( + author_id, mid, reaction.name, thread ) ) def on_reaction_removed( - self, - mid=None, - author_id=None, - thread_id=None, - thread_type=None, - at=None, - msg=None, + self, mid=None, author_id=None, thread=None, at=None, msg=None, ): """Called when the client is listening, and somebody removes reaction from a message. Args: mid: Message ID, that user reacted to author_id: The ID of the person who removed reaction - thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` - thread_type (ThreadType): Type of thread that the action was sent to. See :ref:`intro_threads` + thread: Thread that the action was sent to. See :ref:`intro_threads` at (datetime.datetime): When the action was executed msg: A full set of the data received """ log.info( - "{} removed reaction from {} message in {} ({})".format( - author_id, mid, thread_id, thread_type - ) + "{} removed reaction from {} message in {}".format(author_id, mid, thread) ) - def on_block( - self, author_id=None, thread_id=None, thread_type=None, at=None, msg=None - ): + def on_block(self, author_id=None, thread=None, at=None, msg=None): """Called when the client is listening, and somebody blocks client. Args: author_id: The ID of the person who blocked - thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` - thread_type (ThreadType): Type of thread that the action was sent to. See :ref:`intro_threads` + thread: Thread that the action was sent to. See :ref:`intro_threads` at (datetime.datetime): When the action was executed msg: A full set of the data received """ - log.info( - "{} blocked {} ({}) thread".format(author_id, thread_id, thread_type.name) - ) + log.info("{} blocked {}".format(author_id, thread)) - def on_unblock( - self, author_id=None, thread_id=None, thread_type=None, at=None, msg=None - ): + def on_unblock(self, author_id=None, thread=None, at=None, msg=None): """Called when the client is listening, and somebody blocks client. Args: author_id: The ID of the person who unblocked - thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` - thread_type (ThreadType): Type of thread that the action was sent to. See :ref:`intro_threads` + thread: Thread that the action was sent to. See :ref:`intro_threads` at (datetime.datetime): When the action was executed msg: A full set of the data received """ - log.info( - "{} unblocked {} ({}) thread".format(author_id, thread_id, thread_type.name) - ) + log.info("{} unblocked {}".format(author_id, thread)) def on_live_location( - self, - mid=None, - location=None, - author_id=None, - thread_id=None, - thread_type=None, - at=None, - msg=None, + self, mid=None, location=None, author_id=None, thread=None, at=None, msg=None, ): """Called when the client is listening and somebody sends live location info. @@ -2286,14 +2091,13 @@ class Client: mid: The action ID location (LiveLocationAttachment): Sent location info author_id: The ID of the person who sent location info - thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` - thread_type (ThreadType): Type of thread that the action was sent to. See :ref:`intro_threads` + thread: Thread that the action was sent to. See :ref:`intro_threads` at (datetime.datetime): When the action was executed msg: A full set of the data received """ log.info( - "{} sent live location info in {} ({}) with latitude {} and longitude {}".format( - author_id, thread_id, thread_type, location.latitude, location.longitude + "{} sent live location info in {} with latitude {} and longitude {}".format( + author_id, thread, location.latitude, location.longitude ) ) @@ -2302,8 +2106,7 @@ class Client: mid=None, caller_id=None, is_video_call=None, - thread_id=None, - thread_type=None, + thread=None, at=None, metadata=None, msg=None, @@ -2317,15 +2120,12 @@ class Client: mid: The action ID caller_id: The ID of the person who started the call is_video_call: True if it's video call - thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` - thread_type (ThreadType): Type of thread that the action was sent to. See :ref:`intro_threads` + thread: Thread that the action was sent to. See :ref:`intro_threads` at (datetime.datetime): When the action was executed metadata: Extra metadata about the action msg: A full set of the data received """ - log.info( - "{} started call in {} ({})".format(caller_id, thread_id, thread_type.name) - ) + log.info("{} started call in {}".format(caller_id, thread)) def on_call_ended( self, @@ -2333,8 +2133,7 @@ class Client: caller_id=None, is_video_call=None, call_duration=None, - thread_id=None, - thread_type=None, + thread=None, at=None, metadata=None, msg=None, @@ -2349,23 +2148,19 @@ class Client: caller_id: The ID of the person who ended the call is_video_call: True if it was video call call_duration (datetime.timedelta): Call duration - thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` - thread_type (ThreadType): Type of thread that the action was sent to. See :ref:`intro_threads` + thread: Thread that the action was sent to. See :ref:`intro_threads` at (datetime.datetime): When the action was executed metadata: Extra metadata about the action msg: A full set of the data received """ - log.info( - "{} ended call in {} ({})".format(caller_id, thread_id, thread_type.name) - ) + log.info("{} ended call in {}".format(caller_id, thread)) def on_user_joined_call( self, mid=None, joined_id=None, is_video_call=None, - thread_id=None, - thread_type=None, + thread=None, at=None, metadata=None, msg=None, @@ -2376,23 +2171,19 @@ class Client: mid: The action ID joined_id: The ID of the person who joined the call is_video_call: True if it's video call - thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` - thread_type (ThreadType): Type of thread that the action was sent to. See :ref:`intro_threads` + thread: Thread that the action was sent to. See :ref:`intro_threads` at (datetime.datetime): When the action was executed metadata: Extra metadata about the action msg: A full set of the data received """ - log.info( - "{} joined call in {} ({})".format(joined_id, thread_id, thread_type.name) - ) + log.info("{} joined call in {}".format(joined_id, thread)) def on_poll_created( self, mid=None, poll=None, author_id=None, - thread_id=None, - thread_type=None, + thread=None, at=None, metadata=None, msg=None, @@ -2403,17 +2194,12 @@ class Client: mid: The action ID poll (Poll): Created poll author_id: The ID of the person who created the poll - thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` - thread_type (ThreadType): Type of thread that the action was sent to. See :ref:`intro_threads` + thread: Thread that the action was sent to. See :ref:`intro_threads` at (datetime.datetime): When the action was executed metadata: Extra metadata about the action msg: A full set of the data received """ - log.info( - "{} created poll {} in {} ({})".format( - author_id, poll, thread_id, thread_type.name - ) - ) + log.info("{} created poll {} in {}".format(author_id, poll, thread)) def on_poll_voted( self, @@ -2422,8 +2208,7 @@ class Client: added_options=None, removed_options=None, author_id=None, - thread_id=None, - thread_type=None, + thread=None, at=None, metadata=None, msg=None, @@ -2434,25 +2219,19 @@ class Client: mid: The action ID poll (Poll): Poll, that user voted in author_id: The ID of the person who voted in the poll - thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` - thread_type (ThreadType): Type of thread that the action was sent to. See :ref:`intro_threads` + thread: Thread that the action was sent to. See :ref:`intro_threads` at (datetime.datetime): When the action was executed metadata: Extra metadata about the action msg: A full set of the data received """ - log.info( - "{} voted in poll {} in {} ({})".format( - author_id, poll, thread_id, thread_type.name - ) - ) + log.info("{} voted in poll {} in {}".format(author_id, poll, thread)) def on_plan_created( self, mid=None, plan=None, author_id=None, - thread_id=None, - thread_type=None, + thread=None, at=None, metadata=None, msg=None, @@ -2463,50 +2242,34 @@ class Client: mid: The action ID plan (Plan): Created plan author_id: The ID of the person who created the plan - thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` - thread_type (ThreadType): Type of thread that the action was sent to. See :ref:`intro_threads` + thread: Thread that the action was sent to. See :ref:`intro_threads` at (datetime.datetime): When the action was executed metadata: Extra metadata about the action msg: A full set of the data received """ - log.info( - "{} created plan {} in {} ({})".format( - author_id, plan, thread_id, thread_type.name - ) - ) + log.info("{} created plan {} in {}".format(author_id, plan, thread)) def on_plan_ended( - self, - mid=None, - plan=None, - thread_id=None, - thread_type=None, - at=None, - metadata=None, - msg=None, + self, mid=None, plan=None, thread=None, at=None, metadata=None, msg=None, ): """Called when the client is listening, and a plan ends. Args: mid: The action ID plan (Plan): Ended plan - thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` - thread_type (ThreadType): Type of thread that the action was sent to. See :ref:`intro_threads` + thread: Thread that the action was sent to. See :ref:`intro_threads` at (datetime.datetime): When the action was executed metadata: Extra metadata about the action msg: A full set of the data received """ - log.info( - "Plan {} has ended in {} ({})".format(plan, thread_id, thread_type.name) - ) + log.info("Plan {} has ended in {}".format(plan, thread)) def on_plan_edited( self, mid=None, plan=None, author_id=None, - thread_id=None, - thread_type=None, + thread=None, at=None, metadata=None, msg=None, @@ -2517,25 +2280,19 @@ class Client: mid: The action ID plan (Plan): Edited plan author_id: The ID of the person who edited the plan - thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` - thread_type (ThreadType): Type of thread that the action was sent to. See :ref:`intro_threads` + thread: Thread that the action was sent to. See :ref:`intro_threads` at (datetime.datetime): When the action was executed metadata: Extra metadata about the action msg: A full set of the data received """ - log.info( - "{} edited plan {} in {} ({})".format( - author_id, plan, thread_id, thread_type.name - ) - ) + log.info("{} edited plan {} in {}".format(author_id, plan, thread)) def on_plan_deleted( self, mid=None, plan=None, author_id=None, - thread_id=None, - thread_type=None, + thread=None, at=None, metadata=None, msg=None, @@ -2546,17 +2303,12 @@ class Client: mid: The action ID plan (Plan): Deleted plan author_id: The ID of the person who deleted the plan - thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` - thread_type (ThreadType): Type of thread that the action was sent to. See :ref:`intro_threads` + thread: Thread that the action was sent to. See :ref:`intro_threads` at (datetime.datetime): When the action was executed metadata: Extra metadata about the action msg: A full set of the data received """ - log.info( - "{} deleted plan {} in {} ({})".format( - author_id, plan, thread_id, thread_type.name - ) - ) + log.info("{} deleted plan {} in {}".format(author_id, plan, thread)) def on_plan_participation( self, @@ -2564,8 +2316,7 @@ class Client: plan=None, take_part=None, author_id=None, - thread_id=None, - thread_type=None, + thread=None, at=None, metadata=None, msg=None, @@ -2577,23 +2328,18 @@ class Client: plan (Plan): Plan take_part (bool): Whether the person takes part in the plan or not author_id: The ID of the person who will participate in the plan or not - thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` - thread_type (ThreadType): Type of thread that the action was sent to. See :ref:`intro_threads` + thread: Thread that the action was sent to. See :ref:`intro_threads` at (datetime.datetime): When the action was executed metadata: Extra metadata about the action msg: A full set of the data received """ if take_part: log.info( - "{} will take part in {} in {} ({})".format( - author_id, plan, thread_id, thread_type.name - ) + "{} will take part in {} in {} ({})".format(author_id, plan, thread) ) else: log.info( - "{} won't take part in {} in {} ({})".format( - author_id, plan, thread_id, thread_type.name - ) + "{} won't take part in {} in {} ({})".format(author_id, plan, thread) ) def on_qprimer(self, at=None, msg=None): diff --git a/fbchat/_thread.py b/fbchat/_thread.py index 16d2324..8609766 100644 --- a/fbchat/_thread.py +++ b/fbchat/_thread.py @@ -6,27 +6,6 @@ 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): """Used to specify where a thread is located (inbox, pending, archived, other).""" @@ -106,7 +85,7 @@ class ThreadABC(metaclass=abc.ABCMeta): ) data["lightweight_action_attachment[lwa_type]"] = "WAVE" # TODO: This! - # if thread_type == ThreadType.USER: + # 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 @@ -372,7 +351,7 @@ class ThreadABC(metaclass=abc.ABCMeta): "typ": "1" if typing else "0", "thread": self.id, # TODO: This - "to": self.id if thread_type == ThreadType.USER else "", + # "to": self.id if isinstance(self, _user.User) else "", "source": "mercury-chat", } j = self.session._payload_post("/ajax/messaging/typ.php", data) diff --git a/tests/conftest.py b/tests/conftest.py index d3978b0..ad71946 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,7 @@ import json from utils import * from contextlib import contextmanager -from fbchat import ThreadType, Message, Mention +from fbchat import Message, Mention @pytest.fixture(scope="session") @@ -13,14 +13,14 @@ def session(): @pytest.fixture(scope="session") def user(client2): - return {"id": client2.id, "type": ThreadType.USER} + return {"id": client2.id, "type": None} @pytest.fixture(scope="session") def group(pytestconfig): return { "id": load_variable("group_id", pytestconfig.cache), - "type": ThreadType.GROUP, + "type": None, } @@ -29,11 +29,9 @@ def group(pytestconfig): params=["user", "group", pytest.param("none", marks=[pytest.mark.xfail()])], ) def thread(request, user, group): - return { - "user": user, - "group": group, - "none": {"id": "0", "type": ThreadType.GROUP}, - }[request.param] + return {"user": user, "group": group, "none": {"id": "0", "type": None},}[ + request.param + ] @pytest.fixture(scope="session") @@ -103,9 +101,7 @@ def compare(client, thread): def inner(caught_event, **kwargs): d = { "author_id": client.id, - "thread_id": client.id - if thread["type"] == ThreadType.USER - else thread["id"], + "thread_id": client.id if thread["type"] == None else thread["id"], "thread_type": thread["type"], } d.update(kwargs) diff --git a/tests/test_fetch.py b/tests/test_fetch.py index 4ddd66a..86e65ed 100644 --- a/tests/test_fetch.py +++ b/tests/test_fetch.py @@ -1,7 +1,7 @@ import pytest 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 pytestmark = pytest.mark.online diff --git a/tests/test_plans.py b/tests/test_plans.py index 184654a..fae3cf9 100644 --- a/tests/test_plans.py +++ b/tests/test_plans.py @@ -1,6 +1,6 @@ import pytest -from fbchat import Plan, FBchatFacebookError, ThreadType +from fbchat import Plan, FBchatFacebookError from utils import random_hex, subset from time import time @@ -93,7 +93,7 @@ def test_on_plan_ended(client, thread, catch_event, compare): x.wait(180) assert subset( 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"], ) diff --git a/tests/test_polls.py b/tests/test_polls.py index 19bc20f..274f259 100644 --- a/tests/test_polls.py +++ b/tests/test_polls.py @@ -1,6 +1,6 @@ import pytest -from fbchat import Poll, PollOption, ThreadType +from fbchat import Poll, PollOption from utils import random_hex, subset 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): event, poll, _ = poll_data - assert subset( - event, - author_id=client1.id, - thread_id=group["id"], - thread_type=ThreadType.GROUP, - ) + assert subset(event, author_id=client1.id, thread=group) assert subset( 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, ) - assert subset( - x.res, - author_id=client1.id, - thread_id=group["id"], - thread_type=ThreadType.GROUP, - ) + assert subset(x.res, author_id=client1.id, thread=group) assert subset( vars(x.res["poll"]), title=poll.title, options_count=len(options + new_options) ) diff --git a/tests/test_search.py b/tests/test_search.py index bd7b440..57fd969 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -1,5 +1,4 @@ import pytest -from fbchat import ThreadType pytestmark = pytest.mark.online diff --git a/tests/test_thread.py b/tests/test_thread.py index 53cf7b5..6fc597c 100644 --- a/tests/test_thread.py +++ b/tests/test_thread.py @@ -1,12 +1,6 @@ import pytest import fbchat -from fbchat import ThreadType, 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() +from fbchat import ThreadColor, ThreadABC, Thread def test_thread_color_from_graphql(): diff --git a/tests/test_thread_interraction.py b/tests/test_thread_interraction.py index 6da93ce..df05fdb 100644 --- a/tests/test_thread_interraction.py +++ b/tests/test_thread_interraction.py @@ -1,6 +1,6 @@ import pytest -from fbchat import Message, ThreadType, FBchatFacebookError, TypingStatus, ThreadColor +from fbchat import Message, FBchatFacebookError, TypingStatus, ThreadColor from utils import random_hex, subset 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): title = random_hex() with catch_event("on_title_change") as x: - client1.change_thread_title(title, group["id"], thread_type=ThreadType.GROUP) - assert subset( - x.res, - author_id=client1.id, - new_title=title, - thread_id=group["id"], - thread_type=ThreadType.GROUP, - ) + client1.change_thread_title(title, group["id"]) + assert subset(x.res, author_id=client1.id, new_title=title, thread=group) def test_change_nickname(client, client_all, catch_event, compare): diff --git a/tests/utils.py b/tests/utils.py index 09e8755..bf5f9a4 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -5,7 +5,7 @@ import pytest from os import environ from random import randrange 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())