diff --git a/fbchat/__init__.py b/fbchat/__init__.py index 4e67a7f..274c228 100644 --- a/fbchat/__init__.py +++ b/fbchat/__init__.py @@ -59,6 +59,24 @@ from ._delta_class import ( ThreadsRead, MessageEvent, ) +from ._delta_type import ( + ColorSet, + EmojiSet, + NicknameSet, + AdminsAdded, + AdminsRemoved, + ApprovalModeSet, + CallStarted, + CallEnded, + CallJoined, + PollCreated, + PollVoted, + PlanCreated, + PlanEnded, + PlanEdited, + PlanDeleted, + PlanResponded, +) from ._client import Client diff --git a/fbchat/_client.py b/fbchat/_client.py index 4e33a71..42b1c93 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -16,6 +16,7 @@ from . import ( _event_common, _client_payload, _delta_class, + _delta_type, ) from ._thread import ThreadLocation @@ -609,244 +610,8 @@ class Client: """ def _parse_delta(self, delta): - 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_type = delta.get("type") - metadata = delta.get("messageMetadata") - - if metadata: - mid = metadata["messageId"] - author_id = str(metadata["actorFbId"]) - at = _util.millis_to_datetime(int(metadata.get("timestamp"))) - - # Color change - if delta_type == "change_thread_theme": - thread = get_thread(metadata) - self.on_color_change( - mid=mid, - author_id=author_id, - new_color=_thread.ThreadABC._parse_color( - delta["untypedData"]["theme_color"] - ), - thread=get_thread(metadata), - at=at, - metadata=metadata, - ) - - # Emoji change - elif delta_type == "change_thread_icon": - new_emoji = delta["untypedData"]["thread_icon"] - self.on_emoji_change( - mid=mid, - author_id=author_id, - new_emoji=new_emoji, - thread=get_thread(metadata), - at=at, - metadata=metadata, - ) - - # Nickname change - elif delta_type == "change_thread_nickname": - changed_for = str(delta["untypedData"]["participant_id"]) - new_nickname = delta["untypedData"]["nickname"] - self.on_nickname_change( - mid=mid, - author_id=author_id, - changed_for=changed_for, - new_nickname=new_nickname, - thread=get_thread(metadata), - at=at, - metadata=metadata, - ) - - # Admin added or removed in a group thread - elif delta_type == "change_thread_admins": - target_id = delta["untypedData"]["TARGET_ID"] - admin_event = delta["untypedData"]["ADMIN_EVENT"] - if admin_event == "add_admin": - self.on_admin_added( - mid=mid, - added_id=target_id, - author_id=author_id, - group=get_thread(metadata), - at=at, - ) - elif admin_event == "remove_admin": - self.on_admin_removed( - mid=mid, - removed_id=target_id, - author_id=author_id, - group=get_thread(metadata), - at=at, - ) - - # Group approval mode change - elif delta_type == "change_thread_approval_mode": - approval_mode = bool(int(delta["untypedData"]["APPROVAL_MODE"])) - self.on_approval_mode_change( - mid=mid, - approval_mode=approval_mode, - author_id=author_id, - group=get_thread(metadata), - at=at, - ) - - # Game played - elif delta_type == "instant_game_update": - game_id = delta["untypedData"]["game_id"] - game_name = delta["untypedData"]["game_name"] - score = delta["untypedData"].get("score") - if score is not None: - score = int(score) - leaderboard = delta["untypedData"].get("leaderboard") - if leaderboard is not None: - leaderboard = _util.parse_json(leaderboard)["scores"] - self.on_game_played( - mid=mid, - author_id=author_id, - game_id=game_id, - game_name=game_name, - score=score, - leaderboard=leaderboard, - thread=get_thread(metadata), - at=at, - metadata=metadata, - ) - - # Group call started/ended - elif delta_type == "rtc_call_log": - call_status = delta["untypedData"]["event"] - call_duration = _util.seconds_to_timedelta( - int(delta["untypedData"]["call_duration"]) - ) - is_video_call = bool(int(delta["untypedData"]["is_video_call"])) - if call_status == "call_started": - self.on_call_started( - mid=mid, - caller_id=author_id, - is_video_call=is_video_call, - thread=get_thread(metadata), - at=at, - metadata=metadata, - ) - elif call_status == "call_ended": - self.on_call_ended( - mid=mid, - caller_id=author_id, - is_video_call=is_video_call, - call_duration=call_duration, - thread=get_thread(metadata), - at=at, - metadata=metadata, - ) - - # User joined to group call - elif delta_type == "participant_joined_group_call": - 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=get_thread(metadata), - at=at, - metadata=metadata, - ) - - # Group poll event - elif delta_type == "group_poll": - event_type = delta["untypedData"]["event_type"] - poll_json = _util.parse_json(delta["untypedData"]["question_json"]) - poll = _poll.Poll._from_graphql(self.session, poll_json) - if event_type == "question_creation": - # User created group poll - self.on_poll_created( - mid=mid, - poll=poll, - author_id=author_id, - thread=get_thread(metadata), - at=at, - metadata=metadata, - ) - elif event_type == "update_vote": - # User voted on group poll - added = _util.parse_json(delta["untypedData"]["added_option_ids"]) - removed = _util.parse_json(delta["untypedData"]["removed_option_ids"]) - self.on_poll_voted( - mid=mid, - poll=poll, - added_options=added, - removed_options=removed, - author_id=author_id, - thread=get_thread(metadata), - at=at, - metadata=metadata, - ) - - # Plan created - elif delta_type == "lightweight_event_create": - self.on_plan_created( - mid=mid, - plan=PlanData._from_pull(self.session, delta["untypedData"]), - author_id=author_id, - thread=get_thread(metadata), - at=at, - metadata=metadata, - ) - - # Plan ended - elif delta_type == "lightweight_event_notify": - self.on_plan_ended( - mid=mid, - plan=PlanData._from_pull(self.session, delta["untypedData"]), - thread=get_thread(metadata), - at=at, - metadata=metadata, - ) - - # Plan edited - elif delta_type == "lightweight_event_update": - self.on_plan_edited( - mid=mid, - plan=PlanData._from_pull(self.session, delta["untypedData"]), - author_id=author_id, - thread=get_thread(metadata), - at=at, - metadata=metadata, - ) - - # Plan deleted - elif delta_type == "lightweight_event_delete": - self.on_plan_deleted( - mid=mid, - plan=PlanData._from_pull(self.session, delta["untypedData"]), - author_id=author_id, - thread=get_thread(metadata), - at=at, - metadata=metadata, - ) - - # Plan participation change - elif delta_type == "lightweight_event_rsvp": - take_part = delta["untypedData"]["guest_status"] == "GOING" - self.on_plan_participation( - mid=mid, - plan=PlanData._from_pull(self.session, delta["untypedData"]), - take_part=take_part, - author_id=author_id, - thread=get_thread(metadata), - at=at, - metadata=metadata, - ) - # Client payload (that weird numbers) - elif delta.get("class") == "ClientPayload": + if delta.get("class") == "ClientPayload": for event in _client_payload.parse_client_payloads(self.session, delta): self.on_event(event) @@ -855,6 +620,9 @@ class Client: if event: self.on_event(event) + elif delta.get("type"): + self.on_event(_delta_type.parse_delta(self.session, delta)) + # Unknown message type else: self.on_unknown_messsage_type(msg=delta) @@ -984,120 +752,6 @@ class Client: """Called when the client is listening, and an event happens.""" log.info("Got event: %s", event) - def on_color_change( - self, - mid=None, - author_id=None, - new_color=None, - thread=None, - at=None, - metadata=None, - ): - """Called when the client is listening, and somebody changes a thread's color. - - Args: - mid: The action ID - author_id: The ID of the person who changed the color - new_color: The new color. Not limited to the ones in `ThreadABC.set_color` - 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("Color change from {} in {}: {}".format(author_id, thread, new_color)) - - def on_emoji_change( - self, - mid=None, - author_id=None, - new_emoji=None, - thread=None, - at=None, - metadata=None, - ): - """Called when the client is listening, and somebody changes a thread's emoji. - - Args: - mid: The action ID - author_id: The ID of the person who changed the emoji - new_emoji: The new emoji - 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("Emoji change from {} in {}: {}".format(author_id, thread, new_emoji)) - - def on_nickname_change( - self, - mid=None, - author_id=None, - changed_for=None, - new_nickname=None, - thread=None, - at=None, - metadata=None, - ): - """Called when the client is listening, and somebody changes a nickname. - - Args: - mid: The action ID - 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: 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( - "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, group=None, at=None - ): - """Called when the client is listening, and somebody adds an admin to a group. - - Args: - 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 - group: Group that the action was sent to. See :ref:`intro_threads` - at (datetime.datetime): When the action was executed - """ - log.info("{} added admin: {} in {}".format(author_id, added_id, group)) - - def on_admin_removed( - self, mid=None, removed_id=None, author_id=None, group=None, at=None - ): - """Called when the client is listening, and somebody is removed as an admin in a group. - - Args: - 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 - group: Group that the action was sent to. See :ref:`intro_threads` - at (datetime.datetime): When the action was executed - """ - 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, group=None, at=None, - ): - """Called when the client is listening, and somebody changes approval mode in a group. - - Args: - mid: The action ID - approval_mode: True if approval mode is activated - author_id: The ID of the person who changed approval mode - group: Group that the action was sent to. See :ref:`intro_threads` - at (datetime.datetime): When the action was executed - """ - if approval_mode: - log.info("{} activated approval mode in {}".format(author_id, group)) - else: - log.info("{} disabled approval mode in {}".format(author_id, group)) - def on_friend_request(self, from_id=None): """Called when the client is listening, and somebody sends a friend request. @@ -1128,229 +782,6 @@ class Client: """ pass - def on_game_played( - self, - mid=None, - author_id=None, - game_id=None, - game_name=None, - score=None, - leaderboard=None, - thread=None, - at=None, - metadata=None, - ): - """Called when the client is listening, and somebody plays a game. - - Args: - mid: The action ID - author_id: The ID of the person who played the game - game_id: The ID of the game - game_name: Name of the game - score: Score obtained in the game - leaderboard: Actual leader board of the game in the thread - 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('{} played "{}" in {}'.format(author_id, game_name, thread)) - - def on_call_started( - self, - mid=None, - caller_id=None, - is_video_call=None, - thread=None, - at=None, - metadata=None, - ): - """Called when the client is listening, and somebody starts a call in a group. - - Todo: - Make this work with private calls. - - Args: - 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: 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("{} started call in {}".format(caller_id, thread)) - - def on_call_ended( - self, - mid=None, - caller_id=None, - is_video_call=None, - call_duration=None, - thread=None, - at=None, - metadata=None, - ): - """Called when the client is listening, and somebody ends a call in a group. - - Todo: - Make this work with private calls. - - Args: - mid: The action ID - 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: 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("{} ended call in {}".format(caller_id, thread)) - - def on_user_joined_call( - self, - mid=None, - joined_id=None, - is_video_call=None, - thread=None, - at=None, - metadata=None, - ): - """Called when the client is listening, and somebody joins a group call. - - Args: - 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: 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("{} joined call in {}".format(joined_id, thread)) - - def on_poll_created( - self, mid=None, poll=None, author_id=None, thread=None, at=None, metadata=None, - ): - """Called when the client is listening, and somebody creates a group poll. - - Args: - mid: The action ID - poll (Poll): Created poll - author_id: The ID of the person who created the poll - 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("{} created poll {} in {}".format(author_id, poll, thread)) - - def on_poll_voted( - self, - mid=None, - poll=None, - added_options=None, - removed_options=None, - author_id=None, - thread=None, - at=None, - metadata=None, - ): - """Called when the client is listening, and somebody votes in a group poll. - - Args: - mid: The action ID - poll (Poll): Poll, that user voted in - author_id: The ID of the person who voted in the poll - 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("{} voted in poll {} in {}".format(author_id, poll, thread)) - - def on_plan_created( - self, mid=None, plan=None, author_id=None, thread=None, at=None, metadata=None, - ): - """Called when the client is listening, and somebody creates a plan. - - Args: - mid: The action ID - plan (Plan): Created plan - author_id: The ID of the person who created the plan - 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("{} created plan {} in {}".format(author_id, plan, thread)) - - def on_plan_ended(self, mid=None, plan=None, thread=None, at=None, metadata=None): - """Called when the client is listening, and a plan ends. - - Args: - mid: The action ID - plan (Plan): Ended plan - 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("Plan {} has ended in {}".format(plan, thread)) - - def on_plan_edited( - self, mid=None, plan=None, author_id=None, thread=None, at=None, metadata=None, - ): - """Called when the client is listening, and somebody edits a plan. - - Args: - mid: The action ID - plan (Plan): Edited plan - author_id: The ID of the person who edited the plan - 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("{} edited plan {} in {}".format(author_id, plan, thread)) - - def on_plan_deleted( - self, mid=None, plan=None, author_id=None, thread=None, at=None, metadata=None, - ): - """Called when the client is listening, and somebody deletes a plan. - - Args: - mid: The action ID - plan (Plan): Deleted plan - author_id: The ID of the person who deleted the plan - 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("{} deleted plan {} in {}".format(author_id, plan, thread)) - - def on_plan_participation( - self, - mid=None, - plan=None, - take_part=None, - author_id=None, - thread=None, - at=None, - metadata=None, - ): - """Called when the client is listening, and somebody takes part in a plan or not. - - Args: - mid: The action ID - 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: 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 - """ - if take_part: - log.info( - "{} will take part in {} in {} ({})".format(author_id, plan, thread) - ) - else: - log.info( - "{} won't take part in {} in {} ({})".format(author_id, plan, thread) - ) - def on_chat_timestamp(self, buddylist=None): """Called when the client receives chat online presence update. diff --git a/fbchat/_delta_type.py b/fbchat/_delta_type.py new file mode 100644 index 0000000..127c976 --- /dev/null +++ b/fbchat/_delta_type.py @@ -0,0 +1,329 @@ +import attr +import datetime +from ._event_common import attrs_event, Event, UnknownEvent, ThreadEvent +from . import _util, _user, _thread, _poll, _plan + +from typing import Sequence, Optional + + +@attrs_event +class ColorSet(ThreadEvent): + """Somebody set the color in a thread.""" + + #: The new color. Not limited to the ones in `ThreadABC.set_color` + color = attr.ib(type=str) + #: When the color was set + at = attr.ib(type=datetime.datetime) + + @classmethod + def _parse(cls, session, data): + author, thread, at = cls._parse_metadata(session, data) + color = _thread.ThreadABC._parse_color(data["untypedData"]["theme_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) + #: 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"] + return cls(author=author, thread=thread, emoji=emoji, at=at) + + +@attrs_event +class NicknameSet(ThreadEvent): + """Somebody set the nickname of a person in a thread.""" + + #: The person whose nickname was set + subject = attr.ib(type=str) + #: The new nickname. If ``None``, the nickname was cleared + nickname = attr.ib(type=Optional[str]) + #: When the nickname was set + at = attr.ib(type=datetime.datetime) + + @classmethod + def _parse(cls, session, data): + author, thread, at = cls._parse_metadata(session, data) + subject = _user.User(session=session, id=data["untypedData"]["participant_id"]) + nickname = data["untypedData"]["nickname"] or None # None if "" + return cls( + author=author, thread=thread, subject=subject, nickname=nickname, at=at + ) + + +@attrs_event +class AdminsAdded(ThreadEvent): + """Somebody added admins to a group.""" + + #: The people that were set as admins + added = attr.ib(type=Sequence[_user.User]) + #: When the admins were added + at = attr.ib(type=datetime.datetime) + + @classmethod + def _parse(cls, session, data): + author, thread, at = cls._parse_metadata(session, data) + subject = _user.User(session=session, id=data["untypedData"]["TARGET_ID"]) + return cls(author=author, thread=thread, added=[subject], at=at) + + +@attrs_event +class AdminsRemoved(ThreadEvent): + """Somebody removed admins from a group.""" + + #: The people that were removed as admins + removed = attr.ib(type=Sequence[_user.User]) + #: When the admins were removed + at = attr.ib(type=datetime.datetime) + + @classmethod + def _parse(cls, session, data): + author, thread, at = cls._parse_metadata(session, data) + subject = _user.User(session=session, id=data["untypedData"]["TARGET_ID"]) + return cls(author=author, thread=thread, removed=[subject], at=at) + + +@attrs_event +class ApprovalModeSet(ThreadEvent): + """Somebody changed the approval mode in a group.""" + + require_admin_approval = attr.ib(type=bool) + #: When the approval mode was set + at = attr.ib(type=datetime.datetime) + + @classmethod + def _parse(cls, session, data): + author, thread, at = cls._parse_metadata(session, data) + raa = data["untypedData"]["APPROVAL_MODE"] == "1" + return cls(author=author, thread=thread, require_admin_approval=raa, at=at) + + +@attrs_event +class CallStarted(ThreadEvent): + """Somebody started a call.""" + + #: When the call was started + 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, at=at) + + +@attrs_event +class CallEnded(ThreadEvent): + """Somebody ended a call.""" + + #: How long the call took + duration = attr.ib(type=datetime.timedelta) + #: When the call ended + at = attr.ib(type=datetime.datetime) + + @classmethod + def _parse(cls, session, data): + author, thread, at = cls._parse_metadata(session, data) + duration = _util.seconds_to_timedelta(int(data["untypedData"]["call_duration"])) + return cls(author=author, thread=thread, duration=duration, at=at) + + +@attrs_event +class CallJoined(ThreadEvent): + """Somebody joined a call.""" + + #: When the call ended + 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, at=at) + + +@attrs_event +class PollCreated(ThreadEvent): + """Somebody created a group poll.""" + + #: The new poll + poll = attr.ib(type=_poll.Poll) + #: When the poll was created + at = attr.ib(type=datetime.datetime) + + @classmethod + def _parse(cls, session, data): + author, thread, at = cls._parse_metadata(session, data) + poll_data = _util.parse_json(data["untypedData"]["question_json"]) + poll = _poll.Poll._from_graphql(session, poll_data) + return cls(author=author, thread=thread, poll=poll, at=at) + + +@attrs_event +class PollVoted(ThreadEvent): + """Somebody voted in a group poll.""" + + #: The updated poll + poll = attr.ib(type=_poll.Poll) + #: Ids of the voted options + added_ids = attr.ib(type=Sequence[str]) + #: Ids of the un-voted options + removed_ids = attr.ib(type=Sequence[str]) + #: When the poll was voted in + at = attr.ib(type=datetime.datetime) + + @classmethod + def _parse(cls, session, data): + author, thread, at = cls._parse_metadata(session, data) + poll_data = _util.parse_json(data["untypedData"]["question_json"]) + poll = _poll.Poll._from_graphql(session, poll_data) + added_ids = _util.parse_json(data["untypedData"]["added_option_ids"]) + removed_ids = _util.parse_json(data["untypedData"]["removed_option_ids"]) + return cls( + author=author, + thread=thread, + poll=poll, + added_ids=[str(x) for x in added_ids], + removed_ids=[str(x) for x in removed_ids], + at=at, + ) + + +@attrs_event +class PlanCreated(ThreadEvent): + """Somebody created a plan in a group.""" + + #: The new plan + plan = attr.ib(type=_plan.PlanData) + #: When the plan was created + at = attr.ib(type=datetime.datetime) + + @classmethod + def _parse(cls, session, data): + author, thread, at = cls._parse_metadata(session, data) + plan = _plan.PlanData._from_pull(session, data["untypedData"]) + return cls(author=author, thread=thread, plan=plan, at=at) + + +@attrs_event +class PlanEnded(ThreadEvent): + """A plan ended.""" + + #: The ended plan + plan = attr.ib(type=_plan.PlanData) + #: When the plan ended + at = attr.ib(type=datetime.datetime) + + @classmethod + def _parse(cls, session, data): + author, thread, at = cls._parse_metadata(session, data) + plan = _plan.PlanData._from_pull(session, data["untypedData"]) + return cls(author=author, thread=thread, plan=plan, at=at) + + +@attrs_event +class PlanEdited(ThreadEvent): + """Somebody changed a plan in a group.""" + + #: The updated plan + plan = attr.ib(type=_plan.PlanData) + #: When the plan was updated + at = attr.ib(type=datetime.datetime) + + @classmethod + def _parse(cls, session, data): + author, thread, at = cls._parse_metadata(session, data) + plan = _plan.PlanData._from_pull(session, data["untypedData"]) + return cls(author=author, thread=thread, plan=plan, at=at) + + +@attrs_event +class PlanDeleted(ThreadEvent): + """Somebody removed a plan in a group.""" + + #: The removed plan + plan = attr.ib(type=_plan.PlanData) + #: When the plan was removed + at = attr.ib(type=datetime.datetime) + + @classmethod + def _parse(cls, session, data): + author, thread, at = cls._parse_metadata(session, data) + plan = _plan.PlanData._from_pull(session, data["untypedData"]) + return cls(author=author, thread=thread, plan=plan, at=at) + + +@attrs_event +class PlanResponded(ThreadEvent): + """Somebody responded to a plan in a group.""" + + #: The plan that was responded to + plan = attr.ib(type=_plan.PlanData) + #: Whether the author will go to the plan or not + take_part = attr.ib(type=bool) + #: When the plan was removed + at = attr.ib(type=datetime.datetime) + + @classmethod + def _parse(cls, session, data): + author, thread, at = cls._parse_metadata(session, data) + plan = _plan.PlanData._from_pull(session, data["untypedData"]) + take_part = data["untypedData"]["guest_status"] == "GOING" + return cls(author=author, thread=thread, plan=plan, take_part=take_part, at=at) + + +def parse_delta(session, data): + type_ = data.get("type") + if type_ == "change_thread_theme": + return ColorSet._parse(session, data) + elif type_ == "change_thread_icon": + return EmojiSet._parse(session, data) + elif type_ == "change_thread_nickname": + return NicknameSet._parse(session, data) + elif type_ == "change_thread_admins": + event_type = data["untypedData"]["ADMIN_EVENT"] + if event_type == "add_admin": + return AdminsAdded._parse(session, data) + elif event_type == "remove_admin": + return AdminsRemoved._parse(session, data) + else: + pass + elif type_ == "change_thread_approval_mode": + return ApprovalModeSet._parse(session, data) + elif type_ == "instant_game_update": + pass # TODO: This + elif type_ == "messenger_call_log": # Previously "rtc_call_log" + event_type = data["untypedData"]["event"] + if event_type == "group_call_started": + return CallStarted._parse(session, data) + elif event_type in ["group_call_ended", "one_on_one_call_ended"]: + return CallEnded._parse(session, data) + else: + pass + elif type_ == "participant_joined_group_call": + return CallJoined._parse(session, data) + elif type_ == "group_poll": + event_type = data["untypedData"]["event_type"] + if event_type == "question_creation": + return PollCreated._parse(session, data) + elif event_type == "update_vote": + return PollVoted._parse(session, data) + else: + pass + elif type_ == "lightweight_event_create": + return PlanCreated._parse(session, data) + elif type_ == "lightweight_event_notify": + return PlanEnded._parse(session, data) + elif type_ == "lightweight_event_update": + return PlanEdited._parse(session, data) + elif type_ == "lightweight_event_delete": + return PlanDeleted._parse(session, data) + elif type_ == "lightweight_event_rsvp": + return PlanResponded._parse(session, data) + return UnknownEvent(data=data) diff --git a/tests/test_delta_type.py b/tests/test_delta_type.py new file mode 100644 index 0000000..151b5e2 --- /dev/null +++ b/tests/test_delta_type.py @@ -0,0 +1,954 @@ +import datetime +import pytest +from fbchat import ( + _util, + ParseError, + User, + Group, + Poll, + PollOption, + PlanData, + GuestStatus, + UnknownEvent, + ColorSet, + EmojiSet, + NicknameSet, + AdminsAdded, + AdminsRemoved, + ApprovalModeSet, + CallStarted, + CallEnded, + CallJoined, + PollCreated, + PollVoted, + PlanCreated, + PlanEnded, + PlanEdited, + PlanDeleted, + PlanResponded, +) +from fbchat._message import Message, MessageData +from fbchat._delta_type import parse_delta + + +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_delta(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_delta(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_delta(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_delta(session, data) + + +def test_admins_added(session): + data = { + "irisSeqId": "1111111", + "irisTags": ["DeltaAdminTextMessage", "is_from_iris_fanout"], + "messageMetadata": { + "actorFbId": "1234", + "adminText": "You added Abc Def as a group admin.", + "folderId": {"systemFolderId": "INBOX"}, + "messageId": "mid.$XYZ", + "offlineThreadingId": "11223344556677889900", + "skipBumpThread": True, + "tags": ["source:titan:web"], + "threadKey": {"threadFbId": "4321"}, + "threadReadStateEffect": "KEEP_AS_IS", + "timestamp": "1500000000000", + "unsendType": "deny_log_message", + }, + "participants": ["1234", "2345", "3456"], + "requestContext": {"apiArgs": {}}, + "tqSeqId": "1111", + "type": "change_thread_admins", + "untypedData": { + "THREAD_CATEGORY": "GROUP", + "TARGET_ID": "2345", + "ADMIN_TYPE": "0", + "ADMIN_EVENT": "add_admin", + }, + "class": "AdminTextMessage", + } + assert AdminsAdded( + author=User(session=session, id="1234"), + thread=Group(session=session, id="4321"), + added=[User(session=session, id="2345")], + at=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), + ) == parse_delta(session, data) + + +def test_admins_removed(session): + data = { + "irisSeqId": "1111111", + "irisTags": ["DeltaAdminTextMessage", "is_from_iris_fanout"], + "messageMetadata": { + "actorFbId": "1234", + "adminText": "You removed yourself as a group admin.", + "folderId": {"systemFolderId": "INBOX"}, + "messageId": "mid.$XYZ", + "offlineThreadingId": "11223344556677889900", + "skipBumpThread": True, + "tags": ["source:titan:web"], + "threadKey": {"threadFbId": "4321"}, + "threadReadStateEffect": "KEEP_AS_IS", + "timestamp": "1500000000000", + "unsendType": "deny_log_message", + }, + "participants": ["1234", "2345", "3456"], + "requestContext": {"apiArgs": {}}, + "tqSeqId": "1111", + "type": "change_thread_admins", + "untypedData": { + "THREAD_CATEGORY": "GROUP", + "TARGET_ID": "1234", + "ADMIN_TYPE": "0", + "ADMIN_EVENT": "remove_admin", + }, + "class": "AdminTextMessage", + } + assert AdminsRemoved( + author=User(session=session, id="1234"), + 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_approvalmode_set(session): + data = { + "irisSeqId": "1111111", + "irisTags": ["DeltaAdminTextMessage", "is_from_iris_fanout"], + "messageMetadata": { + "actorFbId": "1234", + "adminText": "You turned on member approval and will review requests to join the group.", + "folderId": {"systemFolderId": "INBOX"}, + "messageId": "mid.$XYZ", + "offlineThreadingId": "11223344556677889900", + "skipBumpThread": True, + "tags": ["source:titan:web", "no_push"], + "threadKey": {"threadFbId": "4321"}, + "threadReadStateEffect": "KEEP_AS_IS", + "timestamp": "1500000000000", + "unsendType": "deny_log_message", + }, + "participants": ["1234", "2345", "3456"], + "requestContext": {"apiArgs": {}}, + "tqSeqId": "1111", + "type": "change_thread_approval_mode", + "untypedData": {"APPROVAL_MODE": "1", "THREAD_CATEGORY": "GROUP"}, + "class": "AdminTextMessage", + } + assert ApprovalModeSet( + author=User(session=session, id="1234"), + thread=Group(session=session, id="4321"), + require_admin_approval=True, + at=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), + ) == parse_delta(session, data) + + +def test_approvalmode_unset(session): + data = { + "irisSeqId": "1111111", + "irisTags": ["DeltaAdminTextMessage", "is_from_iris_fanout"], + "messageMetadata": { + "actorFbId": "1234", + "adminText": "You turned off member approval. Anyone with the link can join the group.", + "folderId": {"systemFolderId": "INBOX"}, + "messageId": "mid.$XYZ", + "offlineThreadingId": "11223344556677889900", + "skipBumpThread": True, + "tags": ["source:titan:web", "no_push"], + "threadKey": {"threadFbId": "4321"}, + "threadReadStateEffect": "KEEP_AS_IS", + "timestamp": "1500000000000", + "unsendType": "deny_log_message", + }, + "participants": ["1234", "2345", "3456"], + "requestContext": {"apiArgs": {}}, + "tqSeqId": "1111", + "type": "change_thread_approval_mode", + "untypedData": {"APPROVAL_MODE": "0", "THREAD_CATEGORY": "GROUP"}, + "class": "AdminTextMessage", + } + assert ApprovalModeSet( + author=User(session=session, id="1234"), + thread=Group(session=session, id="4321"), + require_admin_approval=False, + at=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), + ) == parse_delta(session, data) + + +def test_call_started(session): + data = { + "irisSeqId": "1111111", + "irisTags": ["DeltaAdminTextMessage", "is_from_iris_fanout"], + "messageMetadata": { + "actorFbId": "1234", + "adminText": "You started a call.", + "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": "messenger_call_log", + "untypedData": { + "call_capture_attachments": "", + "caller_id": "1234", + "conference_name": "MESSENGER:134845267536444", + "rating": "", + "messenger_call_instance_id": "0", + "video": "", + "event": "group_call_started", + "server_info": "XYZ123ABC", + "call_duration": "0", + "callee_id": "0", + }, + "class": "AdminTextMessage", + } + data2 = { + "callState": "AUDIO_GROUP_CALL", + "messageMetadata": { + "actorFbId": "1234", + "folderId": {"systemFolderId": "INBOX"}, + "messageId": "mid.$XYZ", + "offlineThreadingId": "11223344556677889900", + "skipBumpThread": False, + "tags": [], + "threadKey": {"threadFbId": "4321"}, + "threadReadStateEffect": "KEEP_AS_IS", + "timestamp": "1500000000000", + }, + "serverInfoData": "XYZ123ABC", + "class": "RtcCallData", + } + assert CallStarted( + author=User(session=session, id="1234"), + thread=Group(session=session, id="4321"), + at=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), + ) == parse_delta(session, data) + + +def test_group_call_ended(session): + data = { + "irisSeqId": "1111111", + "irisTags": ["DeltaAdminTextMessage", "is_from_iris_fanout"], + "messageMetadata": { + "actorFbId": "1234", + "adminText": "The call ended.", + "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": "messenger_call_log", + "untypedData": { + "call_capture_attachments": "", + "caller_id": "1234", + "conference_name": "MESSENGER:1234567890", + "rating": "0", + "messenger_call_instance_id": "1234567890", + "video": "", + "event": "group_call_ended", + "server_info": "XYZ123ABC", + "call_duration": "31", + "callee_id": "0", + }, + "class": "AdminTextMessage", + } + data2 = { + "callState": "NO_ONGOING_CALL", + "messageMetadata": { + "actorFbId": "1234", + "folderId": {"systemFolderId": "INBOX"}, + "messageId": "mid.$XYZ", + "offlineThreadingId": "11223344556677889900", + "skipBumpThread": False, + "tags": [], + "threadKey": {"threadFbId": "4321"}, + "threadReadStateEffect": "KEEP_AS_IS", + "timestamp": "1500000000000", + }, + "class": "RtcCallData", + } + assert CallEnded( + author=User(session=session, id="1234"), + thread=Group(session=session, id="4321"), + duration=datetime.timedelta(seconds=31), + at=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), + ) == parse_delta(session, data) + + +def test_user_call_ended(session): + data = { + "irisSeqId": "1111111", + "irisTags": ["DeltaAdminTextMessage", "is_from_iris_fanout"], + "messageMetadata": { + "actorFbId": "1234", + "adminText": "Abc called you.", + "folderId": {"systemFolderId": "INBOX"}, + "messageId": "mid.$XYZ", + "offlineThreadingId": "11223344556677889900", + "skipBumpThread": False, + "skipSnippetUpdate": False, + "tags": ["source:generic_admin_text", "no_push"], + "threadKey": {"otherUserFbId": "1234"}, + "threadReadStateEffect": "KEEP_AS_IS", + "timestamp": "1500000000000", + "unsendType": "deny_log_message", + }, + "requestContext": {"apiArgs": {}}, + "type": "messenger_call_log", + "untypedData": { + "call_capture_attachments": "", + "caller_id": "1234", + "conference_name": "MESSENGER:1234567890", + "rating": "0", + "messenger_call_instance_id": "1234567890", + "video": "", + "event": "one_on_one_call_ended", + "server_info": "", + "call_duration": "3", + "callee_id": "100002950119740", + }, + "class": "AdminTextMessage", + } + assert CallEnded( + author=User(session=session, id="1234"), + thread=User(session=session, id="1234"), + duration=datetime.timedelta(seconds=3), + at=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), + ) == parse_delta(session, data) + + +def test_call_joined(session): + data = { + "irisSeqId": "1111111", + "irisTags": ["DeltaAdminTextMessage", "is_from_iris_fanout"], + "messageMetadata": { + "actorFbId": "1234", + "adminText": "Abc joined the call.", + "folderId": {"systemFolderId": "INBOX"}, + "messageId": "mid.$XYZ", + "offlineThreadingId": "11223344556677889900", + "skipBumpThread": False, + "tags": ["source:titan:web"], + "threadKey": {"threadFbId": "4321"}, + "threadReadStateEffect": "MARK_UNREAD", + "timestamp": "1500000000000", + "unsendType": "deny_log_message", + }, + "participants": ["1234", "2345", "3456"], + "requestContext": {"apiArgs": {}}, + "tqSeqId": "1111", + "type": "participant_joined_group_call", + "untypedData": { + "server_info_data": "XYZ123ABC", + "group_call_type": "0", + "joining_user": "2345", + }, + "class": "AdminTextMessage", + } + assert CallJoined( + author=User(session=session, id="1234"), + thread=Group(session=session, id="4321"), + at=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), + ) == parse_delta(session, data) + + +def test_poll_created(session): + poll_data = { + "id": "112233", + "text": "A poll", + "total_count": 2, + "viewer_has_voted": "true", + "options": [ + { + "id": "1001", + "text": "Option A", + "total_count": 1, + "viewer_has_voted": "true", + "voters": ["1234"], + }, + { + "id": "1002", + "text": "Option B", + "total_count": 0, + "viewer_has_voted": "false", + "voters": [], + }, + ], + } + data = { + "irisSeqId": "1111111", + "irisTags": ["DeltaAdminTextMessage", "is_from_iris_fanout"], + "messageMetadata": { + "actorFbId": "1234", + "adminText": "You created a poll: A poll.", + "folderId": {"systemFolderId": "INBOX"}, + "messageId": "mid.$XYZ", + "offlineThreadingId": "11223344556677889900", + "skipBumpThread": False, + "tags": ["source:titan:web"], + "threadKey": {"threadFbId": "4321"}, + "threadReadStateEffect": "MARK_UNREAD", + "timestamp": "1500000000000", + "unsendType": "deny_log_message", + }, + "participants": ["1234", "2345", "3456"], + "requestContext": {"apiArgs": {}}, + "tqSeqId": "1111", + "type": "group_poll", + "untypedData": { + "added_option_ids": "[]", + "removed_option_ids": "[]", + "question_json": _util.json_minimal(poll_data), + "event_type": "question_creation", + "question_id": "112233", + }, + "class": "AdminTextMessage", + } + assert PollCreated( + author=User(session=session, id="1234"), + thread=Group(session=session, id="4321"), + poll=Poll( + session=session, + id="112233", + question="A poll", + options=[ + PollOption( + id="1001", + text="Option A", + vote=True, + voters=["1234"], + votes_count=1, + ), + PollOption( + id="1002", text="Option B", vote=False, voters=[], votes_count=0 + ), + ], + options_count=2, + ), + at=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), + ) == parse_delta(session, data) + + +def test_poll_answered(session): + poll_data = { + "id": "112233", + "text": "A poll", + "total_count": 3, + "viewer_has_voted": "true", + "options": [ + { + "id": "1002", + "text": "Option B", + "total_count": 2, + "viewer_has_voted": "true", + "voters": ["1234", "2345"], + }, + { + "id": "1003", + "text": "Option C", + "total_count": 1, + "viewer_has_voted": "true", + "voters": ["1234"], + }, + { + "id": "1001", + "text": "Option A", + "total_count": 0, + "viewer_has_voted": "false", + "voters": [], + }, + ], + } + data = { + "irisSeqId": "1111111", + "irisTags": ["DeltaAdminTextMessage", "is_from_iris_fanout"], + "messageMetadata": { + "actorFbId": "1234", + "adminText": 'You changed your vote to "Option B" and 1 other option in the poll: A poll.', + "folderId": {"systemFolderId": "INBOX"}, + "messageId": "mid.$XYZ", + "offlineThreadingId": "11223344556677889900", + "skipBumpThread": False, + "tags": ["source:titan:web"], + "threadKey": {"threadFbId": "4321"}, + "threadReadStateEffect": "MARK_UNREAD", + "timestamp": "1500000000000", + "unsendType": "deny_log_message", + }, + "participants": ["1234", "2345", "3456"], + "requestContext": {"apiArgs": {}}, + "tqSeqId": "1111", + "type": "group_poll", + "untypedData": { + "added_option_ids": "[1002,1003]", + "removed_option_ids": "[1001]", + "question_json": _util.json_minimal(poll_data), + "event_type": "update_vote", + "question_id": "112233", + }, + "class": "AdminTextMessage", + } + assert PollVoted( + author=User(session=session, id="1234"), + thread=Group(session=session, id="4321"), + poll=Poll( + session=session, + id="112233", + question="A poll", + options=[ + PollOption( + id="1002", + text="Option B", + vote=True, + voters=["1234", "2345"], + votes_count=2, + ), + PollOption( + id="1003", + text="Option C", + vote=True, + voters=["1234"], + votes_count=1, + ), + PollOption( + id="1001", text="Option A", vote=False, voters=[], votes_count=0 + ), + ], + options_count=3, + ), + added_ids=["1002", "1003"], + removed_ids=["1001"], + at=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), + ) == parse_delta(session, data) + + +def test_plan_created(session): + guest_list = [ + {"guest_list_state": "INVITED", "node": {"id": "3456"}}, + {"guest_list_state": "INVITED", "node": {"id": "2345"}}, + {"guest_list_state": "GOING", "node": {"id": "1234"}}, + ] + data = { + "irisSeqId": "1111111", + "irisTags": ["DeltaAdminTextMessage", "is_from_iris_fanout"], + "messageMetadata": { + "actorFbId": "1234", + "adminText": "You created a plan.", + "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": "lightweight_event_create", + "untypedData": { + "event_timezone": "", + "event_creator_id": "1234", + "event_id": "112233", + "event_type": "EVENT", + "event_track_rsvp": "1", + "event_title": "A plan", + "event_time": "1600000000", + "event_seconds_to_notify_before": "3600", + "guest_state_list": _util.json_minimal(guest_list), + }, + "class": "AdminTextMessage", + } + assert PlanCreated( + author=User(session=session, id="1234"), + thread=Group(session=session, id="4321"), + plan=PlanData( + session=session, + id="112233", + time=datetime.datetime( + 2020, 9, 13, 12, 26, 40, tzinfo=datetime.timezone.utc + ), + title="A plan", + author_id="1234", + guests={ + "1234": GuestStatus.GOING, + "2345": GuestStatus.INVITED, + "3456": GuestStatus.INVITED, + }, + ), + at=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), + ) == parse_delta(session, data) + + +@pytest.mark.skip(reason="Need to gather test data") +def test_plan_ended(session): + data = {} + assert PlanEnded( + author=User(session=session, id="1234"), + thread=Group(session=session, id="4321"), + plan=PlanData( + session=session, + id="112233", + time=datetime.datetime( + 2020, 9, 13, 12, 26, 40, tzinfo=datetime.timezone.utc + ), + title="A plan", + author_id="1234", + guests={ + "1234": GuestStatus.GOING, + "2345": GuestStatus.INVITED, + "3456": GuestStatus.INVITED, + }, + ), + at=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), + ) == parse_delta(session, data) + + +def test_plan_edited(session): + guest_list = [ + {"guest_list_state": "INVITED", "node": {"id": "3456"}}, + {"guest_list_state": "INVITED", "node": {"id": "2345"}}, + {"guest_list_state": "GOING", "node": {"id": "1234"}}, + ] + data = { + "irisSeqId": "1111111", + "irisTags": ["DeltaAdminTextMessage", "is_from_iris_fanout"], + "messageMetadata": { + "actorFbId": "1234", + "adminText": "You named the plan A plan.", + "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": "lightweight_event_update", + "untypedData": { + "event_creator_id": "1234", + "latitude": "0", + "event_title": "A plan", + "event_seconds_to_notify_before": "3600", + "guest_state_list": _util.json_minimal(guest_list), + "event_end_time": "0", + "event_timezone": "", + "event_id": "112233", + "event_type": "EVENT", + "event_location_id": "2233445566", + "event_location_name": "", + "event_time": "1600000000", + "event_note": "", + "longitude": "0", + }, + "class": "AdminTextMessage", + } + assert PlanEdited( + author=User(session=session, id="1234"), + thread=Group(session=session, id="4321"), + plan=PlanData( + session=session, + id="112233", + time=datetime.datetime( + 2020, 9, 13, 12, 26, 40, tzinfo=datetime.timezone.utc + ), + title="A plan", + location_id="2233445566", + author_id="1234", + guests={ + "1234": GuestStatus.GOING, + "2345": GuestStatus.INVITED, + "3456": GuestStatus.INVITED, + }, + ), + at=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), + ) == parse_delta(session, data) + + +def test_plan_deleted(session): + guest_list = [ + {"guest_list_state": "GOING", "node": {"id": "1234"}}, + {"guest_list_state": "INVITED", "node": {"id": "3456"}}, + {"guest_list_state": "INVITED", "node": {"id": "2345"}}, + ] + data = { + "irisSeqId": "1111111", + "irisTags": ["DeltaAdminTextMessage", "is_from_iris_fanout"], + "messageMetadata": { + "actorFbId": "1234", + "adminText": "You deleted the plan A plan for Mon, 20 Jan at 15:30.", + "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": "lightweight_event_delete", + "untypedData": { + "event_end_time": "0", + "event_timezone": "", + "event_id": "112233", + "event_type": "EVENT", + "event_location_id": "2233445566", + "latitude": "0", + "event_title": "A plan", + "event_time": "1600000000", + "event_seconds_to_notify_before": "3600", + "guest_state_list": _util.json_minimal(guest_list), + "event_note": "", + "longitude": "0", + }, + "class": "AdminTextMessage", + } + assert PlanDeleted( + author=User(session=session, id="1234"), + thread=Group(session=session, id="4321"), + plan=PlanData( + session=session, + id="112233", + time=datetime.datetime( + 2020, 9, 13, 12, 26, 40, tzinfo=datetime.timezone.utc + ), + title="A plan", + location_id="2233445566", + author_id=None, + guests={ + "1234": GuestStatus.GOING, + "2345": GuestStatus.INVITED, + "3456": GuestStatus.INVITED, + }, + ), + at=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), + ) == parse_delta(session, data) + + +def test_plan_participation(session): + guest_list = [ + {"guest_list_state": "DECLINED", "node": {"id": "1234"}}, + {"guest_list_state": "GOING", "node": {"id": "2345"}}, + {"guest_list_state": "INVITED", "node": {"id": "3456"}}, + ] + data = { + "irisSeqId": "1111111", + "irisTags": ["DeltaAdminTextMessage", "is_from_iris_fanout"], + "messageMetadata": { + "actorFbId": "1234", + "adminText": "You responded Can't Go to def.", + "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": "lightweight_event_rsvp", + "untypedData": { + "event_creator_id": "2345", + "guest_status": "DECLINED", + "latitude": "0", + "event_track_rsvp": "1", + "event_title": "A plan", + "event_seconds_to_notify_before": "3600", + "guest_state_list": _util.json_minimal(guest_list), + "event_end_time": "0", + "event_timezone": "", + "event_id": "112233", + "event_type": "EVENT", + "guest_id": "1234", + "event_location_id": "2233445566", + "event_time": "1600000000", + "event_note": "", + "longitude": "0", + }, + "class": "AdminTextMessage", + } + assert PlanResponded( + author=User(session=session, id="1234"), + thread=Group(session=session, id="4321"), + plan=PlanData( + session=session, + id="112233", + time=datetime.datetime( + 2020, 9, 13, 12, 26, 40, tzinfo=datetime.timezone.utc + ), + title="A plan", + location_id="2233445566", + author_id="2345", + guests={ + "1234": GuestStatus.DECLINED, + "2345": GuestStatus.GOING, + "3456": GuestStatus.INVITED, + }, + ), + take_part=False, + at=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), + ) == parse_delta(session, data) + + +def test_parse_delta_unknown(session): + assert UnknownEvent(data={"abc": 10}) == parse_delta(session, {"abc": 10})