From 4abe5659ae55850f982cf89bf4655aa32e47d280 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Mon, 20 Jan 2020 18:02:55 +0100 Subject: [PATCH] Move /t_ms delta class parsing to separate file and add tests --- fbchat/__init__.py | 9 ++ fbchat/_client.py | 284 +------------------------------------ fbchat/_delta_class.py | 187 ++++++++++++++++++++++++ fbchat/_event_common.py | 10 +- fbchat/_message.py | 8 +- tests/conftest.py | 9 +- tests/test_delta_class.py | 275 +++++++++++++++++++++++++++++++++++ tests/test_event_common.py | 21 +++ 8 files changed, 522 insertions(+), 281 deletions(-) create mode 100644 fbchat/_delta_class.py create mode 100644 tests/test_delta_class.py diff --git a/fbchat/__init__.py b/fbchat/__init__.py index 0cd6d70..4e67a7f 100644 --- a/fbchat/__init__.py +++ b/fbchat/__init__.py @@ -50,6 +50,15 @@ from ._client_payload import ( UnsendEvent, MessageReplyEvent, ) +from ._delta_class import ( + PeopleAdded, + PersonRemoved, + TitleSet, + UnfetchedThreadEvent, + MessagesDelivered, + ThreadsRead, + MessageEvent, +) from ._client import Client diff --git a/fbchat/_client.py b/fbchat/_client.py index ddde55e..4e33a71 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -15,6 +15,7 @@ from . import ( _message, _event_common, _client_payload, + _delta_class, ) from ._thread import ThreadLocation @@ -618,7 +619,6 @@ class Client: return None delta_type = delta.get("type") - delta_class = delta.get("class") metadata = delta.get("messageMetadata") if metadata: @@ -626,30 +626,8 @@ class Client: author_id = str(metadata["actorFbId"]) at = _util.millis_to_datetime(int(metadata.get("timestamp"))) - # Added participants - if "addedParticipants" in delta: - added_ids = [str(x["userFbId"]) for x in delta["addedParticipants"]] - self.on_people_added( - mid=mid, - added_ids=added_ids, - author_id=author_id, - group=get_thread(metadata), - at=at, - ) - - # Left/removed participants - elif "leftParticipantFbId" in delta: - removed_id = str(delta["leftParticipantFbId"]) - self.on_person_removed( - mid=mid, - removed_id=removed_id, - author_id=author_id, - group=get_thread(metadata), - at=at, - ) - # Color change - elif delta_type == "change_thread_theme": + if delta_type == "change_thread_theme": thread = get_thread(metadata) self.on_color_change( mid=mid, @@ -662,13 +640,6 @@ class Client: metadata=metadata, ) - elif delta_class == "MarkFolderSeen": - locations = [ - ThreadLocation(folder.lstrip("FOLDER_")) for folder in delta["folders"] - ] - at = _util.millis_to_datetime(int(delta["timestamp"])) - self._on_seen(locations=locations, at=at) - # Emoji change elif delta_type == "change_thread_icon": new_emoji = delta["untypedData"]["thread_icon"] @@ -681,45 +652,6 @@ class Client: metadata=metadata, ) - # Thread title change - elif delta_class == "ThreadName": - new_title = delta["name"] - self.on_title_change( - mid=mid, - author_id=author_id, - new_title=new_title, - group=get_thread(metadata), - at=at, - metadata=metadata, - ) - - # Forced fetch - elif delta_class == "ForcedFetch": - mid = delta.get("messageId") - if mid is None: - self.on_unknown_messsage_type(msg=delta) - else: - group = get_thread(delta) - 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"])) - if fetch_data.get("__typename") == "ThreadImageMessage": - # Thread image change - image_metadata = fetch_data.get("image_with_metadata") - image_id = ( - int(image_metadata["legacy_attachment_id"]) - if image_metadata - else None - ) - self.on_image_change( - mid=mid, - author_id=author_id, - new_image=image_id, - group=group, - at=at, - ) - # Nickname change elif delta_type == "change_thread_nickname": changed_for = str(delta["untypedData"]["participant_id"]) @@ -766,52 +698,6 @@ class Client: at=at, ) - # Message delivered - elif delta_class == "DeliveryReceipt": - message_ids = delta["messageIds"] - delivered_for = str( - delta.get("actorFbId") or delta["threadKey"]["otherUserFbId"] - ) - at = _util.millis_to_datetime(int(delta["deliveredWatermarkTimestampMs"])) - self.on_message_delivered( - msg_ids=message_ids, - delivered_for=delivered_for, - thread=get_thread(delta), - at=at, - metadata=metadata, - ) - - # Message seen - elif delta_class == "ReadReceipt": - 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"])) - self.on_message_seen( - seen_by=seen_by, - thread=get_thread(delta), - seen_at=seen_at, - at=at, - metadata=metadata, - ) - - # Messages marked as seen - elif delta_class == "MarkRead": - seen_at = _util.millis_to_datetime( - int(delta.get("actionTimestampMs") or delta.get("actionTimestamp")) - ) - watermark_ts = delta.get("watermarkTimestampMs") or delta.get( - "watermarkTimestamp" - ) - at = _util.millis_to_datetime(int(watermark_ts)) - - threads = [] - if "folders" not in delta: - threads = [ - get_thread({"threadKey": thr}) for thr in delta.get("threadKeys") - ] - - self.on_marked_seen(threads=threads, seen_at=seen_at, at=at, metadata=delta) - # Game played elif delta_type == "instant_game_update": game_id = delta["untypedData"]["game_id"] @@ -834,10 +720,6 @@ class Client: metadata=metadata, ) - # Skip "no operation" events - elif delta_class == "NoOp": - pass - # Group call started/ended elif delta_type == "rtc_call_log": call_status = delta["untypedData"]["event"] @@ -964,28 +846,14 @@ class Client: ) # Client payload (that weird numbers) - elif delta_class == "ClientPayload": + elif delta.get("class") == "ClientPayload": for event in _client_payload.parse_client_payloads(self.session, delta): self.on_event(event) - # New message - elif delta.get("class") == "NewMessage": - thread = get_thread(metadata) - self.on_message( - mid=mid, - author_id=author_id, - message_object=_message.MessageData._from_pull( - thread, - delta, - mid=mid, - tags=metadata.get("tags"), - author=author_id, - created_at=at, - ), - thread=thread, - at=at, - metadata=metadata, - ) + elif delta.get("class"): + event = _delta_class.parse_delta(self.session, delta) + if event: + self.on_event(event) # Unknown message type else: @@ -1116,27 +984,6 @@ class Client: """Called when the client is listening, and an event happens.""" log.info("Got event: %s", event) - def on_message( - self, - mid=None, - author_id=None, - message_object=None, - thread=None, - at=None, - metadata=None, - ): - """Called when the client is listening, and somebody sends a message. - - Args: - mid: The message ID - author_id: The ID of the author - message_object (Message): The message (As a `Message` object) - 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 - """ - log.info("{} from {} in {}".format(message_object, author_id, thread)) - def on_color_change( self, mid=None, @@ -1179,41 +1026,6 @@ class Client: """ 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, - group=None, - at=None, - metadata=None, - ): - """Called when the client is listening, and somebody changes a thread's title. - - Args: - mid: The action ID - author_id: The ID of the person who changed the title - new_title: The new title - 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 - """ - 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, group=None, at=None - ): - """Called when the client is listening, and somebody changes a thread's image. - - Args: - mid: The action ID - author_id: The ID of the person who changed the image - new_image: The ID of the new image - group: Group that the action was sent to. See :ref:`intro_threads` - at (datetime.datetime): When the action was executed - """ - log.info("{} changed group image in {}".format(author_id, group)) - def on_nickname_change( self, mid=None, @@ -1286,78 +1098,6 @@ class Client: else: log.info("{} disabled approval mode in {}".format(author_id, group)) - def on_message_seen( - self, seen_by=None, thread=None, seen_at=None, at=None, metadata=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: 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 - """ - 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=None, at=None, metadata=None, - ): - """Called when the client is listening, and somebody marks messages as delivered. - - Args: - msg_ids: The messages that are marked as delivered - delivered_for: The person that marked the messages as delivered - 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 - """ - log.info( - "Messages {} delivered to {} in {} at {}".format( - msg_ids, delivered_for, thread, at - ) - ) - - def on_marked_seen(self, threads=None, seen_at=None, at=None, metadata=None): - """Called when the client is listening, and the client has successfully marked threads as seen. - - Args: - threads: The threads that were marked - author_id: The ID of the person who changed the emoji - seen_at (datetime.datetime): When the threads were seen - at (datetime.datetime): When the action was executed - metadata: Extra metadata about the action - """ - log.info("Marked messages as seen in threads {} at {}".format(threads, seen_at)) - - def on_people_added( - self, mid=None, added_ids=None, author_id=None, group=None, at=None - ): - """Called when the client is listening, and somebody adds people to a group thread. - - Args: - 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 - group: Group that the action was sent to. See :ref:`intro_threads` - at (datetime.datetime): When the action was executed - """ - log.info("{} added: {} in {}".format(author_id, ", ".join(added_ids), group)) - - def on_person_removed( - self, mid=None, removed_id=None, author_id=None, group=None, at=None - ): - """Called when the client is listening, and somebody removes a person from a group thread. - - Args: - 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 - group: Group that the action was sent to. See :ref:`intro_threads` - at (datetime.datetime): When the action was executed - """ - log.info("{} removed: {} in {}".format(author_id, removed_id, group)) - def on_friend_request(self, from_id=None): """Called when the client is listening, and somebody sends a friend request. @@ -1366,16 +1106,6 @@ class Client: """ log.info("Friend request from {}".format(from_id)) - def _on_seen(self, locations=None, at=None): - """ - Todo: - Document this, and make it public - - Args: - locations: --- - at: A timestamp of the action - """ - def on_inbox(self, unseen=None, unread=None, recent_unread=None): """ Todo: diff --git a/fbchat/_delta_class.py b/fbchat/_delta_class.py new file mode 100644 index 0000000..62cffae --- /dev/null +++ b/fbchat/_delta_class.py @@ -0,0 +1,187 @@ +import attr +import datetime +from ._event_common import attrs_event, Event, UnknownEvent, ThreadEvent +from . import _util, _user, _group, _thread, _message + +from typing import Sequence + + +@attrs_event +class PeopleAdded(ThreadEvent): + """somebody added people to a group thread.""" + + # TODO: Add message id + + thread = attr.ib(type=_group.Group) # Set the correct type + #: The people who got added + added = attr.ib(type=Sequence[_user.User]) + #: When the people were added + at = attr.ib(type=datetime.datetime) + + @classmethod + def _parse(cls, session, data): + author, thread, at = cls._parse_metadata(session, data) + added = [ + # TODO: Parse user name + _user.User(session=session, id=x["userFbId"]) + for x in data["addedParticipants"] + ] + return cls(author=author, thread=thread, added=added, at=at) + + +@attrs_event +class PersonRemoved(ThreadEvent): + """Somebody removed a person from a group thread.""" + + # TODO: Add message id + + thread = attr.ib(type=_group.Group) # Set the correct type + #: Person who got removed + removed = attr.ib(type=_message.Message) + #: When the person were removed + at = attr.ib(type=datetime.datetime) + + @classmethod + def _parse(cls, session, data): + author, thread, at = cls._parse_metadata(session, data) + removed = _user.User(session=session, id=data["leftParticipantFbId"]) + 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=_group.Group) # Set the correct type + #: The new title + title = attr.ib(type=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) + + +@attrs_event +class UnfetchedThreadEvent(Event): + """A message was received, but the data must be fetched manually. + + Use `Message.fetch` to retrieve the message data. + + This is usually used when somebody changes the group's photo, or when a new pending + group is created. + """ + + # TODO: Present this in a way that users can fetch the changed group photo easily + + #: The thread the message was sent to + thread = attr.ib(type=_thread.ThreadABC) + #: The message + message = attr.ib(type=_message.Message) + + @classmethod + def _parse(cls, session, data): + thread = ThreadEvent._get_thread(session, data) + message = _message.Message(thread=thread, id=data["messageId"]) + return cls(thread=thread, message=message) + + +@attrs_event +class MessagesDelivered(ThreadEvent): + """Somebody marked messages as delivered in a thread.""" + + #: The messages that were marked as delivered + messages = attr.ib(type=Sequence[_message.Message]) + #: When the messages were delivered + at = attr.ib(type=datetime.datetime) + + @classmethod + def _parse(cls, session, data): + author = _user.User(session=session, id=data["actorFbId"]) + thread = cls._get_thread(session, data) + messages = [_message.Message(thread=thread, id=x) for x in data["messageIds"]] + at = _util.millis_to_datetime(int(data["deliveredWatermarkTimestampMs"])) + return cls(author=author, thread=thread, messages=messages, at=at) + + +@attrs_event +class ThreadsRead(Event): + """Somebody marked threads as read/seen.""" + + #: The person who marked the threads as read + author = attr.ib(type=_thread.ThreadABC) + #: The threads that were marked as read + threads = attr.ib(type=Sequence[_thread.ThreadABC]) + #: When the threads were read + at = attr.ib(type=datetime.datetime) + + @classmethod + def _parse_read_receipt(cls, session, data): + author = _user.User(session=session, id=data["actorFbId"]) + thread = ThreadEvent._get_thread(session, data) + at = _util.millis_to_datetime(int(data["actionTimestampMs"])) + return cls(author=author, threads=[thread], at=at) + + @classmethod + def _parse(cls, session, data): + author = _user.User(session=session, id=session.user_id) + threads = [ + ThreadEvent._get_thread(session, {"threadKey": x}) + for x in data["threadKeys"] + ] + at = _util.millis_to_datetime(int(data["actionTimestamp"])) + return cls(author=author, threads=threads, at=at) + + +@attrs_event +class MessageEvent(ThreadEvent): + """Somebody sent a message to a thread.""" + + #: The sent message + message = attr.ib(type=_message.Message) + #: When the threads were read + at = attr.ib(type=datetime.datetime) + + @classmethod + def _parse(cls, session, data): + author, thread, at = cls._parse_metadata(session, data) + message = _message.MessageData._from_pull( + thread, data, author=author.id, created_at=at, + ) + return cls(author=author, thread=thread, message=message, at=at) + + +def parse_delta(session, data): + class_ = data.get("class") + if class_ == "ParticipantsAddedToGroupThread": + return PeopleAdded._parse(session, data) + elif class_ == "ParticipantLeftGroupThread": + return PersonRemoved._parse(session, data) + elif class_ == "MarkFolderSeen": + # TODO: Finish this + folders = [ + _thread.ThreadLocation(folder.lstrip("FOLDER_")) + for folder in data["folders"] + ] + at = _util.millis_to_datetime(int(data["timestamp"])) + return None + elif class_ == "ThreadName": + return TitleSet._parse(session, data) + elif class_ == "ForcedFetch": + return UnfetchedThreadEvent._parse(session, data) + elif class_ == "DeliveryReceipt": + return MessagesDelivered._parse(session, data) + elif class_ == "ReadReceipt": + return ThreadsRead._parse_read_receipt(session, data) + elif class_ == "MarkRead": + return ThreadsRead._parse(session, data) + elif class_ == "NoOp": + # Skip "no operation" events + return None + elif class_ == "ClientPayload": + return X._parse(session, data) + elif class_ == "NewMessage": + return MessageEvent._parse(session, data) + return UnknownEvent(data=data) diff --git a/fbchat/_event_common.py b/fbchat/_event_common.py index 5da21c4..169fc9a 100644 --- a/fbchat/_event_common.py +++ b/fbchat/_event_common.py @@ -1,7 +1,7 @@ import attr import abc from ._core import kw_only -from . import _exception, _thread, _group, _user, _message +from . import _exception, _util, _thread, _group, _user, _message #: Default attrs settings for events attrs_event = attr.s(slots=True, kw_only=kw_only, frozen=True) @@ -48,3 +48,11 @@ class ThreadEvent(Event): elif "otherUserFbId" in key: return _user.User(session=session, id=str(key["otherUserFbId"])) raise _exception.ParseError("Could not find thread data", data=data) + + @staticmethod + def _parse_metadata(session, data): + metadata = data["messageMetadata"] + author = _user.User(session=session, id=metadata["actorFbId"]) + thread = ThreadEvent._get_thread(session, metadata) + at = _util.millis_to_datetime(int(metadata["timestamp"])) + return author, thread, at diff --git a/fbchat/_message.py b/fbchat/_message.py index ab33aa3..d08aba2 100644 --- a/fbchat/_message.py +++ b/fbchat/_message.py @@ -367,7 +367,11 @@ class MessageData(Message): ) @classmethod - def _from_pull(cls, thread, data, mid, tags, author, created_at): + def _from_pull(cls, thread, data, author, created_at): + metadata = data["messageMetadata"] + + tags = metadata.get("tags") + mentions = [] if data.get("data") and data["data"].get("prng"): try: @@ -415,7 +419,7 @@ class MessageData(Message): return cls( thread=thread, - id=mid, + id=metadata["messageId"], author=author, created_at=created_at, text=data.get("body"), diff --git a/tests/conftest.py b/tests/conftest.py index ad71946..a327ac8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,7 +8,14 @@ from fbchat import Message, Mention @pytest.fixture(scope="session") def session(): - return object() # TODO: Add a mocked session + class FakeSession: + # TODO: Add a further mocked session + user_id = "31415926536" + + def __repr__(self): + return "" + + return FakeSession() @pytest.fixture(scope="session") diff --git a/tests/test_delta_class.py b/tests/test_delta_class.py new file mode 100644 index 0000000..a15432f --- /dev/null +++ b/tests/test_delta_class.py @@ -0,0 +1,275 @@ +import datetime +import pytest +from fbchat import ( + ParseError, + User, + Group, + UnknownEvent, + PeopleAdded, + PersonRemoved, + TitleSet, + UnfetchedThreadEvent, + MessagesDelivered, + ThreadsRead, + MessageEvent, +) +from fbchat._message import Message, MessageData +from fbchat._delta_class 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, + "messageId": "mid.$XYZ", + "threadKey": {"threadFbId": "1234"}, + "class": "ForcedFetch", + } + thread = Group(session=session, id="1234") + assert UnfetchedThreadEvent( + thread=thread, message=Message(thread=thread, id="mid.$XYZ") + ) == parse_delta(session, data) + + +def test_delivery_receipt(session): + data = { + "actorFbId": "1234", + "deliveredWatermarkTimestampMs": "1500000000000", + "irisSeqId": "1111111", + "irisTags": ["DeltaDeliveryReceipt"], + "messageIds": ["mid.$XYZ", "mid.$ABC"], + "requestContext": {"apiArgs": {}}, + "threadKey": {"threadFbId": "4321"}, + "class": "DeliveryReceipt", + } + thread = Group(session=session, id="4321") + assert MessagesDelivered( + author=User(session=session, id="1234"), + thread=thread, + messages=[ + Message(thread=thread, id="mid.$XYZ"), + Message(thread=thread, id="mid.$ABC"), + ], + at=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), + ) == parse_delta(session, data) + + +def test_read_receipt(session): + data = { + "actionTimestampMs": "1600000000000", + "actorFbId": "1234", + "irisSeqId": "1111111", + "irisTags": ["DeltaReadReceipt", "is_from_iris_fanout"], + "requestContext": {"apiArgs": {}}, + "threadKey": {"threadFbId": "4321"}, + "tqSeqId": "1111", + "watermarkTimestampMs": "1500000000000", + "class": "ReadReceipt", + } + assert ThreadsRead( + author=User(session=session, id="1234"), + threads=[Group(session=session, id="4321")], + at=datetime.datetime(2020, 9, 13, 12, 26, 40, tzinfo=datetime.timezone.utc), + ) == parse_delta(session, data) + + +def test_mark_read(session): + data = { + "actionTimestamp": "1600000000000", + "irisSeqId": "1111111", + "irisTags": ["DeltaMarkRead", "is_from_iris_fanout"], + "threadKeys": [{"threadFbId": "1234"}, {"otherUserFbId": "2345"}], + "tqSeqId": "1111", + "watermarkTimestamp": "1500000000000", + "class": "MarkRead", + } + assert ThreadsRead( + author=User(session=session, id=session.user_id), + threads=[Group(session=session, id="1234"), User(session=session, id="2345")], + at=datetime.datetime(2020, 9, 13, 12, 26, 40, tzinfo=datetime.timezone.utc), + ) == parse_delta(session, data) + + +def test_new_message_user(session): + data = { + "attachments": [], + "body": "test", + "irisSeqId": "1111111", + "irisTags": ["DeltaNewMessage"], + "messageMetadata": { + "actorFbId": "1234", + "folderId": {"systemFolderId": "INBOX"}, + "messageId": "mid.$XYZ", + "offlineThreadingId": "11223344556677889900", + "skipBumpThread": False, + "skipSnippetUpdate": False, + "tags": ["source:messenger:web"], + "threadKey": {"otherUserFbId": "1234"}, + "threadReadStateEffect": "KEEP_AS_IS", + "timestamp": "1600000000000", + }, + "requestContext": {"apiArgs": {}}, + "class": "NewMessage", + } + assert MessageEvent( + author=User(session=session, id="1234"), + thread=User(session=session, id="1234"), + message=MessageData( + thread=User(session=session, id="1234"), + id="mid.$XYZ", + author="1234", + text="test", + created_at=datetime.datetime( + 2020, 9, 13, 12, 26, 40, tzinfo=datetime.timezone.utc + ), + ), + at=datetime.datetime(2020, 9, 13, 12, 26, 40, tzinfo=datetime.timezone.utc), + ) == parse_delta(session, data) + + +def test_new_message_group(session): + data = { + "attachments": [], + "body": "test", + "irisSeqId": "1111111", + "irisTags": ["DeltaNewMessage", "is_from_iris_fanout"], + "messageMetadata": { + "actorFbId": "4321", + "folderId": {"systemFolderId": "INBOX"}, + "messageId": "mid.$XYZ", + "offlineThreadingId": "11223344556677889900", + "skipBumpThread": False, + "tags": ["source:messenger:web"], + "threadKey": {"threadFbId": "1234"}, + "threadReadStateEffect": "KEEP_AS_IS", + "timestamp": "1600000000000", + }, + "participants": ["4321", "5432", "6543"], + "requestContext": {"apiArgs": {}}, + "tqSeqId": "1111", + "class": "NewMessage", + } + assert MessageEvent( + author=User(session=session, id="4321"), + thread=Group(session=session, id="1234"), + message=MessageData( + thread=Group(session=session, id="1234"), + id="mid.$XYZ", + author="4321", + text="test", + created_at=datetime.datetime( + 2020, 9, 13, 12, 26, 40, tzinfo=datetime.timezone.utc + ), + ), + at=datetime.datetime(2020, 9, 13, 12, 26, 40, tzinfo=datetime.timezone.utc), + ) == parse_delta(session, data) + + +def test_noop(session): + assert parse_delta(session, {"class": "NoOp"}) is None + + +def test_parse_delta_unknown(session): + assert UnknownEvent(data={"abc": 10}) == parse_delta(session, {"abc": 10}) diff --git a/tests/test_event_common.py b/tests/test_event_common.py index 2b462f9..74f73eb 100644 --- a/tests/test_event_common.py +++ b/tests/test_event_common.py @@ -1,4 +1,5 @@ import pytest +import datetime from fbchat import Group, User, ParseError, ThreadEvent @@ -55,3 +56,23 @@ def test_thread_event_get_thread_unknown(session): data = {"threadKey": {"abc": "1234"}} with pytest.raises(ParseError, match="Could not find thread data"): ThreadEvent._get_thread(session, data) + + +def test_thread_event_parse_metadata(session): + data = { + "actorFbId": "4321", + "folderId": {"systemFolderId": "INBOX"}, + "messageId": "mid.$XYZ", + "offlineThreadingId": "112233445566", + "skipBumpThread": False, + "skipSnippetUpdate": False, + "tags": ["source:messenger:web"], + "threadKey": {"otherUserFbId": "1234"}, + "threadReadStateEffect": "KEEP_AS_IS", + "timestamp": "1500000000000", + } + assert ( + User(session=session, id="4321"), + User(session=session, id="1234"), + datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), + ) == ThreadEvent._parse_metadata(session, {"messageMetadata": data})