diff --git a/fbchat/client.py b/fbchat/client.py index 3ccf30a..4d0cce9 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -984,6 +984,30 @@ class Client(object): plan = graphql_to_plan(j["payload"]) return plan + def _getPrivateData(self): + j = self.graphql_request(GraphQL(doc_id='1868889766468115')) + return j['viewer'] + + def getPhoneNumbers(self): + """ + Fetches a list of user phone numbers. + + :return: List of phone numbers + :rtype: list + """ + data = self._getPrivateData() + return [j['phone_number']['universal_number'] for j in data['user']['all_phones']] + + def getEmails(self): + """ + Fetches a list of user emails. + + :return: List of emails + :rtype: list + """ + data = self._getPrivateData() + return [j['display_email'] for j in data['all_emails']] + """ END FETCH METHODS """ @@ -1040,6 +1064,25 @@ class Client(object): if message.sticker: data['sticker_id'] = message.sticker.uid + if message.quick_replies: + xmd = {"quick_replies": []} + for quick_reply in message.quick_replies: + q = dict() + q["content_type"] = quick_reply._type + q["payload"] = quick_reply.payload + q["external_payload"] = quick_reply.external_payload + q["data"] = quick_reply.data + if quick_reply.is_response: + q["ignore_for_webhook"] = False + if isinstance(quick_reply, QuickReplyText): + q["title"] = quick_reply.title + if not isinstance(quick_reply, QuickReplyLocation): + q["image_url"] = quick_reply.image_url + xmd["quick_replies"].append(q) + if len(message.quick_replies) == 1 and message.quick_replies[0].is_response: + xmd["quick_replies"] = xmd["quick_replies"][0] + data['platform_xmd'] = json.dumps(xmd) + return data def _doSendRequest(self, data, get_thread_id=False): @@ -1111,6 +1154,36 @@ class Client(object): data['specific_to_list[0]'] = "fbid:{}".format(thread_id) return self._doSendRequest(data) + def quickReply(self, quick_reply, payload=None, thread_id=None, thread_type=None): + """ + Replies to a chosen quick reply + + :param quick_reply: Quick reply to reply to + :param payload: Optional answer to the quick reply + :param thread_id: User/Group ID to send to. See :ref:`intro_threads` + :param thread_type: See :ref:`intro_threads` + :type quick_reply: models.QuickReply + :type thread_type: models.ThreadType + :return: :ref:`Message ID ` of the sent message + :raises: FBchatException if request failed + """ + quick_reply.is_response = True + if isinstance(quick_reply, QuickReplyText): + return self.send(Message(text=quick_reply.title, quick_replies=[quick_reply])) + elif isinstance(quick_reply, QuickReplyLocation): + if not isinstance(payload, LocationAttachment): raise ValueError("Payload must be an instance of `fbchat.models.LocationAttachment`") + return self.sendLocation(payload, thread_id=thread_id, thread_type=thread_type) + elif isinstance(quick_reply, QuickReplyEmail): + if not payload: payload = self.getEmails()[0] + quick_reply.external_payload = quick_reply.payload + quick_reply.payload = payload + return self.send(Message(text=payload, quick_replies=[quick_reply])) + elif isinstance(quick_reply, QuickReplyPhoneNumber): + if not payload: payload = self.getPhoneNumbers()[0] + quick_reply.external_payload = quick_reply.payload + quick_reply.payload = payload + return self.send(Message(text=payload, quick_replies=[quick_reply])) + def unsend(self, mid): """ Unsends a message (removes for everyone) diff --git a/fbchat/graphql.py b/fbchat/graphql.py index 81f7411..442feaa 100644 --- a/fbchat/graphql.py +++ b/fbchat/graphql.py @@ -267,6 +267,24 @@ def graphql_to_plan(a): rtn.invited = [m.get('node').get('id') for m in guests if m.get('guest_list_state') == "INVITED"] return rtn +def graphql_to_quick_reply(q, is_response=False): + data = dict() + _type = q.get('content_type').lower() + if q.get('payload'): data["payload"] = q["payload"] + if q.get('data'): data["data"] = q["data"] + if q.get('image_url') and _type is not QuickReplyLocation._type: data["image_url"] = q["image_url"] + data["is_response"] = is_response + if _type == QuickReplyText._type: + if q.get('title') is not None: data["title"] = q["title"] + rtn = QuickReplyText(**data) + elif _type == QuickReplyLocation._type: + rtn = QuickReplyLocation(**data) + elif _type == QuickReplyPhoneNumber._type: + rtn = QuickReplyPhoneNumber(**data) + elif _type == QuickReplyEmail._type: + rtn = QuickReplyEmail(**data) + return rtn + def graphql_to_message(message): if message.get('message_sender') is None: message['message_sender'] = {} @@ -290,6 +308,12 @@ def graphql_to_message(message): } if message.get('blob_attachments') is not None: rtn.attachments = [graphql_to_attachment(attachment) for attachment in message['blob_attachments']] + if message.get('platform_xmd_encoded'): + quick_replies = json.loads(message['platform_xmd_encoded']).get('quick_replies') + if isinstance(quick_replies, list): + rtn.quick_replies = [graphql_to_quick_reply(q) for q in quick_replies] + elif isinstance(quick_replies, dict): + rtn.quick_replies = [graphql_to_quick_reply(quick_replies, is_response=True)] if message.get('extensible_attachment') is not None: attachment = graphql_to_extensible_attachment(message['extensible_attachment']) if isinstance(attachment, UnsentMessage): diff --git a/fbchat/models.py b/fbchat/models.py index e6bc193..e126159 100644 --- a/fbchat/models.py +++ b/fbchat/models.py @@ -190,10 +190,12 @@ class Message(object): 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): + 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: @@ -204,6 +206,9 @@ class Message(object): 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 @@ -521,6 +526,73 @@ class Mention(object): 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