Repository: line/line-bot-sdk-python
Pull request #120 (2018-07-31 08:59:09)

Implement quick reply
https://github.com/line/line-bot-sdk-python/issues/119

be-hase: Thanks :)
DongShanHu: Hello , I want to ask some questions
when I want to test quick reply function, I use "from linebot.model import * ,and follow your step to do, but the error is the quick reply is not defined......???

be-hase: @DongShanHu 
This quick reply feature is not released yet. please wait.

Patch:
diff --git a/README.rst b/README.rst
index 5904bcc..26e1402 100644
--- a/README.rst
+++ b/README.rst
@@ -600,6 +600,16 @@ TemplateSendMessage - ImageCarouselTemplate
         )
     )
 
+With QuickReply
+^^^^^^^^^^^^^^^
+
+.. code:: python
+
+    text_message = TextSendMessage(text='Hello, world',
+                                   quick_reply=QuickReply(items=[
+                                       QuickReplyButton(action=MessageAction(label="label", text="text"))
+                                   ]))
+
 Webhook
 -------
 
diff --git a/examples/flask-kitchensink/app.py b/examples/flask-kitchensink/app.py
index ead9608..d50ea97 100644
--- a/examples/flask-kitchensink/app.py
+++ b/examples/flask-kitchensink/app.py
@@ -19,6 +19,7 @@ import os
 import sys
 import tempfile
 from argparse import ArgumentParser
+
 from flask import Flask, request, abort
 
 from linebot import (
@@ -33,13 +34,14 @@ from linebot.models import (
     TemplateSendMessage, ConfirmTemplate, MessageAction,
     ButtonsTemplate, ImageCarouselTemplate, ImageCarouselColumn, URIAction,
     PostbackAction, DatetimePickerAction,
+    CameraAction, CameraRollAction, LocationAction,
     CarouselTemplate, CarouselColumn, PostbackEvent,
     StickerMessage, StickerSendMessage, LocationMessage, LocationSendMessage,
     ImageMessage, VideoMessage, AudioMessage, FileMessage,
     UnfollowEvent, FollowEvent, JoinEvent, LeaveEvent, BeaconEvent,
     FlexSendMessage, BubbleContainer, ImageComponent, BoxComponent,
     TextComponent, SpacerComponent, IconComponent, ButtonComponent,
-    SeparatorComponent,
+    SeparatorComponent, QuickReply, QuickReplyButton
 )
 
 app = Flask(__name__)
@@ -278,6 +280,34 @@ def handle_text_message(event):
             event.reply_token,
             message
         )
+    elif text == 'quick_reply':
+        line_bot_api.reply_message(
+            event.reply_token,
+            TextSendMessage(
+                text='Quick reply',
+                quick_reply=QuickReply(
+                    items=[
+                        QuickReplyButton(
+                            action=PostbackAction(label="label1", data="data1")
+                        ),
+                        QuickReplyButton(
+                            action=MessageAction(label="label2", text="text2")
+                        ),
+                        QuickReplyButton(
+                            action=DatetimePickerAction(label="label3",
+                                                        data="data3",
+                                                        mode="date")
+                        ),
+                        QuickReplyButton(
+                            action=CameraAction(label="label4")
+                        ),
+                        QuickReplyButton(
+                            action=CameraRollAction(label="label5")
+                        ),
+                        QuickReplyButton(
+                            action=LocationAction(label="label6")
+                        ),
+                    ])))
     else:
         line_bot_api.reply_message(
             event.reply_token, TextSendMessage(text=event.message.text))
diff --git a/linebot/__about__.py b/linebot/__about__.py
index add47f4..04f5347 100644
--- a/linebot/__about__.py
+++ b/linebot/__about__.py
@@ -16,7 +16,7 @@
 
 from __future__ import unicode_literals
 
-__version__ = '1.7.2'
+__version__ = '1.8.0'
 __author__ = 'LINE Corporation'
 __copyright__ = 'Copyright 2016, LINE Corporation'
 __license__ = 'Apache 2.0'
diff --git a/linebot/models/__init__.py b/linebot/models/__init__.py
index d322b91..819a993 100644
--- a/linebot/models/__init__.py
+++ b/linebot/models/__init__.py
@@ -20,6 +20,9 @@ from .actions import (  # noqa
     MessageAction,
     URIAction,
     DatetimePickerAction,
+    CameraAction,
+    CameraRollAction,
+    LocationAction,
     Action as TemplateAction,  # backward compatibility
     PostbackAction as PostbackTemplateAction,  # backward compatibility
     MessageAction as MessageTemplateAction,  # backward compatibility
@@ -47,6 +50,23 @@ from .events import (  # noqa
     Beacon,
     Link,
 )
+from .flex_message import (  # noqa
+    FlexSendMessage,
+    FlexContainer,
+    BubbleContainer,
+    BubbleStyle,
+    BlockStyle,
+    CarouselContainer,
+    FlexComponent,
+    BoxComponent,
+    ButtonComponent,
+    FillerComponent,
+    IconComponent,
+    ImageComponent,
+    SeparatorComponent,
+    SpacerComponent,
+    TextComponent
+)
 from .imagemap import (  # noqa
     ImagemapSendMessage,
     BaseSize,
@@ -86,6 +106,8 @@ from .send_messages import (  # noqa
     AudioSendMessage,
     LocationSendMessage,
     StickerSendMessage,
+    QuickReply,
+    QuickReplyButton,
 )
 from .sources import (  # noqa
     Source,
@@ -103,20 +125,3 @@ from .template import (  # noqa
     ImageCarouselTemplate,
     ImageCarouselColumn,
 )
-from .flex_message import (  # noqa
-    FlexSendMessage,
-    FlexContainer,
-    BubbleContainer,
-    BubbleStyle,
-    BlockStyle,
-    CarouselContainer,
-    FlexComponent,
-    BoxComponent,
-    ButtonComponent,
-    FillerComponent,
-    IconComponent,
-    ImageComponent,
-    SeparatorComponent,
-    SpacerComponent,
-    TextComponent
-)
diff --git a/linebot/models/actions.py b/linebot/models/actions.py
index 2755d2d..07143dd 100644
--- a/linebot/models/actions.py
+++ b/linebot/models/actions.py
@@ -30,7 +30,10 @@ def get_action(action):
             'postback': PostbackAction,
             'message': MessageAction,
             'uri': URIAction,
-            'datetimepicker': DatetimePickerAction
+            'datetimepicker': DatetimePickerAction,
+            'camera': CameraAction,
+            'cameraRoll': CameraRollAction,
+            'location': LocationAction,
         }
     )
     return action_obj
@@ -176,3 +179,69 @@ class DatetimePickerAction(Action):
         self.initial = initial
         self.max = max
         self.min = min
+
+
+class CameraAction(Action):
+    """CameraAction.
+
+    https://developers.line.me/en/reference/messaging-api/#camera-action
+
+    This action can be configured only with quick reply buttons.
+    When a button associated with this action is tapped,
+    the camera screen in the LINE app is opened.
+    """
+
+    def __init__(self, label=None, **kwargs):
+        """__init__ method.
+
+        :param str label: Label for the action
+        :param kwargs:
+        """
+        super(CameraAction, self).__init__(**kwargs)
+
+        self.type = 'camera'
+        self.label = label
+
+
+class CameraRollAction(Action):
+    """CameraRollAction.
+
+    https://developers.line.me/en/reference/messaging-api/#camera-roll-action
+
+    This action can be configured only with quick reply buttons.
+    When a button associated with this action is tapped,
+    the camera roll screen in the LINE app is opened.
+    """
+
+    def __init__(self, label=None, **kwargs):
+        """__init__ method.
+
+        :param str label: Label for the action
+        :param kwargs:
+        """
+        super(CameraRollAction, self).__init__(**kwargs)
+
+        self.type = 'cameraRoll'
+        self.label = label
+
+
+class LocationAction(Action):
+    """LocationRollAction.
+
+    https://developers.line.me/en/reference/messaging-api/#location-action
+
+    This action can be configured only with quick reply buttons.
+    When a button associated with this action is tapped,
+    the location screen in the LINE app is opened.
+    """
+
+    def __init__(self, label=None, **kwargs):
+        """__init__ method.
+
+        :param str label: Label for the action
+        :param kwargs:
+        """
+        super(LocationAction, self).__init__(**kwargs)
+
+        self.type = 'location'
+        self.label = label
diff --git a/linebot/models/send_messages.py b/linebot/models/send_messages.py
index fe55eaa..855e03d 100644
--- a/linebot/models/send_messages.py
+++ b/linebot/models/send_messages.py
@@ -20,20 +20,24 @@ from abc import ABCMeta
 
 from future.utils import with_metaclass
 
+from .actions import get_action
 from .base import Base
 
 
 class SendMessage(with_metaclass(ABCMeta, Base)):
     """Abstract Base Class of SendMessage."""
 
-    def __init__(self, **kwargs):
+    def __init__(self, quick_reply=None, **kwargs):
         """__init__ method.
 
+        :param quick_reply: QuickReply object
+        :type quick_reply: T <= :py:class:`linebot.models.send_messages.QuickReply`
         :param kwargs:
         """
         super(SendMessage, self).__init__(**kwargs)
 
         self.type = None
+        self.quick_reply = self.get_or_new_from_json_dict(quick_reply, QuickReply)
 
 
 class TextSendMessage(SendMessage):
@@ -42,13 +46,15 @@ class TextSendMessage(SendMessage):
     https://devdocs.line.me/en/#text
     """
 
-    def __init__(self, text=None, **kwargs):
+    def __init__(self, text=None, quick_reply=None, **kwargs):
         """__init__ method.
 
         :param str text: Message text
+        :param quick_reply: QuickReply object
+        :type quick_reply: T <= :py:class:`linebot.models.send_messages.QuickReply`
         :param kwargs:
         """
-        super(TextSendMessage, self).__init__(**kwargs)
+        super(TextSendMessage, self).__init__(quick_reply=quick_reply, **kwargs)
 
         self.type = 'text'
         self.text = text
@@ -60,7 +66,8 @@ class ImageSendMessage(SendMessage):
     https://devdocs.line.me/en/#image
     """
 
-    def __init__(self, original_content_url=None, preview_image_url=None, **kwargs):
+    def __init__(self, original_content_url=None, preview_image_url=None,
+                 quick_reply=None, **kwargs):
         """__init__ method.
 
         :param str original_content_url: Image URL.
@@ -73,9 +80,11 @@ class ImageSendMessage(SendMessage):
             JPEG
             Max: 240 x 240
             Max: 1 MB
+        :param quick_reply: QuickReply object
+        :type quick_reply: T <= :py:class:`linebot.models.send_messages.QuickReply`
         :param kwargs:
         """
-        super(ImageSendMessage, self).__init__(**kwargs)
+        super(ImageSendMessage, self).__init__(quick_reply=quick_reply, **kwargs)
 
         self.type = 'image'
         self.original_content_url = original_content_url
@@ -88,7 +97,8 @@ class VideoSendMessage(SendMessage):
     https://devdocs.line.me/en/#video
     """
 
-    def __init__(self, original_content_url=None, preview_image_url=None, **kwargs):
+    def __init__(self, original_content_url=None, preview_image_url=None,
+                 quick_reply=None, **kwargs):
         """__init__ method.
 
         :param str original_content_url: URL of video file.
@@ -101,9 +111,11 @@ class VideoSendMessage(SendMessage):
             JPEG
             Max: 240 x 240
             Max: 1 MB
+        :param quick_reply: QuickReply object
+        :type quick_reply: T <= :py:class:`linebot.models.send_messages.QuickReply`
         :param kwargs:
         """
-        super(VideoSendMessage, self).__init__(**kwargs)
+        super(VideoSendMessage, self).__init__(quick_reply=quick_reply, **kwargs)
 
         self.type = 'video'
         self.original_content_url = original_content_url
@@ -116,7 +128,7 @@ class AudioSendMessage(SendMessage):
     https://devdocs.line.me/en/#audio
     """
 
-    def __init__(self, original_content_url=None, duration=None, **kwargs):
+    def __init__(self, original_content_url=None, duration=None, quick_reply=None, **kwargs):
         """__init__ method.
 
         :param str original_content_url: URL of audio file.
@@ -125,9 +137,11 @@ class AudioSendMessage(SendMessage):
             Less than 1 minute
             Max 10 MB
         :param long duration: Length of audio file (milliseconds).
+        :param quick_reply: QuickReply object
+        :type quick_reply: T <= :py:class:`linebot.models.send_messages.QuickReply`
         :param kwargs:
         """
-        super(AudioSendMessage, self).__init__(**kwargs)
+        super(AudioSendMessage, self).__init__(quick_reply=quick_reply, **kwargs)
 
         self.type = 'audio'
         self.original_content_url = original_content_url
@@ -140,16 +154,19 @@ class LocationSendMessage(SendMessage):
     https://devdocs.line.me/en/#location
     """
 
-    def __init__(self, title=None, address=None, latitude=None, longitude=None, **kwargs):
+    def __init__(self, title=None, address=None, latitude=None, longitude=None,
+                 quick_reply=None, **kwargs):
         """__init__ method.
 
         :param str title: Title
         :param str address: Address
         :param float latitude: Latitude
         :param float longitude: Longitude
+        :param quick_reply: QuickReply object
+        :type quick_reply: T <= :py:class:`linebot.models.send_messages.QuickReply`
         :param kwargs:
         """
-        super(LocationSendMessage, self).__init__(**kwargs)
+        super(LocationSendMessage, self).__init__(quick_reply=quick_reply, **kwargs)
 
         self.type = 'location'
         self.title = title
@@ -164,15 +181,63 @@ class StickerSendMessage(SendMessage):
     https://devdocs.line.me/en/#sticker
     """
 
-    def __init__(self, package_id=None, sticker_id=None, **kwargs):
+    def __init__(self, package_id=None, sticker_id=None, quick_reply=None, **kwargs):
         """__init__ method.
 
         :param str package_id: Package ID
         :param str sticker_id: Sticker ID
+        :param quick_reply: QuickReply object
+        :type quick_reply: T <= :py:class:`linebot.models.send_messages.QuickReply`
         :param kwargs:
         """
-        super(StickerSendMessage, self).__init__(**kwargs)
+        super(StickerSendMessage, self).__init__(quick_reply=quick_reply, **kwargs)
 
         self.type = 'sticker'
         self.package_id = package_id
         self.sticker_id = sticker_id
+
+
+class QuickReply(with_metaclass(ABCMeta, Base)):
+    """QuickReply.
+
+    https://developers.line.me/en/docs/messaging-api/using-quick-reply/
+    """
+
+    def __init__(self, items=None, **kwargs):
+        """__init__ method.
+
+        :param items: Quick reply button objects
+        :type items: list[T <= :py:class:`linebot.models.send_messages.QuickReplyButton`]
+        :param kwargs:
+        """
+        super(QuickReply, self).__init__(**kwargs)
+
+        new_items = []
+        if items:
+            for item in items:
+                new_items.append(self.get_or_new_from_json_dict(
+                    item, QuickReplyButton
+                ))
+        self.items = new_items
+
+
+class QuickReplyButton(with_metaclass(ABCMeta, Base)):
+    """QuickReplyButton.
+
+    https://developers.line.me/en/reference/messaging-api/#items-object
+    """
+
+    def __init__(self, image_url=None, action=None, **kwargs):
+        """__init__ method.
+
+        :param str image_url: URL of the icon that is displayed
+            at the beginning of the button
+        :param action: Action performed when this button is tapped
+        :type action: T <= :py:class:`linebot.models.actions.Action`
+        :param kwargs:
+        """
+        super(QuickReplyButton, self).__init__(**kwargs)
+
+        self.type = 'action'
+        self.image_url = image_url
+        self.action = get_action(action)


Test patch:
diff --git a/tests/api/test_send_text_message_with_quick_reply.py b/tests/api/test_send_text_message_with_quick_reply.py
new file mode 100644
index 0000000..1ff8f5e
--- /dev/null
+++ b/tests/api/test_send_text_message_with_quick_reply.py
@@ -0,0 +1,146 @@
+# -*- coding: utf-8 -*-
+
+#  Licensed under the Apache License, Version 2.0 (the "License"); you may
+#  not use this file except in compliance with the License. You may obtain
+#  a copy of the License at
+#
+#       https://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#  License for the specific language governing permissions and limitations
+#  under the License.
+
+from __future__ import unicode_literals, absolute_import
+
+import json
+import unittest
+
+import responses
+
+from linebot import (
+    LineBotApi
+)
+from linebot.models import (
+    TextSendMessage, QuickReply, QuickReplyButton,
+    PostbackAction, MessageAction, DatetimePickerAction,
+    CameraAction, CameraRollAction, LocationAction
+)
+
+
+class TestLineBotApi(unittest.TestCase):
+    def setUp(self):
+        self.tested = LineBotApi('channel_secret')
+
+        # test data
+        self.text_message = TextSendMessage(text='Hello, world')
+        self.message = [{"type": "text", "text": "Hello, world"}]
+
+    @responses.activate
+    def test_push_text_message_with_quick_reply(self):
+        responses.add(
+            responses.POST,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/message/push',
+            json={}, status=200
+        )
+
+        self.tested.push_message('to',
+                                 TextSendMessage(
+                                     text='Hello, world',
+                                     quick_reply=QuickReply(items=[
+                                         QuickReplyButton(
+                                             image_url='https://example.com',
+                                             action=PostbackAction(label="label1", data="data1")
+                                         ),
+                                         QuickReplyButton(
+                                             action=MessageAction(label="label2", text="text2")
+                                         ),
+                                         QuickReplyButton(
+                                             action=DatetimePickerAction(label="label3",
+                                                                         data="data3",
+                                                                         mode="date")
+                                         ),
+                                         QuickReplyButton(
+                                             action=CameraAction(label="label4")
+                                         ),
+                                         QuickReplyButton(
+                                             action=CameraRollAction(label="label5")
+                                         ),
+                                         QuickReplyButton(
+                                             action=LocationAction(label="label6")
+                                         ),
+                                     ])))
+
+        request = responses.calls[0].request
+        self.assertEqual(request.method, 'POST')
+        self.assertEqual(
+            request.url,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/message/push')
+        self.assertEqual(
+            json.loads(request.body),
+            {
+                "to": "to",
+                "messages": [
+                    {
+                        "type": "text",
+                        "text": "Hello, world",
+                        "quickReply": {
+                            "items": [
+                                {
+                                    "type": "action",
+                                    "imageUrl": "https://example.com",
+                                    "action": {
+                                        "type": "postback",
+                                        "label": "label1",
+                                        "data": "data1",
+                                    }
+                                },
+                                {
+                                    "type": "action",
+                                    "action": {
+                                        "type": "message",
+                                        "label": "label2",
+                                        "text": "text2",
+                                    }
+                                },
+                                {
+                                    "type": "action",
+                                    "action": {
+                                        "type": "datetimepicker",
+                                        "label": "label3",
+                                        "data": "data3",
+                                        "mode": "date",
+                                    }
+                                },
+                                {
+                                    "type": "action",
+                                    "action": {
+                                        "type": "camera",
+                                        "label": "label4",
+                                    }
+                                },
+                                {
+                                    "type": "action",
+                                    "action": {
+                                        "type": "cameraRoll",
+                                        "label": "label5",
+                                    }
+                                },
+                                {
+                                    "type": "action",
+                                    "action": {
+                                        "type": "location",
+                                        "label": "label6",
+                                    }
+                                },
+                            ]
+                        }
+                    }
+                ]
+            }
+        )
+
+
+if __name__ == '__main__':
+    unittest.main()