diff --git a/README.md b/README.md index eb174cc..d120113 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ FBotics currently supports sending following message types: - Templates - Button Template - Generic Template + - List Template ## Installation @@ -76,4 +77,4 @@ To create coverage report: ```sh make coverage -``` \ No newline at end of file +``` diff --git a/docs/autogen.py b/docs/autogen.py index 29324a5..dc5ff4c 100644 --- a/docs/autogen.py +++ b/docs/autogen.py @@ -18,12 +18,16 @@ EXCLUDE = {} PAGES = [ + { + "page": "list_template/list_template.md", + "classes": [ + fbotics.models.payloads.list_template.ListTemplatePayload, + ], + }, { "page": "generic_template/generic_template.md", "classes": [ fbotics.models.payloads.generic_template.GenericTemplatePayload, - fbotics.models.payloads.generic_template.GenericElement, - fbotics.models.payloads.generic_template.GenericDefaultAction, ], }, { diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 1c207cf..1a5a9ce 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -21,4 +21,7 @@ nav: - Button Template: button_template/examples.md - Generic Template: - Template Payload: generic_template/generic_template.md - - Example: generic_template/example.md \ No newline at end of file + - Example: generic_template/example.md +- List Template: + - Template Payload: list_template/list_template.md + - Example: list_template/example.md diff --git a/docs/templates/generic_template/example.md b/docs/templates/generic_template/example.md index 27ba9fe..88a1d95 100644 --- a/docs/templates/generic_template/example.md +++ b/docs/templates/generic_template/example.md @@ -3,9 +3,7 @@ This is an example to send Generic Templates using FBotics: ```python from fbotics.client import Client from fbotics.models.buttons import WebUrlButton -from fbotics.models.payloads.generic_template import ( - GenericElement, -) +from fbotics.models.payloads.element import Element from fbotics.models.quick_reply import QuickReply client = Client(page_access_token=PAGE_ACCESS_TOKEN) @@ -17,14 +15,14 @@ buttons = [ ) ] -ge = GenericElement( - dict( - title="Title1", - image_url="http://i67.tinypic.com/262vb5l.jpg", - subtitle="Subtitle1", - buttons=buttons, - ) -) +ge = Element( + dict( + title="Title1", + image_url="http://i67.tinypic.com/262vb5l.jpg", + subtitle="Subtitle1", + buttons=buttons, + ) + ) qr1 = QuickReply( dict( @@ -53,4 +51,4 @@ response = client.send_generic_template(

-

\ No newline at end of file +

diff --git a/docs/templates/list_template/example.md b/docs/templates/list_template/example.md new file mode 100644 index 0000000..61d1355 --- /dev/null +++ b/docs/templates/list_template/example.md @@ -0,0 +1,65 @@ +This is an example to send List Templates using FBotics: + +```python +from fbotics.client import Client +from fbotics.models.buttons import WebUrlButton +from fbotics.models.payloads.element Element +from fbotics.models.quick_reply import QuickReply + +client = Client(page_access_token=PAGE_ACCESS_TOKEN) + + +buttons = [ + WebUrlButton( + dict(type="web_url", url="http://www.google.com", title="Web URL Button") + ) + ] + +e1 = Element( + dict( + title="Title1", + image_url="http://i67.tinypic.com/262vb5l.jpg", + subtitle="Subtitle1", + buttons=buttons, + ) + ) + +e2 = Element( + dict( + title="Title1", + image_url="http://i67.tinypic.com/262vb5l.jpg", + subtitle="Subtitle1", + buttons=buttons, + ) + ) + +qr1 = QuickReply( + dict( + content_type="text", + title="Yes", + payload="payload1", + image_url="http://i64.tinypic.com/1hothh.png", + ) +) + +qr2 = QuickReply( + dict( + content_type="text", + title="No", + payload="payload2", + image_url="http://i63.tinypic.com/2pqpbth.png", + ) +) + +response = client.send_generic_template( + recipient_id=RECIPIENT_ID, + quick_replies=[qr1, qr2], + elements=[e1, e2], + buttons=buttons +) +``` + +

+ + +

diff --git a/docs/templates/list_template/list_template.md b/docs/templates/list_template/list_template.md new file mode 100644 index 0000000..84d2e8d --- /dev/null +++ b/docs/templates/list_template/list_template.md @@ -0,0 +1,3 @@ +The list template is a list of 2-4 structured items with an optional global button rendered at the bottom. Each item may contain a thumbnail image, title, subtitle, and one button. You may also specify a default_action object that sets a URL that will be opened in the Messenger webview when the item is tapped. + +{{autogenerated}} diff --git a/fbotics/_version.py b/fbotics/_version.py index 2641d78..6955b74 100644 --- a/fbotics/_version.py +++ b/fbotics/_version.py @@ -120,7 +120,7 @@ def versions_from_parentdir(parentdir_prefix, root, verbose): dirname = os.path.basename(root) if dirname.startswith(parentdir_prefix): return { - "version": dirname[len(parentdir_prefix) :], + "version": dirname[len(parentdir_prefix):], "full-revisionid": None, "dirty": False, "error": None, @@ -132,8 +132,8 @@ def versions_from_parentdir(parentdir_prefix, root, verbose): if verbose: print( - "Tried directories %s but none started with prefix %s" - % (str(rootdirs), parentdir_prefix) + "Tried directories %s but none started with prefix %s" + % (str(rootdirs), parentdir_prefix) ) raise NotThisMethod("rootdir doesn't start with parentdir_prefix") @@ -190,7 +190,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. TAG = "tag: " - tags = set([r[len(TAG) :] for r in refs if r.startswith(TAG)]) + tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) if not tags: # Either we're using git < 1.8.3, or there really are no tags. We use # a heuristic: assume all version tags have a digit. The old git %d @@ -207,7 +207,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): for ref in sorted(tags): # sorting will prefer e.g. "2.0" over "2.0rc1" if ref.startswith(tag_prefix): - r = ref[len(tag_prefix) :] + r = ref[len(tag_prefix):] if verbose: print("picking %s" % r) return { @@ -307,7 +307,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): tag_prefix, ) return pieces - pieces["closest-tag"] = full_tag[len(tag_prefix) :] + pieces["closest-tag"] = full_tag[len(tag_prefix):] # distance: number of commits since tag pieces["distance"] = int(mo.group(2)) diff --git a/fbotics/client/__init__.py b/fbotics/client/__init__.py index cca7a2a..4babc3a 100644 --- a/fbotics/client/__init__.py +++ b/fbotics/client/__init__.py @@ -6,6 +6,7 @@ from fbotics.models.message import Message from fbotics.models.payloads.button_template import ButtonTemplatePayload from fbotics.models.payloads.generic_template import GenericTemplatePayload +from fbotics.models.payloads.list_template import ListTemplatePayload from fbotics.models.payloads.rich_media import RichMediaPayload from fbotics.models.recipient import Recipient @@ -17,13 +18,13 @@ def __init__(self, page_access_token=None): self.page_access_token = page_access_token def send_button_template( - self, - recipient_id=None, - user_ref=None, - phone_number=None, - text=None, - quick_replies=None, - buttons=None, + self, + recipient_id=None, + user_ref=None, + phone_number=None, + text=None, + quick_replies=None, + buttons=None, ): """Sends a button template to the recipient. @@ -46,12 +47,12 @@ def send_button_template( return response def send_generic_template( - self, - recipient_id=None, - user_ref=None, - phone_number=None, - elements=None, - quick_replies=None, + self, + recipient_id=None, + user_ref=None, + phone_number=None, + elements=None, + quick_replies=None, ): """Sends a generic template to the recipient. @@ -60,7 +61,7 @@ def send_generic_template( user_ref: optional. user_ref from the checkbox plugin phone_number: Optional. Phone number of the recipient with the format +1(212)555-2368. Your bot must be approved for Customer Matching to send messages this way. elements: An array of element objects that describe instances of the generic template to be sent. Specifying multiple elements will send a horizontally scrollable carousel of templates. A maximum of 10 elements is supported. - buttons: Set of 1-3 buttons that appear as call-to-actions. + quick_replies: An array of objects the describe the quick reply buttons to send. A maximum of 11 quick replies are supported. """ @@ -73,13 +74,43 @@ def send_generic_template( response = self._post(message, recipient_id, user_ref, phone_number) return response + def send_list_template( + self, + recipient_id=None, + user_ref=None, + phone_number=None, + elements=None, + buttons=None, + quick_replies=None + ): + """Sends a list template to the recipient. + + # Arguments + recipient_id: page specific id of the recipient + user_ref: optional. user_ref from the checkbox plugin + phone_number: Optional. Phone number of the recipient with the format +1(212)555-2368. Your bot must be approved for Customer Matching to send messages this way. + elements: Array of objects that describe items in the list. Minimum of 2 elements required. Maximum of 4 elements is supported. + buttons: Button to display at the bottom of the list. Maximum of 1 button is supported. + quick_replies: An array of objects the describe the quick reply buttons to send. A maximum of 11 quick replies are supported. + + """ + + list_template_payload = ListTemplatePayload( + dict(template_type="list", elements=elements, buttons=buttons) + ) + attachment = Attachment(dict(type="template", payload=list_template_payload)) + message = Message({"quick_replies": quick_replies, "attachment": attachment}) + + response = self._post(message, recipient_id, user_ref, phone_number) + return response + def send_quick_replies( - self, - recipient_id=None, - user_ref=None, - phone_number=None, - text=None, - quick_replies=None, + self, + recipient_id=None, + user_ref=None, + phone_number=None, + text=None, + quick_replies=None, ): """Sends quick replies to the recipient. @@ -116,12 +147,12 @@ def send_text(self, recipient_id=None, user_ref=None, phone_number=None, text=No return response def send_image( - self, - recipient_id=None, - user_ref=None, - phone_number=None, - url=None, - quick_replies=None, + self, + recipient_id=None, + user_ref=None, + phone_number=None, + url=None, + quick_replies=None, ): """Sends an image to the recipient. @@ -140,12 +171,12 @@ def send_image( return response def send_audio( - self, - recipient_id=None, - user_ref=None, - phone_number=None, - url=None, - quick_replies=None, + self, + recipient_id=None, + user_ref=None, + phone_number=None, + url=None, + quick_replies=None, ): """Sends an audio to the recipient. @@ -164,12 +195,12 @@ def send_audio( return response def send_file( - self, - recipient_id=None, - user_ref=None, - phone_number=None, - url=None, - quick_replies=None, + self, + recipient_id=None, + user_ref=None, + phone_number=None, + url=None, + quick_replies=None, ): """Sends a file to the recipient. @@ -210,8 +241,8 @@ def _post(self, message, recipient_id=None, user_ref=None, phone_number=None): response = requests.post(API_URL, params=params, json=request.to_primitive()) json_response = response.json() if ( - response.status_code == 400 - and json_response.get("error", {}).get("type", "") == "OAuthException" + response.status_code == 400 + and json_response.get("error", {}).get("type", "") == "OAuthException" ): raise OAuthException(json_response.get("error").get("message", "")) return response diff --git a/fbotics/models/attachment.py b/fbotics/models/attachment.py index a77cd25..13386fe 100644 --- a/fbotics/models/attachment.py +++ b/fbotics/models/attachment.py @@ -4,12 +4,15 @@ from fbotics.models.payloads.button_template import ButtonTemplatePayload from fbotics.models.payloads.generic_template import GenericTemplatePayload +from fbotics.models.payloads.list_template import ListTemplatePayload from fbotics.models.payloads.rich_media import RichMediaPayload def payload_claim_function(field, data): if "url" in data and field.name == "payload": return RichMediaPayload + if "top_element_style" in data and field.name == "payload": + return ListTemplatePayload if "elements" in data and field.name == "payload": return GenericTemplatePayload if "text" in data and field.name == "payload": @@ -31,6 +34,6 @@ class Attachment(Model): required=True, choices=["image", "audio", "video", "file", "template"] ) payload = PolyModelType( - [RichMediaPayload, GenericTemplatePayload, ButtonTemplatePayload], + [RichMediaPayload, GenericTemplatePayload, ButtonTemplatePayload, ListTemplatePayload], claim_function=payload_claim_function, ) diff --git a/fbotics/models/payloads/element.py b/fbotics/models/payloads/element.py new file mode 100644 index 0000000..6275c5f --- /dev/null +++ b/fbotics/models/payloads/element.py @@ -0,0 +1,55 @@ +from schematics import Model +from schematics.types import StringType, ListType, ModelType +from schematics.types.compound import PolyModelType + +from fbotics.models.buttons import PostbackButton, WebUrlButton + + +def button_claim_function(field, data): + if "url" in data: + return WebUrlButton + if "payload" in data: + return PostbackButton + else: + return None + + +class DefaultAction(Model): + """The default action executed when the template is tapped. + + # Arguments + type: Type of button. Must be web_url. + url: This URL is opened in a mobile browser when the button is tapped. Must use HTTPS protocol if messenger_extensions is true. + webview_height_ratio: Optional. Height of the Webview. Valid values: compact, tall, full. Defaults to full. + + """ + + type = StringType(required=True, default="web_url", choices=["web_url"]) + webview_height_ratio = StringType( + required=False, default="full", choices=["compact", "tall", "full"] + ) + url = StringType() + + +class Element(Model): + """The generic template supports a maximum of 10 elements per message. At least one property must be set in addition to title. + + # Arguments + title: The title to display in the template. 80 character limit. + subtitle: Optional. The subtitle to display in the template. 80 character limit. + image_url: Optional. The URL of the image to display in the template. + default_action: Optional. The default action executed when the template is tapped. Accepts the same properties as URL button, except title. + buttons: Optional. An array of buttons to append to the template. A maximum of 3 buttons per element is supported. + + """ + + title = StringType(required=True, max_length=80) + image_url = StringType(required=False, max_length=80) + subtitle = StringType(required=False) + default_action = ModelType(DefaultAction, required=False) + buttons = ListType( + PolyModelType( + [PostbackButton, WebUrlButton], claim_function=button_claim_function + ), + max_size=3, + ) diff --git a/fbotics/models/payloads/generic_template.py b/fbotics/models/payloads/generic_template.py index 328eb47..5001e1a 100644 --- a/fbotics/models/payloads/generic_template.py +++ b/fbotics/models/payloads/generic_template.py @@ -1,8 +1,8 @@ from schematics import Model from schematics.types import StringType, ListType, ModelType, BooleanType -from schematics.types.compound import PolyModelType from fbotics.models.buttons import PostbackButton, WebUrlButton +from fbotics.models.payloads.element import Element def button_claim_function(field, data): @@ -14,47 +14,6 @@ def button_claim_function(field, data): return None -class GenericDefaultAction(Model): - """The default action executed when the template is tapped. - - # Arguments - type: Type of button. Must be web_url. - url: This URL is opened in a mobile browser when the button is tapped. Must use HTTPS protocol if messenger_extensions is true. - webview_height_ratio: Optional. Height of the Webview. Valid values: compact, tall, full. Defaults to full. - - """ - - type = StringType(required=True, default="web_url", choices=["web_url"]) - webview_height_ratio = StringType( - required=False, default="full", choices=["compact", "tall", "full"] - ) - url = StringType() - - -class GenericElement(Model): - """The generic template supports a maximum of 10 elements per message. At least one property must be set in addition to title. - - # Arguments - title: The title to display in the template. 80 character limit. - subtitle: Optional. The subtitle to display in the template. 80 character limit. - image_url: Optional. The URL of the image to display in the template. - default_action: Optional. The default action executed when the template is tapped. Accepts the same properties as URL button, except title. - buttons: Optional. An array of buttons to append to the template. A maximum of 3 buttons per element is supported. - - """ - - title = StringType(required=True, max_length=80) - image_url = StringType(required=False, max_length=80) - subtitle = StringType(required=False) - default_action = ModelType(GenericDefaultAction, required=False) - buttons = ListType( - PolyModelType( - [PostbackButton, WebUrlButton], claim_function=button_claim_function - ), - max_size=3, - ) - - class GenericTemplatePayload(Model): """The generic template is a simple structured message that includes a title, subtitle, image, and up to three buttons. You may also specify a default_action object that sets a URL that will be opened in the Messenger webview when the template is tapped. @@ -67,4 +26,4 @@ class GenericTemplatePayload(Model): template_type = StringType(required=False, default="generic", choices=["generic"]) sharable = BooleanType(default=False) - elements = ListType(ModelType(GenericElement), max_size=10) + elements = ListType(ModelType(Element), max_size=10) diff --git a/fbotics/models/payloads/list_template.py b/fbotics/models/payloads/list_template.py new file mode 100644 index 0000000..bcd40cc --- /dev/null +++ b/fbotics/models/payloads/list_template.py @@ -0,0 +1,39 @@ +from schematics import Model +from schematics.types import StringType, ListType, ModelType, BooleanType +from schematics.types.compound import PolyModelType + +from fbotics.models.buttons import PostbackButton, WebUrlButton +from fbotics.models.payloads.element import Element + + +def button_claim_function(field, data): + if "url" in data: + return WebUrlButton + if "payload" in data: + return PostbackButton + else: + return None + + +class ListTemplatePayload(Model): + """The list template is a list of 2-4 structured items with an optional global button rendered at the bottom. Each item may contain a thumbnail image, title, subtitle, and one button. You may also specify a default_action object that sets a URL that will be opened in the Messenger webview when the item is tapped. + + # Arguments + template_type: Value must be list. + top_element_style: Optional. Sets the format of the first list items. Messenger web client currently only renders compact. + elements: Array of objects that describe items in the list. Minimum of 2 elements required. Maximum of 4 elements is supported. + shareable: Optional. Set to true to enable the native share button in Messenger for the template message. Defaults to false. + buttons: Optional. Button to display at the bottom of the list. Maximum of 1 button is supported. + + """ + + template_type = StringType(required=False, default="list", choices=["list"]) + top_element_style = StringType(required=False, default="compact", choices=["compact", "large"]) + elements = ListType(ModelType(Element), min_size=2, max_size=4) + sharable = BooleanType(default=False) + buttons = ListType( + PolyModelType( + [PostbackButton, WebUrlButton], claim_function=button_claim_function + ), + max_size=1, + ) diff --git a/fbotics/models/quick_reply.py b/fbotics/models/quick_reply.py index a224358..3b43880 100644 --- a/fbotics/models/quick_reply.py +++ b/fbotics/models/quick_reply.py @@ -29,9 +29,9 @@ def validate_content_type(self, data, value): if data["content_type"] == "text" and not data["title"]: raise ValidationError("Field title is required when content_type is text") if ( - data["content_type"] == "text" - and data["image_url"] - and not data.get("payload", "") + data["content_type"] == "text" + and data["image_url"] + and not data.get("payload", "") ): raise ValidationError( "When content_type is text and image_url is set, payload should be set to at least an empty string." diff --git a/fbotics/tests/client/test_send_generic_template.py b/fbotics/tests/client/test_send_generic_template.py index 29b13e3..e88d13a 100644 --- a/fbotics/tests/client/test_send_generic_template.py +++ b/fbotics/tests/client/test_send_generic_template.py @@ -1,5 +1,5 @@ from fbotics.models.buttons import WebUrlButton -from fbotics.models.payloads.generic_template import GenericElement +from fbotics.models.payloads.generic_template import Element from fbotics.models.quick_reply import QuickReply @@ -16,7 +16,7 @@ def test_send_generic_template_returns_200_status_code(client, recipient_id): ) ] - ge = GenericElement( + ge = Element( dict( title="Title1", image_url="http://i67.tinypic.com/262vb5l.jpg", diff --git a/fbotics/tests/client/test_send_list_template.py b/fbotics/tests/client/test_send_list_template.py new file mode 100644 index 0000000..62d07ed --- /dev/null +++ b/fbotics/tests/client/test_send_list_template.py @@ -0,0 +1,58 @@ +from fbotics.models.buttons import WebUrlButton +from fbotics.models.payloads.element import Element +from fbotics.models.quick_reply import QuickReply + + +def test_send_list_template_returns_200_status_code(client, recipient_id): + """ + GIVEN a client and a recipient id + WHEN a list template is sent to the recipient + THEN the status code of the response is 200 + """ + + buttons = [ + WebUrlButton( + dict(type="web_url", url="http://www.google.com", title="Web URL Button") + ) + ] + + e1 = Element( + dict( + title="Title1", + image_url="http://i67.tinypic.com/262vb5l.jpg", + subtitle="Subtitle1", + buttons=buttons, + ) + ) + + e2 = Element( + dict( + title="Title1", + image_url="http://i67.tinypic.com/262vb5l.jpg", + subtitle="Subtitle1", + buttons=buttons, + ) + ) + + qr1 = QuickReply( + dict( + content_type="text", + title="Yes", + payload="payload1", + image_url="http://i64.tinypic.com/1hothh.png", + ) + ) + + qr2 = QuickReply( + dict( + content_type="text", + title="No", + payload="payload2", + image_url="http://i63.tinypic.com/2pqpbth.png", + ) + ) + + response = client.send_list_template( + recipient_id=recipient_id, quick_replies=[qr1, qr2], elements=[e1, e2], buttons=buttons + ) + assert response.status_code == 200 diff --git a/fbotics/tests/client/test_send_text.py b/fbotics/tests/client/test_send_text.py index b14e175..a491575 100644 --- a/fbotics/tests/client/test_send_text.py +++ b/fbotics/tests/client/test_send_text.py @@ -1,4 +1,5 @@ import pytest + from fbotics.client.exceptions import OAuthException from fbotics.tests import ANY @@ -26,7 +27,7 @@ def test_exception_when_sending_text_message_to_invalid_recipient(client): def test_response_content_when_sending_text_message_to_valid_recipient( - client, recipient_id + client, recipient_id ): """ GIVEN a client and a recipient id diff --git a/fbotics/tests/models/test_quick_reply.py b/fbotics/tests/models/test_quick_reply.py index 5a29b7f..167d08a 100644 --- a/fbotics/tests/models/test_quick_reply.py +++ b/fbotics/tests/models/test_quick_reply.py @@ -16,7 +16,7 @@ def test_validation_when_content_type_is_text_and_title_does_not_exist(client): def test_validation_when_content_type_is_text_and_image_url_is_set_but_payload_is_not_set( - client + client ): """ GIVEN a QuickReply object with a text content type and an image url, but without a payload @@ -29,7 +29,7 @@ def test_validation_when_content_type_is_text_and_image_url_is_set_but_payload_i def test_validation_when_content_type_is_text_and_title_is_empty_and_image_url_is_not_set( - client + client ): """ GIVEN a QuickReply object with a text content type and an empty title, but without an image_url