Merge pull request #515 from carpedm20/refactor-listen-parsing
Refactor listen parsing
This commit is contained in:
@@ -1,19 +1,17 @@
|
|||||||
import fbchat
|
import fbchat
|
||||||
|
|
||||||
# Subclass fbchat.Client and override required methods
|
|
||||||
class EchoBot(fbchat.Client):
|
|
||||||
def on_message(self, author_id, message_object, thread, **kwargs):
|
|
||||||
self.mark_as_delivered(thread.id, message_object.id)
|
|
||||||
self.mark_as_read(thread.id)
|
|
||||||
|
|
||||||
print("{} from {} in {}".format(message_object, thread))
|
|
||||||
|
|
||||||
# If you're not the author, echo
|
|
||||||
if author_id != self.session.user_id:
|
|
||||||
thread.send_text(message_object.text)
|
|
||||||
|
|
||||||
|
|
||||||
session = fbchat.Session.login("<email>", "<password>")
|
session = fbchat.Session.login("<email>", "<password>")
|
||||||
|
|
||||||
echo_bot = EchoBot(session)
|
listener = fbchat.Listener.connect(session, chat_on=False, foreground=False)
|
||||||
echo_bot.listen()
|
|
||||||
|
|
||||||
|
def on_message(event):
|
||||||
|
print(f"{event.message.text} from {event.author.id} in {event.thread.id}")
|
||||||
|
# If you're not the author, echo
|
||||||
|
if event.author.id != session.user_id:
|
||||||
|
event.thread.send_text(event.message.text)
|
||||||
|
|
||||||
|
|
||||||
|
for event in listener.listen():
|
||||||
|
if isinstance(event, fbchat.MessageEvent):
|
||||||
|
on_message(event)
|
||||||
|
@@ -14,64 +14,77 @@ old_nicknames = {
|
|||||||
"12345678904": "User nr. 4's nickname",
|
"12345678904": "User nr. 4's nickname",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class KeepBot(fbchat.Client):
|
|
||||||
def on_color_change(self, author_id, new_color, thread, **kwargs):
|
|
||||||
if old_thread_id == thread.id and old_color != new_color:
|
|
||||||
print(
|
|
||||||
"{} changed the thread color. It will be changed back".format(author_id)
|
|
||||||
)
|
|
||||||
thread.set_color(old_color)
|
|
||||||
|
|
||||||
def on_emoji_change(self, author_id, new_emoji, thread, **kwargs):
|
|
||||||
if old_thread_id == thread.id and new_emoji != old_emoji:
|
|
||||||
print(
|
|
||||||
"{} changed the thread emoji. It will be changed back".format(author_id)
|
|
||||||
)
|
|
||||||
thread.set_emoji(old_emoji)
|
|
||||||
|
|
||||||
def on_people_added(self, added_ids, author_id, thread, **kwargs):
|
|
||||||
if old_thread_id == thread.id and author_id != self.session.user_id:
|
|
||||||
print("{} got added. They will be removed".format(added_ids))
|
|
||||||
for added_id in added_ids:
|
|
||||||
thread.remove_participant(added_id)
|
|
||||||
|
|
||||||
def on_person_removed(self, removed_id, author_id, thread, **kwargs):
|
|
||||||
# No point in trying to add ourself
|
|
||||||
if (
|
|
||||||
old_thread_id == thread.id
|
|
||||||
and removed_id != self.session.user_id
|
|
||||||
and author_id != self.session.user_id
|
|
||||||
):
|
|
||||||
print("{} got removed. They will be re-added".format(removed_id))
|
|
||||||
thread.add_participants(removed_id)
|
|
||||||
|
|
||||||
def on_title_change(self, author_id, new_title, thread, **kwargs):
|
|
||||||
if old_thread_id == thread.id and old_title != new_title:
|
|
||||||
print(
|
|
||||||
"{} changed the thread title. It will be changed back".format(author_id)
|
|
||||||
)
|
|
||||||
thread.set_title(old_title)
|
|
||||||
|
|
||||||
def on_nickname_change(
|
|
||||||
self, author_id, changed_for, new_nickname, thread, **kwargs
|
|
||||||
):
|
|
||||||
if (
|
|
||||||
old_thread_id == thread.id
|
|
||||||
and changed_for in old_nicknames
|
|
||||||
and old_nicknames[changed_for] != new_nickname
|
|
||||||
):
|
|
||||||
print(
|
|
||||||
"{} changed {}'s' nickname. It will be changed back".format(
|
|
||||||
author_id, changed_for
|
|
||||||
)
|
|
||||||
)
|
|
||||||
thread.set_nickname(
|
|
||||||
changed_for, old_nicknames[changed_for],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
session = fbchat.Session.login("<email>", "<password>")
|
session = fbchat.Session.login("<email>", "<password>")
|
||||||
|
|
||||||
keep_bot = KeepBot(session)
|
listener = fbchat.Listener.connect(session, chat_on=False, foreground=False)
|
||||||
keep_bot.listen()
|
|
||||||
|
|
||||||
|
def on_color_set(event: fbchat.ColorSet):
|
||||||
|
if old_thread_id != event.thread.id:
|
||||||
|
return
|
||||||
|
if old_color != event.color:
|
||||||
|
print(f"{event.author.id} changed the thread color. It will be changed back")
|
||||||
|
event.thread.set_color(old_color)
|
||||||
|
|
||||||
|
|
||||||
|
def on_emoji_set(event: fbchat.EmojiSet):
|
||||||
|
if old_thread_id != event.thread.id:
|
||||||
|
return
|
||||||
|
if old_emoji != event.emoji:
|
||||||
|
print(f"{event.author.id} changed the thread emoji. It will be changed back")
|
||||||
|
event.thread.set_emoji(old_emoji)
|
||||||
|
|
||||||
|
|
||||||
|
def on_title_set(event: fbchat.TitleSet):
|
||||||
|
if old_thread_id != event.thread.id:
|
||||||
|
return
|
||||||
|
if old_title != event.title:
|
||||||
|
print(f"{event.author.id} changed the thread title. It will be changed back")
|
||||||
|
event.thread.set_title(old_title)
|
||||||
|
|
||||||
|
|
||||||
|
def on_nickname_set(event: fbchat.NicknameSet):
|
||||||
|
if old_thread_id != event.thread.id:
|
||||||
|
return
|
||||||
|
old_nickname = old_nicknames.get(event.subject.id)
|
||||||
|
if old_nickname != event.nickname:
|
||||||
|
print(
|
||||||
|
f"{event.author.id} changed {event.subject.id}'s' nickname."
|
||||||
|
" It will be changed back"
|
||||||
|
)
|
||||||
|
event.thread.set_nickname(event.subject.id, old_nickname)
|
||||||
|
|
||||||
|
|
||||||
|
def on_people_added(event: fbchat.PeopleAdded):
|
||||||
|
if old_thread_id != event.thread.id:
|
||||||
|
return
|
||||||
|
if event.author.id != session.user_id:
|
||||||
|
print(f"{', '.join(x.id for x in event.added)} got added. They will be removed")
|
||||||
|
for added in event.added:
|
||||||
|
event.thread.remove_participant(added.id)
|
||||||
|
|
||||||
|
|
||||||
|
def on_person_removed(event: fbchat.PersonRemoved):
|
||||||
|
if old_thread_id != event.thread.id:
|
||||||
|
return
|
||||||
|
# No point in trying to add ourself
|
||||||
|
if event.removed.id == session.user_id:
|
||||||
|
return
|
||||||
|
if event.author.id != session.user_id:
|
||||||
|
print(f"{event.removed.id} got removed. They will be re-added")
|
||||||
|
event.thread.add_participants([removed.id])
|
||||||
|
|
||||||
|
|
||||||
|
for event in listener.listen():
|
||||||
|
if isinstance(event, fbchat.ColorSet):
|
||||||
|
on_color_set(event)
|
||||||
|
elif isinstance(event, fbchat.EmojiSet):
|
||||||
|
on_emoji_set(event)
|
||||||
|
elif isinstance(event, fbchat.TitleSet):
|
||||||
|
on_title_set(event)
|
||||||
|
elif isinstance(event, fbchat.NicknameSet):
|
||||||
|
on_nickname_set(event)
|
||||||
|
elif isinstance(event, fbchat.PeopleAdded):
|
||||||
|
on_people_added(event)
|
||||||
|
elif isinstance(event, fbchat.PersonRemoved):
|
||||||
|
on_person_removed(event)
|
||||||
|
@@ -1,23 +1,19 @@
|
|||||||
import fbchat
|
import fbchat
|
||||||
|
|
||||||
|
|
||||||
class RemoveBot(fbchat.Client):
|
|
||||||
def on_message(self, author_id, message_object, thread, **kwargs):
|
|
||||||
# We can only kick people from group chats, so no need to try if it's a user chat
|
|
||||||
if message_object.text == "Remove me!" and isinstance(thread, fbchat.Group):
|
|
||||||
print("{} will be removed from {}".format(author_id, thread))
|
|
||||||
thread.remove_participant(author_id)
|
|
||||||
else:
|
|
||||||
# Sends the data to the inherited on_message, so that we can still see when a message is recieved
|
|
||||||
super(RemoveBot, self).on_message(
|
|
||||||
author_id=author_id,
|
|
||||||
message_object=message_object,
|
|
||||||
thread=thread,
|
|
||||||
**kwargs,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
session = fbchat.Session.login("<email>", "<password>")
|
session = fbchat.Session.login("<email>", "<password>")
|
||||||
|
|
||||||
remove_bot = RemoveBot(session)
|
listener = fbchat.Listener.connect(session, chat_on=False, foreground=False)
|
||||||
remove_bot.listen()
|
|
||||||
|
|
||||||
|
def on_message(event):
|
||||||
|
# We can only kick people from group chats, so no need to try if it's a user chat
|
||||||
|
if not isinstance(event.thread, fbchat.Group):
|
||||||
|
return
|
||||||
|
if message.text == "Remove me!":
|
||||||
|
print(f"{event.author.id} will be removed from {event.thread.id}")
|
||||||
|
event.thread.remove_participant(event.author.id)
|
||||||
|
|
||||||
|
|
||||||
|
for event in listener.listen():
|
||||||
|
if isinstance(event, fbchat.MessageEvent):
|
||||||
|
on_message(event)
|
||||||
|
@@ -42,6 +42,45 @@ from ._quick_reply import (
|
|||||||
from ._poll import Poll, PollOption
|
from ._poll import Poll, PollOption
|
||||||
from ._plan import GuestStatus, Plan, PlanData
|
from ._plan import GuestStatus, Plan, PlanData
|
||||||
|
|
||||||
|
# Listen events
|
||||||
|
from ._event_common import Event, UnknownEvent, ThreadEvent
|
||||||
|
from ._client_payload import (
|
||||||
|
ReactionEvent,
|
||||||
|
UserStatusEvent,
|
||||||
|
LiveLocationEvent,
|
||||||
|
UnsendEvent,
|
||||||
|
MessageReplyEvent,
|
||||||
|
)
|
||||||
|
from ._delta_class import (
|
||||||
|
PeopleAdded,
|
||||||
|
PersonRemoved,
|
||||||
|
TitleSet,
|
||||||
|
UnfetchedThreadEvent,
|
||||||
|
MessagesDelivered,
|
||||||
|
ThreadsRead,
|
||||||
|
MessageEvent,
|
||||||
|
)
|
||||||
|
from ._delta_type import (
|
||||||
|
ColorSet,
|
||||||
|
EmojiSet,
|
||||||
|
NicknameSet,
|
||||||
|
AdminsAdded,
|
||||||
|
AdminsRemoved,
|
||||||
|
ApprovalModeSet,
|
||||||
|
CallStarted,
|
||||||
|
CallEnded,
|
||||||
|
CallJoined,
|
||||||
|
PollCreated,
|
||||||
|
PollVoted,
|
||||||
|
PlanCreated,
|
||||||
|
PlanEnded,
|
||||||
|
PlanEdited,
|
||||||
|
PlanDeleted,
|
||||||
|
PlanResponded,
|
||||||
|
)
|
||||||
|
from ._event import Typing, FriendRequest, Presence
|
||||||
|
from ._mqtt import Listener
|
||||||
|
|
||||||
from ._client import Client
|
from ._client import Client
|
||||||
|
|
||||||
__title__ = "fbchat"
|
__title__ = "fbchat"
|
||||||
@@ -54,7 +93,7 @@ __license__ = "BSD 3-Clause"
|
|||||||
__author__ = "Taehoon Kim; Moreels Pieter-Jan; Mads Marquart"
|
__author__ = "Taehoon Kim; Moreels Pieter-Jan; Mads Marquart"
|
||||||
__email__ = "carpedm20@gmail.com"
|
__email__ = "carpedm20@gmail.com"
|
||||||
|
|
||||||
__all__ = ("Session", "Client")
|
__all__ = ("Session", "Listener", "Client")
|
||||||
|
|
||||||
# Everything below is taken from the excellent trio project:
|
# Everything below is taken from the excellent trio project:
|
||||||
|
|
||||||
|
1270
fbchat/_client.py
1270
fbchat/_client.py
File diff suppressed because it is too large
Load Diff
136
fbchat/_client_payload.py
Normal file
136
fbchat/_client_payload.py
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import attr
|
||||||
|
import datetime
|
||||||
|
from ._event_common import attrs_event, UnknownEvent, ThreadEvent
|
||||||
|
from . import _exception, _util, _user, _message
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
@attrs_event
|
||||||
|
class ReactionEvent(ThreadEvent):
|
||||||
|
"""Somebody reacted to a message."""
|
||||||
|
|
||||||
|
#: Message that the user reacted to
|
||||||
|
message = attr.ib(type=_message.Message)
|
||||||
|
|
||||||
|
reaction = attr.ib(type=Optional[str])
|
||||||
|
"""The reaction.
|
||||||
|
|
||||||
|
Not limited to the ones in `Message.react`.
|
||||||
|
|
||||||
|
If ``None``, the reaction was removed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _parse(cls, session, data):
|
||||||
|
thread = cls._get_thread(session, data)
|
||||||
|
return cls(
|
||||||
|
author=_user.User(session=session, id=str(data["userId"])),
|
||||||
|
thread=thread,
|
||||||
|
message=_message.Message(thread=thread, id=data["messageId"]),
|
||||||
|
reaction=data["reaction"] if data["action"] == 0 else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@attrs_event
|
||||||
|
class UserStatusEvent(ThreadEvent):
|
||||||
|
#: Whether the user was blocked or unblocked
|
||||||
|
blocked = attr.ib(type=bool)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _parse(cls, session, data):
|
||||||
|
return cls(
|
||||||
|
author=_user.User(session=session, id=str(data["actorFbid"])),
|
||||||
|
thread=cls._get_thread(session, data),
|
||||||
|
blocked=not data["canViewerReply"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@attrs_event
|
||||||
|
class LiveLocationEvent(ThreadEvent):
|
||||||
|
"""Somebody sent live location info."""
|
||||||
|
|
||||||
|
# TODO: This!
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _parse(cls, session, data):
|
||||||
|
from . import _location
|
||||||
|
|
||||||
|
thread = cls._get_thread(session, data)
|
||||||
|
for location_data in data["messageLiveLocations"]:
|
||||||
|
message = _message.Message(thread=thread, id=data["messageId"])
|
||||||
|
author = _user.User(session=session, id=str(location_data["senderId"]))
|
||||||
|
location = _location.LiveLocationAttachment._from_pull(location_data)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@attrs_event
|
||||||
|
class UnsendEvent(ThreadEvent):
|
||||||
|
"""Somebody unsent a message (which deletes it for everyone)."""
|
||||||
|
|
||||||
|
#: The unsent message
|
||||||
|
message = attr.ib(type=_message.Message)
|
||||||
|
#: When the message was unsent
|
||||||
|
at = attr.ib(type=datetime.datetime)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _parse(cls, session, data):
|
||||||
|
thread = cls._get_thread(session, data)
|
||||||
|
return cls(
|
||||||
|
author=_user.User(session=session, id=str(data["senderID"])),
|
||||||
|
thread=thread,
|
||||||
|
message=_message.Message(thread=thread, id=data["messageID"]),
|
||||||
|
at=_util.millis_to_datetime(data["deletionTimestamp"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@attrs_event
|
||||||
|
class MessageReplyEvent(ThreadEvent):
|
||||||
|
"""Somebody replied to a message."""
|
||||||
|
|
||||||
|
#: The sent message
|
||||||
|
message = attr.ib(type=_message.MessageData)
|
||||||
|
#: The message that was replied to
|
||||||
|
replied_to = attr.ib(type=_message.MessageData)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _parse(cls, session, data):
|
||||||
|
metadata = data["message"]["messageMetadata"]
|
||||||
|
thread = cls._get_thread(session, metadata)
|
||||||
|
return cls(
|
||||||
|
author=_user.User(session=session, id=str(metadata["actorFbId"])),
|
||||||
|
thread=thread,
|
||||||
|
message=_message.MessageData._from_reply(thread, data["message"]),
|
||||||
|
replied_to=_message.MessageData._from_reply(
|
||||||
|
thread, data["repliedToMessage"]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_client_delta(session, data):
|
||||||
|
if "deltaMessageReaction" in data:
|
||||||
|
return ReactionEvent._parse(session, data["deltaMessageReaction"])
|
||||||
|
elif "deltaChangeViewerStatus" in data:
|
||||||
|
# TODO: Parse all `reason`
|
||||||
|
if data["deltaChangeViewerStatus"]["reason"] == 2:
|
||||||
|
return UserStatusEvent._parse(session, data["deltaChangeViewerStatus"])
|
||||||
|
elif "liveLocationData" in data:
|
||||||
|
return LiveLocationEvent._parse(session, data["liveLocationData"])
|
||||||
|
elif "deltaRecallMessageData" in data:
|
||||||
|
return UnsendEvent._parse(session, data["deltaRecallMessageData"])
|
||||||
|
elif "deltaMessageReply" in data:
|
||||||
|
return MessageReplyEvent._parse(session, data["deltaMessageReply"])
|
||||||
|
return UnknownEvent(source="client payload", data=data)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_client_payloads(session, data):
|
||||||
|
payload = _util.parse_json("".join(chr(z) for z in data["payload"]))
|
||||||
|
|
||||||
|
try:
|
||||||
|
for delta in payload["deltas"]:
|
||||||
|
yield parse_client_delta(session, delta)
|
||||||
|
except _exception.ParseError:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
raise _exception.ParseError("Error parsing ClientPayload", data=payload) from e
|
190
fbchat/_delta_class.py
Normal file
190
fbchat/_delta_class.py
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
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):
|
||||||
|
thread = cls._get_thread(session, data)
|
||||||
|
if "actorFbId" in data:
|
||||||
|
author = _user.User(session=session, id=data["actorFbId"])
|
||||||
|
else:
|
||||||
|
author = thread
|
||||||
|
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(source="Delta class", data=data)
|
329
fbchat/_delta_type.py
Normal file
329
fbchat/_delta_type.py
Normal file
@@ -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(source="Delta type", data=data)
|
123
fbchat/_event.py
Normal file
123
fbchat/_event.py
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import attr
|
||||||
|
import datetime
|
||||||
|
from ._event_common import attrs_event, Event, UnknownEvent, ThreadEvent
|
||||||
|
from . import (
|
||||||
|
_exception,
|
||||||
|
_util,
|
||||||
|
_user,
|
||||||
|
_group,
|
||||||
|
_thread,
|
||||||
|
_client_payload,
|
||||||
|
_delta_class,
|
||||||
|
_delta_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
from typing import Mapping
|
||||||
|
|
||||||
|
|
||||||
|
@attrs_event
|
||||||
|
class Typing(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):
|
||||||
|
author = _user.User(session=session, id=str(data["sender_fbid"]))
|
||||||
|
status = data["state"] == 1
|
||||||
|
return cls(author=author, thread=author, status=status)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _parse(cls, session, data):
|
||||||
|
# TODO: Rename this method
|
||||||
|
author = _user.User(session=session, id=str(data["sender_fbid"]))
|
||||||
|
thread = _group.Group(session=session, id=str(data["thread"]))
|
||||||
|
status = data["state"] == 1
|
||||||
|
return cls(author=author, thread=thread, status=status)
|
||||||
|
|
||||||
|
|
||||||
|
@attrs_event
|
||||||
|
class FriendRequest(Event):
|
||||||
|
"""Somebody sent a friend request."""
|
||||||
|
|
||||||
|
#: The user that sent the request
|
||||||
|
author = attr.ib(type=_user.User)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _parse(cls, session, data):
|
||||||
|
author = _user.User(session=session, id=str(data["from"]))
|
||||||
|
return cls(author=author)
|
||||||
|
|
||||||
|
|
||||||
|
@attrs_event
|
||||||
|
class Presence(Event):
|
||||||
|
"""The list of active statuses was updated.
|
||||||
|
|
||||||
|
Chat online presence update.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# TODO: Document this better!
|
||||||
|
|
||||||
|
#: User ids mapped to their active status
|
||||||
|
statuses = attr.ib(type=Mapping[str, _user.ActiveStatus])
|
||||||
|
#: ``True`` if the list is fully updated and ``False`` if it's partially updated
|
||||||
|
full = attr.ib(type=bool)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _parse(cls, session, data):
|
||||||
|
statuses = {
|
||||||
|
str(d["u"]): _user.ActiveStatus._from_orca_presence(d) for d in data["list"]
|
||||||
|
}
|
||||||
|
return cls(statuses=statuses, full=data["list_type"] == "full")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_delta(session, data):
|
||||||
|
try:
|
||||||
|
class_ = data.get("class")
|
||||||
|
if class_ == "ClientPayload":
|
||||||
|
yield from _client_payload.parse_client_payloads(session, data)
|
||||||
|
elif class_ == "AdminTextMessage":
|
||||||
|
yield _delta_type.parse_delta(session, data)
|
||||||
|
else:
|
||||||
|
event = _delta_class.parse_delta(session, data)
|
||||||
|
if event: # Skip `None`
|
||||||
|
yield event
|
||||||
|
except _exception.ParseError:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
raise _exception.ParseError("Error parsing delta", data=data) from e
|
||||||
|
|
||||||
|
|
||||||
|
def parse_events(session, topic, data):
|
||||||
|
# See Mqtt._configure_connect_options for information about these topics
|
||||||
|
try:
|
||||||
|
if topic == "/t_ms":
|
||||||
|
if "deltas" not in data:
|
||||||
|
return
|
||||||
|
for delta in data["deltas"]:
|
||||||
|
yield from parse_delta(session, delta)
|
||||||
|
|
||||||
|
elif topic == "/thread_typing":
|
||||||
|
yield Typing._parse(session, data)
|
||||||
|
|
||||||
|
elif topic == "/orca_typing_notifications":
|
||||||
|
yield Typing._parse_orca(session, data)
|
||||||
|
|
||||||
|
elif topic == "/legacy_web":
|
||||||
|
if data.get("type") == "jewel_requests_add":
|
||||||
|
yield FriendRequest._parse(session, data)
|
||||||
|
else:
|
||||||
|
yield UnknownEvent(source="/legacy_web", data=data)
|
||||||
|
|
||||||
|
elif topic == "/orca_presence":
|
||||||
|
yield Presence._parse(session, data)
|
||||||
|
|
||||||
|
else:
|
||||||
|
yield UnknownEvent(source=topic, data=data)
|
||||||
|
except _exception.ParseError:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
raise _exception.ParseError(
|
||||||
|
"Error parsing MQTT topic {}".format(topic), data=data
|
||||||
|
) from e
|
60
fbchat/_event_common.py
Normal file
60
fbchat/_event_common.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import attr
|
||||||
|
import abc
|
||||||
|
from ._core import kw_only
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
@attrs_event
|
||||||
|
class Event(metaclass=abc.ABCMeta):
|
||||||
|
"""Base class for all events."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _parse(cls, session, data):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
@attrs_event
|
||||||
|
class UnknownEvent(Event):
|
||||||
|
"""Represent an unknown event."""
|
||||||
|
|
||||||
|
#: Some data describing the unknown event's origin
|
||||||
|
source = attr.ib()
|
||||||
|
#: The unknown data. This cannot be relied on, it's only for debugging purposes.
|
||||||
|
data = attr.ib()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _parse(cls, session, data):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
@attrs_event
|
||||||
|
class ThreadEvent(Event):
|
||||||
|
"""Represent an event that was done by a user/page in a thread."""
|
||||||
|
|
||||||
|
#: The person who did the action
|
||||||
|
author = attr.ib(type=_user.User) # Or Union[User, Page]?
|
||||||
|
#: Thread that the action was done in
|
||||||
|
thread = attr.ib(type=_thread.ThreadABC)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_thread(session, data):
|
||||||
|
# TODO: Handle pages? Is it even possible?
|
||||||
|
key = data["threadKey"]
|
||||||
|
|
||||||
|
if "threadFbId" in key:
|
||||||
|
return _group.Group(session=session, id=str(key["threadFbId"]))
|
||||||
|
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
|
@@ -319,7 +319,7 @@ class MessageData(Message):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _from_reply(cls, thread, data, replied_to=None):
|
def _from_reply(cls, thread, data):
|
||||||
tags = data["messageMetadata"].get("tags")
|
tags = data["messageMetadata"].get("tags")
|
||||||
metadata = data.get("messageMetadata", {})
|
metadata = data.get("messageMetadata", {})
|
||||||
|
|
||||||
@@ -360,13 +360,18 @@ class MessageData(Message):
|
|||||||
attachments=attachments,
|
attachments=attachments,
|
||||||
quick_replies=cls._parse_quick_replies(data.get("platform_xmd_encoded")),
|
quick_replies=cls._parse_quick_replies(data.get("platform_xmd_encoded")),
|
||||||
unsent=unsent,
|
unsent=unsent,
|
||||||
reply_to_id=replied_to.id if replied_to else None,
|
reply_to_id=data["messageReply"]["replyToMessageId"]["id"]
|
||||||
replied_to=replied_to,
|
if "messageReply" in data
|
||||||
|
else None,
|
||||||
forwarded=cls._get_forwarded_from_tags(tags),
|
forwarded=cls._get_forwarded_from_tags(tags),
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@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 = []
|
mentions = []
|
||||||
if data.get("data") and data["data"].get("prng"):
|
if data.get("data") and data["data"].get("prng"):
|
||||||
try:
|
try:
|
||||||
@@ -414,7 +419,7 @@ class MessageData(Message):
|
|||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
thread=thread,
|
thread=thread,
|
||||||
id=mid,
|
id=metadata["messageId"],
|
||||||
author=author,
|
author=author,
|
||||||
created_at=created_at,
|
created_at=created_at,
|
||||||
text=data.get("body"),
|
text=data.get("body"),
|
||||||
|
@@ -2,11 +2,13 @@ import attr
|
|||||||
import random
|
import random
|
||||||
import paho.mqtt.client
|
import paho.mqtt.client
|
||||||
import requests
|
import requests
|
||||||
from ._core import log
|
from ._core import log, attrs_default
|
||||||
from . import _util, _exception, _graphql
|
from . import _util, _exception, _session, _graphql, _event_common, _event
|
||||||
|
|
||||||
|
from typing import Iterable
|
||||||
|
|
||||||
|
|
||||||
def get_cookie_header(session, url):
|
def get_cookie_header(session: requests.Session, url: str) -> str:
|
||||||
"""Extract a cookie header from a requests session."""
|
"""Extract a cookie header from a requests session."""
|
||||||
# The cookies are extracted this way to make sure they're escaped correctly
|
# The cookies are extracted this way to make sure they're escaped correctly
|
||||||
return requests.cookies.get_cookie_header(
|
return requests.cookies.get_cookie_header(
|
||||||
@@ -14,25 +16,34 @@ def get_cookie_header(session, url):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def generate_session_id():
|
def generate_session_id() -> int:
|
||||||
"""Generate a random session ID between 1 and 9007199254740991."""
|
"""Generate a random session ID between 1 and 9007199254740991."""
|
||||||
return random.randint(1, 2 ** 53)
|
return random.randint(1, 2 ** 53)
|
||||||
|
|
||||||
|
|
||||||
@attr.s(slots=True)
|
@attrs_default
|
||||||
class Mqtt(object):
|
class Listener:
|
||||||
_session = attr.ib()
|
"""Helper, to listen for incoming Facebook events."""
|
||||||
_mqtt = attr.ib()
|
|
||||||
_on_message = attr.ib()
|
_session = attr.ib(type=_session.Session)
|
||||||
_chat_on = attr.ib()
|
_mqtt = attr.ib(type=paho.mqtt.client.Client)
|
||||||
_foreground = attr.ib()
|
_chat_on = attr.ib(type=bool)
|
||||||
_sequence_id = attr.ib()
|
_foreground = attr.ib(type=bool)
|
||||||
_sync_token = attr.ib(None)
|
_sequence_id = attr.ib(type=int)
|
||||||
|
_sync_token = attr.ib(None, type=str)
|
||||||
|
_events = attr.ib(None, type=Iterable[_event_common.Event])
|
||||||
|
|
||||||
_HOST = "edge-chat.facebook.com"
|
_HOST = "edge-chat.facebook.com"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def connect(cls, session, on_message, chat_on, foreground):
|
def connect(cls, session, chat_on: bool, foreground: bool):
|
||||||
|
"""Initialize a connection to the Facebook MQTT service.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: The session to use when making requests.
|
||||||
|
chat_on: Whether ...
|
||||||
|
foreground: Whether ...
|
||||||
|
"""
|
||||||
mqtt = paho.mqtt.client.Client(
|
mqtt = paho.mqtt.client.Client(
|
||||||
client_id="mqttwsclient",
|
client_id="mqttwsclient",
|
||||||
clean_session=True,
|
clean_session=True,
|
||||||
@@ -50,7 +61,6 @@ class Mqtt(object):
|
|||||||
self = cls(
|
self = cls(
|
||||||
session=session,
|
session=session,
|
||||||
mqtt=mqtt,
|
mqtt=mqtt,
|
||||||
on_message=on_message,
|
|
||||||
chat_on=chat_on,
|
chat_on=chat_on,
|
||||||
foreground=foreground,
|
foreground=foreground,
|
||||||
sequence_id=cls._fetch_sequence_id(session),
|
sequence_id=cls._fetch_sequence_id(session),
|
||||||
@@ -109,11 +119,14 @@ class Mqtt(object):
|
|||||||
|
|
||||||
log.debug("MQTT payload: %s, %s", message.topic, j)
|
log.debug("MQTT payload: %s, %s", message.topic, j)
|
||||||
|
|
||||||
# Call the external callback
|
try:
|
||||||
self._on_message(message.topic, j)
|
# TODO: Don't handle this in a callback
|
||||||
|
self._events = list(_event.parse_events(self._session, message.topic, j))
|
||||||
|
except _exception.ParseError:
|
||||||
|
log.exception("Failed parsing MQTT data")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _fetch_sequence_id(session):
|
def _fetch_sequence_id(session) -> int:
|
||||||
"""Fetch sequence ID."""
|
"""Fetch sequence ID."""
|
||||||
params = {
|
params = {
|
||||||
"limit": 1,
|
"limit": 1,
|
||||||
@@ -258,14 +271,11 @@ class Mqtt(object):
|
|||||||
path="/chat?sid={}".format(session_id), headers=headers
|
path="/chat?sid={}".format(session_id), headers=headers
|
||||||
)
|
)
|
||||||
|
|
||||||
def loop_once(self):
|
def _loop_once(self) -> bool:
|
||||||
"""Run the listening loop once.
|
|
||||||
|
|
||||||
Returns whether to keep listening or not.
|
|
||||||
"""
|
|
||||||
rc = self._mqtt.loop(timeout=1.0)
|
rc = self._mqtt.loop(timeout=1.0)
|
||||||
|
|
||||||
# If disconnect() has been called
|
# If disconnect() has been called
|
||||||
|
# Beware, internal API, may have to change this to something more stable!
|
||||||
if self._mqtt._state == paho.mqtt.client.mqtt_cs_disconnecting:
|
if self._mqtt._state == paho.mqtt.client.mqtt_cs_disconnecting:
|
||||||
return False # Stop listening
|
return False # Stop listening
|
||||||
|
|
||||||
@@ -298,23 +308,45 @@ class Mqtt(object):
|
|||||||
|
|
||||||
return True # Keep listening
|
return True # Keep listening
|
||||||
|
|
||||||
def disconnect(self):
|
def listen(self) -> Iterable[_event_common.Event]:
|
||||||
|
"""Run the listening loop continually.
|
||||||
|
|
||||||
|
Yields events when they arrive.
|
||||||
|
|
||||||
|
This will automatically reconnect on errors.
|
||||||
|
"""
|
||||||
|
while self._loop_once():
|
||||||
|
if self._events:
|
||||||
|
yield from self._events
|
||||||
|
self._events = None
|
||||||
|
|
||||||
|
def disconnect(self) -> None:
|
||||||
|
"""Disconnect the MQTT listener.
|
||||||
|
|
||||||
|
Can be called while listening, which will stop the listening loop.
|
||||||
|
|
||||||
|
The `Listener` object should not be used after this is called!
|
||||||
|
"""
|
||||||
self._mqtt.disconnect()
|
self._mqtt.disconnect()
|
||||||
|
|
||||||
def set_foreground(self, value):
|
def set_foreground(self, value: bool) -> None:
|
||||||
|
"""Set the `foreground` value while listening."""
|
||||||
|
# TODO: Document what this actually does!
|
||||||
payload = _util.json_minimal({"foreground": value})
|
payload = _util.json_minimal({"foreground": value})
|
||||||
info = self._mqtt.publish("/foreground_state", payload=payload, qos=1)
|
info = self._mqtt.publish("/foreground_state", payload=payload, qos=1)
|
||||||
self._foreground = value
|
self._foreground = value
|
||||||
# TODO: We can't wait for this, since the loop is running with .loop_forever()
|
# TODO: We can't wait for this, since the loop is running within the same thread
|
||||||
# info.wait_for_publish()
|
# info.wait_for_publish()
|
||||||
|
|
||||||
def set_chat_on(self, value):
|
def set_chat_on(self, value: bool) -> None:
|
||||||
|
"""Set the `chat_on` value while listening."""
|
||||||
|
# TODO: Document what this actually does!
|
||||||
# TODO: Is this the right request to make?
|
# TODO: Is this the right request to make?
|
||||||
data = {"make_user_available_when_in_foreground": value}
|
data = {"make_user_available_when_in_foreground": value}
|
||||||
payload = _util.json_minimal(data)
|
payload = _util.json_minimal(data)
|
||||||
info = self._mqtt.publish("/set_client_settings", payload=payload, qos=1)
|
info = self._mqtt.publish("/set_client_settings", payload=payload, qos=1)
|
||||||
self._chat_on = value
|
self._chat_on = value
|
||||||
# TODO: We can't wait for this, since the loop is running with .loop_forever()
|
# TODO: We can't wait for this, since the loop is running within the same thread
|
||||||
# info.wait_for_publish()
|
# info.wait_for_publish()
|
||||||
|
|
||||||
# def send_additional_contacts(self, additional_contacts):
|
# def send_additional_contacts(self, additional_contacts):
|
||||||
|
@@ -8,7 +8,14 @@ from fbchat import Message, Mention
|
|||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def 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 "<FakeSession>"
|
||||||
|
|
||||||
|
return FakeSession()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
|
174
tests/test_client_payload.py
Normal file
174
tests/test_client_payload.py
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import datetime
|
||||||
|
import pytest
|
||||||
|
from fbchat import (
|
||||||
|
ParseError,
|
||||||
|
User,
|
||||||
|
Group,
|
||||||
|
UnknownEvent,
|
||||||
|
ReactionEvent,
|
||||||
|
UserStatusEvent,
|
||||||
|
LiveLocationEvent,
|
||||||
|
UnsendEvent,
|
||||||
|
MessageReplyEvent,
|
||||||
|
)
|
||||||
|
from fbchat._message import Message, MessageData
|
||||||
|
from fbchat._client_payload import parse_client_delta, parse_client_payloads
|
||||||
|
|
||||||
|
|
||||||
|
def test_reaction_event_added(session):
|
||||||
|
data = {
|
||||||
|
"threadKey": {"otherUserFbId": 1234},
|
||||||
|
"messageId": "mid.$XYZ",
|
||||||
|
"action": 0,
|
||||||
|
"userId": 4321,
|
||||||
|
"reaction": "😍",
|
||||||
|
"senderId": 4321,
|
||||||
|
"offlineThreadingId": "6623596674408921967",
|
||||||
|
}
|
||||||
|
thread = User(session=session, id="1234")
|
||||||
|
assert ReactionEvent(
|
||||||
|
author=User(session=session, id="4321"),
|
||||||
|
thread=thread,
|
||||||
|
message=Message(thread=thread, id="mid.$XYZ"),
|
||||||
|
reaction="😍",
|
||||||
|
) == parse_client_delta(session, {"deltaMessageReaction": data})
|
||||||
|
|
||||||
|
|
||||||
|
def test_reaction_event_removed(session):
|
||||||
|
data = {
|
||||||
|
"threadKey": {"threadFbId": 1234},
|
||||||
|
"messageId": "mid.$XYZ",
|
||||||
|
"action": 1,
|
||||||
|
"userId": 4321,
|
||||||
|
"senderId": 4321,
|
||||||
|
"offlineThreadingId": "6623586106713014836",
|
||||||
|
}
|
||||||
|
thread = Group(session=session, id="1234")
|
||||||
|
assert ReactionEvent(
|
||||||
|
author=User(session=session, id="4321"),
|
||||||
|
thread=thread,
|
||||||
|
message=Message(thread=thread, id="mid.$XYZ"),
|
||||||
|
reaction=None,
|
||||||
|
) == parse_client_delta(session, {"deltaMessageReaction": data})
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_status_blocked(session):
|
||||||
|
data = {
|
||||||
|
"threadKey": {"otherUserFbId": 1234},
|
||||||
|
"canViewerReply": False,
|
||||||
|
"reason": 2,
|
||||||
|
"actorFbid": 4321,
|
||||||
|
}
|
||||||
|
assert UserStatusEvent(
|
||||||
|
author=User(session=session, id="4321"),
|
||||||
|
thread=User(session=session, id="1234"),
|
||||||
|
blocked=True,
|
||||||
|
) == parse_client_delta(session, {"deltaChangeViewerStatus": data})
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_status_unblocked(session):
|
||||||
|
data = {
|
||||||
|
"threadKey": {"otherUserFbId": 1234},
|
||||||
|
"canViewerReply": True,
|
||||||
|
"reason": 2,
|
||||||
|
"actorFbid": 1234,
|
||||||
|
}
|
||||||
|
assert UserStatusEvent(
|
||||||
|
author=User(session=session, id="1234"),
|
||||||
|
thread=User(session=session, id="1234"),
|
||||||
|
blocked=False,
|
||||||
|
) == parse_client_delta(session, {"deltaChangeViewerStatus": data})
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason="need to gather test data")
|
||||||
|
def test_live_location(session):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_message_reply(session):
|
||||||
|
message = {
|
||||||
|
"messageMetadata": {
|
||||||
|
"threadKey": {"otherUserFbId": 1234},
|
||||||
|
"messageId": "mid.$XYZ",
|
||||||
|
"offlineThreadingId": "112233445566",
|
||||||
|
"actorFbId": 1234,
|
||||||
|
"timestamp": 1500000000000,
|
||||||
|
"tags": ["source:messenger:web", "cg-enabled", "sent", "inbox"],
|
||||||
|
"threadReadStateEffect": 3,
|
||||||
|
"skipBumpThread": False,
|
||||||
|
"skipSnippetUpdate": False,
|
||||||
|
"unsendType": "can_unsend",
|
||||||
|
"folderId": {"systemFolderId": 0},
|
||||||
|
},
|
||||||
|
"body": "xyz",
|
||||||
|
"attachments": [],
|
||||||
|
"irisSeqId": 1111111,
|
||||||
|
"messageReply": {"replyToMessageId": {"id": "mid.$ABC"}, "status": 0,},
|
||||||
|
"requestContext": {"apiArgs": "..."},
|
||||||
|
"irisTags": ["DeltaNewMessage"],
|
||||||
|
}
|
||||||
|
reply = {
|
||||||
|
"messageMetadata": {
|
||||||
|
"threadKey": {"otherUserFbId": 1234},
|
||||||
|
"messageId": "mid.$ABC",
|
||||||
|
"offlineThreadingId": "665544332211",
|
||||||
|
"actorFbId": 4321,
|
||||||
|
"timestamp": 1600000000000,
|
||||||
|
"tags": ["inbox", "sent", "source:messenger:web"],
|
||||||
|
},
|
||||||
|
"body": "abc",
|
||||||
|
"attachments": [],
|
||||||
|
"requestContext": {"apiArgs": "..."},
|
||||||
|
"irisTags": [],
|
||||||
|
}
|
||||||
|
data = {
|
||||||
|
"message": message,
|
||||||
|
"repliedToMessage": reply,
|
||||||
|
"status": 0,
|
||||||
|
}
|
||||||
|
thread = User(session=session, id="1234")
|
||||||
|
assert MessageReplyEvent(
|
||||||
|
author=User(session=session, id="1234"),
|
||||||
|
thread=thread,
|
||||||
|
message=MessageData(
|
||||||
|
thread=thread,
|
||||||
|
id="mid.$XYZ",
|
||||||
|
author="1234",
|
||||||
|
created_at=datetime.datetime(
|
||||||
|
2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc
|
||||||
|
),
|
||||||
|
text="xyz",
|
||||||
|
reply_to_id="mid.$ABC",
|
||||||
|
),
|
||||||
|
replied_to=MessageData(
|
||||||
|
thread=thread,
|
||||||
|
id="mid.$ABC",
|
||||||
|
author="4321",
|
||||||
|
created_at=datetime.datetime(
|
||||||
|
2020, 9, 13, 12, 26, 40, tzinfo=datetime.timezone.utc
|
||||||
|
),
|
||||||
|
text="abc",
|
||||||
|
),
|
||||||
|
) == parse_client_delta(session, {"deltaMessageReply": data})
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_client_delta_unknown(session):
|
||||||
|
assert UnknownEvent(
|
||||||
|
source="client payload", data={"abc": 10}
|
||||||
|
) == parse_client_delta(session, {"abc": 10})
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_client_payloads_empty(session):
|
||||||
|
# This is never something that happens, it's just so that we can test the parsing
|
||||||
|
# payload = '{"deltas":[]}'
|
||||||
|
payload = [123, 34, 100, 101, 108, 116, 97, 115, 34, 58, 91, 93, 125]
|
||||||
|
data = {"payload": payload, "class": "ClientPayload"}
|
||||||
|
assert [] == list(parse_client_payloads(session, data))
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_client_payloads_invalid(session):
|
||||||
|
# payload = '{"invalid":"data"}'
|
||||||
|
payload = [123, 34, 105, 110, 118, 97, 108, 105, 100, 34, 58, 34, 97, 34, 125]
|
||||||
|
data = {"payload": payload, "class": "ClientPayload"}
|
||||||
|
with pytest.raises(ParseError, match="Error parsing ClientPayload"):
|
||||||
|
list(parse_client_payloads(session, data))
|
299
tests/test_delta_class.py
Normal file
299
tests/test_delta_class.py
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
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_group(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_delivery_receipt_user(session):
|
||||||
|
data = {
|
||||||
|
"deliveredWatermarkTimestampMs": "1500000000000",
|
||||||
|
"irisSeqId": "1111111",
|
||||||
|
"irisTags": ["DeltaDeliveryReceipt", "is_from_iris_fanout"],
|
||||||
|
"messageIds": ["mid.$XYZ", "mid.$ABC"],
|
||||||
|
"requestContext": {"apiArgs": {}},
|
||||||
|
"threadKey": {"otherUserFbId": "1234"},
|
||||||
|
"class": "DeliveryReceipt",
|
||||||
|
}
|
||||||
|
thread = User(session=session, id="1234")
|
||||||
|
assert MessagesDelivered(
|
||||||
|
author=thread,
|
||||||
|
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(source="Delta class", data={"abc": 10}) == parse_delta(
|
||||||
|
session, {"abc": 10}
|
||||||
|
)
|
956
tests/test_delta_type.py
Normal file
956
tests/test_delta_type.py
Normal file
@@ -0,0 +1,956 @@
|
|||||||
|
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(source="Delta type", data={"abc": 10}) == parse_delta(
|
||||||
|
session, {"abc": 10}
|
||||||
|
)
|
137
tests/test_event.py
Normal file
137
tests/test_event.py
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import datetime
|
||||||
|
from fbchat import (
|
||||||
|
_util,
|
||||||
|
User,
|
||||||
|
Group,
|
||||||
|
Message,
|
||||||
|
ParseError,
|
||||||
|
UnknownEvent,
|
||||||
|
Typing,
|
||||||
|
FriendRequest,
|
||||||
|
Presence,
|
||||||
|
ReactionEvent,
|
||||||
|
UnfetchedThreadEvent,
|
||||||
|
ActiveStatus,
|
||||||
|
)
|
||||||
|
from fbchat._event import parse_delta, parse_events
|
||||||
|
|
||||||
|
|
||||||
|
def test_t_ms_full(session):
|
||||||
|
"""A full example of parsing of data in /t_ms."""
|
||||||
|
payload = {
|
||||||
|
"deltas": [
|
||||||
|
{
|
||||||
|
"deltaMessageReaction": {
|
||||||
|
"threadKey": {"threadFbId": 4321},
|
||||||
|
"messageId": "mid.$XYZ",
|
||||||
|
"action": 0,
|
||||||
|
"userId": 1234,
|
||||||
|
"reaction": "😢",
|
||||||
|
"senderId": 1234,
|
||||||
|
"offlineThreadingId": "1122334455",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
data = {
|
||||||
|
"deltas": [
|
||||||
|
{
|
||||||
|
"payload": [ord(x) for x in _util.json_minimal(payload)],
|
||||||
|
"class": "ClientPayload",
|
||||||
|
},
|
||||||
|
{"class": "NoOp",},
|
||||||
|
{
|
||||||
|
"forceInsert": False,
|
||||||
|
"messageId": "mid.$ABC",
|
||||||
|
"threadKey": {"threadFbId": "4321"},
|
||||||
|
"class": "ForcedFetch",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"firstDeltaSeqId": 111111,
|
||||||
|
"lastIssuedSeqId": 111113,
|
||||||
|
"queueEntityId": 1234,
|
||||||
|
}
|
||||||
|
thread = Group(session=session, id="4321")
|
||||||
|
assert [
|
||||||
|
ReactionEvent(
|
||||||
|
author=User(session=session, id="1234"),
|
||||||
|
thread=thread,
|
||||||
|
message=Message(thread=thread, id="mid.$XYZ"),
|
||||||
|
reaction="😢",
|
||||||
|
),
|
||||||
|
UnfetchedThreadEvent(
|
||||||
|
thread=thread, message=Message(thread=thread, id="mid.$ABC"),
|
||||||
|
),
|
||||||
|
] == list(parse_events(session, "/t_ms", data))
|
||||||
|
|
||||||
|
|
||||||
|
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(
|
||||||
|
author=User(session=session, id="1234"),
|
||||||
|
thread=Group(session=session, id="4321"),
|
||||||
|
status=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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(
|
||||||
|
author=User(session=session, id="1234"),
|
||||||
|
thread=User(session=session, id="1234"),
|
||||||
|
status=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_friend_request(session):
|
||||||
|
data = {"type": "jewel_requests_add", "from": "1234"}
|
||||||
|
(event,) = parse_events(session, "/legacy_web", data)
|
||||||
|
assert event == FriendRequest(author=User(session=session, id="1234"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_orca_presence_inc(session):
|
||||||
|
data = {
|
||||||
|
"list_type": "inc",
|
||||||
|
"list": [
|
||||||
|
{"u": 1234, "p": 0, "l": 1500000000, "vc": 74},
|
||||||
|
{"u": 2345, "p": 2, "c": 9969664, "vc": 10},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
(event,) = parse_events(session, "/orca_presence", data)
|
||||||
|
la = datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc)
|
||||||
|
assert event == Presence(
|
||||||
|
statuses={
|
||||||
|
"1234": ActiveStatus(active=False, last_active=la),
|
||||||
|
"2345": ActiveStatus(active=True),
|
||||||
|
},
|
||||||
|
full=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_orca_presence_full(session):
|
||||||
|
data = {
|
||||||
|
"list_type": "full",
|
||||||
|
"list": [
|
||||||
|
{"u": 1234, "p": 2, "c": 5767242},
|
||||||
|
{"u": 2345, "p": 2, "l": 1500000000},
|
||||||
|
{"u": 3456, "p": 2, "c": 9961482},
|
||||||
|
{"u": 4567, "p": 0, "l": 1500000000},
|
||||||
|
{"u": 5678, "p": 0},
|
||||||
|
{"u": 6789, "p": 2, "c": 14168154},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
(event,) = parse_events(session, "/orca_presence", data)
|
||||||
|
la = datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc)
|
||||||
|
assert event == Presence(
|
||||||
|
statuses={
|
||||||
|
"1234": ActiveStatus(active=True),
|
||||||
|
"2345": ActiveStatus(active=True, last_active=la),
|
||||||
|
"3456": ActiveStatus(active=True),
|
||||||
|
"4567": ActiveStatus(active=False, last_active=la),
|
||||||
|
"5678": ActiveStatus(active=False),
|
||||||
|
"6789": ActiveStatus(active=True),
|
||||||
|
},
|
||||||
|
full=True,
|
||||||
|
)
|
78
tests/test_event_common.py
Normal file
78
tests/test_event_common.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import pytest
|
||||||
|
import datetime
|
||||||
|
from fbchat import Group, User, ParseError, ThreadEvent
|
||||||
|
|
||||||
|
|
||||||
|
def test_thread_event_get_thread_group1(session):
|
||||||
|
data = {
|
||||||
|
"threadKey": {"threadFbId": 1234},
|
||||||
|
"messageId": "mid.$gAAT4Sw1WSGh14A3MOFvrsiDvr3Yc",
|
||||||
|
"offlineThreadingId": "6623583531508397596",
|
||||||
|
"actorFbId": 4321,
|
||||||
|
"timestamp": 1500000000000,
|
||||||
|
"tags": [
|
||||||
|
"inbox",
|
||||||
|
"sent",
|
||||||
|
"tq",
|
||||||
|
"blindly_apply_message_folder",
|
||||||
|
"source:messenger:web",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
assert Group(session=session, id="1234") == ThreadEvent._get_thread(session, data)
|
||||||
|
|
||||||
|
|
||||||
|
def test_thread_event_get_thread_group2(session):
|
||||||
|
data = {
|
||||||
|
"actorFbId": "4321",
|
||||||
|
"folderId": {"systemFolderId": "INBOX"},
|
||||||
|
"messageId": "mid.$XYZ",
|
||||||
|
"offlineThreadingId": "112233445566",
|
||||||
|
"skipBumpThread": False,
|
||||||
|
"tags": ["source:messenger:web"],
|
||||||
|
"threadKey": {"threadFbId": "1234"},
|
||||||
|
"threadReadStateEffect": "KEEP_AS_IS",
|
||||||
|
"timestamp": "1500000000000",
|
||||||
|
}
|
||||||
|
assert Group(session=session, id="1234") == ThreadEvent._get_thread(session, data)
|
||||||
|
|
||||||
|
|
||||||
|
def test_thread_event_get_thread_user(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="1234") == ThreadEvent._get_thread(session, data)
|
||||||
|
|
||||||
|
|
||||||
|
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})
|
Reference in New Issue
Block a user