diff --git a/fbchat/__init__.py b/fbchat/__init__.py index 838ea96..91bacb4 100644 --- a/fbchat/__init__.py +++ b/fbchat/__init__.py @@ -7,6 +7,7 @@ from __future__ import unicode_literals +from .models import * from .client import * __title__ = "fbchat" diff --git a/fbchat/_attachment.py b/fbchat/_attachment.py new file mode 100644 index 0000000..20cd50e --- /dev/null +++ b/fbchat/_attachment.py @@ -0,0 +1,73 @@ +# -*- coding: UTF-8 -*- +from __future__ import unicode_literals + + +class Attachment(object): + #: The attachment ID + uid = None + + def __init__(self, uid=None): + """Represents a Facebook attachment""" + self.uid = uid + + +class UnsentMessage(Attachment): + def __init__(self, *args, **kwargs): + """Represents an unsent message attachment""" + super(UnsentMessage, self).__init__(*args, **kwargs) + + +class ShareAttachment(Attachment): + #: ID of the author of the shared post + author = None + #: Target URL + url = None + #: Original URL if Facebook redirects the URL + original_url = None + #: Title of the attachment + title = None + #: Description of the attachment + description = None + #: Name of the source + source = None + #: URL of the attachment image + image_url = None + #: URL of the original image if Facebook uses `safe_image` + original_image_url = None + #: Width of the image + image_width = None + #: Height of the image + image_height = None + #: List of additional attachments + attachments = None + + def __init__( + self, + author=None, + url=None, + original_url=None, + title=None, + description=None, + source=None, + image_url=None, + original_image_url=None, + image_width=None, + image_height=None, + attachments=None, + **kwargs + ): + """Represents a shared item (eg. URL) that has been sent as a Facebook attachment""" + super(ShareAttachment, self).__init__(**kwargs) + self.author = author + self.url = url + self.original_url = original_url + self.title = title + self.description = description + self.source = source + self.image_url = image_url + self.original_image_url = original_image_url + self.image_width = image_width + self.image_height = image_height + if attachments is None: + attachments = [] + self.attachments = attachments diff --git a/fbchat/_core.py b/fbchat/_core.py new file mode 100644 index 0000000..e62df71 --- /dev/null +++ b/fbchat/_core.py @@ -0,0 +1,12 @@ +# -*- coding: UTF-8 -*- +from __future__ import unicode_literals + +import aenum + + +class Enum(aenum.Enum): + """Used internally by fbchat to support enumerations""" + + def __repr__(self): + # For documentation: + return "{}.{}".format(type(self).__name__, self.name) diff --git a/fbchat/_exception.py b/fbchat/_exception.py new file mode 100644 index 0000000..c1dd0d6 --- /dev/null +++ b/fbchat/_exception.py @@ -0,0 +1,32 @@ +# -*- coding: UTF-8 -*- +from __future__ import unicode_literals + + +class FBchatException(Exception): + """Custom exception thrown by fbchat. All exceptions in the fbchat module inherits this""" + + +class FBchatFacebookError(FBchatException): + #: The error code that Facebook returned + fb_error_code = None + #: The error message that Facebook returned (In the user's own language) + fb_error_message = None + #: The status code that was sent in the http response (eg. 404) (Usually only set if not successful, aka. not 200) + request_status_code = None + + def __init__( + self, + message, + fb_error_code=None, + fb_error_message=None, + request_status_code=None, + ): + super(FBchatFacebookError, self).__init__(message) + """Thrown by fbchat when Facebook returns an error""" + self.fb_error_code = str(fb_error_code) + self.fb_error_message = fb_error_message + self.request_status_code = request_status_code + + +class FBchatUserError(FBchatException): + """Thrown by fbchat when wrong values are entered""" diff --git a/fbchat/_file.py b/fbchat/_file.py new file mode 100644 index 0000000..9acfd11 --- /dev/null +++ b/fbchat/_file.py @@ -0,0 +1,198 @@ +# -*- coding: UTF-8 -*- +from __future__ import unicode_literals + +from ._attachment import Attachment + + +class FileAttachment(Attachment): + #: Url where you can download the file + url = None + #: Size of the file in bytes + size = None + #: Name of the file + name = None + #: Whether Facebook determines that this file may be harmful + is_malicious = None + + def __init__(self, url=None, size=None, name=None, is_malicious=None, **kwargs): + """Represents a file that has been sent as a Facebook attachment""" + super(FileAttachment, self).__init__(**kwargs) + self.url = url + self.size = size + self.name = name + self.is_malicious = is_malicious + + +class AudioAttachment(Attachment): + #: Name of the file + filename = None + #: Url of the audio file + url = None + #: Duration of the audioclip in milliseconds + duration = None + #: Audio type + audio_type = None + + def __init__( + self, filename=None, url=None, duration=None, audio_type=None, **kwargs + ): + """Represents an audio file that has been sent as a Facebook attachment""" + super(AudioAttachment, self).__init__(**kwargs) + self.filename = filename + self.url = url + self.duration = duration + self.audio_type = audio_type + + +class ImageAttachment(Attachment): + #: The extension of the original image (eg. 'png') + original_extension = None + #: Width of original image + width = None + #: Height of original image + height = None + + #: Whether the image is animated + is_animated = None + + #: URL to a thumbnail of the image + thumbnail_url = None + + #: URL to a medium preview of the image + preview_url = None + #: Width of the medium preview image + preview_width = None + #: Height of the medium preview image + preview_height = None + + #: URL to a large preview of the image + large_preview_url = None + #: Width of the large preview image + large_preview_width = None + #: Height of the large preview image + large_preview_height = None + + #: URL to an animated preview of the image (eg. for gifs) + animated_preview_url = None + #: Width of the animated preview image + animated_preview_width = None + #: Height of the animated preview image + animated_preview_height = None + + def __init__( + self, + original_extension=None, + width=None, + height=None, + is_animated=None, + thumbnail_url=None, + preview=None, + large_preview=None, + animated_preview=None, + **kwargs + ): + """ + Represents an image that has been sent as a Facebook attachment + To retrieve the full image url, use: :func:`fbchat.Client.fetchImageUrl`, + and pass it the uid of the image attachment + """ + super(ImageAttachment, self).__init__(**kwargs) + self.original_extension = original_extension + if width is not None: + width = int(width) + self.width = width + if height is not None: + height = int(height) + self.height = height + self.is_animated = is_animated + self.thumbnail_url = thumbnail_url + + if preview is None: + preview = {} + self.preview_url = preview.get("uri") + self.preview_width = preview.get("width") + self.preview_height = preview.get("height") + + if large_preview is None: + large_preview = {} + self.large_preview_url = large_preview.get("uri") + self.large_preview_width = large_preview.get("width") + self.large_preview_height = large_preview.get("height") + + if animated_preview is None: + animated_preview = {} + self.animated_preview_url = animated_preview.get("uri") + self.animated_preview_width = animated_preview.get("width") + self.animated_preview_height = animated_preview.get("height") + + +class VideoAttachment(Attachment): + #: Size of the original video in bytes + size = None + #: Width of original video + width = None + #: Height of original video + height = None + #: Length of video in milliseconds + duration = None + #: URL to very compressed preview video + preview_url = None + + #: URL to a small preview image of the video + small_image_url = None + #: Width of the small preview image + small_image_width = None + #: Height of the small preview image + small_image_height = None + + #: URL to a medium preview image of the video + medium_image_url = None + #: Width of the medium preview image + medium_image_width = None + #: Height of the medium preview image + medium_image_height = None + + #: URL to a large preview image of the video + large_image_url = None + #: Width of the large preview image + large_image_width = None + #: Height of the large preview image + large_image_height = None + + def __init__( + self, + size=None, + width=None, + height=None, + duration=None, + preview_url=None, + small_image=None, + medium_image=None, + large_image=None, + **kwargs + ): + """Represents a video that has been sent as a Facebook attachment""" + super(VideoAttachment, self).__init__(**kwargs) + self.size = size + self.width = width + self.height = height + self.duration = duration + self.preview_url = preview_url + + if small_image is None: + small_image = {} + self.small_image_url = small_image.get("uri") + self.small_image_width = small_image.get("width") + self.small_image_height = small_image.get("height") + + if medium_image is None: + medium_image = {} + self.medium_image_url = medium_image.get("uri") + self.medium_image_width = medium_image.get("width") + self.medium_image_height = medium_image.get("height") + + if large_image is None: + large_image = {} + self.large_image_url = large_image.get("uri") + self.large_image_width = large_image.get("width") + self.large_image_height = large_image.get("height") diff --git a/fbchat/_group.py b/fbchat/_group.py new file mode 100644 index 0000000..1a21bf3 --- /dev/null +++ b/fbchat/_group.py @@ -0,0 +1,67 @@ +# -*- coding: UTF-8 -*- +from __future__ import unicode_literals + +from ._thread import ThreadType, Thread + + +class Group(Thread): + #: Unique list (set) of the group thread's participant user IDs + participants = None + #: A dict, containing user nicknames mapped to their IDs + nicknames = None + #: A :class:`ThreadColor`. The groups's message color + color = None + #: The groups's default emoji + emoji = None + # Set containing user IDs of thread admins + admins = None + # True if users need approval to join + approval_mode = None + # Set containing user IDs requesting to join + approval_requests = None + # Link for joining group + join_link = 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 + ): + """Represents a Facebook group. Inherits `Thread`""" + 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 + + +class Room(Group): + # True is room is not discoverable + privacy_mode = None + + def __init__(self, uid, privacy_mode=None, **kwargs): + """Deprecated. Use :class:`Group` instead""" + super(Room, self).__init__(uid, **kwargs) + self.type = ThreadType.ROOM + self.privacy_mode = privacy_mode diff --git a/fbchat/_location.py b/fbchat/_location.py new file mode 100644 index 0000000..b45a942 --- /dev/null +++ b/fbchat/_location.py @@ -0,0 +1,45 @@ +# -*- coding: UTF-8 -*- +from __future__ import unicode_literals + +from ._attachment import Attachment + + +class LocationAttachment(Attachment): + """Latitude and longitude OR address is provided by Facebook""" + + #: Latitude of the location + latitude = None + #: Longitude of the location + longitude = None + #: URL of image showing the map of the location + image_url = None + #: Width of the image + image_width = None + #: Height of the image + image_height = None + #: URL to Bing maps with the location + url = None + # Address of the location + address = None + + def __init__(self, latitude=None, longitude=None, address=None, **kwargs): + """Represents a user location""" + super(LocationAttachment, self).__init__(**kwargs) + self.latitude = latitude + self.longitude = longitude + self.address = address + + +class LiveLocationAttachment(LocationAttachment): + #: Name of the location + name = None + #: Timestamp when live location expires + expiration_time = None + #: True if live location is expired + is_expired = None + + def __init__(self, name=None, expiration_time=None, is_expired=None, **kwargs): + """Represents a live user location""" + super(LiveLocationAttachment, self).__init__(**kwargs) + self.expiration_time = expiration_time + self.is_expired = is_expired diff --git a/fbchat/_message.py b/fbchat/_message.py new file mode 100644 index 0000000..c26b772 --- /dev/null +++ b/fbchat/_message.py @@ -0,0 +1,166 @@ +# -*- coding: UTF-8 -*- +from __future__ import unicode_literals + +from string import Formatter +from ._core import Enum + + +class EmojiSize(Enum): + """Used to specify the size of a sent emoji""" + + LARGE = "369239383222810" + MEDIUM = "369239343222814" + SMALL = "369239263222822" + + +class MessageReaction(Enum): + """Used to specify a message reaction""" + + LOVE = "😍" + SMILE = "😆" + WOW = "😮" + SAD = "😢" + ANGRY = "😠" + YES = "👍" + NO = "👎" + + +class Mention(object): + #: The thread ID the mention is pointing at + thread_id = None + #: The character where the mention starts + offset = None + #: The length of the mention + length = None + + def __init__(self, thread_id, offset=0, length=10): + """Represents a @mention""" + self.thread_id = thread_id + self.offset = offset + self.length = length + + def __repr__(self): + return self.__unicode__() + + def __unicode__(self): + return "".format( + self.thread_id, self.offset, self.length + ) + + +class Message(object): + #: The actual message + text = None + #: A list of :class:`Mention` objects + mentions = None + #: A :class:`EmojiSize`. Size of a sent emoji + emoji_size = None + #: The message ID + uid = None + #: ID of the sender + author = None + #: Timestamp of when the message was sent + timestamp = None + #: Whether the message is read + is_read = None + #: A list of pepole IDs who read the message, works only with :func:`fbchat.Client.fetchThreadMessages` + read_by = None + #: A dict with user's IDs as keys, and their :class:`MessageReaction` as values + reactions = None + #: The actual message + text = None + #: A :class:`Sticker` + sticker = None + #: A list of attachments + attachments = None + #: A list of :class:`QuickReply` + quick_replies = None + #: Whether the message is unsent (deleted for everyone) + unsent = None + + def __init__( + self, + text=None, + mentions=None, + emoji_size=None, + sticker=None, + attachments=None, + quick_replies=None, + ): + """Represents a Facebook message""" + self.text = text + if mentions is None: + mentions = [] + self.mentions = mentions + self.emoji_size = emoji_size + self.sticker = sticker + if attachments is None: + attachments = [] + self.attachments = attachments + if quick_replies is None: + quick_replies = [] + self.quick_replies = quick_replies + self.reactions = {} + self.read_by = [] + self.deleted = False + + def __repr__(self): + return self.__unicode__() + + def __unicode__(self): + return "".format( + self.uid, repr(self.text), self.mentions, self.emoji_size, self.attachments + ) + + @classmethod + def formatMentions(cls, text, *args, **kwargs): + """Like `str.format`, but takes tuples with a thread id and text instead. + + Returns a `Message` object, with the formatted string and relevant mentions. + + ``` + >>> Message.formatMentions("Hey {!r}! My name is {}", ("1234", "Peter"), ("4321", "Michael")) + , ] emoji_size=None attachments=[]> + + >>> Message.formatMentions("Hey {p}! My name is {}", ("1234", "Michael"), p=("4321", "Peter")) + , ] emoji_size=None attachments=[]> + ``` + """ + result = "" + mentions = list() + offset = 0 + f = Formatter() + field_names = [field_name[1] for field_name in f.parse(text)] + automatic = "" in field_names + i = 0 + + for (literal_text, field_name, format_spec, conversion) in f.parse(text): + offset += len(literal_text) + result += literal_text + + if field_name is None: + continue + + if field_name == "": + field_name = str(i) + i += 1 + elif automatic and field_name.isdigit(): + raise ValueError( + "cannot switch from automatic field numbering to manual field specification" + ) + + thread_id, name = f.get_field(field_name, args, kwargs)[0] + + if format_spec: + name = f.format_field(name, format_spec) + if conversion: + name = f.convert_field(name, conversion) + + result += name + mentions.append( + Mention(thread_id=thread_id, offset=offset, length=len(name)) + ) + offset += len(name) + + message = cls(text=result, mentions=mentions) + return message diff --git a/fbchat/_page.py b/fbchat/_page.py new file mode 100644 index 0000000..53a2bf1 --- /dev/null +++ b/fbchat/_page.py @@ -0,0 +1,35 @@ +# -*- coding: UTF-8 -*- +from __future__ import unicode_literals + +from ._thread import ThreadType, Thread + + +class Page(Thread): + #: The page's custom url + url = None + #: The name of the page's location city + city = None + #: Amount of likes the page has + likes = None + #: Some extra information about the page + sub_title = None + #: The page's category + category = None + + def __init__( + self, + uid, + url=None, + city=None, + likes=None, + sub_title=None, + category=None, + **kwargs + ): + """Represents a Facebook page. Inherits `Thread`""" + 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 diff --git a/fbchat/_plan.py b/fbchat/_plan.py new file mode 100644 index 0000000..d9524dc --- /dev/null +++ b/fbchat/_plan.py @@ -0,0 +1,46 @@ +# -*- coding: UTF-8 -*- +from __future__ import unicode_literals + + +class Plan(object): + #: ID of the plan + uid = None + #: Plan time (unix time stamp), only precise down to the minute + time = None + #: Plan title + title = None + #: Plan location name + location = None + #: Plan location ID + location_id = None + #: ID of the plan creator + author_id = None + #: List of the people IDs who will take part in the plan + going = None + #: List of the people IDs who won't take part in the plan + declined = None + #: List of the people IDs who are invited to the plan + invited = None + + def __init__(self, time, title, location=None, location_id=None): + """Represents a plan""" + self.time = int(time) + self.title = title + self.location = location or "" + self.location_id = location_id or "" + self.author_id = None + self.going = [] + self.declined = [] + self.invited = [] + + def __repr__(self): + return self.__unicode__() + + def __unicode__(self): + return "".format( + self.uid, + repr(self.title), + self.time, + repr(self.location), + repr(self.location_id), + ) diff --git a/fbchat/_poll.py b/fbchat/_poll.py new file mode 100644 index 0000000..1276e1a --- /dev/null +++ b/fbchat/_poll.py @@ -0,0 +1,52 @@ +# -*- coding: UTF-8 -*- +from __future__ import unicode_literals + + +class Poll(object): + #: ID of the poll + uid = None + #: Title of the poll + title = None + #: List of :class:`PollOption`, can be fetched with :func:`fbchat.Client.fetchPollOptions` + options = None + #: Options count + options_count = None + + def __init__(self, title, options): + """Represents a poll""" + self.title = title + self.options = options + + def __repr__(self): + return self.__unicode__() + + def __unicode__(self): + return "".format( + self.uid, repr(self.title), self.options + ) + + +class PollOption(object): + #: ID of the poll option + uid = None + #: Text of the poll option + text = None + #: Whether vote when creating or client voted + vote = None + #: ID of the users who voted for this poll option + voters = None + #: Votes count + votes_count = None + + def __init__(self, text, vote=False): + """Represents a poll option""" + self.text = text + self.vote = vote + + def __repr__(self): + return self.__unicode__() + + def __unicode__(self): + return "".format( + self.uid, repr(self.text), self.voters + ) diff --git a/fbchat/_quick_reply.py b/fbchat/_quick_reply.py new file mode 100644 index 0000000..3c3cbf1 --- /dev/null +++ b/fbchat/_quick_reply.py @@ -0,0 +1,76 @@ +# -*- coding: UTF-8 -*- +from __future__ import unicode_literals + +from ._attachment import Attachment + + +class QuickReply(object): + #: Payload of the quick reply + payload = None + #: External payload for responses + external_payload = None + #: Additional data + data = None + #: Whether it's a response for a quick reply + is_response = None + + def __init__(self, payload=None, data=None, is_response=False): + """Represents a quick reply""" + self.payload = payload + self.data = data + self.is_response = is_response + + def __repr__(self): + return self.__unicode__() + + def __unicode__(self): + return "<{}: payload={!r}>".format(self.__class__.__name__, self.payload) + + +class QuickReplyText(QuickReply): + #: Title of the quick reply + title = None + #: URL of the quick reply image (optional) + image_url = None + #: Type of the quick reply + _type = "text" + + def __init__(self, title=None, image_url=None, **kwargs): + """Represents a text quick reply""" + super(QuickReplyText, self).__init__(**kwargs) + self.title = title + self.image_url = image_url + + +class QuickReplyLocation(QuickReply): + #: Type of the quick reply + _type = "location" + + def __init__(self, **kwargs): + """Represents a location quick reply (Doesn't work on mobile)""" + super(QuickReplyLocation, self).__init__(**kwargs) + self.is_response = False + + +class QuickReplyPhoneNumber(QuickReply): + #: URL of the quick reply image (optional) + image_url = None + #: Type of the quick reply + _type = "user_phone_number" + + def __init__(self, image_url=None, **kwargs): + """Represents a phone number quick reply (Doesn't work on mobile)""" + super(QuickReplyPhoneNumber, self).__init__(**kwargs) + self.image_url = image_url + + +class QuickReplyEmail(QuickReply): + #: URL of the quick reply image (optional) + image_url = None + #: Type of the quick reply + _type = "user_email" + + def __init__(self, image_url=None, **kwargs): + """Represents an email quick reply (Doesn't work on mobile)""" + super(QuickReplyEmail, self).__init__(**kwargs) + self.image_url = image_url diff --git a/fbchat/_sticker.py b/fbchat/_sticker.py new file mode 100644 index 0000000..e575681 --- /dev/null +++ b/fbchat/_sticker.py @@ -0,0 +1,36 @@ +# -*- coding: UTF-8 -*- +from __future__ import unicode_literals + +from ._attachment import Attachment + + +class Sticker(Attachment): + #: The sticker-pack's ID + pack = None + #: Whether the sticker is animated + is_animated = False + + # If the sticker is animated, the following should be present + #: URL to a medium spritemap + medium_sprite_image = None + #: URL to a large spritemap + large_sprite_image = None + #: The amount of frames present in the spritemap pr. row + frames_per_row = None + #: The amount of frames present in the spritemap pr. coloumn + frames_per_col = None + #: The frame rate the spritemap is intended to be played in + frame_rate = None + + #: URL to the sticker's image + url = None + #: Width of the sticker + width = None + #: Height of the sticker + height = None + #: The sticker's label/name + label = None + + def __init__(self, *args, **kwargs): + """Represents a Facebook sticker that has been sent to a Facebook thread as an attachment""" + super(Sticker, self).__init__(*args, **kwargs) diff --git a/fbchat/_thread.py b/fbchat/_thread.py new file mode 100644 index 0000000..37efd6d --- /dev/null +++ b/fbchat/_thread.py @@ -0,0 +1,84 @@ +# -*- coding: UTF-8 -*- +from __future__ import unicode_literals + +from ._core import Enum + + +class ThreadType(Enum): + """Used to specify what type of Facebook thread is being used. See :ref:`intro_threads` for more info""" + + USER = 1 + GROUP = 2 + ROOM = 2 + PAGE = 3 + + +class ThreadLocation(Enum): + """Used to specify where a thread is located (inbox, pending, archived, other).""" + + INBOX = "INBOX" + PENDING = "PENDING" + ARCHIVED = "ARCHIVED" + OTHER = "OTHER" + + +class ThreadColor(Enum): + """Used to specify a thread colors""" + + MESSENGER_BLUE = "#0084ff" + VIKING = "#44bec7" + GOLDEN_POPPY = "#ffc300" + RADICAL_RED = "#fa3c4c" + SHOCKING = "#d696bb" + PICTON_BLUE = "#6699cc" + FREE_SPEECH_GREEN = "#13cf13" + PUMPKIN = "#ff7e29" + LIGHT_CORAL = "#e68585" + MEDIUM_SLATE_BLUE = "#7646ff" + DEEP_SKY_BLUE = "#20cef5" + FERN = "#67b868" + CAMEO = "#d4a88c" + BRILLIANT_ROSE = "#ff5ca1" + BILOBA_FLOWER = "#a695c7" + + +class Thread(object): + #: The unique identifier of the thread. Can be used a `thread_id`. See :ref:`intro_threads` for more info + uid = None + #: Specifies the type of thread. Can be used a `thread_type`. See :ref:`intro_threads` for more info + type = None + #: A url to the thread's picture + photo = None + #: The name of the thread + name = None + #: Timestamp of last message + last_message_timestamp = None + #: Number of messages in the thread + message_count = None + #: Set :class:`Plan` + plan = None + + def __init__( + self, + _type, + uid, + photo=None, + name=None, + last_message_timestamp=None, + message_count=None, + plan=None, + ): + """Represents a Facebook thread""" + self.uid = str(uid) + self.type = _type + self.photo = photo + self.name = name + self.last_message_timestamp = last_message_timestamp + self.message_count = message_count + self.plan = plan + + def __repr__(self): + return self.__unicode__() + + def __unicode__(self): + return "<{} {} ({})>".format(self.type.name, self.name, self.uid) diff --git a/fbchat/_user.py b/fbchat/_user.py new file mode 100644 index 0000000..75a95c6 --- /dev/null +++ b/fbchat/_user.py @@ -0,0 +1,85 @@ +# -*- coding: UTF-8 -*- +from __future__ import unicode_literals + +from ._core import Enum +from ._thread import ThreadType, Thread + + +class TypingStatus(Enum): + """Used to specify whether the user is typing or has stopped typing""" + + STOPPED = 0 + TYPING = 1 + + +class User(Thread): + #: The profile url + url = None + #: The users first name + first_name = None + #: The users last name + last_name = None + #: Whether the user and the client are friends + is_friend = None + #: The user's gender + gender = None + #: From 0 to 1. How close the client is to the user + affinity = None + #: The user's nickname + nickname = None + #: The clients nickname, as seen by the user + own_nickname = None + #: A :class:`ThreadColor`. The message color + color = None + #: The default emoji + emoji = 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 + ): + """Represents a Facebook user. Inherits `Thread`""" + 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 + + +class ActiveStatus(object): + #: Whether the user is active now + active = None + #: Timestamp when the user was last active + last_active = None + #: Whether the user is playing Messenger game now + in_game = None + + def __init__(self, active=None, last_active=None, in_game=None): + self.active = active + self.last_active = last_active + self.in_game = in_game + + def __repr__(self): + return self.__unicode__() + + def __unicode__(self): + return "".format( + self.active, self.last_active, self.in_game + ) diff --git a/fbchat/models.py b/fbchat/models.py index 673ef6b..5c192c0 100644 --- a/fbchat/models.py +++ b/fbchat/models.py @@ -1,962 +1,29 @@ # -*- coding: UTF-8 -*- +"""This file is here to maintain backwards compatability, and to re-export our models +into the global module (see `__init__.py`). +A common pattern was to use `from fbchat.models import *`, hence we need this while +transitioning to a better code structure. +""" from __future__ import unicode_literals -import aenum -from string import Formatter - -class FBchatException(Exception): - """Custom exception thrown by fbchat. All exceptions in the fbchat module inherits this""" - - -class FBchatFacebookError(FBchatException): - #: The error code that Facebook returned - fb_error_code = None - #: The error message that Facebook returned (In the user's own language) - fb_error_message = None - #: The status code that was sent in the http response (eg. 404) (Usually only set if not successful, aka. not 200) - request_status_code = None - - def __init__( - self, - message, - fb_error_code=None, - fb_error_message=None, - request_status_code=None, - ): - super(FBchatFacebookError, self).__init__(message) - """Thrown by fbchat when Facebook returns an error""" - self.fb_error_code = str(fb_error_code) - self.fb_error_message = fb_error_message - self.request_status_code = request_status_code - - -class FBchatUserError(FBchatException): - """Thrown by fbchat when wrong values are entered""" - - -class Thread(object): - #: The unique identifier of the thread. Can be used a `thread_id`. See :ref:`intro_threads` for more info - uid = None - #: Specifies the type of thread. Can be used a `thread_type`. See :ref:`intro_threads` for more info - type = None - #: A url to the thread's picture - photo = None - #: The name of the thread - name = None - #: Timestamp of last message - last_message_timestamp = None - #: Number of messages in the thread - message_count = None - #: Set :class:`Plan` - plan = None - - def __init__( - self, - _type, - uid, - photo=None, - name=None, - last_message_timestamp=None, - message_count=None, - plan=None, - ): - """Represents a Facebook thread""" - self.uid = str(uid) - self.type = _type - self.photo = photo - self.name = name - self.last_message_timestamp = last_message_timestamp - self.message_count = message_count - self.plan = plan - - def __repr__(self): - return self.__unicode__() - - def __unicode__(self): - return "<{} {} ({})>".format(self.type.name, self.name, self.uid) - - -class User(Thread): - #: The profile url - url = None - #: The users first name - first_name = None - #: The users last name - last_name = None - #: Whether the user and the client are friends - is_friend = None - #: The user's gender - gender = None - #: From 0 to 1. How close the client is to the user - affinity = None - #: The user's nickname - nickname = None - #: The clients nickname, as seen by the user - own_nickname = None - #: A :class:`ThreadColor`. The message color - color = None - #: The default emoji - emoji = 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 - ): - """Represents a Facebook user. Inherits `Thread`""" - 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 - - -class Group(Thread): - #: Unique list (set) of the group thread's participant user IDs - participants = None - #: A dict, containing user nicknames mapped to their IDs - nicknames = None - #: A :class:`ThreadColor`. The groups's message color - color = None - #: The groups's default emoji - emoji = None - # Set containing user IDs of thread admins - admins = None - # True if users need approval to join - approval_mode = None - # Set containing user IDs requesting to join - approval_requests = None - # Link for joining group - join_link = 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 - ): - """Represents a Facebook group. Inherits `Thread`""" - 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 - - -class Room(Group): - # True is room is not discoverable - privacy_mode = None - - def __init__(self, uid, privacy_mode=None, **kwargs): - """Deprecated. Use :class:`Group` instead""" - super(Room, self).__init__(uid, **kwargs) - self.type = ThreadType.ROOM - self.privacy_mode = privacy_mode - - -class Page(Thread): - #: The page's custom url - url = None - #: The name of the page's location city - city = None - #: Amount of likes the page has - likes = None - #: Some extra information about the page - sub_title = None - #: The page's category - category = None - - def __init__( - self, - uid, - url=None, - city=None, - likes=None, - sub_title=None, - category=None, - **kwargs - ): - """Represents a Facebook page. Inherits `Thread`""" - 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 - - -class Message(object): - #: The actual message - text = None - #: A list of :class:`Mention` objects - mentions = None - #: A :class:`EmojiSize`. Size of a sent emoji - emoji_size = None - #: The message ID - uid = None - #: ID of the sender - author = None - #: Timestamp of when the message was sent - timestamp = None - #: Whether the message is read - is_read = None - #: A list of pepole IDs who read the message, works only with :func:`fbchat.Client.fetchThreadMessages` - read_by = None - #: A dict with user's IDs as keys, and their :class:`MessageReaction` as values - reactions = None - #: The actual message - text = None - #: A :class:`Sticker` - sticker = None - #: A list of attachments - attachments = None - #: A list of :class:`QuickReply` - quick_replies = None - #: Whether the message is unsent (deleted for everyone) - unsent = None - - def __init__( - self, - text=None, - mentions=None, - emoji_size=None, - sticker=None, - attachments=None, - quick_replies=None, - ): - """Represents a Facebook message""" - self.text = text - if mentions is None: - mentions = [] - self.mentions = mentions - self.emoji_size = emoji_size - self.sticker = sticker - if attachments is None: - attachments = [] - self.attachments = attachments - if quick_replies is None: - quick_replies = [] - self.quick_replies = quick_replies - self.reactions = {} - self.read_by = [] - self.deleted = False - - def __repr__(self): - return self.__unicode__() - - def __unicode__(self): - return "".format( - self.uid, repr(self.text), self.mentions, self.emoji_size, self.attachments - ) - - @classmethod - def formatMentions(cls, text, *args, **kwargs): - """Like `str.format`, but takes tuples with a thread id and text instead. - - Returns a `Message` object, with the formatted string and relevant mentions. - - ``` - >>> Message.formatMentions("Hey {!r}! My name is {}", ("1234", "Peter"), ("4321", "Michael")) - , ] emoji_size=None attachments=[]> - - >>> Message.formatMentions("Hey {p}! My name is {}", ("1234", "Michael"), p=("4321", "Peter")) - , ] emoji_size=None attachments=[]> - ``` - """ - result = "" - mentions = list() - offset = 0 - f = Formatter() - field_names = [field_name[1] for field_name in f.parse(text)] - automatic = "" in field_names - i = 0 - - for (literal_text, field_name, format_spec, conversion) in f.parse(text): - offset += len(literal_text) - result += literal_text - - if field_name is None: - continue - - if field_name == "": - field_name = str(i) - i += 1 - elif automatic and field_name.isdigit(): - raise ValueError( - "cannot switch from automatic field numbering to manual field specification" - ) - - thread_id, name = f.get_field(field_name, args, kwargs)[0] - - if format_spec: - name = f.format_field(name, format_spec) - if conversion: - name = f.convert_field(name, conversion) - - result += name - mentions.append( - Mention(thread_id=thread_id, offset=offset, length=len(name)) - ) - offset += len(name) - - message = cls(text=result, mentions=mentions) - return message - - -class Attachment(object): - #: The attachment ID - uid = None - - def __init__(self, uid=None): - """Represents a Facebook attachment""" - self.uid = uid - - -class UnsentMessage(Attachment): - def __init__(self, *args, **kwargs): - """Represents an unsent message attachment""" - super(UnsentMessage, self).__init__(*args, **kwargs) - - -class Sticker(Attachment): - #: The sticker-pack's ID - pack = None - #: Whether the sticker is animated - is_animated = False - - # If the sticker is animated, the following should be present - #: URL to a medium spritemap - medium_sprite_image = None - #: URL to a large spritemap - large_sprite_image = None - #: The amount of frames present in the spritemap pr. row - frames_per_row = None - #: The amount of frames present in the spritemap pr. coloumn - frames_per_col = None - #: The frame rate the spritemap is intended to be played in - frame_rate = None - - #: URL to the sticker's image - url = None - #: Width of the sticker - width = None - #: Height of the sticker - height = None - #: The sticker's label/name - label = None - - def __init__(self, *args, **kwargs): - """Represents a Facebook sticker that has been sent to a Facebook thread as an attachment""" - super(Sticker, self).__init__(*args, **kwargs) - - -class ShareAttachment(Attachment): - #: ID of the author of the shared post - author = None - #: Target URL - url = None - #: Original URL if Facebook redirects the URL - original_url = None - #: Title of the attachment - title = None - #: Description of the attachment - description = None - #: Name of the source - source = None - #: URL of the attachment image - image_url = None - #: URL of the original image if Facebook uses `safe_image` - original_image_url = None - #: Width of the image - image_width = None - #: Height of the image - image_height = None - #: List of additional attachments - attachments = None - - def __init__( - self, - author=None, - url=None, - original_url=None, - title=None, - description=None, - source=None, - image_url=None, - original_image_url=None, - image_width=None, - image_height=None, - attachments=None, - **kwargs - ): - """Represents a shared item (eg. URL) that has been sent as a Facebook attachment""" - super(ShareAttachment, self).__init__(**kwargs) - self.author = author - self.url = url - self.original_url = original_url - self.title = title - self.description = description - self.source = source - self.image_url = image_url - self.original_image_url = original_image_url - self.image_width = image_width - self.image_height = image_height - if attachments is None: - attachments = [] - self.attachments = attachments - - -class LocationAttachment(Attachment): - """Latitude and longitude OR address is provided by Facebook""" - - #: Latitude of the location - latitude = None - #: Longitude of the location - longitude = None - #: URL of image showing the map of the location - image_url = None - #: Width of the image - image_width = None - #: Height of the image - image_height = None - #: URL to Bing maps with the location - url = None - # Address of the location - address = None - - def __init__(self, latitude=None, longitude=None, address=None, **kwargs): - """Represents a user location""" - super(LocationAttachment, self).__init__(**kwargs) - self.latitude = latitude - self.longitude = longitude - self.address = address - - -class LiveLocationAttachment(LocationAttachment): - #: Name of the location - name = None - #: Timestamp when live location expires - expiration_time = None - #: True if live location is expired - is_expired = None - - def __init__(self, name=None, expiration_time=None, is_expired=None, **kwargs): - """Represents a live user location""" - super(LiveLocationAttachment, self).__init__(**kwargs) - self.expiration_time = expiration_time - self.is_expired = is_expired - - -class FileAttachment(Attachment): - #: Url where you can download the file - url = None - #: Size of the file in bytes - size = None - #: Name of the file - name = None - #: Whether Facebook determines that this file may be harmful - is_malicious = None - - def __init__(self, url=None, size=None, name=None, is_malicious=None, **kwargs): - """Represents a file that has been sent as a Facebook attachment""" - super(FileAttachment, self).__init__(**kwargs) - self.url = url - self.size = size - self.name = name - self.is_malicious = is_malicious - - -class AudioAttachment(Attachment): - #: Name of the file - filename = None - #: Url of the audio file - url = None - #: Duration of the audioclip in milliseconds - duration = None - #: Audio type - audio_type = None - - def __init__( - self, filename=None, url=None, duration=None, audio_type=None, **kwargs - ): - """Represents an audio file that has been sent as a Facebook attachment""" - super(AudioAttachment, self).__init__(**kwargs) - self.filename = filename - self.url = url - self.duration = duration - self.audio_type = audio_type - - -class ImageAttachment(Attachment): - #: The extension of the original image (eg. 'png') - original_extension = None - #: Width of original image - width = None - #: Height of original image - height = None - - #: Whether the image is animated - is_animated = None - - #: URL to a thumbnail of the image - thumbnail_url = None - - #: URL to a medium preview of the image - preview_url = None - #: Width of the medium preview image - preview_width = None - #: Height of the medium preview image - preview_height = None - - #: URL to a large preview of the image - large_preview_url = None - #: Width of the large preview image - large_preview_width = None - #: Height of the large preview image - large_preview_height = None - - #: URL to an animated preview of the image (eg. for gifs) - animated_preview_url = None - #: Width of the animated preview image - animated_preview_width = None - #: Height of the animated preview image - animated_preview_height = None - - def __init__( - self, - original_extension=None, - width=None, - height=None, - is_animated=None, - thumbnail_url=None, - preview=None, - large_preview=None, - animated_preview=None, - **kwargs - ): - """ - Represents an image that has been sent as a Facebook attachment - To retrieve the full image url, use: :func:`fbchat.Client.fetchImageUrl`, - and pass it the uid of the image attachment - """ - super(ImageAttachment, self).__init__(**kwargs) - self.original_extension = original_extension - if width is not None: - width = int(width) - self.width = width - if height is not None: - height = int(height) - self.height = height - self.is_animated = is_animated - self.thumbnail_url = thumbnail_url - - if preview is None: - preview = {} - self.preview_url = preview.get("uri") - self.preview_width = preview.get("width") - self.preview_height = preview.get("height") - - if large_preview is None: - large_preview = {} - self.large_preview_url = large_preview.get("uri") - self.large_preview_width = large_preview.get("width") - self.large_preview_height = large_preview.get("height") - - if animated_preview is None: - animated_preview = {} - self.animated_preview_url = animated_preview.get("uri") - self.animated_preview_width = animated_preview.get("width") - self.animated_preview_height = animated_preview.get("height") - - -class VideoAttachment(Attachment): - #: Size of the original video in bytes - size = None - #: Width of original video - width = None - #: Height of original video - height = None - #: Length of video in milliseconds - duration = None - #: URL to very compressed preview video - preview_url = None - - #: URL to a small preview image of the video - small_image_url = None - #: Width of the small preview image - small_image_width = None - #: Height of the small preview image - small_image_height = None - - #: URL to a medium preview image of the video - medium_image_url = None - #: Width of the medium preview image - medium_image_width = None - #: Height of the medium preview image - medium_image_height = None - - #: URL to a large preview image of the video - large_image_url = None - #: Width of the large preview image - large_image_width = None - #: Height of the large preview image - large_image_height = None - - def __init__( - self, - size=None, - width=None, - height=None, - duration=None, - preview_url=None, - small_image=None, - medium_image=None, - large_image=None, - **kwargs - ): - """Represents a video that has been sent as a Facebook attachment""" - super(VideoAttachment, self).__init__(**kwargs) - self.size = size - self.width = width - self.height = height - self.duration = duration - self.preview_url = preview_url - - if small_image is None: - small_image = {} - self.small_image_url = small_image.get("uri") - self.small_image_width = small_image.get("width") - self.small_image_height = small_image.get("height") - - if medium_image is None: - medium_image = {} - self.medium_image_url = medium_image.get("uri") - self.medium_image_width = medium_image.get("width") - self.medium_image_height = medium_image.get("height") - - if large_image is None: - large_image = {} - self.large_image_url = large_image.get("uri") - self.large_image_width = large_image.get("width") - self.large_image_height = large_image.get("height") - - -class Mention(object): - #: The thread ID the mention is pointing at - thread_id = None - #: The character where the mention starts - offset = None - #: The length of the mention - length = None - - def __init__(self, thread_id, offset=0, length=10): - """Represents a @mention""" - self.thread_id = thread_id - self.offset = offset - self.length = length - - def __repr__(self): - return self.__unicode__() - - def __unicode__(self): - return "".format( - self.thread_id, self.offset, self.length - ) - - -class QuickReply(object): - #: Payload of the quick reply - payload = None - #: External payload for responses - external_payload = None - #: Additional data - data = None - #: Whether it's a response for a quick reply - is_response = None - - def __init__(self, payload=None, data=None, is_response=False): - """Represents a quick reply""" - self.payload = payload - self.data = data - self.is_response = is_response - - def __repr__(self): - return self.__unicode__() - - def __unicode__(self): - return "<{}: payload={!r}>".format(self.__class__.__name__, self.payload) - - -class QuickReplyText(QuickReply): - #: Title of the quick reply - title = None - #: URL of the quick reply image (optional) - image_url = None - #: Type of the quick reply - _type = "text" - - def __init__(self, title=None, image_url=None, **kwargs): - """Represents a text quick reply""" - super(QuickReplyText, self).__init__(**kwargs) - self.title = title - self.image_url = image_url - - -class QuickReplyLocation(QuickReply): - #: Type of the quick reply - _type = "location" - - def __init__(self, **kwargs): - """Represents a location quick reply (Doesn't work on mobile)""" - super(QuickReplyLocation, self).__init__(**kwargs) - self.is_response = False - - -class QuickReplyPhoneNumber(QuickReply): - #: URL of the quick reply image (optional) - image_url = None - #: Type of the quick reply - _type = "user_phone_number" - - def __init__(self, image_url=None, **kwargs): - """Represents a phone number quick reply (Doesn't work on mobile)""" - super(QuickReplyPhoneNumber, self).__init__(**kwargs) - self.image_url = image_url - - -class QuickReplyEmail(QuickReply): - #: URL of the quick reply image (optional) - image_url = None - #: Type of the quick reply - _type = "user_email" - - def __init__(self, image_url=None, **kwargs): - """Represents an email quick reply (Doesn't work on mobile)""" - super(QuickReplyEmail, self).__init__(**kwargs) - self.image_url = image_url - - -class Poll(object): - #: ID of the poll - uid = None - #: Title of the poll - title = None - #: List of :class:`PollOption`, can be fetched with :func:`fbchat.Client.fetchPollOptions` - options = None - #: Options count - options_count = None - - def __init__(self, title, options): - """Represents a poll""" - self.title = title - self.options = options - - def __repr__(self): - return self.__unicode__() - - def __unicode__(self): - return "".format( - self.uid, repr(self.title), self.options - ) - - -class PollOption(object): - #: ID of the poll option - uid = None - #: Text of the poll option - text = None - #: Whether vote when creating or client voted - vote = None - #: ID of the users who voted for this poll option - voters = None - #: Votes count - votes_count = None - - def __init__(self, text, vote=False): - """Represents a poll option""" - self.text = text - self.vote = vote - - def __repr__(self): - return self.__unicode__() - - def __unicode__(self): - return "".format( - self.uid, repr(self.text), self.voters - ) - - -class Plan(object): - #: ID of the plan - uid = None - #: Plan time (unix time stamp), only precise down to the minute - time = None - #: Plan title - title = None - #: Plan location name - location = None - #: Plan location ID - location_id = None - #: ID of the plan creator - author_id = None - #: List of the people IDs who will take part in the plan - going = None - #: List of the people IDs who won't take part in the plan - declined = None - #: List of the people IDs who are invited to the plan - invited = None - - def __init__(self, time, title, location=None, location_id=None): - """Represents a plan""" - self.time = int(time) - self.title = title - self.location = location or "" - self.location_id = location_id or "" - self.author_id = None - self.going = [] - self.declined = [] - self.invited = [] - - def __repr__(self): - return self.__unicode__() - - def __unicode__(self): - return "".format( - self.uid, - repr(self.title), - self.time, - repr(self.location), - repr(self.location_id), - ) - - -class ActiveStatus(object): - #: Whether the user is active now - active = None - #: Timestamp when the user was last active - last_active = None - #: Whether the user is playing Messenger game now - in_game = None - - def __init__(self, active=None, last_active=None, in_game=None): - self.active = active - self.last_active = last_active - self.in_game = in_game - - def __repr__(self): - return self.__unicode__() - - def __unicode__(self): - return "".format( - self.active, self.last_active, self.in_game - ) - - -class Enum(aenum.Enum): - """Used internally by fbchat to support enumerations""" - - def __repr__(self): - # For documentation: - return "{}.{}".format(type(self).__name__, self.name) - - -class ThreadType(Enum): - """Used to specify what type of Facebook thread is being used. See :ref:`intro_threads` for more info""" - - USER = 1 - GROUP = 2 - ROOM = 2 - PAGE = 3 - - -class ThreadLocation(Enum): - """Used to specify where a thread is located (inbox, pending, archived, other).""" - - INBOX = "INBOX" - PENDING = "PENDING" - ARCHIVED = "ARCHIVED" - OTHER = "OTHER" - - -class TypingStatus(Enum): - """Used to specify whether the user is typing or has stopped typing""" - - STOPPED = 0 - TYPING = 1 - - -class EmojiSize(Enum): - """Used to specify the size of a sent emoji""" - - LARGE = "369239383222810" - MEDIUM = "369239343222814" - SMALL = "369239263222822" - - -class ThreadColor(Enum): - """Used to specify a thread colors""" - - MESSENGER_BLUE = "#0084ff" - VIKING = "#44bec7" - GOLDEN_POPPY = "#ffc300" - RADICAL_RED = "#fa3c4c" - SHOCKING = "#d696bb" - PICTON_BLUE = "#6699cc" - FREE_SPEECH_GREEN = "#13cf13" - PUMPKIN = "#ff7e29" - LIGHT_CORAL = "#e68585" - MEDIUM_SLATE_BLUE = "#7646ff" - DEEP_SKY_BLUE = "#20cef5" - FERN = "#67b868" - CAMEO = "#d4a88c" - BRILLIANT_ROSE = "#ff5ca1" - BILOBA_FLOWER = "#a695c7" - - -class MessageReaction(Enum): - """Used to specify a message reaction""" - - LOVE = "😍" - SMILE = "😆" - WOW = "😮" - SAD = "😢" - ANGRY = "😠" - YES = "👍" - NO = "👎" +from ._core import Enum +from ._exception import FBchatException, FBchatFacebookError, FBchatUserError +from ._thread import ThreadType, ThreadLocation, ThreadColor, Thread +from ._user import TypingStatus, User, ActiveStatus +from ._group import Group, Room +from ._page import Page +from ._message import EmojiSize, MessageReaction, Mention, Message +from ._attachment import Attachment, UnsentMessage, ShareAttachment +from ._sticker import Sticker +from ._location import LocationAttachment, LiveLocationAttachment +from ._file import FileAttachment, AudioAttachment, ImageAttachment, VideoAttachment +from ._quick_reply import ( + QuickReply, + QuickReplyText, + QuickReplyLocation, + QuickReplyPhoneNumber, + QuickReplyEmail, +) +from ._poll import Poll, PollOption +from ._plan import Plan