diff --git a/fbchat/_attachment.py b/fbchat/_attachment.py index ec48721..afc54fb 100644 --- a/fbchat/_attachment.py +++ b/fbchat/_attachment.py @@ -2,7 +2,7 @@ import attr from . import _util -@attr.s(cmp=False) +@attr.s class Attachment: """Represents a Facebook attachment.""" @@ -10,12 +10,12 @@ class Attachment: uid = attr.ib(None) -@attr.s(cmp=False) +@attr.s class UnsentMessage(Attachment): """Represents an unsent message attachment.""" -@attr.s(cmp=False) +@attr.s class ShareAttachment(Attachment): """Represents a shared item (e.g. URL) attachment.""" diff --git a/fbchat/_file.py b/fbchat/_file.py index 10edf9c..f547138 100644 --- a/fbchat/_file.py +++ b/fbchat/_file.py @@ -3,7 +3,7 @@ from . import _util from ._attachment import Attachment -@attr.s(cmp=False) +@attr.s class FileAttachment(Attachment): """Represents a file that has been sent as a Facebook attachment.""" @@ -29,7 +29,7 @@ class FileAttachment(Attachment): ) -@attr.s(cmp=False) +@attr.s class AudioAttachment(Attachment): """Represents an audio file that has been sent as a Facebook attachment.""" @@ -55,7 +55,7 @@ class AudioAttachment(Attachment): ) -@attr.s(cmp=False, init=False) +@attr.s(init=False) class ImageAttachment(Attachment): """Represents an image that has been sent as a Facebook attachment. @@ -166,7 +166,7 @@ class ImageAttachment(Attachment): ) -@attr.s(cmp=False, init=False) +@attr.s(init=False) class VideoAttachment(Attachment): """Represents a video that has been sent as a Facebook attachment.""" diff --git a/fbchat/_group.py b/fbchat/_group.py index d8eea84..2c1b1da 100644 --- a/fbchat/_group.py +++ b/fbchat/_group.py @@ -3,10 +3,12 @@ from . import _util, _plan from ._thread import ThreadType, Thread -@attr.s(cmp=False, init=False) +@attr.s class Group(Thread): """Represents a Facebook group. Inherits `Thread`.""" + type = ThreadType.GROUP + #: Unique list (set) of the group thread's participant user IDs participants = attr.ib(factory=set, converter=lambda x: set() if x is None else x) #: A dictionary, containing user nicknames mapped to their IDs @@ -26,38 +28,6 @@ class Group(Thread): # Link for joining group join_link = attr.ib(None) - def __init__( - self, - uid, - participants=None, - nicknames=None, - color=None, - emoji=None, - admins=None, - approval_mode=None, - approval_requests=None, - join_link=None, - privacy_mode=None, - **kwargs - ): - super(Group, self).__init__(ThreadType.GROUP, uid, **kwargs) - if participants is None: - participants = set() - self.participants = participants - if nicknames is None: - nicknames = [] - self.nicknames = nicknames - self.color = color - self.emoji = emoji - if admins is None: - admins = set() - self.admins = admins - self.approval_mode = approval_mode - if approval_requests is None: - approval_requests = set() - self.approval_requests = approval_requests - self.join_link = join_link - @classmethod def _from_graphql(cls, data): if data.get("image") is None: diff --git a/fbchat/_location.py b/fbchat/_location.py index 3a4f2af..823364a 100644 --- a/fbchat/_location.py +++ b/fbchat/_location.py @@ -3,7 +3,7 @@ from ._attachment import Attachment from . import _util -@attr.s(cmp=False) +@attr.s class LocationAttachment(Attachment): """Represents a user location. @@ -53,7 +53,7 @@ class LocationAttachment(Attachment): return rtn -@attr.s(cmp=False, init=False) +@attr.s class LiveLocationAttachment(LocationAttachment): """Represents a live user location.""" @@ -64,11 +64,6 @@ class LiveLocationAttachment(LocationAttachment): #: True if live location is expired is_expired = attr.ib(None) - def __init__(self, name=None, expires_at=None, is_expired=None, **kwargs): - super(LiveLocationAttachment, self).__init__(**kwargs) - self.expires_at = expires_at - self.is_expired = is_expired - @classmethod def _from_pull(cls, data): return cls( @@ -96,7 +91,7 @@ class LiveLocationAttachment(LocationAttachment): if target.get("coordinate") else None, name=data["title_with_entities"]["text"], - expires_at=_util.millis_to_datetime(target.get("expiration_time")), + expires_at=_util.seconds_to_datetime(target.get("expiration_time")), is_expired=target.get("is_expired"), ) media = data.get("media") diff --git a/fbchat/_message.py b/fbchat/_message.py index 329fe85..90f037b 100644 --- a/fbchat/_message.py +++ b/fbchat/_message.py @@ -42,7 +42,7 @@ class MessageReaction(Enum): NO = "πŸ‘Ž" -@attr.s(cmp=False) +@attr.s class Mention: """Represents a ``@mention``.""" @@ -54,7 +54,7 @@ class Mention: length = attr.ib(10) -@attr.s(cmp=False) +@attr.s class Message: """Represents a Facebook message.""" diff --git a/fbchat/_page.py b/fbchat/_page.py index 76bbad7..dea3523 100644 --- a/fbchat/_page.py +++ b/fbchat/_page.py @@ -3,10 +3,12 @@ from . import _plan from ._thread import ThreadType, Thread -@attr.s(cmp=False, init=False) +@attr.s class Page(Thread): """Represents a Facebook page. Inherits `Thread`.""" + type = ThreadType.PAGE + #: The page's custom URL url = attr.ib(None) #: The name of the page's location city @@ -18,23 +20,6 @@ class Page(Thread): #: The page's category category = attr.ib(None) - def __init__( - self, - uid, - url=None, - city=None, - likes=None, - sub_title=None, - category=None, - **kwargs - ): - super(Page, self).__init__(ThreadType.PAGE, uid, **kwargs) - self.url = url - self.city = city - self.likes = likes - self.sub_title = sub_title - self.category = category - @classmethod def _from_graphql(cls, data): if data.get("profile_picture") is None: diff --git a/fbchat/_plan.py b/fbchat/_plan.py index 373f8da..8daa86b 100644 --- a/fbchat/_plan.py +++ b/fbchat/_plan.py @@ -10,7 +10,7 @@ class GuestStatus(Enum): DECLINED = 3 -@attr.s(cmp=False) +@attr.s class Plan: """Represents a plan.""" diff --git a/fbchat/_poll.py b/fbchat/_poll.py index 12a29b2..86a7c14 100644 --- a/fbchat/_poll.py +++ b/fbchat/_poll.py @@ -1,7 +1,7 @@ import attr -@attr.s(cmp=False) +@attr.s class Poll: """Represents a poll.""" @@ -24,7 +24,7 @@ class Poll: ) -@attr.s(cmp=False) +@attr.s class PollOption: """Represents a poll option.""" diff --git a/fbchat/_quick_reply.py b/fbchat/_quick_reply.py index 5a9a262..802f473 100644 --- a/fbchat/_quick_reply.py +++ b/fbchat/_quick_reply.py @@ -2,7 +2,7 @@ import attr from ._attachment import Attachment -@attr.s(cmp=False) +@attr.s class QuickReply: """Represents a quick reply.""" @@ -16,7 +16,7 @@ class QuickReply: is_response = attr.ib(False) -@attr.s(cmp=False, init=False) +@attr.s class QuickReplyText(QuickReply): """Represents a text quick reply.""" @@ -27,25 +27,16 @@ class QuickReplyText(QuickReply): #: Type of the quick reply _type = "text" - def __init__(self, title=None, image_url=None, **kwargs): - super(QuickReplyText, self).__init__(**kwargs) - self.title = title - self.image_url = image_url - -@attr.s(cmp=False, init=False) +@attr.s class QuickReplyLocation(QuickReply): """Represents a location quick reply (Doesn't work on mobile).""" #: Type of the quick reply _type = "location" - def __init__(self, **kwargs): - super(QuickReplyLocation, self).__init__(**kwargs) - self.is_response = False - -@attr.s(cmp=False, init=False) +@attr.s class QuickReplyPhoneNumber(QuickReply): """Represents a phone number quick reply (Doesn't work on mobile).""" @@ -54,12 +45,8 @@ class QuickReplyPhoneNumber(QuickReply): #: Type of the quick reply _type = "user_phone_number" - def __init__(self, image_url=None, **kwargs): - super(QuickReplyPhoneNumber, self).__init__(**kwargs) - self.image_url = image_url - -@attr.s(cmp=False, init=False) +@attr.s class QuickReplyEmail(QuickReply): """Represents an email quick reply (Doesn't work on mobile).""" @@ -68,10 +55,6 @@ class QuickReplyEmail(QuickReply): #: Type of the quick reply _type = "user_email" - def __init__(self, image_url=None, **kwargs): - super(QuickReplyEmail, self).__init__(**kwargs) - self.image_url = image_url - def graphql_to_quick_reply(q, is_response=False): data = dict() diff --git a/fbchat/_sticker.py b/fbchat/_sticker.py index a56ce2a..cf0f0eb 100644 --- a/fbchat/_sticker.py +++ b/fbchat/_sticker.py @@ -2,7 +2,7 @@ import attr from ._attachment import Attachment -@attr.s(cmp=False, init=False) +@attr.s class Sticker(Attachment): """Represents a Facebook sticker that has been sent to a thread as an attachment.""" @@ -32,9 +32,6 @@ class Sticker(Attachment): #: The sticker's label/name label = attr.ib(None) - def __init__(self, uid=None): - super(Sticker, self).__init__(uid=uid) - @classmethod def _from_graphql(cls, data): if not data: diff --git a/fbchat/_thread.py b/fbchat/_thread.py index 0659257..4a098e3 100644 --- a/fbchat/_thread.py +++ b/fbchat/_thread.py @@ -67,14 +67,14 @@ class ThreadColor(Enum): return cls._extend_if_invalid(value) -@attr.s(cmp=False, init=False) +@attr.s class Thread: """Represents a Facebook thread.""" #: The unique identifier of the thread. Can be used a ``thread_id``. See :ref:`intro_threads` for more info uid = attr.ib(converter=str) #: Specifies the type of thread. Can be used a ``thread_type``. See :ref:`intro_threads` for more info - type = attr.ib() + type = None #: A URL to the thread's picture photo = attr.ib(None) #: The name of the thread @@ -86,24 +86,6 @@ class Thread: #: Set :class:`Plan` plan = attr.ib(None) - def __init__( - self, - _type, - uid, - photo=None, - name=None, - last_active=None, - message_count=None, - plan=None, - ): - self.uid = str(uid) - self.type = _type - self.photo = photo - self.name = name - self.last_active = last_active - self.message_count = message_count - self.plan = plan - @staticmethod def _parse_customization_info(data): if data is None or data.get("customization_info") is None: diff --git a/fbchat/_user.py b/fbchat/_user.py index 4eb0fbc..8e168fe 100644 --- a/fbchat/_user.py +++ b/fbchat/_user.py @@ -41,10 +41,12 @@ class TypingStatus(Enum): TYPING = 1 -@attr.s(cmp=False, init=False) +@attr.s class User(Thread): """Represents a Facebook user. Inherits `Thread`.""" + type = ThreadType.USER + #: The profile URL url = attr.ib(None) #: The users first name @@ -66,33 +68,6 @@ class User(Thread): #: The default emoji emoji = attr.ib(None) - def __init__( - self, - uid, - url=None, - first_name=None, - last_name=None, - is_friend=None, - gender=None, - affinity=None, - nickname=None, - own_nickname=None, - color=None, - emoji=None, - **kwargs - ): - super(User, self).__init__(ThreadType.USER, uid, **kwargs) - self.url = url - self.first_name = first_name - self.last_name = last_name - self.is_friend = is_friend - self.gender = gender - self.affinity = affinity - self.nickname = nickname - self.own_nickname = own_nickname - self.color = color - self.emoji = emoji - @classmethod def _from_graphql(cls, data): if data.get("profile_picture") is None: @@ -179,7 +154,7 @@ class User(Thread): ) -@attr.s(cmp=False) +@attr.s class ActiveStatus: #: Whether the user is active now active = attr.ib(None) diff --git a/tests/test_attachment.py b/tests/test_attachment.py new file mode 100644 index 0000000..f94d6ca --- /dev/null +++ b/tests/test_attachment.py @@ -0,0 +1,456 @@ +import pytest +import datetime +import fbchat +from fbchat._attachment import UnsentMessage, ShareAttachment + + +def test_parse_unsent_message(): + data = { + "legacy_attachment_id": "ee.mid.$xyz", + "story_attachment": { + "description": {"text": "You removed a message"}, + "media": None, + "source": None, + "style_list": ["globally_deleted_message_placeholder", "fallback"], + "title_with_entities": {"text": ""}, + "properties": [], + "url": None, + "deduplication_key": "deadbeef123", + "action_links": [], + "messaging_attribution": None, + "messenger_call_to_actions": [], + "xma_layout_info": None, + "target": None, + "subattachments": [], + }, + "genie_attachment": {"genie_message": None}, + } + assert UnsentMessage( + uid="ee.mid.$xyz" + ) == fbchat._message.graphql_to_extensible_attachment(data) + + +def test_share_from_graphql_minimal(): + data = { + "target": {}, + "url": "a.com", + "title_with_entities": {"text": "a.com"}, + "subattachments": [], + } + assert ShareAttachment( + url="a.com", original_url="a.com", title="a.com" + ) == ShareAttachment._from_graphql(data) + + +def test_share_from_graphql_link(): + data = { + "description": {"text": ""}, + "media": { + "animated_image": None, + "image": None, + "playable_duration_in_ms": 0, + "is_playable": False, + "playable_url": None, + }, + "source": {"text": "a.com"}, + "style_list": ["share", "fallback"], + "title_with_entities": {"text": "a.com"}, + "properties": [], + "url": "http://l.facebook.com/l.php?u=http%3A%2F%2Fa.com%2F&h=def&s=1", + "deduplication_key": "ee.mid.$xyz", + "action_links": [{"title": "About this website", "url": None}], + "messaging_attribution": None, + "messenger_call_to_actions": [], + "xma_layout_info": None, + "target": {"__typename": "ExternalUrl"}, + "subattachments": [], + } + assert ShareAttachment( + author=None, + url="http://l.facebook.com/l.php?u=http%3A%2F%2Fa.com%2F&h=def&s=1", + original_url="http://a.com/", + title="a.com", + description="", + source="a.com", + image_url=None, + original_image_url=None, + image_width=None, + image_height=None, + attachments=[], + uid="ee.mid.$xyz", + ) == ShareAttachment._from_graphql(data) + + +def test_share_from_graphql_link_with_image(): + data = { + "description": { + "text": ( + "Create an account or log in to Facebook." + " Connect with friends, family and other people you know." + " Share photos and videos, send messages and get updates." + ) + }, + "media": { + "animated_image": None, + "image": { + "uri": "https://www.facebook.com/rsrc.php/v3/x.png", + "height": 325, + "width": 325, + }, + "playable_duration_in_ms": 0, + "is_playable": False, + "playable_url": None, + }, + "source": None, + "style_list": ["share", "fallback"], + "title_with_entities": {"text": "Facebook – log in or sign up"}, + "properties": [], + "url": "http://facebook.com/", + "deduplication_key": "deadbeef123", + "action_links": [], + "messaging_attribution": None, + "messenger_call_to_actions": [], + "xma_layout_info": None, + "target": {"__typename": "ExternalUrl"}, + "subattachments": [], + } + assert ShareAttachment( + author=None, + url="http://facebook.com/", + original_url="http://facebook.com/", + title="Facebook – log in or sign up", + description=( + "Create an account or log in to Facebook." + " Connect with friends, family and other people you know." + " Share photos and videos, send messages and get updates." + ), + source=None, + image_url="https://www.facebook.com/rsrc.php/v3/x.png", + original_image_url="https://www.facebook.com/rsrc.php/v3/x.png", + image_width=325, + image_height=325, + attachments=[], + uid="deadbeef123", + ) == ShareAttachment._from_graphql(data) + + +def test_share_from_graphql_video(): + data = { + "description": { + "text": ( + "Rick Astley's official music video for β€œNever Gonna Give You Up”" + " Listen to Rick Astley: https://RickAstley.lnk.to/_listenYD" + " Subscribe to the official Rick As..." + ) + }, + "media": { + "animated_image": None, + "image": { + "uri": ( + "https://external-arn2-1.xx.fbcdn.net/safe_image.php?d=xyz123" + "&w=960&h=540&url=https%3A%2F%2Fi.ytimg.com%2Fvi%2FdQw4w9WgXcQ" + "%2Fmaxresdefault.jpg&sx=0&sy=0&sw=1280&sh=720&_nc_hash=abc123" + ), + "height": 540, + "width": 960, + }, + "playable_duration_in_ms": 0, + "is_playable": True, + "playable_url": "https://www.youtube.com/embed/dQw4w9WgXcQ?autoplay=1", + }, + "source": {"text": "youtube.com"}, + "style_list": ["share", "fallback"], + "title_with_entities": { + "text": "Rick Astley - Never Gonna Give You Up (Video)" + }, + "properties": [ + {"key": "width", "value": {"text": "1280"}}, + {"key": "height", "value": {"text": "720"}}, + ], + "url": "https://l.facebook.com/l.php?u=https%3A%2F%2Fyoutu.be%2FdQw4w9WgXcQ", + "deduplication_key": "ee.mid.$gAAT4Sw1WSGhzQ9uRWVtEpZHZ8ZPV", + "action_links": [{"title": "About this website", "url": None}], + "messaging_attribution": None, + "messenger_call_to_actions": [], + "xma_layout_info": None, + "target": {"__typename": "ExternalUrl"}, + "subattachments": [], + } + assert ShareAttachment( + author=None, + url="https://l.facebook.com/l.php?u=https%3A%2F%2Fyoutu.be%2FdQw4w9WgXcQ", + original_url="https://youtu.be/dQw4w9WgXcQ", + title="Rick Astley - Never Gonna Give You Up (Video)", + description=( + "Rick Astley's official music video for β€œNever Gonna Give You Up”" + " Listen to Rick Astley: https://RickAstley.lnk.to/_listenYD" + " Subscribe to the official Rick As..." + ), + source="youtube.com", + image_url=( + "https://external-arn2-1.xx.fbcdn.net/safe_image.php?d=xyz123" + "&w=960&h=540&url=https%3A%2F%2Fi.ytimg.com%2Fvi%2FdQw4w9WgXcQ" + "%2Fmaxresdefault.jpg&sx=0&sy=0&sw=1280&sh=720&_nc_hash=abc123" + ), + original_image_url="https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg", + image_width=960, + image_height=540, + attachments=[], + uid="ee.mid.$gAAT4Sw1WSGhzQ9uRWVtEpZHZ8ZPV", + ) == ShareAttachment._from_graphql(data) + + +def test_share_with_image_subattachment(): + data = { + "description": {"text": "Abc"}, + "media": { + "animated_image": None, + "image": { + "uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t1.0-9/1.jpg", + "height": 960, + "width": 720, + }, + "playable_duration_in_ms": 0, + "is_playable": False, + "playable_url": None, + }, + "source": {"text": "Def"}, + "style_list": ["attached_story", "fallback"], + "title_with_entities": {"text": ""}, + "properties": [], + "url": "https://www.facebook.com/groups/11223344/permalink/1234/", + "deduplication_key": "deadbeef123", + "action_links": [ + {"title": None, "url": None}, + {"title": None, "url": "https://www.facebook.com/groups/11223344/"}, + { + "title": "Report Post to Admin", + "url": "https://www.facebook.com/groups/11223344/members/", + }, + ], + "messaging_attribution": None, + "messenger_call_to_actions": [], + "xma_layout_info": None, + "target": { + "__typename": "Story", + "title": None, + "description": {"text": "Abc"}, + "actors": [ + { + "__typename": "User", + "name": "Def", + "id": "1111", + "short_name": "Def", + "url": "https://www.facebook.com/some-user", + "profile_picture": { + "uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t1.0-1/c123.123.123.123a/s50x50/img.jpg", + "height": 50, + "width": 50, + }, + } + ], + "to": { + "__typename": "Group", + "name": "Some group", + "url": "https://www.facebook.com/groups/11223344/", + }, + "attachments": [ + { + "url": "https://www.facebook.com/photo.php?fbid=4321&set=gm.1234&type=3", + "media": { + "is_playable": False, + "image": { + "uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t1.0-9/1.jpg", + "height": 960, + "width": 720, + }, + }, + } + ], + "attached_story": None, + }, + "subattachments": [ + { + "description": {"text": "Abc"}, + "media": { + "animated_image": None, + "image": { + "uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t1.0-9/1.jpg", + "height": 960, + "width": 720, + }, + "playable_duration_in_ms": 0, + "is_playable": False, + "playable_url": None, + }, + "source": None, + "style_list": ["photo", "games_app", "fallback"], + "title_with_entities": {"text": ""}, + "properties": [ + {"key": "photoset_reference_token", "value": {"text": "gm.1234"}}, + {"key": "layout_x", "value": {"text": "0"}}, + {"key": "layout_y", "value": {"text": "0"}}, + {"key": "layout_w", "value": {"text": "0"}}, + {"key": "layout_h", "value": {"text": "0"}}, + ], + "url": "https://www.facebook.com/photo.php?fbid=4321&set=gm.1234&type=3", + "deduplication_key": "deadbeef456", + "action_links": [], + "messaging_attribution": None, + "messenger_call_to_actions": [], + "xma_layout_info": None, + "target": {"__typename": "Photo"}, + } + ], + } + assert ShareAttachment( + author="1111", + url="https://www.facebook.com/groups/11223344/permalink/1234/", + original_url="https://www.facebook.com/groups/11223344/permalink/1234/", + title="", + description="Abc", + source="Def", + image_url="https://scontent-arn2-1.xx.fbcdn.net/v/t1.0-9/1.jpg", + original_image_url="https://scontent-arn2-1.xx.fbcdn.net/v/t1.0-9/1.jpg", + image_width=720, + image_height=960, + attachments=[None], + uid="deadbeef123", + ) == ShareAttachment._from_graphql(data) + + +def test_share_with_video_subattachment(): + data = { + "description": {"text": "Abc"}, + "media": { + "animated_image": None, + "image": { + "uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t15.5256-10/p180x540/1.jpg", + "height": 540, + "width": 960, + }, + "playable_duration_in_ms": 24469, + "is_playable": True, + "playable_url": "https://video-arn2-1.xx.fbcdn.net/v/t42.9040-2/vid.mp4", + }, + "source": {"text": "Def"}, + "style_list": ["attached_story", "fallback"], + "title_with_entities": {"text": ""}, + "properties": [], + "url": "https://www.facebook.com/groups/11223344/permalink/1234/", + "deduplication_key": "deadbeef123", + "action_links": [ + {"title": None, "url": None}, + {"title": None, "url": "https://www.facebook.com/groups/11223344/"}, + {"title": None, "url": None}, + {"title": "A watch party is currently playing this video.", "url": None}, + ], + "messaging_attribution": None, + "messenger_call_to_actions": [], + "xma_layout_info": None, + "target": { + "__typename": "Story", + "title": None, + "description": {"text": "Abc"}, + "actors": [ + { + "__typename": "User", + "name": "Def", + "id": "1111", + "short_name": "Def", + "url": "https://www.facebook.com/some-user", + "profile_picture": { + "uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t1.0-1/c1.0.50.50a/p50x50/profile.jpg", + "height": 50, + "width": 50, + }, + } + ], + "to": { + "__typename": "Group", + "name": "Some group", + "url": "https://www.facebook.com/groups/11223344/", + }, + "attachments": [ + { + "url": "https://www.facebook.com/some-user/videos/2222/", + "media": { + "is_playable": True, + "image": { + "uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t15.5256-10/p180x540/1.jpg", + "height": 540, + "width": 960, + }, + }, + } + ], + "attached_story": None, + }, + "subattachments": [ + { + "description": None, + "media": { + "animated_image": None, + "image": { + "uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t15.5256-10/p180x540/1.jpg", + "height": 540, + "width": 960, + }, + "playable_duration_in_ms": 24469, + "is_playable": True, + "playable_url": "https://video-arn2-1.xx.fbcdn.net/v/t42.9040-2/vid.mp4", + }, + "source": None, + "style_list": [ + "video_autoplay", + "video_inline", + "video", + "games_app", + "fallback", + ], + "title_with_entities": {"text": ""}, + "properties": [ + { + "key": "can_autoplay_result", + "value": {"text": "ugc_default_allowed"}, + } + ], + "url": "https://www.facebook.com/some-user/videos/2222/", + "deduplication_key": "deadbeef456", + "action_links": [], + "messaging_attribution": None, + "messenger_call_to_actions": [], + "xma_layout_info": None, + "target": { + "__typename": "Video", + "video_id": "2222", + "video_messenger_cta_payload": None, + }, + } + ], + } + assert ShareAttachment( + author="1111", + url="https://www.facebook.com/groups/11223344/permalink/1234/", + original_url="https://www.facebook.com/groups/11223344/permalink/1234/", + title="", + description="Abc", + source="Def", + image_url="https://scontent-arn2-1.xx.fbcdn.net/v/t15.5256-10/p180x540/1.jpg", + original_image_url="https://scontent-arn2-1.xx.fbcdn.net/v/t15.5256-10/p180x540/1.jpg", + image_width=960, + image_height=540, + attachments=[ + fbchat.VideoAttachment( + uid="2222", + duration=datetime.timedelta(seconds=24, microseconds=469000), + preview_url="https://video-arn2-1.xx.fbcdn.net/v/t42.9040-2/vid.mp4", + medium_image={ + "uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t15.5256-10/p180x540/1.jpg", + "width": 960, + "height": 540, + }, + ) + ], + uid="deadbeef123", + ) == ShareAttachment._from_graphql(data) diff --git a/tests/test_core.py b/tests/test_core.py new file mode 100644 index 0000000..a35404c --- /dev/null +++ b/tests/test_core.py @@ -0,0 +1,14 @@ +import pytest +from fbchat._core import Enum + + +@pytest.mark.filterwarnings("ignore::DeprecationWarning") +def test_enum_extend_if_invalid(): + class TestEnum(Enum): + A = 1 + B = 2 + + assert TestEnum._extend_if_invalid(1) == TestEnum.A + assert TestEnum._extend_if_invalid(3) == TestEnum.UNKNOWN_3 + assert TestEnum._extend_if_invalid(3) == TestEnum.UNKNOWN_3 + assert TestEnum(3) == TestEnum.UNKNOWN_3 diff --git a/tests/test_file.py b/tests/test_file.py new file mode 100644 index 0000000..a403968 --- /dev/null +++ b/tests/test_file.py @@ -0,0 +1,358 @@ +import datetime +import fbchat +from fbchat._file import ( + FileAttachment, + AudioAttachment, + ImageAttachment, + VideoAttachment, + graphql_to_attachment, + graphql_to_subattachment, +) + + +def test_imageattachment_from_list(): + data = { + "__typename": "MessageImage", + "id": "bWVzc2...", + "legacy_attachment_id": "1234", + "image": {"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/s261x260/1.jpg"}, + "image1": { + "height": 463, + "width": 960, + "uri": "https://scontent-arn2-1.xx.fbcdn.net/v/2.jpg", + }, + "image2": { + "height": 988, + "width": 2048, + "uri": "https://scontent-arn2-1.xx.fbcdn.net/v/s2048x2048/3.jpg", + }, + "original_dimensions": {"x": 2833, "y": 1367}, + "photo_encodings": [], + } + assert ImageAttachment( + uid="1234", + width=2833, + height=1367, + thumbnail_url="https://scontent-arn2-1.xx.fbcdn.net/v/s261x260/1.jpg", + preview={ + "uri": "https://scontent-arn2-1.xx.fbcdn.net/v/2.jpg", + "width": 960, + "height": 463, + }, + large_preview={ + "uri": "https://scontent-arn2-1.xx.fbcdn.net/v/s2048x2048/3.jpg", + "width": 2048, + "height": 988, + }, + ) == ImageAttachment._from_list({"node": data}) + + +def test_videoattachment_from_list(): + data = { + "__typename": "MessageVideo", + "id": "bWVzc2...", + "legacy_attachment_id": "1234", + "image": { + "uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t15.3394-10/p261x260/1.jpg" + }, + "image1": { + "height": 368, + "width": 640, + "uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t15.3394-10/2.jpg", + }, + "image2": { + "height": 368, + "width": 640, + "uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t15.3394-10/3.jpg", + }, + "original_dimensions": {"x": 640, "y": 368}, + } + assert VideoAttachment( + uid="1234", + width=640, + height=368, + small_image={ + "uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t15.3394-10/p261x260/1.jpg" + }, + medium_image={ + "uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t15.3394-10/2.jpg", + "width": 640, + "height": 368, + }, + large_image={ + "uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t15.3394-10/3.jpg", + "width": 640, + "height": 368, + }, + ) == VideoAttachment._from_list({"node": data}) + + +def test_graphql_to_attachment_empty(): + assert fbchat.Attachment() == graphql_to_attachment({"__typename": "Unknown"}) + + +def test_graphql_to_attachment_simple(): + data = {"__typename": "Unknown", "legacy_attachment_id": "1234"} + assert fbchat.Attachment(uid="1234") == graphql_to_attachment(data) + + +def test_graphql_to_attachment_file(): + data = { + "__typename": "MessageFile", + "attribution_app": None, + "attribution_metadata": None, + "filename": "file.txt", + "url": "https://l.facebook.com/l.php?u=https%3A%2F%2Fcdn.fbsbx.com%2Fv%2Ffile.txt&h=AT1...&s=1", + "content_type": "attach:text", + "is_malicious": False, + "message_file_fbid": "1234", + "url_shimhash": "AT0...", + "url_skipshim": True, + } + assert FileAttachment( + uid="1234", + url="https://l.facebook.com/l.php?u=https%3A%2F%2Fcdn.fbsbx.com%2Fv%2Ffile.txt&h=AT1...&s=1", + size=None, + name="file.txt", + is_malicious=False, + ) == graphql_to_attachment(data) + + +def test_graphql_to_attachment_audio(): + data = { + "__typename": "MessageAudio", + "attribution_app": None, + "attribution_metadata": None, + "filename": "audio.mp3", + "playable_url": "https://cdn.fbsbx.com/v/audio.mp3?dl=1", + "playable_duration_in_ms": 27745, + "is_voicemail": False, + "audio_type": "FILE_ATTACHMENT", + "url_shimhash": "AT0...", + "url_skipshim": True, + } + assert AudioAttachment( + uid=None, + filename="audio.mp3", + url="https://cdn.fbsbx.com/v/audio.mp3?dl=1", + duration=datetime.timedelta(seconds=27, microseconds=745000), + audio_type="FILE_ATTACHMENT", + ) == graphql_to_attachment(data) + + +def test_graphql_to_attachment_image1(): + data = { + "__typename": "MessageImage", + "attribution_app": None, + "attribution_metadata": None, + "filename": "image-1234", + "preview": { + "uri": "https://scontent-arn2-1.xx.fbcdn.net/v/1.png", + "height": 128, + "width": 128, + }, + "large_preview": { + "uri": "https://scontent-arn2-1.xx.fbcdn.net/v/2.png", + "height": 128, + "width": 128, + }, + "thumbnail": {"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/p50x50/3.png"}, + "photo_encodings": [], + "legacy_attachment_id": "1234", + "original_dimensions": {"x": 128, "y": 128}, + "original_extension": "png", + "render_as_sticker": False, + "blurred_image_uri": None, + } + assert ImageAttachment( + uid="1234", + original_extension="png", + width=None, + height=None, + is_animated=False, + thumbnail_url="https://scontent-arn2-1.xx.fbcdn.net/v/p50x50/3.png", + preview={ + "uri": "https://scontent-arn2-1.xx.fbcdn.net/v/1.png", + "width": 128, + "height": 128, + }, + large_preview={ + "uri": "https://scontent-arn2-1.xx.fbcdn.net/v/2.png", + "width": 128, + "height": 128, + }, + ) == graphql_to_attachment(data) + + +def test_graphql_to_attachment_image2(): + data = { + "__typename": "MessageAnimatedImage", + "attribution_app": None, + "attribution_metadata": None, + "filename": "gif-1234", + "animated_image": { + "uri": "https://cdn.fbsbx.com/v/1.gif", + "height": 128, + "width": 128, + }, + "legacy_attachment_id": "1234", + "preview_image": { + "uri": "https://cdn.fbsbx.com/v/1.gif", + "height": 128, + "width": 128, + }, + "original_dimensions": {"x": 128, "y": 128}, + } + assert ImageAttachment( + uid="1234", + original_extension="gif", + width=None, + height=None, + is_animated=True, + preview={"uri": "https://cdn.fbsbx.com/v/1.gif", "width": 128, "height": 128}, + animated_preview={ + "uri": "https://cdn.fbsbx.com/v/1.gif", + "width": 128, + "height": 128, + }, + ) == graphql_to_attachment(data) + + +def test_graphql_to_attachment_video(): + data = { + "__typename": "MessageVideo", + "attribution_app": None, + "attribution_metadata": None, + "filename": "video-4321.mp4", + "playable_url": "https://video-arn2-1.xx.fbcdn.net/v/video-4321.mp4", + "chat_image": { + "height": 96, + "width": 168, + "uri": "https://scontent-arn2-1.xx.fbcdn.net/v/s168x128/1.jpg", + }, + "legacy_attachment_id": "1234", + "video_type": "FILE_ATTACHMENT", + "original_dimensions": {"x": 640, "y": 368}, + "playable_duration_in_ms": 6000, + "large_image": { + "height": 368, + "width": 640, + "uri": "https://scontent-arn2-1.xx.fbcdn.net/v/2.jpg", + }, + "inbox_image": { + "height": 260, + "width": 452, + "uri": "https://scontent-arn2-1.xx.fbcdn.net/v/p261x260/3.jpg", + }, + } + assert VideoAttachment( + uid="1234", + width=None, + height=None, + duration=datetime.timedelta(seconds=6), + preview_url="https://video-arn2-1.xx.fbcdn.net/v/video-4321.mp4", + small_image={ + "uri": "https://scontent-arn2-1.xx.fbcdn.net/v/s168x128/1.jpg", + "width": 168, + "height": 96, + }, + medium_image={ + "uri": "https://scontent-arn2-1.xx.fbcdn.net/v/p261x260/3.jpg", + "width": 452, + "height": 260, + }, + large_image={ + "uri": "https://scontent-arn2-1.xx.fbcdn.net/v/2.jpg", + "width": 640, + "height": 368, + }, + ) == graphql_to_attachment(data) + + +def test_graphql_to_subattachment_empty(): + assert None is graphql_to_subattachment({}) + + +def test_graphql_to_subattachment_image(): + data = { + "description": {"text": "Abc"}, + "media": { + "animated_image": None, + "image": { + "uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t1.0-9/1.jpg", + "height": 960, + "width": 720, + }, + "playable_duration_in_ms": 0, + "is_playable": False, + "playable_url": None, + }, + "source": None, + "style_list": ["photo", "games_app", "fallback"], + "title_with_entities": {"text": ""}, + "properties": [ + {"key": "photoset_reference_token", "value": {"text": "gm.4321"}}, + {"key": "layout_x", "value": {"text": "0"}}, + {"key": "layout_y", "value": {"text": "0"}}, + {"key": "layout_w", "value": {"text": "0"}}, + {"key": "layout_h", "value": {"text": "0"}}, + ], + "url": "https://www.facebook.com/photo.php?fbid=1234&set=gm.4321&type=3", + "deduplication_key": "8334...", + "action_links": [], + "messaging_attribution": None, + "messenger_call_to_actions": [], + "xma_layout_info": None, + "target": {"__typename": "Photo"}, + } + assert None is graphql_to_subattachment(data) + + +def test_graphql_to_subattachment_video(): + data = { + "description": None, + "media": { + "animated_image": None, + "image": { + "uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t15.5256-10/p180x540/1.jpg", + "height": 540, + "width": 960, + }, + "playable_duration_in_ms": 24469, + "is_playable": True, + "playable_url": "https://video-arn2-1.xx.fbcdn.net/v/t42.9040-2/vid.mp4", + }, + "source": None, + "style_list": [ + "video_autoplay", + "video_inline", + "video", + "games_app", + "fallback", + ], + "title_with_entities": {"text": ""}, + "properties": [ + {"key": "can_autoplay_result", "value": {"text": "ugc_default_allowed"}} + ], + "url": "https://www.facebook.com/some-username/videos/1234/", + "deduplication_key": "ddb7...", + "action_links": [], + "messaging_attribution": None, + "messenger_call_to_actions": [], + "xma_layout_info": None, + "target": { + "__typename": "Video", + "video_id": "1234", + "video_messenger_cta_payload": None, + }, + } + assert VideoAttachment( + uid="1234", + duration=datetime.timedelta(seconds=24, microseconds=469000), + preview_url="https://video-arn2-1.xx.fbcdn.net/v/t42.9040-2/vid.mp4", + medium_image={ + "uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t15.5256-10/p180x540/1.jpg", + "width": 960, + "height": 540, + }, + ) == graphql_to_subattachment(data) diff --git a/tests/test_graphql.py b/tests/test_graphql.py new file mode 100644 index 0000000..7f48101 --- /dev/null +++ b/tests/test_graphql.py @@ -0,0 +1,35 @@ +import pytest +import json +from fbchat._graphql import ConcatJSONDecoder, queries_to_json, response_to_json + + +@pytest.mark.parametrize( + "content,result", + [ + ("", []), + ('{"a":"b"}', [{"a": "b"}]), + ('{"a":"b"}{"b":"c"}', [{"a": "b"}, {"b": "c"}]), + (' \n{"a": "b" } \n { "b" \n\n : "c" }', [{"a": "b"}, {"b": "c"}]), + ], +) +def test_concat_json_decoder(content, result): + assert result == json.loads(content, cls=ConcatJSONDecoder) + + +def test_queries_to_json(): + assert {"q0": "A", "q1": "B", "q2": "C"} == json.loads( + queries_to_json("A", "B", "C") + ) + + +def test_response_to_json(): + data = ( + '{"q1":{"data":{"b":"c"}}}\r\n' + '{"q0":{"response":[1,2]}}\r\n' + "{\n" + ' "successful_results": 2,\n' + ' "error_results": 0,\n' + ' "skipped_results": 0\n' + "}" + ) + assert [[1, 2], {"b": "c"}] == response_to_json(data) diff --git a/tests/test_group.py b/tests/test_group.py new file mode 100644 index 0000000..cb96c98 --- /dev/null +++ b/tests/test_group.py @@ -0,0 +1,43 @@ +from fbchat._group import Group + + +def test_group_from_graphql(): + data = { + "name": "Group ABC", + "thread_key": {"thread_fbid": "11223344"}, + "image": None, + "is_group_thread": True, + "all_participants": { + "nodes": [ + {"messaging_actor": {"id": "1234"}}, + {"messaging_actor": {"id": "2345"}}, + {"messaging_actor": {"id": "3456"}}, + ] + }, + "customization_info": { + "participant_customizations": [], + "outgoing_bubble_color": None, + "emoji": "πŸ˜€", + }, + "thread_admins": [{"id": "1234"}], + "group_approval_queue": {"nodes": []}, + "approval_mode": 0, + "joinable_mode": {"mode": "0", "link": ""}, + "event_reminders": {"nodes": []}, + } + assert Group( + uid="11223344", + photo=None, + name="Group ABC", + last_active=None, + message_count=None, + plan=None, + participants={"1234", "2345", "3456"}, + nicknames={}, + color=None, + emoji="πŸ˜€", + admins={"1234"}, + approval_mode=False, + approval_requests=set(), + join_link="", + ) == Group._from_graphql(data) diff --git a/tests/test_location.py b/tests/test_location.py new file mode 100644 index 0000000..3c5215d --- /dev/null +++ b/tests/test_location.py @@ -0,0 +1,91 @@ +import pytest +import datetime +from fbchat._location import LocationAttachment, LiveLocationAttachment + + +def test_location_attachment_from_graphql(): + data = { + "description": {"text": ""}, + "media": { + "animated_image": None, + "image": { + "uri": "https://external-arn2-1.xx.fbcdn.net/static_map.php?v=1020&osm_provider=2&size=545x280&zoom=15&markers=55.40000000%2C12.43220000&language=en", + "height": 280, + "width": 545, + }, + "playable_duration_in_ms": 0, + "is_playable": False, + "playable_url": None, + }, + "source": None, + "style_list": ["message_location", "fallback"], + "title_with_entities": {"text": "Your location"}, + "properties": [ + {"key": "width", "value": {"text": "545"}}, + {"key": "height", "value": {"text": "280"}}, + ], + "url": "https://l.facebook.com/l.php?u=https%3A%2F%2Fwww.bing.com%2Fmaps%2Fdefault.aspx%3Fv%3D2%26pc%3DFACEBK%26mid%3D8100%26where1%3D55.4%252C%2B12.4322%26FORM%3DFBKPL1%26mkt%3Den-GB&h=a&s=1", + "deduplication_key": "400828513928715", + "action_links": [], + "messaging_attribution": None, + "messenger_call_to_actions": [], + "xma_layout_info": None, + "target": {"__typename": "MessageLocation"}, + "subattachments": [], + } + expected = LocationAttachment(latitude=55.4, longitude=12.4322, uid=400828513928715) + expected.image_url = "https://external-arn2-1.xx.fbcdn.net/static_map.php?v=1020&osm_provider=2&size=545x280&zoom=15&markers=55.40000000%2C12.43220000&language=en" + expected.image_width = 545 + expected.image_height = 280 + expected.url = "https://l.facebook.com/l.php?u=https%3A%2F%2Fwww.bing.com%2Fmaps%2Fdefault.aspx%3Fv%3D2%26pc%3DFACEBK%26mid%3D8100%26where1%3D55.4%252C%2B12.4322%26FORM%3DFBKPL1%26mkt%3Den-GB&h=a&s=1" + assert expected == LocationAttachment._from_graphql(data) + + +@pytest.mark.skip(reason="need to gather test data") +def test_live_location_from_pull(): + data = ... + assert LiveLocationAttachment(...) == LiveLocationAttachment._from_pull(data) + + +def test_live_location_from_graphql_expired(): + data = { + "description": {"text": "Last update 4 Jan"}, + "media": None, + "source": None, + "style_list": ["message_live_location", "fallback"], + "title_with_entities": {"text": "Location-sharing ended"}, + "properties": [], + "url": "https://www.facebook.com/", + "deduplication_key": "2254535444791641", + "action_links": [], + "messaging_attribution": None, + "messenger_call_to_actions": [], + "target": { + "__typename": "MessageLiveLocation", + "live_location_id": "2254535444791641", + "is_expired": True, + "expiration_time": 1546626345, + "sender": {"id": "100007056224713"}, + "coordinate": None, + "location_title": None, + "sender_destination": None, + "stop_reason": "CANCELED", + }, + "subattachments": [], + } + expected = LiveLocationAttachment( + uid=2254535444791641, + name="Location-sharing ended", + expires_at=datetime.datetime( + 2019, 1, 4, 18, 25, 45, tzinfo=datetime.timezone.utc + ), + is_expired=True, + ) + expected.url = "https://www.facebook.com/" + assert expected == LiveLocationAttachment._from_graphql(data) + + +@pytest.mark.skip(reason="need to gather test data") +def test_live_location_from_graphql(): + data = ... + assert LiveLocationAttachment(...) == LiveLocationAttachment._from_graphql(data) diff --git a/tests/test_message.py b/tests/test_message.py new file mode 100644 index 0000000..e1c31ce --- /dev/null +++ b/tests/test_message.py @@ -0,0 +1,140 @@ +import pytest +import fbchat +from fbchat._message import ( + EmojiSize, + Mention, + Message, + graphql_to_extensible_attachment, +) + + +@pytest.mark.parametrize( + "tags,size", + [ + (None, None), + (["hot_emoji_size:unknown"], None), + (["bunch", "of:different", "tags:large", "hot_emoji_size:s"], EmojiSize.SMALL), + (["hot_emoji_size:s"], EmojiSize.SMALL), + (["hot_emoji_size:m"], EmojiSize.MEDIUM), + (["hot_emoji_size:l"], EmojiSize.LARGE), + (["hot_emoji_size:small"], EmojiSize.SMALL), + (["hot_emoji_size:medium"], EmojiSize.MEDIUM), + (["hot_emoji_size:large"], EmojiSize.LARGE), + ], +) +def test_emojisize_from_tags(tags, size): + assert size is EmojiSize._from_tags(tags) + + +def test_graphql_to_extensible_attachment_empty(): + assert None is graphql_to_extensible_attachment({}) + + +@pytest.mark.parametrize( + "obj,type_", + [ + # UnsentMessage testing is done in test_attachment.py + (fbchat.LocationAttachment, "MessageLocation"), + (fbchat.LiveLocationAttachment, "MessageLiveLocation"), + (fbchat.ShareAttachment, "ExternalUrl"), + (fbchat.ShareAttachment, "Story"), + ], +) +def test_graphql_to_extensible_attachment_dispatch(monkeypatch, obj, type_): + monkeypatch.setattr(obj, "_from_graphql", lambda data: True) + data = {"story_attachment": {"target": {"__typename": type_}}} + assert graphql_to_extensible_attachment(data) + + +def test_message_format_mentions(): + expected = Message( + text="Hey 'Peter'! My name is Michael", + mentions=[ + Mention(thread_id="1234", offset=4, length=7), + Mention(thread_id="4321", offset=24, length=7), + ], + ) + assert expected == Message.format_mentions( + "Hey {!r}! My name is {}", ("1234", "Peter"), ("4321", "Michael") + ) + assert expected == Message.format_mentions( + "Hey {p!r}! My name is {}", ("4321", "Michael"), p=("1234", "Peter") + ) + + +def test_message_get_forwarded_from_tags(): + assert not Message._get_forwarded_from_tags(None) + assert not Message._get_forwarded_from_tags(["hot_emoji_size:unknown"]) + assert Message._get_forwarded_from_tags( + ["attachment:photo", "inbox", "sent", "source:chat:forward", "tq"] + ) + + +def test_message_to_send_data_minimal(): + assert {"action_type": "ma-type:user-generated-message", "body": "Hey"} == Message( + text="Hey" + )._to_send_data() + + +def test_message_to_send_data_mentions(): + msg = Message( + text="Hey 'Peter'! My name is Michael", + mentions=[ + Mention(thread_id="1234", offset=4, length=7), + Mention(thread_id="4321", offset=24, length=7), + ], + ) + assert { + "action_type": "ma-type:user-generated-message", + "body": "Hey 'Peter'! My name is Michael", + "profile_xmd[0][id]": "1234", + "profile_xmd[0][length]": 7, + "profile_xmd[0][offset]": 4, + "profile_xmd[0][type]": "p", + "profile_xmd[1][id]": "4321", + "profile_xmd[1][length]": 7, + "profile_xmd[1][offset]": 24, + "profile_xmd[1][type]": "p", + } == msg._to_send_data() + + +def test_message_to_send_data_sticker(): + msg = Message(sticker=fbchat.Sticker(uid="123")) + assert { + "action_type": "ma-type:user-generated-message", + "sticker_id": "123", + } == msg._to_send_data() + + +def test_message_to_send_data_emoji(): + msg = Message(text="πŸ˜€", emoji_size=EmojiSize.LARGE) + assert { + "action_type": "ma-type:user-generated-message", + "body": "πŸ˜€", + "tags[0]": "hot_emoji_size:large", + } == msg._to_send_data() + msg = Message(emoji_size=EmojiSize.LARGE) + assert { + "action_type": "ma-type:user-generated-message", + "sticker_id": "369239383222810", + } == msg._to_send_data() + + +@pytest.mark.skip(reason="need to be added") +def test_message_to_send_data_quick_replies(): + raise NotImplementedError + + +@pytest.mark.skip(reason="need to gather test data") +def test_message_from_graphql(): + pass + + +@pytest.mark.skip(reason="need to gather test data") +def test_message_from_reply(): + pass + + +@pytest.mark.skip(reason="need to gather test data") +def test_message_from_pull(): + pass diff --git a/tests/test_page.py b/tests/test_page.py new file mode 100644 index 0000000..e5ba22e --- /dev/null +++ b/tests/test_page.py @@ -0,0 +1,20 @@ +from fbchat._page import Page + + +def test_page_from_graphql(): + data = { + "id": "123456", + "name": "Some school", + "profile_picture": {"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/..."}, + "url": "https://www.facebook.com/some-school/", + "category_type": "SCHOOL", + "city": None, + } + assert Page( + uid="123456", + photo="https://scontent-arn2-1.xx.fbcdn.net/v/...", + name="Some school", + url="https://www.facebook.com/some-school/", + city=None, + category="SCHOOL", + ) == Page._from_graphql(data) diff --git a/tests/test_plan.py b/tests/test_plan.py new file mode 100644 index 0000000..f483744 --- /dev/null +++ b/tests/test_plan.py @@ -0,0 +1,150 @@ +import datetime +from fbchat._plan import GuestStatus, Plan + + +def test_plan_properties(): + plan = Plan(time=..., title=...) + plan.guests = { + "1234": GuestStatus.INVITED, + "2345": GuestStatus.INVITED, + "3456": GuestStatus.GOING, + "4567": GuestStatus.DECLINED, + } + assert set(plan.invited) == {"1234", "2345"} + assert plan.going == ["3456"] + assert plan.declined == ["4567"] + + +def test_plan_from_pull(): + data = { + "event_timezone": "", + "event_creator_id": "1234", + "event_id": "1111", + "event_type": "EVENT", + "event_track_rsvp": "1", + "event_title": "abc", + "event_time": "1500000000", + "event_seconds_to_notify_before": "3600", + "guest_state_list": ( + '[{"guest_list_state":"INVITED","node":{"id":"1234"}},' + '{"guest_list_state":"INVITED","node":{"id":"2356"}},' + '{"guest_list_state":"DECLINED","node":{"id":"3456"}},' + '{"guest_list_state":"GOING","node":{"id":"4567"}}]' + ), + } + plan = Plan( + time=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), + title="abc", + ) + plan.uid = "1111" + plan.author_id = "1234" + plan.guests = { + "1234": GuestStatus.INVITED, + "2356": GuestStatus.INVITED, + "3456": GuestStatus.DECLINED, + "4567": GuestStatus.GOING, + } + assert plan == Plan._from_pull(data) + + +def test_plan_from_fetch(): + data = { + "message_thread_id": 123456789, + "event_time": 1500000000, + "creator_id": 1234, + "event_time_updated_time": 1450000000, + "title": "abc", + "track_rsvp": 1, + "event_type": "EVENT", + "status": "created", + "message_id": "mid.xyz", + "seconds_to_notify_before": 3600, + "event_time_source": "user", + "repeat_mode": "once", + "creation_time": 1400000000, + "location_id": 0, + "location_name": None, + "latitude": "", + "longitude": "", + "event_id": 0, + "trigger_message_id": "", + "note": "", + "timezone_id": 0, + "end_time": 0, + "list_id": 0, + "payload_id": 0, + "cu_app": "", + "location_sharing_subtype": "", + "reminder_notif_param": [], + "workplace_meeting_id": "", + "genie_fbid": 0, + "galaxy": "", + "oid": 1111, + "type": 8128, + "is_active": True, + "location_address": None, + "event_members": { + "1234": "INVITED", + "2356": "INVITED", + "3456": "DECLINED", + "4567": "GOING", + }, + } + plan = Plan( + time=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), + title="abc", + location="", + location_id="", + ) + plan.uid = 1111 + plan.author_id = 1234 + plan.guests = { + "1234": GuestStatus.INVITED, + "2356": GuestStatus.INVITED, + "3456": GuestStatus.DECLINED, + "4567": GuestStatus.GOING, + } + assert plan == Plan._from_fetch(data) + + +def test_plan_from_graphql(): + data = { + "id": "1111", + "lightweight_event_creator": {"id": "1234"}, + "time": 1500000000, + "lightweight_event_type": "EVENT", + "location_name": None, + "location_coordinates": None, + "location_page": None, + "lightweight_event_status": "CREATED", + "note": "", + "repeat_mode": "ONCE", + "event_title": "abc", + "trigger_message": None, + "seconds_to_notify_before": 3600, + "allows_rsvp": True, + "related_event": None, + "event_reminder_members": { + "edges": [ + {"node": {"id": "1234"}, "guest_list_state": "INVITED"}, + {"node": {"id": "2356"}, "guest_list_state": "INVITED"}, + {"node": {"id": "3456"}, "guest_list_state": "DECLINED"}, + {"node": {"id": "4567"}, "guest_list_state": "GOING"}, + ] + }, + } + plan = Plan( + time=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), + title="abc", + location="", + location_id="", + ) + plan.uid = "1111" + plan.author_id = "1234" + plan.guests = { + "1234": GuestStatus.INVITED, + "2356": GuestStatus.INVITED, + "3456": GuestStatus.DECLINED, + "4567": GuestStatus.GOING, + } + assert plan == Plan._from_graphql(data) diff --git a/tests/test_poll.py b/tests/test_poll.py new file mode 100644 index 0000000..8cca471 --- /dev/null +++ b/tests/test_poll.py @@ -0,0 +1,87 @@ +from fbchat._poll import Poll, PollOption + + +def test_poll_option_from_graphql_unvoted(): + data = { + "id": "123456789", + "text": "abc", + "total_count": 0, + "viewer_has_voted": "false", + "voters": [], + } + assert PollOption( + text="abc", vote=False, voters=[], votes_count=0, uid=123456789 + ) == PollOption._from_graphql(data) + + +def test_poll_option_from_graphql_voted(): + data = { + "id": "123456789", + "text": "abc", + "total_count": 2, + "viewer_has_voted": "true", + "voters": ["1234", "2345"], + } + assert PollOption( + text="abc", vote=True, voters=["1234", "2345"], votes_count=2, uid=123456789 + ) == PollOption._from_graphql(data) + + +def test_poll_option_from_graphql_alternate_format(): + # Format received when fetching poll options + data = { + "id": "123456789", + "text": "abc", + "viewer_has_voted": True, + "voters": { + "count": 2, + "edges": [{"node": {"id": "1234"}}, {"node": {"id": "2345"}}], + }, + } + assert PollOption( + text="abc", vote=True, voters=["1234", "2345"], votes_count=2, uid=123456789 + ) == PollOption._from_graphql(data) + + +def test_poll_from_graphql(): + data = { + "id": "123456789", + "text": "Some poll", + "total_count": 5, + "viewer_has_voted": "true", + "options": [ + { + "id": "1111", + "text": "Abc", + "total_count": 1, + "viewer_has_voted": "true", + "voters": ["1234"], + }, + { + "id": "2222", + "text": "Def", + "total_count": 2, + "viewer_has_voted": "false", + "voters": ["2345", "3456"], + }, + { + "id": "3333", + "text": "Ghi", + "total_count": 0, + "viewer_has_voted": "false", + "voters": [], + }, + ], + } + assert Poll( + title="Some poll", + options=[ + PollOption(text="Abc", vote=True, voters=["1234"], votes_count=1, uid=1111), + PollOption( + text="Def", vote=False, voters=["2345", "3456"], votes_count=2, uid=2222 + ), + PollOption(text="Ghi", vote=False, voters=[], votes_count=0, uid=3333), + ], + options_count=5, + uid=123456789, + ) == Poll._from_graphql(data) diff --git a/tests/test_quick_reply.py b/tests/test_quick_reply.py new file mode 100644 index 0000000..ff2a19e --- /dev/null +++ b/tests/test_quick_reply.py @@ -0,0 +1,49 @@ +from fbchat._quick_reply import ( + QuickReplyText, + QuickReplyLocation, + QuickReplyPhoneNumber, + QuickReplyEmail, + graphql_to_quick_reply, +) + + +def test_parse_minimal(): + data = { + "content_type": "text", + "payload": None, + "external_payload": None, + "data": None, + "title": "A", + "image_url": None, + } + assert QuickReplyText(title="A") == graphql_to_quick_reply(data) + data = {"content_type": "location"} + assert QuickReplyLocation() == graphql_to_quick_reply(data) + data = {"content_type": "user_phone_number"} + assert QuickReplyPhoneNumber() == graphql_to_quick_reply(data) + data = {"content_type": "user_email"} + assert QuickReplyEmail() == graphql_to_quick_reply(data) + + +def test_parse_text_full(): + data = { + "content_type": "text", + "title": "A", + "payload": "Some payload", + "image_url": "https://example.com/image.jpg", + "data": None, + } + assert QuickReplyText( + payload="Some payload", + data=None, + is_response=False, + title="A", + image_url="https://example.com/image.jpg", + ) == graphql_to_quick_reply(data) + + +def test_parse_with_is_response(): + data = {"content_type": "text"} + assert QuickReplyText(is_response=True) == graphql_to_quick_reply( + data, is_response=True + ) diff --git a/tests/test_sticker.py b/tests/test_sticker.py new file mode 100644 index 0000000..d56af9f --- /dev/null +++ b/tests/test_sticker.py @@ -0,0 +1,86 @@ +import pytest +from fbchat._sticker import Sticker + + +def test_from_graphql_none(): + assert None == Sticker._from_graphql(None) + + +def test_from_graphql_minimal(): + assert Sticker(uid=1) == Sticker._from_graphql({"id": 1}) + + +def test_from_graphql_normal(): + assert Sticker( + uid="369239383222810", + pack="227877430692340", + is_animated=False, + medium_sprite_image=None, + large_sprite_image=None, + frames_per_row=None, + frames_per_col=None, + frame_rate=None, + url="https://scontent-arn2-1.xx.fbcdn.net/v/redacted.png", + width=274, + height=274, + label="Like, thumbs up", + ) == Sticker._from_graphql( + { + "id": "369239383222810", + "pack": {"id": "227877430692340"}, + "label": "Like, thumbs up", + "frame_count": 1, + "frame_rate": 83, + "frames_per_row": 1, + "frames_per_column": 1, + "sprite_image_2x": None, + "sprite_image": None, + "padded_sprite_image": None, + "padded_sprite_image_2x": None, + "url": "https://scontent-arn2-1.xx.fbcdn.net/v/redacted.png", + "height": 274, + "width": 274, + } + ) + + +def test_from_graphql_animated(): + assert Sticker( + uid="144885035685763", + pack="350357561732812", + is_animated=True, + medium_sprite_image="https://scontent-arn2-1.xx.fbcdn.net/v/redacted2.png", + large_sprite_image="https://scontent-arn2-1.fbcdn.net/v/redacted3.png", + frames_per_row=2, + frames_per_col=2, + frame_rate=142, + url="https://scontent-arn2-1.fbcdn.net/v/redacted1.png", + width=240, + height=293, + label="Love, cat with heart", + ) == Sticker._from_graphql( + { + "id": "144885035685763", + "pack": {"id": "350357561732812"}, + "label": "Love, cat with heart", + "frame_count": 4, + "frame_rate": 142, + "frames_per_row": 2, + "frames_per_column": 2, + "sprite_image_2x": { + "uri": "https://scontent-arn2-1.fbcdn.net/v/redacted3.png" + }, + "sprite_image": { + "uri": "https://scontent-arn2-1.xx.fbcdn.net/v/redacted2.png" + }, + "padded_sprite_image": { + "uri": "https://scontent-arn2-1.xx.fbcdn.net/v/unused1.png" + }, + "padded_sprite_image_2x": { + "uri": "https://scontent-arn2-1.xx.fbcdn.net/v/unused2.png" + }, + "url": "https://scontent-arn2-1.fbcdn.net/v/redacted1.png", + "height": 293, + "width": 240, + } + ) diff --git a/tests/test_thread.py b/tests/test_thread.py new file mode 100644 index 0000000..baea7ad --- /dev/null +++ b/tests/test_thread.py @@ -0,0 +1,65 @@ +import pytest +import fbchat +from fbchat._thread import ThreadType, ThreadColor, Thread + + +def test_thread_type_to_class(): + assert fbchat.User == ThreadType.USER._to_class() + assert fbchat.Group == ThreadType.GROUP._to_class() + assert fbchat.Page == ThreadType.PAGE._to_class() + + +def test_thread_color_from_graphql(): + assert None is ThreadColor._from_graphql(None) + assert ThreadColor.MESSENGER_BLUE is ThreadColor._from_graphql("") + assert ThreadColor.VIKING is ThreadColor._from_graphql("FF44BEC7") + assert ThreadColor._from_graphql("DEADBEEF") is getattr( + ThreadColor, "UNKNOWN_#ADBEEF" + ) + + +def test_thread_parse_customization_info_empty(): + assert {} == Thread._parse_customization_info(None) + assert {} == Thread._parse_customization_info({"customization_info": None}) + + +def test_thread_parse_customization_info_group(): + data = { + "thread_key": {"thread_fbid": "11111", "other_user_id": None}, + "customization_info": { + "emoji": "πŸŽ‰", + "participant_customizations": [ + {"participant_id": "123456789", "nickname": "A"}, + {"participant_id": "987654321", "nickname": "B"}, + ], + "outgoing_bubble_color": "FFFF5CA1", + }, + "customization_enabled": True, + "thread_type": "GROUP", + # ... Other irrelevant fields + } + expected = { + "emoji": "πŸŽ‰", + "color": ThreadColor.BRILLIANT_ROSE, + "nicknames": {"123456789": "A", "987654321": "B"}, + } + assert expected == Thread._parse_customization_info(data) + + +def test_thread_parse_customization_info_user(): + data = { + "thread_key": {"thread_fbid": None, "other_user_id": "987654321"}, + "customization_info": { + "emoji": None, + "participant_customizations": [ + {"participant_id": "123456789", "nickname": "A"}, + {"participant_id": "987654321", "nickname": "B"}, + ], + "outgoing_bubble_color": None, + }, + "customization_enabled": True, + "thread_type": "ONE_TO_ONE", + # ... Other irrelevant fields + } + expected = {"emoji": None, "color": None, "own_nickname": "A", "nickname": "B"} + assert expected == Thread._parse_customization_info(data) diff --git a/tests/test_user.py b/tests/test_user.py new file mode 100644 index 0000000..b8ed5ea --- /dev/null +++ b/tests/test_user.py @@ -0,0 +1,194 @@ +import pytest +import datetime +from fbchat._user import User, ActiveStatus + + +def test_user_from_graphql(): + data = { + "id": "1234", + "name": "Abc Def Ghi", + "first_name": "Abc", + "last_name": "Ghi", + "profile_picture": {"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/..."}, + "is_viewer_friend": True, + "url": "https://www.facebook.com/profile.php?id=1234", + "gender": "FEMALE", + "viewer_affinity": 0.4560002, + } + assert User( + uid="1234", + photo="https://scontent-arn2-1.xx.fbcdn.net/v/...", + name="Abc Def Ghi", + url="https://www.facebook.com/profile.php?id=1234", + first_name="Abc", + last_name="Ghi", + is_friend=True, + gender="female_singular", + ) == User._from_graphql(data) + + +def test_user_from_thread_fetch(): + data = { + "thread_key": {"thread_fbid": None, "other_user_id": "1234"}, + "name": None, + "last_message": { + "nodes": [ + { + "snippet": "aaa", + "message_sender": {"messaging_actor": {"id": "1234"}}, + "timestamp_precise": "1500000000000", + "commerce_message_type": None, + "extensible_attachment": None, + "sticker": None, + "blob_attachments": [], + } + ] + }, + "unread_count": 0, + "messages_count": 1111, + "image": None, + "updated_time_precise": "1500000000000", + "mute_until": None, + "is_pin_protected": False, + "is_viewer_subscribed": True, + "thread_queue_enabled": False, + "folder": "INBOX", + "has_viewer_archived": False, + "is_page_follow_up": False, + "cannot_reply_reason": None, + "ephemeral_ttl_mode": 0, + "customization_info": { + "emoji": None, + "participant_customizations": [ + {"participant_id": "4321", "nickname": "B"}, + {"participant_id": "1234", "nickname": "A"}, + ], + "outgoing_bubble_color": None, + }, + "thread_admins": [], + "approval_mode": None, + "joinable_mode": {"mode": "0", "link": ""}, + "thread_queue_metadata": None, + "event_reminders": {"nodes": []}, + "montage_thread": None, + "last_read_receipt": {"nodes": [{"timestamp_precise": "1500000050000"}]}, + "related_page_thread": None, + "rtc_call_data": { + "call_state": "NO_ONGOING_CALL", + "server_info_data": "", + "initiator": None, + }, + "associated_object": None, + "privacy_mode": 1, + "reactions_mute_mode": "REACTIONS_NOT_MUTED", + "mentions_mute_mode": "MENTIONS_NOT_MUTED", + "customization_enabled": True, + "thread_type": "ONE_TO_ONE", + "participant_add_mode_as_string": None, + "is_canonical_neo_user": False, + "participants_event_status": [], + "page_comm_item": None, + "all_participants": { + "nodes": [ + { + "messaging_actor": { + "id": "1234", + "__typename": "User", + "name": "Abc Def Ghi", + "gender": "FEMALE", + "url": "https://www.facebook.com/profile.php?id=1234", + "big_image_src": { + "uri": "https://scontent-arn2-1.xx.fbcdn.net/v/..." + }, + "short_name": "Abc", + "username": "", + "is_viewer_friend": True, + "is_messenger_user": True, + "is_verified": False, + "is_message_blocked_by_viewer": False, + "is_viewer_coworker": False, + "is_employee": None, + } + }, + { + "messaging_actor": { + "id": "4321", + "__typename": "User", + "name": "Aaa Bbb Ccc", + "gender": "NEUTER", + "url": "https://www.facebook.com/aaabbbccc", + "big_image_src": { + "uri": "https://scontent-arn2-1.xx.fbcdn.net/v/..." + }, + "short_name": "Aaa", + "username": "aaabbbccc", + "is_viewer_friend": False, + "is_messenger_user": True, + "is_verified": False, + "is_message_blocked_by_viewer": False, + "is_viewer_coworker": False, + "is_employee": None, + } + }, + ] + }, + "read_receipts": ..., + "delivery_receipts": ..., + } + assert User( + uid="1234", + photo="https://scontent-arn2-1.xx.fbcdn.net/v/...", + name="Abc Def Ghi", + last_active=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), + message_count=1111, + url="https://www.facebook.com/profile.php?id=1234", + first_name="Abc", + last_name="Def Ghi", + is_friend=True, + gender="female_singular", + nickname="A", + own_nickname="B", + color=None, + emoji=None, + ) == User._from_thread_fetch(data) + + +def test_user_from_all_fetch(): + data = { + "id": "1234", + "name": "Abc Def Ghi", + "firstName": "Abc", + "vanity": "", + "thumbSrc": "https://scontent-arn2-1.xx.fbcdn.net/v/...", + "uri": "https://www.facebook.com/profile.php?id=1234", + "gender": 1, + "i18nGender": 2, + "type": "friend", + "is_friend": True, + "mThumbSrcSmall": None, + "mThumbSrcLarge": None, + "dir": None, + "searchTokens": ["Abc", "Ghi"], + "alternateName": "", + "is_nonfriend_messenger_contact": False, + "is_blocked": False, + } + assert User( + uid="1234", + photo="https://scontent-arn2-1.xx.fbcdn.net/v/...", + name="Abc Def Ghi", + url="https://www.facebook.com/profile.php?id=1234", + first_name="Abc", + is_friend=True, + gender="female_singular", + ) == User._from_all_fetch(data) + + +@pytest.mark.skip(reason="can't gather test data, the pulling is broken") +def test_active_status_from_chatproxy_presence(): + assert ActiveStatus() == ActiveStatus._from_chatproxy_presence(data) + + +@pytest.mark.skip(reason="can't gather test data, the pulling is broken") +def test_active_status_from_buddylist_overlay(): + assert ActiveStatus() == ActiveStatus._from_buddylist_overlay(data) diff --git a/tests/test_util.py b/tests/test_util.py new file mode 100644 index 0000000..8268c4a --- /dev/null +++ b/tests/test_util.py @@ -0,0 +1,309 @@ +import pytest +import fbchat +import datetime +from fbchat._util import ( + strip_json_cruft, + parse_json, + str_base, + generate_message_id, + get_signature_id, + handle_payload_error, + handle_graphql_errors, + check_http_code, + get_jsmods_require, + require_list, + mimetype_to_key, + get_url_parameter, + prefix_url, + seconds_to_datetime, + millis_to_datetime, + datetime_to_seconds, + datetime_to_millis, + seconds_to_timedelta, + millis_to_timedelta, + timedelta_to_seconds, +) + + +def test_strip_json_cruft(): + assert strip_json_cruft('for(;;);{"abc": "def"}') == '{"abc": "def"}' + assert strip_json_cruft('{"abc": "def"}') == '{"abc": "def"}' + + +def test_strip_json_cruft_invalid(): + with pytest.raises(AttributeError): + strip_json_cruft(None) + with pytest.raises(fbchat.FBchatException, match="No JSON object found"): + strip_json_cruft("No JSON object here!") + + +def test_parse_json(): + assert parse_json('{"a":"b"}') == {"a": "b"} + + +def test_parse_json_invalid(): + with pytest.raises(fbchat.FBchatFacebookError, match="Error while parsing JSON"): + parse_json("No JSON object here!") + + +@pytest.mark.parametrize( + "number,base,expected", + [ + (123, 10, "123"), + (1, 36, "1"), + (10, 36, "a"), + (123, 36, "3f"), + (1000, 36, "rs"), + (123456789, 36, "21i3v9"), + ], +) +def test_str_base(number, base, expected): + assert str_base(number, base) == expected + + +def test_generate_message_id(): + # Returns random output, so hard to test more thoroughly + generate_message_id("abc") + + +def test_get_signature_id(): + # Returns random output, so hard to test more thoroughly + get_signature_id() + + +ERROR_DATA = [ + ( + fbchat._exception.FBchatNotLoggedIn, + 1357001, + "Not logged in", + "Please log in to continue.", + ), + ( + fbchat._exception.FBchatPleaseRefresh, + 1357004, + "Sorry, something went wrong", + "Please try closing and re-opening your browser window.", + ), + ( + fbchat._exception.FBchatInvalidParameters, + 1357031, + "This content is no longer available", + ( + "The content you requested cannot be displayed at the moment. It may be" + " temporarily unavailable, the link you clicked on may have expired or you" + " may not have permission to view this page." + ), + ), + ( + fbchat._exception.FBchatInvalidParameters, + 1545010, + "Messages Unavailable", + ( + "Sorry, messages are temporarily unavailable." + " Please try again in a few minutes." + ), + ), + ( + fbchat.FBchatFacebookError, + 1545026, + "Unable to Attach File", + ( + "The type of file you're trying to attach isn't allowed." + " Please try again with a different format." + ), + ), + ( + fbchat._exception.FBchatInvalidParameters, + 1545003, + "Invalid action", + "You cannot perform that action.", + ), + ( + fbchat.FBchatFacebookError, + 1545012, + "Temporary Failure", + "There was a temporary error, please try again.", + ), +] + + +@pytest.mark.parametrize("exception,code,description,summary", ERROR_DATA) +def test_handle_payload_error(exception, code, summary, description): + data = {"error": code, "errorSummary": summary, "errorDescription": description} + with pytest.raises(exception, match=r"Error #\d+ when sending request"): + handle_payload_error(data) + + +def test_handle_payload_error_no_error(): + assert handle_payload_error({}) is None + assert handle_payload_error({"payload": {"abc": ["Something", "else"]}}) is None + + +def test_handle_graphql_errors(): + error = { + "allow_user_retry": False, + "api_error_code": -1, + "code": 1675030, + "debug_info": None, + "description": "Error performing query.", + "fbtrace_id": "CLkuLR752sB", + "is_silent": False, + "is_transient": False, + "message": ( + 'Errors while executing operation "MessengerThreadSharedLinks":' + " At Query.message_thread: Field implementation threw an exception." + " Check your server logs for more information." + ), + "path": ["message_thread"], + "query_path": None, + "requires_reauth": False, + "severity": "CRITICAL", + "summary": "Query error", + } + with pytest.raises(fbchat.FBchatFacebookError, match="GraphQL error"): + handle_graphql_errors({"data": {"message_thread": None}, "errors": [error]}) + + +def test_handle_graphql_errors_singular_error_key(): + with pytest.raises(fbchat.FBchatFacebookError, match="GraphQL error #123"): + handle_graphql_errors({"error": {"code": 123}}) + + +def test_handle_graphql_errors_no_error(): + assert handle_graphql_errors({"data": {"message_thread": None}}) is None + + +def test_check_http_code(): + with pytest.raises(fbchat.FBchatFacebookError): + check_http_code(400) + with pytest.raises(fbchat.FBchatFacebookError): + check_http_code(500) + + +def test_check_http_code_404_handling(): + with pytest.raises(fbchat.FBchatFacebookError, match="invalid id"): + check_http_code(404) + + +def test_check_http_code_no_error(): + assert check_http_code(200) is None + assert check_http_code(302) is None + + +def test_get_jsmods_require_get_image_url(): + data = { + "__ar": 1, + "payload": None, + "jsmods": { + "require": [ + [ + "ServerRedirect", + "redirectPageTo", + [], + [ + "https://scontent-arn2-1.xx.fbcdn.net/v/image.png&dl=1", + False, + False, + ], + ], + ["TuringClientSignalCollectionTrigger", ..., [], ...], + ["TuringClientSignalCollectionTrigger", "retrieveSignals", [], ...], + ["BanzaiODS"], + ["BanzaiScuba"], + ], + "define": ..., + }, + "js": ..., + "css": ..., + "bootloadable": ..., + "resource_map": ..., + "ixData": {}, + "bxData": {}, + "gkxData": ..., + "qexData": {}, + "lid": "123", + } + url = "https://scontent-arn2-1.xx.fbcdn.net/v/image.png&dl=1" + assert get_jsmods_require(data, 3) == url + + +def test_require_list(): + assert require_list([]) == set() + assert require_list([1, 2, 2]) == {1, 2} + assert require_list(1) == {1} + assert require_list("abc") == {"abc"} + + +def test_mimetype_to_key(): + assert mimetype_to_key(None) == "file_id" + assert mimetype_to_key("image/gif") == "gif_id" + assert mimetype_to_key("video/mp4") == "video_id" + assert mimetype_to_key("video/quicktime") == "video_id" + assert mimetype_to_key("image/png") == "image_id" + assert mimetype_to_key("image/jpeg") == "image_id" + assert mimetype_to_key("audio/mpeg") == "audio_id" + assert mimetype_to_key("application/json") == "file_id" + + +def test_get_url_parameter(): + assert get_url_parameter("http://example.com?a=b&c=d", "c") == "d" + assert get_url_parameter("http://example.com?a=b&a=c", "a") == "b" + with pytest.raises(IndexError): + get_url_parameter("http://example.com", "a") + + +def test_prefix_url(): + assert prefix_url("/") == "https://www.facebook.com/" + assert prefix_url("/abc") == "https://www.facebook.com/abc" + assert prefix_url("abc") == "abc" + assert prefix_url("https://m.facebook.com/abc") == "https://m.facebook.com/abc" + + +DT_0 = datetime.datetime(1970, 1, 1, 0, 0, tzinfo=datetime.timezone.utc) +DT = datetime.datetime(2018, 11, 16, 1, 51, 4, 162000, tzinfo=datetime.timezone.utc) +DT_NO_TIMEZONE = datetime.datetime(2018, 11, 16, 1, 51, 4, 162000) + + +def test_seconds_to_datetime(): + assert seconds_to_datetime(0) == DT_0 + assert seconds_to_datetime(1542333064.162) == DT + assert seconds_to_datetime(1542333064.162) != DT_NO_TIMEZONE + + +def test_millis_to_datetime(): + assert millis_to_datetime(0) == DT_0 + assert millis_to_datetime(1542333064162) == DT + assert millis_to_datetime(1542333064162) != DT_NO_TIMEZONE + + +def test_datetime_to_seconds(): + assert datetime_to_seconds(DT_0) == 0 + assert datetime_to_seconds(DT) == 1542333064 # Rounded + datetime_to_seconds(DT_NO_TIMEZONE) # Depends on system timezone + + +def test_datetime_to_millis(): + assert datetime_to_millis(DT_0) == 0 + assert datetime_to_millis(DT) == 1542333064162 + datetime_to_millis(DT_NO_TIMEZONE) # Depends on system timezone + + +def test_seconds_to_timedelta(): + assert seconds_to_timedelta(0.001) == datetime.timedelta(microseconds=1000) + assert seconds_to_timedelta(1) == datetime.timedelta(seconds=1) + assert seconds_to_timedelta(3600) == datetime.timedelta(hours=1) + assert seconds_to_timedelta(86400) == datetime.timedelta(days=1) + + +def test_millis_to_timedelta(): + assert millis_to_timedelta(1) == datetime.timedelta(microseconds=1000) + assert millis_to_timedelta(1000) == datetime.timedelta(seconds=1) + assert millis_to_timedelta(3600000) == datetime.timedelta(hours=1) + assert millis_to_timedelta(86400000) == datetime.timedelta(days=1) + + +def test_timedelta_to_seconds(): + assert timedelta_to_seconds(datetime.timedelta(microseconds=1000)) == 0 # Rounded + assert timedelta_to_seconds(datetime.timedelta(seconds=1)) == 1 + assert timedelta_to_seconds(datetime.timedelta(hours=1)) == 3600 + assert timedelta_to_seconds(datetime.timedelta(days=1)) == 86400