Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Quick replies #342

Merged
merged 15 commits into from
Jan 31, 2019
73 changes: 73 additions & 0 deletions fbchat/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -984,6 +984,30 @@ def fetchPlanInfo(self, plan_id):
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
"""
Expand Down Expand Up @@ -1040,6 +1064,25 @@ def _getSendData(self, message=None, thread_id=None, thread_type=ThreadType.USER
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):
Expand Down Expand Up @@ -1111,6 +1154,36 @@ def wave(self, wave_first=True, thread_id=None, thread_type=None):
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 <intro_message_ids>` 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)
Expand Down
24 changes: 24 additions & 0 deletions fbchat/graphql.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,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'] = {}
Expand All @@ -288,6 +306,12 @@ def graphql_to_message(message):
rtn.reactions = {str(r['user']['id']):MessageReaction(r['reaction']) for r in message.get('message_reactions')}
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):
Expand Down
74 changes: 73 additions & 1 deletion fbchat/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -204,6 +206,9 @@ def __init__(self, text=None, mentions=None, emoji_size=None, sticker=None, atta
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
Expand Down Expand Up @@ -521,6 +526,73 @@ def __repr__(self):
def __unicode__(self):
return '<Mention {}: offset={} length={}>'.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
Expand Down