diff --git a/examples/interract.py b/examples/interract.py index ca9600a9..5ab6f081 100644 --- a/examples/interract.py +++ b/examples/interract.py @@ -51,8 +51,8 @@ # Will change the nickname of the user `` to `` thread.set_nickname(fbchat.User(session=session, id=""), "") -# Will set the typing status of the thread -thread.start_typing() +# Will start typing in the thread +thread.set_typing(True) # Will change the thread color to #0084ff thread.set_color("#0084ff") diff --git a/examples/keepbot.py b/examples/keepbot.py index e52e56f4..7fb242c6 100644 --- a/examples/keepbot.py +++ b/examples/keepbot.py @@ -61,8 +61,8 @@ def on_nickname_set(sender, event: fbchat.NicknameSet): event.thread.set_nickname(event.subject.id, old_nickname) -@events.connect_via(fbchat.PeopleAdded) -def on_people_added(sender, event: fbchat.PeopleAdded): +@events.connect_via(fbchat.ParticipantsAdded) +def on_people_added(sender, event: fbchat.ParticipantsAdded): if old_thread_id != event.thread.id: return if event.author.id != session.user.id: @@ -71,8 +71,8 @@ def on_people_added(sender, event: fbchat.PeopleAdded): event.thread.remove_participant(added.id) -@events.connect_via(fbchat.PersonRemoved) -def on_person_removed(sender, event: fbchat.PersonRemoved): +@events.connect_via(fbchat.ParticipantRemoved) +def on_person_removed(sender, event: fbchat.ParticipantRemoved): if old_thread_id != event.thread.id: return # No point in trying to add ourself diff --git a/fbchat/__init__.py b/fbchat/__init__.py index b74a755b..ed144f03 100644 --- a/fbchat/__init__.py +++ b/fbchat/__init__.py @@ -81,8 +81,8 @@ UnsendEvent, MessageReplyEvent, # _delta_class - PeopleAdded, - PersonRemoved, + ParticipantsAdded, + ParticipantRemoved, TitleSet, UnfetchedThreadEvent, MessagesDelivered, @@ -107,7 +107,7 @@ PlanDeleted, PlanResponded, # __init__ - Typing, + TypingStatus, FriendRequest, Presence, ) diff --git a/fbchat/_events/__init__.py b/fbchat/_events/__init__.py index 7410cdbe..76d44a61 100644 --- a/fbchat/_events/__init__.py +++ b/fbchat/_events/__init__.py @@ -11,20 +11,20 @@ @attrs_event -class Typing(ThreadEvent): +class TypingStatus(ThreadEvent): """Somebody started/stopped typing in a thread.""" #: ``True`` if the user started typing, ``False`` if they stopped status = attr.ib(type=bool) @classmethod - def _parse_orca(cls, session, data): + def _from_orca(cls, session, data): author = _threads.User(session=session, id=str(data["sender_fbid"])) status = data["state"] == 1 return cls(author=author, thread=author, status=status) @classmethod - def _parse_thread_typing(cls, session, data): + def _from_thread_typing(cls, session, data): author = _threads.User(session=session, id=str(data["sender_fbid"])) thread = _threads.Group(session=session, id=str(data["thread"])) status = data["state"] == 1 @@ -89,10 +89,10 @@ def parse_events(session, topic, data): ) from e elif topic == "/thread_typing": - yield Typing._parse_thread_typing(session, data) + yield TypingStatus._from_thread_typing(session, data) elif topic == "/orca_typing_notifications": - yield Typing._parse_orca(session, data) + yield TypingStatus._from_orca(session, data) elif topic == "/legacy_web": if data["type"] == "jewel_requests_add": diff --git a/fbchat/_events/_delta_class.py b/fbchat/_events/_delta_class.py index 9c3c909f..eeccb49c 100644 --- a/fbchat/_events/_delta_class.py +++ b/fbchat/_events/_delta_class.py @@ -8,10 +8,11 @@ @attrs_event -class PeopleAdded(ThreadEvent): - """somebody added people to a group thread.""" +class ParticipantsAdded(ThreadEvent): + """People were added to a group thread.""" # TODO: Add message id + # TODO: Add snippet/admin text thread = attr.ib(type="_threads.Group") # Set the correct type #: The people who got added @@ -29,9 +30,27 @@ def _parse(cls, session, data): ] return cls(author=author, thread=thread, added=added, at=at) + @classmethod + def _from_send(cls, thread: "_threads.Group", added_ids: Sequence[str]): + return cls( + author=thread.session.user, + thread=thread, + added=[_threads.User(session=thread.session, id=id_) for id_ in added_ids], + at=None, + ) + + @classmethod + def _from_fetch(cls, thread: "_threads.Group", data): + author, at = cls._parse_fetch(thread.session, data) + added = [ + _threads.User(session=thread.session, id=id_["id"]) + for id_ in data["participants_added"] + ] + return cls(author=author, thread=thread, added=added, at=at) + @attrs_event -class PersonRemoved(ThreadEvent): +class ParticipantRemoved(ThreadEvent): """Somebody removed a person from a group thread.""" # TODO: Add message id @@ -48,21 +67,45 @@ def _parse(cls, session, data): removed = _threads.User(session=session, id=data["leftParticipantFbId"]) return cls(author=author, thread=thread, removed=removed, at=at) + @classmethod + def _from_send(cls, thread: "_threads.Group", removed_id: str): + return cls( + author=thread.session.user, + thread=thread, + removed=_threads.User(session=thread.session, id=removed_id), + at=None, + ) + + @classmethod + def _from_fetch(cls, thread: "_threads.Group", data): + author, at = cls._parse_fetch(thread.session, data) + removed = _threads.User( + session=thread.session, id=data["participants_removed"][0]["id"] + ) + return cls(author=author, thread=thread, removed=removed, at=at) + @attrs_event class TitleSet(ThreadEvent): """Somebody changed a group's title.""" thread = attr.ib(type="_threads.Group") # Set the correct type - #: The new title - title = attr.ib(type=str) + #: The new title. If ``None``, the title is cleared + title = attr.ib(type=Optional[str]) #: When the title was set at = attr.ib(type=datetime.datetime) @classmethod def _parse(cls, session, data): author, thread, at = cls._parse_metadata(session, data) - return cls(author=author, thread=thread, title=data["name"], at=at) + title = data["name"] or None + return cls(author=author, thread=thread, title=title, at=at) + + @classmethod + def _from_fetch(cls, thread, data): + author, at = cls._parse_fetch(thread.session, data) + title = data["thread_name"] or None + return cls(author=author, thread=thread, title=title, at=at) @attrs_event @@ -184,9 +227,9 @@ def parse_delta(session, data): if class_ == "AdminTextMessage": return _delta_type.parse_admin_message(session, data) elif class_ == "ParticipantsAddedToGroupThread": - return PeopleAdded._parse(session, data) + return ParticipantsAdded._parse(session, data) elif class_ == "ParticipantLeftGroupThread": - return PersonRemoved._parse(session, data) + return ParticipantRemoved._parse(session, data) elif class_ == "MarkFolderSeen": # TODO: Finish this folders = [_models.ThreadLocation._parse(folder) for folder in data["folders"]] diff --git a/fbchat/_events/_delta_type.py b/fbchat/_events/_delta_type.py index 518cea21..c7a86f9d 100644 --- a/fbchat/_events/_delta_type.py +++ b/fbchat/_events/_delta_type.py @@ -21,20 +21,33 @@ def _parse(cls, session, data): color = _threads.ThreadABC._parse_color(data["untypedData"]["theme_color"]) return cls(author=author, thread=thread, color=color, at=at) + @classmethod + def _from_fetch(cls, thread: "_threads.ThreadABC", data): + author, at = cls._parse_fetch(thread.session, data) + color = data["extensible_message_admin_text"]["theme_color"] + color = _threads.ThreadABC._parse_color(color) + return cls(author=author, thread=thread, color=color, at=at) + @attrs_event class EmojiSet(ThreadEvent): """Somebody set the emoji in a thread.""" - #: The new emoji - emoji = attr.ib(type=str) + #: The new emoji. If ``None``, the emoji was reset to the default "LIKE" icon + emoji = attr.ib(type=Optional[str]) #: When the emoji was set at = attr.ib(type=datetime.datetime) @classmethod def _parse(cls, session, data): author, thread, at = cls._parse_metadata(session, data) - emoji = data["untypedData"]["thread_icon"] + emoji = data["untypedData"]["thread_icon"] or None + return cls(author=author, thread=thread, emoji=emoji, at=at) + + @classmethod + def _from_fetch(cls, thread: "_threads.ThreadABC", data): + author, at = cls._parse_fetch(thread.session, data) + emoji = data["extensible_message_admin_text"]["thread_icon"] return cls(author=author, thread=thread, emoji=emoji, at=at) @@ -43,7 +56,7 @@ class NicknameSet(ThreadEvent): """Somebody set the nickname of a person in a thread.""" #: The person whose nickname was set - subject = attr.ib(type=str) + subject = attr.ib(type="_threads.User") #: The new nickname. If ``None``, the nickname was cleared nickname = attr.ib(type=Optional[str]) #: When the nickname was set @@ -60,6 +73,16 @@ def _parse(cls, session, data): author=author, thread=thread, subject=subject, nickname=nickname, at=at ) + @classmethod + def _from_fetch(cls, thread, data): + author, at = cls._parse_fetch(thread.session, data) + extra = data["extensible_message_admin_text"] + subject = _threads.User(session=thread.session, id=extra["participant_id"]) + nickname = extra["nickname"] or None + return cls( + author=author, thread=thread, subject=subject, nickname=nickname, at=at + ) + @attrs_event class AdminsAdded(ThreadEvent): diff --git a/fbchat/_session.py b/fbchat/_session.py index be0d5803..9fd0ae0d 100644 --- a/fbchat/_session.py +++ b/fbchat/_session.py @@ -313,7 +313,7 @@ def _from_session(cls, session): raise _exception.NotLoggedIn("Could not find fb_dtsg") fb_dtsg = res.group(1) - revision = int(r.text.split('"client_revision":', 1)[1].split(",", 1)[0]) + revision = int(user_id) logout_h_element = soup.find("input", {"name": "h"}) logout_h = logout_h_element["value"] if logout_h_element else None diff --git a/fbchat/_threads/_abc.py b/fbchat/_threads/_abc.py index 710d9f52..0c83f446 100644 --- a/fbchat/_threads/_abc.py +++ b/fbchat/_threads/_abc.py @@ -3,7 +3,7 @@ import collections import datetime from .._common import log, attrs_default -from .. import _util, _exception, _session, _graphql, _models +from .. import _util, _exception, _session, _graphql, _events, _models from typing import MutableMapping, Mapping, Any, Iterable, Tuple, Optional @@ -461,26 +461,34 @@ def fetch_images(self, limit: Optional[int]) -> Iterable["_models.Attachment"]: if image: yield image - def set_nickname(self, user_id: str, nickname: str): + def set_nickname(self, user_id: str, nickname: Optional[str]): """Change the nickname of a user in the thread. Args: user_id: User that will have their nickname changed - nickname: New nickname + nickname: New nickname. If ``None``, the nickname will be cleared Example: >>> thread.set_nickname("1234", "A nickname") """ + nickname = nickname or None 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 + self.session._payload_post( + "/messaging/save_thread_nickname/?source=thread_settings", data + ) + return _events.NicknameSet( + author=self.session.user, + thread=self._copy(), + subject=_threads.User(session=self.session, id=str(user_id)), + nickname=nickname, + at=None, ) - def set_color(self, color: str): + def set_color(self, color: str) -> _events.ColorSet: """Change thread color. The new color must be one of the following:: @@ -515,6 +523,9 @@ def set_color(self, color: str): j = self.session._payload_post( "/messaging/save_thread_color/?source=thread_settings&dpr=1", data ) + return _events.ColorSet( + author=self.session.user, thread=self._copy(), color=color, at=None + ) # def set_theme(self, theme_id: str): # data = { @@ -528,22 +539,34 @@ def set_color(self, color: str): # _graphql.from_doc_id("1768656253222505", {"data": data}) # ) - def set_emoji(self, emoji: Optional[str]): + def set_emoji(self, emoji: Optional[str]) -> _events.EmojiSet: """Change thread emoji. Args: emoji: New thread emoji. If ``None``, will be set to the default "LIKE" icon + Todo: + Currently, setting the default "LIKE" icon does not work! + Example: Set the thread emoji to "😊". >>> thread.set_emoji("😊") """ - data = {"emoji_choice": emoji, "thread_or_other_fbid": self.id} + emoji = emoji or None + data = { + "emoji_choice": emoji, + "thread_or_other_fbid": self.id, + # TODO: Do we need this? + # "request_user_id": self.session.user.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 + "/messaging/save_thread_emoji/?source=thread_settings", data + ) + return _events.EmojiSet( + author=self.session.user, thread=self._copy(), emoji=emoji, at=None ) def forward_attachment(self, attachment_id: str): @@ -563,31 +586,32 @@ def forward_attachment(self, attachment_id: str): if not j.get("success"): raise _exception.ExternalError("Failed forwarding attachment", j["error"]) - def _set_typing(self, typing): - data = { - "typ": "1" if typing else "0", - "thread": self.id, - # TODO: This - # "to": self.id if isinstance(self, _user.User) else "", - "source": "mercury-chat", - } - j = self.session._payload_post("/ajax/messaging/typ.php", data) + def set_typing(self, status: bool) -> "_events.TypingStatus": + """Set the current user's typing status in the thread. - def start_typing(self): - """Set the current user to start typing in the thread. + Examples: + Start typing. - Example: - >>> thread.start_typing() - """ - self._set_typing(True) + >>> thread.set_typing(True) - def stop_typing(self): - """Set the current user to stop typing in the thread. + Stop typing. - Example: - >>> thread.stop_typing() + >>> thread.set_typing(False) """ - self._set_typing(False) + from . import _user + + data = { + "typ": "1" if status else "0", + "thread": self.id, + # TODO: Do this in a better way + # Also not sure what to do about pages? + "to": self.id if isinstance(self, _user.User) else "", + "source": "mercury-chat", + } + j = self.session._payload_post("/ajax/messaging/typ.php", data) + return _events.TypingStatus( + author=self.session.user, thread=self._copy(), status=status + ) def create_plan( self, diff --git a/fbchat/_threads/_group.py b/fbchat/_threads/_group.py index 891ec04d..a69fde6b 100644 --- a/fbchat/_threads/_group.py +++ b/fbchat/_threads/_group.py @@ -3,7 +3,7 @@ from ._abc import ThreadABC from . import _user from .._common import attrs_default -from .. import _util, _session, _graphql, _models +from .. import _util, _session, _graphql, _events, _models from typing import Sequence, Iterable, Set, Mapping, Optional @@ -27,9 +27,14 @@ def _to_send_data(self): def _copy(self) -> "Group": return Group(session=self.session, id=self.id) - def add_participants(self, user_ids: Iterable[str]): + def add_participants(self, user_ids: Iterable[str]) -> _events.ParticipantsAdded: """Add users to the group. + If the group's approval mode is set to require admin approval, and you're not an + admin, the participants won't actually be added, they will be set as pending. + + In that case, the returned `ParticipantsAdded` event will not be correct. + Args: user_ids: One or more user IDs to add @@ -51,11 +56,15 @@ def add_participants(self, user_ids: Iterable[str]): "log_message_data[added_participants][{}]".format(i) ] = "fbid:{}".format(user_id) - return self.session._do_send_request(data) + message_id, thread_id = self.session._do_send_request(data) + return _events.ParticipantsAdded._from_send(thread=self, added_ids=user_ids) - def remove_participant(self, user_id: str): + def remove_participant(self, user_id: str) -> _events.ParticipantRemoved: """Remove user from the group. + If the group's approval mode is set to require admin approval, and you're not an + admin, this will fail. + Args: user_id: User ID to remove @@ -63,7 +72,18 @@ def remove_participant(self, user_id: str): >>> group.remove_participant("1234") """ data = {"uid": user_id, "tid": self.id} - j = self.session._payload_post("/chat/remove_participants/", data) + self.session._payload_post("/chat/remove_participants/", data) + return _events.ParticipantRemoved._from_send(thread=self, removed_id=user_id) + + def leave(self) -> _events.ParticipantRemoved: + """Leave the group. + + This will succeed regardless of approval mode and admin status. + + Example: + >>> group.leave() + """ + return self.remove_participant(self.session.user.id) def _admin_status(self, user_ids: Iterable[str], status: bool): data = {"add": status, "thread_fbid": self.id} @@ -95,17 +115,20 @@ def remove_admins(self, user_ids: Iterable[str]): """ self._admin_status(user_ids, False) - def set_title(self, title: str): + def set_title(self, title: Optional[str]) -> _events.TitleSet: """Change title of the group. Args: - title: New title + title: New title. If ``None``, the title is cleared Example: >>> group.set_title("Abc") """ data = {"thread_name": title, "thread_id": self.id} - j = self.session._payload_post("/messaging/set_thread_name/?dpr=1", data) + self.session._payload_post("/messaging/set_thread_name/?dpr=1", data) + return _events.TitleSet( + author=self.session.user, thread=self._copy(), title=title, at=None + ) def set_image(self, image_id: str): """Change the group image from an image id. diff --git a/tests/events/test_color_set.py b/tests/events/test_color_set.py new file mode 100644 index 00000000..a25f5cd8 --- /dev/null +++ b/tests/events/test_color_set.py @@ -0,0 +1,77 @@ +import datetime +from fbchat import ColorSet, Group, User +from fbchat._events import parse_admin_message + + +def test_listen(session): + data = { + "irisSeqId": "1111111", + "irisTags": ["DeltaAdminTextMessage", "is_from_iris_fanout"], + "messageMetadata": { + "actorFbId": "1234", + "adminText": "You changed the chat theme to Orange.", + "folderId": {"systemFolderId": "INBOX"}, + "messageId": "mid.$XYZ", + "offlineThreadingId": "11223344556677889900", + "skipBumpThread": False, + "tags": ["source:titan:web", "no_push"], + "threadKey": {"threadFbId": "4321"}, + "threadReadStateEffect": "MARK_UNREAD", + "timestamp": "1500000000000", + "unsendType": "deny_log_message", + }, + "participants": ["1234", "2345", "3456"], + "requestContext": {"apiArgs": {}}, + "tqSeqId": "1111", + "type": "change_thread_theme", + "untypedData": { + "should_show_icon": "1", + "theme_color": "FFFF7E29", + "accessibility_label": "Orange", + }, + "class": "AdminTextMessage", + } + assert ColorSet( + author=User(session=session, id="1234"), + thread=Group(session=session, id="4321"), + color="#ff7e29", + at=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), + ) == parse_admin_message(session, data) + + +def test_fetch(session): + data = { + "__typename": "GenericAdminTextMessage", + "message_id": "mid.$XYZ", + "offline_threading_id": "1122334455", + "message_sender": {"id": "1234", "email": "1234@facebook.com"}, + "ttl": 0, + "timestamp_precise": "1500000000000", + "unread": True, + "is_sponsored": False, + "ad_id": None, + "ad_client_token": None, + "commerce_message_type": None, + "customizations": [], + "tags_list": ["inbox", "sent", "source:generic_admin_text"], + "platform_xmd_encoded": None, + "message_source_data": None, + "montage_reply_data": None, + "message_reactions": [], + "unsent_timestamp_precise": "0", + "message_unsendability_status": "deny_log_message", + "extensible_message_admin_text": { + "__typename": "ThemeColorExtensibleMessageAdminText", + "theme_color": "FFFFC300", + }, + "extensible_message_admin_text_type": "CHANGE_THREAD_THEME", + "snippet": "You changed the chat theme to Yellow.", + "replied_to_message": None, + } + thread = User(session=session, id="4321") + assert ColorSet( + author=User(session=session, id="1234"), + thread=thread, + color="#ffc300", + at=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), + ) == ColorSet._from_fetch(thread, data) diff --git a/tests/events/test_delta_class.py b/tests/events/test_delta_class.py index 6d9a57b6..9ec62a12 100644 --- a/tests/events/test_delta_class.py +++ b/tests/events/test_delta_class.py @@ -8,9 +8,6 @@ MessageData, ThreadLocation, UnknownEvent, - PeopleAdded, - PersonRemoved, - TitleSet, UnfetchedThreadEvent, MessagesDelivered, ThreadsRead, @@ -20,109 +17,6 @@ from fbchat._events import parse_delta -def test_people_added(session): - data = { - "addedParticipants": [ - { - "fanoutPolicy": "IRIS_MESSAGE_QUEUE", - "firstName": "Abc", - "fullName": "Abc Def", - "initialFolder": "FOLDER_INBOX", - "initialFolderId": {"systemFolderId": "INBOX"}, - "isMessengerUser": False, - "userFbId": "1234", - } - ], - "irisSeqId": "11223344", - "irisTags": ["DeltaParticipantsAddedToGroupThread", "is_from_iris_fanout"], - "messageMetadata": { - "actorFbId": "3456", - "adminText": "You added Abc Def to the group.", - "folderId": {"systemFolderId": "INBOX"}, - "messageId": "mid.$XYZ", - "offlineThreadingId": "1122334455", - "skipBumpThread": False, - "tags": [], - "threadKey": {"threadFbId": "4321"}, - "threadReadStateEffect": "KEEP_AS_IS", - "timestamp": "1500000000000", - "unsendType": "deny_log_message", - }, - "participants": ["1234", "2345", "3456", "4567"], - "requestContext": {"apiArgs": {}}, - "tqSeqId": "1111", - "class": "ParticipantsAddedToGroupThread", - } - assert PeopleAdded( - author=User(session=session, id="3456"), - thread=Group(session=session, id="4321"), - added=[User(session=session, id="1234")], - at=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), - ) == parse_delta(session, data) - - -def test_person_removed(session): - data = { - "irisSeqId": "11223344", - "irisTags": ["DeltaParticipantLeftGroupThread", "is_from_iris_fanout"], - "leftParticipantFbId": "1234", - "messageMetadata": { - "actorFbId": "3456", - "adminText": "You removed Abc Def from the group.", - "folderId": {"systemFolderId": "INBOX"}, - "messageId": "mid.$XYZ", - "offlineThreadingId": "1122334455", - "skipBumpThread": True, - "tags": [], - "threadKey": {"threadFbId": "4321"}, - "threadReadStateEffect": "KEEP_AS_IS", - "timestamp": "1500000000000", - "unsendType": "deny_log_message", - }, - "participants": ["1234", "2345", "3456", "4567"], - "requestContext": {"apiArgs": {}}, - "tqSeqId": "1111", - "class": "ParticipantLeftGroupThread", - } - assert PersonRemoved( - author=User(session=session, id="3456"), - thread=Group(session=session, id="4321"), - removed=User(session=session, id="1234"), - at=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), - ) == parse_delta(session, data) - - -def test_title_set(session): - data = { - "irisSeqId": "11223344", - "irisTags": ["DeltaThreadName", "is_from_iris_fanout"], - "messageMetadata": { - "actorFbId": "3456", - "adminText": "You named the group abc.", - "folderId": {"systemFolderId": "INBOX"}, - "messageId": "mid.$XYZ", - "offlineThreadingId": "1122334455", - "skipBumpThread": False, - "tags": [], - "threadKey": {"threadFbId": "4321"}, - "threadReadStateEffect": "KEEP_AS_IS", - "timestamp": "1500000000000", - "unsendType": "deny_log_message", - }, - "name": "abc", - "participants": ["1234", "2345", "3456", "4567"], - "requestContext": {"apiArgs": {}}, - "tqSeqId": "1111", - "class": "ThreadName", - } - assert TitleSet( - author=User(session=session, id="3456"), - thread=Group(session=session, id="4321"), - title="abc", - at=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), - ) == parse_delta(session, data) - - def test_forced_fetch(session): data = { "forceInsert": False, diff --git a/tests/events/test_delta_type.py b/tests/events/test_delta_type.py index 88d94bbc..271232f3 100644 --- a/tests/events/test_delta_type.py +++ b/tests/events/test_delta_type.py @@ -12,9 +12,6 @@ PlanData, GuestStatus, UnknownEvent, - ColorSet, - EmojiSet, - NicknameSet, AdminsAdded, AdminsRemoved, ApprovalModeSet, @@ -32,141 +29,6 @@ from fbchat._events import parse_admin_message -def test_color_set(session): - data = { - "irisSeqId": "1111111", - "irisTags": ["DeltaAdminTextMessage", "is_from_iris_fanout"], - "messageMetadata": { - "actorFbId": "1234", - "adminText": "You changed the chat theme to Orange.", - "folderId": {"systemFolderId": "INBOX"}, - "messageId": "mid.$XYZ", - "offlineThreadingId": "11223344556677889900", - "skipBumpThread": False, - "tags": ["source:titan:web", "no_push"], - "threadKey": {"threadFbId": "4321"}, - "threadReadStateEffect": "MARK_UNREAD", - "timestamp": "1500000000000", - "unsendType": "deny_log_message", - }, - "participants": ["1234", "2345", "3456"], - "requestContext": {"apiArgs": {}}, - "tqSeqId": "1111", - "type": "change_thread_theme", - "untypedData": { - "should_show_icon": "1", - "theme_color": "FFFF7E29", - "accessibility_label": "Orange", - }, - "class": "AdminTextMessage", - } - assert ColorSet( - author=User(session=session, id="1234"), - thread=Group(session=session, id="4321"), - color="#ff7e29", - at=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), - ) == parse_admin_message(session, data) - - -def test_emoji_set(session): - data = { - "irisSeqId": "1111111", - "irisTags": ["DeltaAdminTextMessage"], - "messageMetadata": { - "actorFbId": "1234", - "adminText": "You set the emoji to 🌟.", - "folderId": {"systemFolderId": "INBOX"}, - "messageId": "mid.$XYZ", - "offlineThreadingId": "11223344556677889900", - "skipBumpThread": False, - "skipSnippetUpdate": False, - "tags": ["source:generic_admin_text"], - "threadKey": {"otherUserFbId": "1234"}, - "threadReadStateEffect": "MARK_UNREAD", - "timestamp": "1500000000000", - "unsendType": "deny_log_message", - }, - "requestContext": {"apiArgs": {}}, - "type": "change_thread_icon", - "untypedData": { - "thread_icon_url": "https://www.facebook.com/images/emoji.php/v9/te0/1/16/1f31f.png", - "thread_icon": "🌟", - }, - "class": "AdminTextMessage", - } - assert EmojiSet( - author=User(session=session, id="1234"), - thread=User(session=session, id="1234"), - emoji="🌟", - at=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), - ) == parse_admin_message(session, data) - - -def test_nickname_set(session): - data = { - "irisSeqId": "1111111", - "irisTags": ["DeltaAdminTextMessage", "is_from_iris_fanout"], - "messageMetadata": { - "actorFbId": "1234", - "adminText": "You set the nickname for Abc Def to abc.", - "folderId": {"systemFolderId": "INBOX"}, - "messageId": "mid.$XYZ", - "offlineThreadingId": "11223344556677889900", - "skipBumpThread": False, - "tags": ["source:titan:web", "no_push"], - "threadKey": {"threadFbId": "4321"}, - "threadReadStateEffect": "MARK_UNREAD", - "timestamp": "1500000000000", - "unsendType": "deny_log_message", - }, - "participants": ["1234", "2345", "3456"], - "requestContext": {"apiArgs": {}}, - "tqSeqId": "1111", - "type": "change_thread_nickname", - "untypedData": {"nickname": "abc", "participant_id": "2345"}, - "class": "AdminTextMessage", - } - assert NicknameSet( - author=User(session=session, id="1234"), - thread=Group(session=session, id="4321"), - subject=User(session=session, id="2345"), - nickname="abc", - at=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), - ) == parse_admin_message(session, data) - - -def test_nickname_clear(session): - data = { - "irisSeqId": "1111111", - "irisTags": ["DeltaAdminTextMessage"], - "messageMetadata": { - "actorFbId": "1234", - "adminText": "You cleared your nickname.", - "folderId": {"systemFolderId": "INBOX"}, - "messageId": "mid.$XYZ", - "offlineThreadingId": "11223344556677889900", - "skipBumpThread": False, - "skipSnippetUpdate": False, - "tags": ["source:generic_admin_text"], - "threadKey": {"otherUserFbId": "1234"}, - "threadReadStateEffect": "MARK_UNREAD", - "timestamp": "1500000000000", - "unsendType": "deny_log_message", - }, - "requestContext": {"apiArgs": {}}, - "type": "change_thread_nickname", - "untypedData": {"nickname": "", "participant_id": "1234"}, - "class": "AdminTextMessage", - } - assert NicknameSet( - author=User(session=session, id="1234"), - thread=User(session=session, id="1234"), - subject=User(session=session, id="1234"), - nickname=None, - at=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), - ) == parse_admin_message(session, data) - - def test_admins_added(session): data = { "irisSeqId": "1111111", diff --git a/tests/events/test_emoji_set.py b/tests/events/test_emoji_set.py new file mode 100644 index 00000000..cfbec70f --- /dev/null +++ b/tests/events/test_emoji_set.py @@ -0,0 +1,82 @@ +import datetime +from fbchat import EmojiSet, Group, User +from fbchat._events import parse_admin_message + + +def test_listen(session): + data = { + "irisSeqId": "1111111", + "irisTags": ["DeltaAdminTextMessage"], + "messageMetadata": { + "actorFbId": "1234", + "adminText": "You set the emoji to 🌟.", + "folderId": {"systemFolderId": "INBOX"}, + "messageId": "mid.$XYZ", + "offlineThreadingId": "11223344556677889900", + "skipBumpThread": False, + "skipSnippetUpdate": False, + "tags": ["source:generic_admin_text"], + "threadKey": {"otherUserFbId": "1234"}, + "threadReadStateEffect": "MARK_UNREAD", + "timestamp": "1500000000000", + "unsendType": "deny_log_message", + }, + "requestContext": {"apiArgs": {}}, + "type": "change_thread_icon", + "untypedData": { + "thread_icon_url": "https://www.facebook.com/images/emoji.php/v9/te0/1/16/1f31f.png", + "thread_icon": "🌟", + }, + "class": "AdminTextMessage", + } + assert EmojiSet( + author=User(session=session, id="1234"), + thread=User(session=session, id="1234"), + emoji="🌟", + at=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), + ) == parse_admin_message(session, data) + + +def test_fetch(session): + data = { + "__typename": "GenericAdminTextMessage", + "message_id": "mid.$XYZ", + "offline_threading_id": "1122334455", + "message_sender": {"id": "1234", "email": "1234@facebook.com",}, + "ttl": 0, + "timestamp_precise": "1500000000000", + "unread": True, + "is_sponsored": False, + "ad_id": None, + "ad_client_token": None, + "commerce_message_type": None, + "customizations": [], + "tags_list": [ + "inbox", + "sent", + "no_push", + "tq", + "blindly_apply_message_folder", + "source:titan:web", + ], + "platform_xmd_encoded": None, + "message_source_data": None, + "montage_reply_data": None, + "message_reactions": [], + "unsent_timestamp_precise": "0", + "message_unsendability_status": "deny_log_message", + "extensible_message_admin_text": { + "__typename": "ThreadIconExtensibleMessageAdminText", + "thread_icon": "😊", + }, + "extensible_message_admin_text_type": "CHANGE_THREAD_ICON", + "snippet": "You set the emoji to 😊.", + "replied_to_message": None, + } + thread = Group(session=session, id="4321") + assert EmojiSet( + author=User(session=session, id="1234"), + thread=thread, + emoji="😊", + at=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), + ) == EmojiSet._from_fetch(thread, data) diff --git a/tests/events/test_main.py b/tests/events/test_main.py index 741a21a6..560d865e 100644 --- a/tests/events/test_main.py +++ b/tests/events/test_main.py @@ -6,7 +6,7 @@ Message, ParseError, UnknownEvent, - Typing, + TypingStatus, FriendRequest, Presence, ReactionEvent, @@ -68,7 +68,7 @@ def test_t_ms_full(session): def test_thread_typing(session): data = {"sender_fbid": 1234, "state": 0, "type": "typ", "thread": "4321"} (event,) = parse_events(session, "/thread_typing", data) - assert event == Typing( + assert event == TypingStatus( author=User(session=session, id="1234"), thread=Group(session=session, id="4321"), status=False, @@ -78,7 +78,7 @@ def test_thread_typing(session): def test_orca_typing_notifications(session): data = {"type": "typ", "sender_fbid": 1234, "state": 1} (event,) = parse_events(session, "/orca_typing_notifications", data) - assert event == Typing( + assert event == TypingStatus( author=User(session=session, id="1234"), thread=User(session=session, id="1234"), status=True, diff --git a/tests/events/test_nickname_set.py b/tests/events/test_nickname_set.py new file mode 100644 index 00000000..f370d698 --- /dev/null +++ b/tests/events/test_nickname_set.py @@ -0,0 +1,108 @@ +import datetime +from fbchat import NicknameSet, Group, User +from fbchat._events import parse_admin_message + + +def test_listen_set(session): + data = { + "irisSeqId": "1111111", + "irisTags": ["DeltaAdminTextMessage", "is_from_iris_fanout"], + "messageMetadata": { + "actorFbId": "1234", + "adminText": "You set the nickname for Abc Def to abc.", + "folderId": {"systemFolderId": "INBOX"}, + "messageId": "mid.$XYZ", + "offlineThreadingId": "11223344556677889900", + "skipBumpThread": False, + "tags": ["source:titan:web", "no_push"], + "threadKey": {"threadFbId": "4321"}, + "threadReadStateEffect": "MARK_UNREAD", + "timestamp": "1500000000000", + "unsendType": "deny_log_message", + }, + "participants": ["1234", "2345", "3456"], + "requestContext": {"apiArgs": {}}, + "tqSeqId": "1111", + "type": "change_thread_nickname", + "untypedData": {"nickname": "abc", "participant_id": "2345"}, + "class": "AdminTextMessage", + } + assert NicknameSet( + author=User(session=session, id="1234"), + thread=Group(session=session, id="4321"), + subject=User(session=session, id="2345"), + nickname="abc", + at=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), + ) == parse_admin_message(session, data) + + +def test_listen_clear(session): + data = { + "irisSeqId": "1111111", + "irisTags": ["DeltaAdminTextMessage"], + "messageMetadata": { + "actorFbId": "1234", + "adminText": "You cleared your nickname.", + "folderId": {"systemFolderId": "INBOX"}, + "messageId": "mid.$XYZ", + "offlineThreadingId": "11223344556677889900", + "skipBumpThread": False, + "skipSnippetUpdate": False, + "tags": ["source:generic_admin_text"], + "threadKey": {"otherUserFbId": "1234"}, + "threadReadStateEffect": "MARK_UNREAD", + "timestamp": "1500000000000", + "unsendType": "deny_log_message", + }, + "requestContext": {"apiArgs": {}}, + "type": "change_thread_nickname", + "untypedData": {"nickname": "", "participant_id": "1234"}, + "class": "AdminTextMessage", + } + assert NicknameSet( + author=User(session=session, id="1234"), + thread=User(session=session, id="1234"), + subject=User(session=session, id="1234"), + nickname=None, + at=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), + ) == parse_admin_message(session, data) + + +def test_fetch(session): + data = { + "__typename": "GenericAdminTextMessage", + "message_id": "mid.$XYZ", + "offline_threading_id": "1122334455", + "message_sender": {"id": "1234", "email": "1234@facebook.com"}, + "ttl": 0, + "timestamp_precise": "1500000000000", + "unread": True, + "is_sponsored": False, + "ad_id": None, + "ad_client_token": None, + "commerce_message_type": None, + "customizations": [], + "tags_list": ["inbox", "sent", "source:generic_admin_text"], + "platform_xmd_encoded": None, + "message_source_data": None, + "montage_reply_data": None, + "message_reactions": [], + "unsent_timestamp_precise": "0", + "message_unsendability_status": "deny_log_message", + "extensible_message_admin_text": { + "__typename": "ThreadNicknameExtensibleMessageAdminText", + "nickname": "abc", + "participant_id": "4321", + }, + "extensible_message_admin_text_type": "CHANGE_THREAD_NICKNAME", + "snippet": "You set the nickname for Abc Def to abc.", + "replied_to_message": None, + } + thread = User(session=session, id="4321") + assert NicknameSet( + author=User(session=session, id="1234"), + thread=thread, + subject=thread, + nickname="abc", + at=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), + ) == NicknameSet._from_fetch(thread, data) diff --git a/tests/events/test_participant_removed.py b/tests/events/test_participant_removed.py new file mode 100644 index 00000000..62fef85f --- /dev/null +++ b/tests/events/test_participant_removed.py @@ -0,0 +1,84 @@ +import datetime +from fbchat import User, Group, ParticipantRemoved +from fbchat._events import parse_delta + + +def test_listen(session): + data = { + "irisSeqId": "11223344", + "irisTags": ["DeltaParticipantLeftGroupThread", "is_from_iris_fanout"], + "leftParticipantFbId": "1234", + "messageMetadata": { + "actorFbId": "3456", + "adminText": "You removed Abc Def from the group.", + "folderId": {"systemFolderId": "INBOX"}, + "messageId": "mid.$XYZ", + "offlineThreadingId": "1122334455", + "skipBumpThread": True, + "tags": [], + "threadKey": {"threadFbId": "4321"}, + "threadReadStateEffect": "KEEP_AS_IS", + "timestamp": "1500000000000", + "unsendType": "deny_log_message", + }, + "participants": ["1234", "2345", "3456", "4567"], + "requestContext": {"apiArgs": {}}, + "tqSeqId": "1111", + "class": "ParticipantLeftGroupThread", + } + assert ParticipantRemoved( + author=User(session=session, id="3456"), + thread=Group(session=session, id="4321"), + removed=User(session=session, id="1234"), + at=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), + ) == parse_delta(session, data) + + +def test_send(session): + thread = Group(session=session, id="4321") + assert ParticipantRemoved( + author=session.user, + thread=thread, + removed=User(session=session, id="1234"), + at=None, + ) == ParticipantRemoved._from_send(thread, "1234") + + +def test_fetch(session): + data = { + "__typename": "ParticipantLeftMessage", + "message_id": "mid.$XYZ", + "offline_threading_id": "1122334455", + "message_sender": {"id": "3456", "email": "3456@facebook.com"}, + "ttl": 0, + "timestamp_precise": "1500000000000", + "unread": False, + "is_sponsored": False, + "ad_id": None, + "ad_client_token": None, + "commerce_message_type": None, + "customizations": [], + "tags_list": [ + "inbox", + "tq", + "blindly_apply_message_folder", + "filtered_content", + "filtered_content_account", + ], + "platform_xmd_encoded": None, + "message_source_data": None, + "montage_reply_data": None, + "message_reactions": [], + "unsent_timestamp_precise": "0", + "message_unsendability_status": "deny_for_non_sender", + "participants_removed": [{"id": "1234"}], + "snippet": "A contact removed Abc Def from the group.", + "replied_to_message": None, + } + thread = Group(session=session, id="4321") + assert ParticipantRemoved( + author=User(session=session, id="3456"), + thread=thread, + removed=User(session=session, id="1234"), + at=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), + ) == ParticipantRemoved._from_fetch(thread, data) diff --git a/tests/events/test_participants_added.py b/tests/events/test_participants_added.py new file mode 100644 index 00000000..b773e226 --- /dev/null +++ b/tests/events/test_participants_added.py @@ -0,0 +1,88 @@ +import datetime +from fbchat import User, Group, ParticipantsAdded +from fbchat._events import parse_delta + + +def test_listen(session): + data = { + "addedParticipants": [ + { + "fanoutPolicy": "IRIS_MESSAGE_QUEUE", + "firstName": "Abc", + "fullName": "Abc Def", + "initialFolder": "FOLDER_INBOX", + "initialFolderId": {"systemFolderId": "INBOX"}, + "isMessengerUser": False, + "userFbId": "1234", + } + ], + "irisSeqId": "11223344", + "irisTags": ["DeltaParticipantsAddedToGroupThread", "is_from_iris_fanout"], + "messageMetadata": { + "actorFbId": "3456", + "adminText": "You added Abc Def to the group.", + "folderId": {"systemFolderId": "INBOX"}, + "messageId": "mid.$XYZ", + "offlineThreadingId": "1122334455", + "skipBumpThread": False, + "tags": [], + "threadKey": {"threadFbId": "4321"}, + "threadReadStateEffect": "KEEP_AS_IS", + "timestamp": "1500000000000", + "unsendType": "deny_log_message", + }, + "participants": ["1234", "2345", "3456", "4567"], + "requestContext": {"apiArgs": {}}, + "tqSeqId": "1111", + "class": "ParticipantsAddedToGroupThread", + } + assert ParticipantsAdded( + author=User(session=session, id="3456"), + thread=Group(session=session, id="4321"), + added=[User(session=session, id="1234")], + at=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), + ) == parse_delta(session, data) + + +def test_send(session): + thread = Group(session=session, id="4321") + assert ParticipantsAdded( + author=session.user, + thread=thread, + added=[User(session=session, id="1234")], + at=None, + ) == ParticipantsAdded._from_send(thread, ["1234"]) + + +def test_fetch(session): + data = { + "__typename": "ParticipantsAddedMessage", + "message_id": "mid.$XYZ", + "offline_threading_id": "1122334455", + "message_sender": {"id": "3456", "email": "3456@facebook.com"}, + "ttl": 0, + "timestamp_precise": "1500000000000", + "unread": False, + "is_sponsored": False, + "ad_id": None, + "ad_client_token": None, + "commerce_message_type": None, + "customizations": [], + "tags_list": ["inbox", "tq", "blindly_apply_message_folder", "source:web"], + "platform_xmd_encoded": None, + "message_source_data": None, + "montage_reply_data": None, + "message_reactions": [], + "unsent_timestamp_precise": "0", + "message_unsendability_status": "deny_for_non_sender", + "participants_added": [{"id": "1234"}], + "snippet": "A contact added Abc Def to the group.", + "replied_to_message": None, + } + thread = Group(session=session, id="4321") + assert ParticipantsAdded( + author=User(session=session, id="3456"), + thread=thread, + added=[User(session=session, id="1234")], + at=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), + ) == ParticipantsAdded._from_fetch(thread, data) diff --git a/tests/events/test_title_set.py b/tests/events/test_title_set.py new file mode 100644 index 00000000..594998c0 --- /dev/null +++ b/tests/events/test_title_set.py @@ -0,0 +1,68 @@ +import datetime +from fbchat import TitleSet, Group, User +from fbchat._events import parse_delta + + +def test_listen(session): + data = { + "irisSeqId": "11223344", + "irisTags": ["DeltaThreadName", "is_from_iris_fanout"], + "messageMetadata": { + "actorFbId": "3456", + "adminText": "You named the group abc.", + "folderId": {"systemFolderId": "INBOX"}, + "messageId": "mid.$XYZ", + "offlineThreadingId": "1122334455", + "skipBumpThread": False, + "tags": [], + "threadKey": {"threadFbId": "4321"}, + "threadReadStateEffect": "KEEP_AS_IS", + "timestamp": "1500000000000", + "unsendType": "deny_log_message", + }, + "name": "abc", + "participants": ["1234", "2345", "3456", "4567"], + "requestContext": {"apiArgs": {}}, + "tqSeqId": "1111", + "class": "ThreadName", + } + assert TitleSet( + author=User(session=session, id="3456"), + thread=Group(session=session, id="4321"), + title="abc", + at=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), + ) == parse_delta(session, data) + + +def test_fetch(session): + data = { + "__typename": "ThreadNameMessage", + "message_id": "mid.$XYZ", + "offline_threading_id": "1122334455", + "message_sender": {"id": "1234", "email": "1234@facebook.com"}, + "ttl": 0, + "timestamp_precise": "1500000000000", + "unread": False, + "is_sponsored": False, + "ad_id": None, + "ad_client_token": None, + "commerce_message_type": None, + "customizations": [], + "tags_list": ["inbox", "sent", "tq", "blindly_apply_message_folder"], + "platform_xmd_encoded": None, + "message_source_data": None, + "montage_reply_data": None, + "message_reactions": [], + "unsent_timestamp_precise": "0", + "message_unsendability_status": "deny_log_message", + "thread_name": "", + "snippet": "You removed the group name.", + "replied_to_message": None, + } + thread = Group(session=session, id="4321") + assert TitleSet( + author=User(session=session, id="1234"), + thread=thread, + title=None, + at=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), + ) == TitleSet._from_fetch(thread, data)