OSDN Git Service

library install
authoryamat0jp <yamat0jp@yahoo.co.jp>
Sat, 1 Sep 2018 05:49:37 +0000 (14:49 +0900)
committeryamat0jp <yamat0jp@yahoo.co.jp>
Sat, 1 Sep 2018 05:49:37 +0000 (14:49 +0900)
23 files changed:
bot.py
linebot/__about__.py [new file with mode: 0644]
linebot/__init__.py [new file with mode: 0644]
linebot/api.py [new file with mode: 0644]
linebot/exceptions.py [new file with mode: 0644]
linebot/http_client.py [new file with mode: 0644]
linebot/models/__init__.py [new file with mode: 0644]
linebot/models/actions.py [new file with mode: 0644]
linebot/models/base.py [new file with mode: 0644]
linebot/models/error.py [new file with mode: 0644]
linebot/models/events.py [new file with mode: 0644]
linebot/models/flex_message.py [new file with mode: 0644]
linebot/models/imagemap.py [new file with mode: 0644]
linebot/models/messages.py [new file with mode: 0644]
linebot/models/responses.py [new file with mode: 0644]
linebot/models/rich_menu.py [new file with mode: 0644]
linebot/models/send_messages.py [new file with mode: 0644]
linebot/models/sources.py [new file with mode: 0644]
linebot/models/template.py [new file with mode: 0644]
linebot/utils.py [new file with mode: 0644]
linebot/webhook.py [new file with mode: 0644]
requirements.txt
runtime.txt [deleted file]

diff --git a/bot.py b/bot.py
index de0b516..d187306 100644 (file)
--- a/bot.py
+++ b/bot.py
@@ -8,22 +8,39 @@ Created on Sat Sep  1 11:18:39 2018
 import tornado.ioloop
 import tornado.web
 import json, os
+from linebot import LineBotApi, WebhookParser
+from linebot.exceptions import InvalidSignatureError
+from linebot.models import MessageEvent, TextMessage, TextSendMessage
 
 
 class WebHookHandler(tornado.web.RequestHandler):
-    def get(self):
-        token = self.get_argument('hub.verify_token','')
-        if token == ch:
-            write(token)
-        else:
-            write('Error, wrong varidation Token.')
+    def post(self):
+        global webhook
+        signature = json.load(self.request.headers['X-Line-Signature'])
+        data = json.load(self.request.body)
+        try:
+            events = webhook.parse(data, signature)
+        except InvalidSignatureError:
+            abort(400)
+        for event in events:
+            if not isinstance(event,MessageEvent):
+                continue
+            if not isinstance(event.message,TextMessage):
+                continue
+            linebot.reply_message(
+                event.rply_token,
+                TextSendMessage(text=event.message.text)
+            )
+        return 'OK'
 
 application = tornado.web.Application([(r'/callback',WebHookHandler)],{
         'debug':True
         })
 
 if __name__ == '__main__':
-    ch_ic = os.environ['Channel_ID']
+    ch_id = os.environ['Channel_ID']
     ch = os.environ['Channel_Secret']
+    linebot = LineBotApi(ch_id)
+    Webhook = WebhookParser(ch)  
     application.listen(5000)
     tornado.ioloop.IOLoop.instance().start()
\ No newline at end of file
diff --git a/linebot/__about__.py b/linebot/__about__.py
new file mode 100644 (file)
index 0000000..04f5347
--- /dev/null
@@ -0,0 +1,26 @@
+# -*- 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.
+
+"""Meta data of line-bot-sdk."""
+
+from __future__ import unicode_literals
+
+__version__ = '1.8.0'
+__author__ = 'LINE Corporation'
+__copyright__ = 'Copyright 2016, LINE Corporation'
+__license__ = 'Apache 2.0'
+
+__all__ = (
+    '__version__'
+)
diff --git a/linebot/__init__.py b/linebot/__init__.py
new file mode 100644 (file)
index 0000000..2598d05
--- /dev/null
@@ -0,0 +1,34 @@
+# -*- 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.
+
+"""linebot package."""
+
+from __future__ import unicode_literals
+
+from .__about__ import (  # noqa
+    __version__
+)
+from .api import (  # noqa
+    LineBotApi,
+)
+from .http_client import (  # noqa
+    HttpClient,
+    RequestsHttpClient,
+    HttpResponse,
+)
+from .webhook import (  # noqa
+    SignatureValidator,
+    WebhookParser,
+    WebhookHandler,
+)
diff --git a/linebot/api.py b/linebot/api.py
new file mode 100644 (file)
index 0000000..70f7faf
--- /dev/null
@@ -0,0 +1,584 @@
+# -*- 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.
+
+"""linebot.api module."""
+
+from __future__ import unicode_literals
+
+import json
+
+from .__about__ import __version__
+from .exceptions import LineBotApiError
+from .http_client import HttpClient, RequestsHttpClient
+from .models import (
+    Error, Profile, MemberIds, Content, RichMenuResponse
+)
+
+
+class LineBotApi(object):
+    """LineBotApi provides interface for LINE messaging API."""
+
+    DEFAULT_API_ENDPOINT = 'https://api.line.me'
+
+    def __init__(self, channel_access_token, endpoint=DEFAULT_API_ENDPOINT,
+                 timeout=HttpClient.DEFAULT_TIMEOUT, http_client=RequestsHttpClient):
+        """__init__ method.
+
+        :param str channel_access_token: Your channel access token
+        :param str endpoint: (optional) Default is https://api.line.me
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is linebot.http_client.HttpClient.DEFAULT_TIMEOUT
+        :type timeout: float | tuple(float, float)
+        :param http_client: (optional) Default is
+            :py:class:`linebot.http_client.RequestsHttpClient`
+        :type http_client: T <= :py:class:`linebot.http_client.HttpClient`
+        """
+        self.endpoint = endpoint
+        self.headers = {
+            'Authorization': 'Bearer ' + channel_access_token,
+            'User-Agent': 'line-bot-sdk-python/' + __version__
+        }
+
+        if http_client:
+            self.http_client = http_client(timeout=timeout)
+        else:
+            self.http_client = RequestsHttpClient(timeout=timeout)
+
+    def reply_message(self, reply_token, messages, timeout=None):
+        """Call reply message API.
+
+        https://devdocs.line.me/en/#reply-message
+
+        Respond to events from users, groups, and rooms.
+
+        Webhooks are used to notify you when an event occurs.
+        For events that you can respond to, a replyToken is issued for replying to messages.
+
+        Because the replyToken becomes invalid after a certain period of time,
+        responses should be sent as soon as a message is received.
+
+        Reply tokens can only be used once.
+
+        :param str reply_token: replyToken received via webhook
+        :param messages: Messages.
+            Max: 5
+        :type messages: T <= :py:class:`linebot.models.send_messages.SendMessage` |
+            list[T <= :py:class:`linebot.models.send_messages.SendMessage`]
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        """
+        if not isinstance(messages, (list, tuple)):
+            messages = [messages]
+
+        data = {
+            'replyToken': reply_token,
+            'messages': [message.as_json_dict() for message in messages]
+        }
+
+        self._post(
+            '/v2/bot/message/reply', data=json.dumps(data), timeout=timeout
+        )
+
+    def push_message(self, to, messages, timeout=None):
+        """Call push message API.
+
+        https://devdocs.line.me/en/#push-message
+
+        Send messages to users, groups, and rooms at any time.
+
+        :param str to: ID of the receiver
+        :param messages: Messages.
+            Max: 5
+        :type messages: T <= :py:class:`linebot.models.send_messages.SendMessage` |
+            list[T <= :py:class:`linebot.models.send_messages.SendMessage`]
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        """
+        if not isinstance(messages, (list, tuple)):
+            messages = [messages]
+
+        data = {
+            'to': to,
+            'messages': [message.as_json_dict() for message in messages]
+        }
+
+        self._post(
+            '/v2/bot/message/push', data=json.dumps(data), timeout=timeout
+        )
+
+    def multicast(self, to, messages, timeout=None):
+        """Call multicast API.
+
+        https://devdocs.line.me/en/#multicast
+
+        Send messages to multiple users at any time.
+
+        :param to: IDs of the receivers
+            Max: 150 users
+        :type to: list[str]
+        :param messages: Messages.
+            Max: 5
+        :type messages: T <= :py:class:`linebot.models.send_messages.SendMessage` |
+            list[T <= :py:class:`linebot.models.send_messages.SendMessage`]
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        """
+        if not isinstance(messages, (list, tuple)):
+            messages = [messages]
+
+        data = {
+            'to': to,
+            'messages': [message.as_json_dict() for message in messages]
+        }
+
+        self._post(
+            '/v2/bot/message/multicast', data=json.dumps(data), timeout=timeout
+        )
+
+    def get_profile(self, user_id, timeout=None):
+        """Call get profile API.
+
+        https://devdocs.line.me/en/#bot-api-get-profile
+
+        Get user profile information.
+
+        :param str user_id: User ID
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        :rtype: :py:class:`linebot.models.responses.Profile`
+        :return: Profile instance
+        """
+        response = self._get(
+            '/v2/bot/profile/{user_id}'.format(user_id=user_id),
+            timeout=timeout
+        )
+
+        return Profile.new_from_json_dict(response.json)
+
+    def get_group_member_profile(self, group_id, user_id, timeout=None):
+        """Call get group member profile API.
+
+        https://devdocs.line.me/en/#get-group-room-member-profile
+
+        Gets the user profile of a member of a group that
+        the bot is in. This can be the user ID of a user who has
+        not added the bot as a friend or has blocked the bot.
+
+        :param str group_id: Group ID
+        :param str user_id: User ID
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        :rtype: :py:class:`linebot.models.responses.Profile`
+        :return: Profile instance
+        """
+        response = self._get(
+            '/v2/bot/group/{group_id}/member/{user_id}'.format(group_id=group_id, user_id=user_id),
+            timeout=timeout
+        )
+
+        return Profile.new_from_json_dict(response.json)
+
+    def get_room_member_profile(self, room_id, user_id, timeout=None):
+        """Call get room member profile API.
+
+        https://devdocs.line.me/en/#get-group-room-member-profile
+
+        Gets the user profile of a member of a room that
+        the bot is in. This can be the user ID of a user who has
+        not added the bot as a friend or has blocked the bot.
+
+        :param str room_id: Room ID
+        :param str user_id: User ID
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        :rtype: :py:class:`linebot.models.responses.Profile`
+        :return: Profile instance
+        """
+        response = self._get(
+            '/v2/bot/room/{room_id}/member/{user_id}'.format(room_id=room_id, user_id=user_id),
+            timeout=timeout
+        )
+
+        return Profile.new_from_json_dict(response.json)
+
+    def get_group_member_ids(self, group_id, start=None, timeout=None):
+        """Call get group member IDs API.
+
+        https://devdocs.line.me/en/#get-group-room-member-ids
+
+        Gets the user IDs of the members of a group that the bot is in.
+        This includes the user IDs of users who have not added the bot as a friend
+        or has blocked the bot.
+
+        :param str group_id: Group ID
+        :param str start: continuationToken
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        :rtype: :py:class:`linebot.models.responses.MemberIds`
+        :return: MemberIds instance
+        """
+        params = None if start is None else {'start': start}
+
+        response = self._get(
+            '/v2/bot/group/{group_id}/members/ids'.format(group_id=group_id),
+            params=params,
+            timeout=timeout
+        )
+
+        return MemberIds.new_from_json_dict(response.json)
+
+    def get_room_member_ids(self, room_id, start=None, timeout=None):
+        """Call get room member IDs API.
+
+        https://devdocs.line.me/en/#get-group-room-member-ids
+
+        Gets the user IDs of the members of a group that the bot is in.
+        This includes the user IDs of users who have not added the bot as a friend
+        or has blocked the bot.
+
+        :param str room_id: Room ID
+        :param str start: continuationToken
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        :rtype: :py:class:`linebot.models.responses.MemberIds`
+        :return: MemberIds instance
+        """
+        params = None if start is None else {'start': start}
+
+        response = self._get(
+            '/v2/bot/room/{room_id}/members/ids'.format(room_id=room_id),
+            params=params,
+            timeout=timeout
+        )
+
+        return MemberIds.new_from_json_dict(response.json)
+
+    def get_message_content(self, message_id, timeout=None):
+        """Call get content API.
+
+        https://devdocs.line.me/en/#get-content
+
+        Retrieve image, video, and audio data sent by users.
+
+        :param str message_id: Message ID
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        :rtype: :py:class:`linebot.models.responses.Content`
+        :return: Content instance
+        """
+        response = self._get(
+            '/v2/bot/message/{message_id}/content'.format(message_id=message_id),
+            stream=True, timeout=timeout
+        )
+
+        return Content(response)
+
+    def leave_group(self, group_id, timeout=None):
+        """Call leave group API.
+
+        https://devdocs.line.me/en/#leave
+
+        Leave a group.
+
+        :param str group_id: Group ID
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        """
+        self._post(
+            '/v2/bot/group/{group_id}/leave'.format(group_id=group_id),
+            timeout=timeout
+        )
+
+    def leave_room(self, room_id, timeout=None):
+        """Call leave room API.
+
+        https://devdocs.line.me/en/#leave
+
+        Leave a room.
+
+        :param str room_id: Room ID
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        """
+        self._post(
+            '/v2/bot/room/{room_id}/leave'.format(room_id=room_id),
+            timeout=timeout
+        )
+
+    def get_rich_menu(self, rich_menu_id, timeout=None):
+        """Call get rich menu API.
+
+        https://developers.line.me/en/docs/messaging-api/reference/#get-rich-menu
+
+        :param str rich_menu_id: ID of the rich menu
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        :rtype: :py:class:`linebot.models.responses.RichMenuResponse`
+        :return: RichMenuResponse instance
+        """
+        response = self._get(
+            '/v2/bot/richmenu/{rich_menu_id}'.format(rich_menu_id=rich_menu_id),
+            timeout=timeout
+        )
+
+        return RichMenuResponse.new_from_json_dict(response.json)
+
+    def create_rich_menu(self, rich_menu, timeout=None):
+        """Call create rich menu API.
+
+        https://developers.line.me/en/docs/messaging-api/reference/#create-rich-menu
+
+        :param rich_menu: Inquired to create a rich menu object.
+        :type rich_menu: T <= :py:class:`linebot.models.rich_menu.RichMenu`
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        :rtype: str
+        :return: rich menu id
+        """
+        response = self._post(
+            '/v2/bot/richmenu', data=rich_menu.as_json_string(), timeout=timeout
+        )
+
+        return response.json.get('richMenuId')
+
+    def delete_rich_menu(self, rich_menu_id, timeout=None):
+        """Call delete rich menu API.
+
+        https://developers.line.me/en/docs/messaging-api/reference/#delete-rich-menu
+
+        :param str rich_menu_id: ID of an uploaded rich menu
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        """
+        self._delete(
+            '/v2/bot/richmenu/{rich_menu_id}'.format(rich_menu_id=rich_menu_id),
+            timeout=timeout
+        )
+
+    def get_rich_menu_id_of_user(self, user_id, timeout=None):
+        """Call get rich menu ID of user API.
+
+        https://developers.line.me/en/docs/messaging-api/reference/#get-rich-menu-id-of-user
+
+        :param str user_id: IDs of the user
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        :rtype: str
+        :return: rich menu id
+        """
+        response = self._get(
+            '/v2/bot/user/{user_id}/richmenu'.format(user_id=user_id),
+            timeout=timeout
+        )
+
+        return response.json.get('richMenuId')
+
+    def link_rich_menu_to_user(self, user_id, rich_menu_id, timeout=None):
+        """Call link rich menu to user API.
+
+        https://developers.line.me/en/docs/messaging-api/reference/#link-rich-menu-to-user
+
+        :param str user_id: ID of an uploaded rich menu
+        :param str rich_menu_id: ID of the user
+        :type timeout: float | tuple(float, float)
+        """
+        self._post(
+            '/v2/bot/user/{user_id}/richmenu/{rich_menu_id}'.format(
+                user_id=user_id,
+                rich_menu_id=rich_menu_id
+            ),
+            timeout=timeout
+        )
+
+    def unlink_rich_menu_from_user(self, user_id, timeout=None):
+        """Call unlink rich menu from user API.
+
+        https://developers.line.me/en/docs/messaging-api/reference/#unlink-rich-menu-from-user
+
+        :param str user_id: ID of the user
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        """
+        self._delete(
+            '/v2/bot/user/{user_id}/richmenu'.format(user_id=user_id),
+            timeout=timeout
+        )
+
+    def get_rich_menu_image(self, rich_menu_id, timeout=None):
+        """Call download rich menu image API.
+
+        https://developers.line.me/en/docs/messaging-api/reference/#download-rich-menu-image
+
+        :param str rich_menu_id: ID of the rich menu with the image to be downloaded
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        :rtype: :py:class:`linebot.models.responses.Content`
+        :return: Content instance
+        """
+        response = self._get(
+            '/v2/bot/richmenu/{rich_menu_id}/content'.format(rich_menu_id=rich_menu_id),
+            timeout=timeout
+        )
+
+        return Content(response)
+
+    def set_rich_menu_image(self, rich_menu_id, content_type, content, timeout=None):
+        """Call upload rich menu image API.
+
+        https://developers.line.me/en/docs/messaging-api/reference/#upload-rich-menu-image
+
+        Uploads and attaches an image to a rich menu.
+
+        :param str rich_menu_id: IDs of the richmenu
+        :param str content_type: image/jpeg or image/png
+        :param content: image content as bytes, or file-like object
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        """
+        self._post(
+            '/v2/bot/richmenu/{rich_menu_id}/content'.format(rich_menu_id=rich_menu_id),
+            data=content,
+            headers={'Content-Type': content_type},
+            timeout=timeout
+        )
+
+    def get_rich_menu_list(self, timeout=None):
+        """Call get rich menu list API.
+
+        https://developers.line.me/en/docs/messaging-api/reference/#get-rich-menu-list
+
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        :rtype: list(T <= :py:class:`linebot.models.reponse.RichMenuResponse`)
+        :return: list[RichMenuResponse] instance
+        """
+        response = self._get(
+            '/v2/bot/richmenu/list',
+            timeout=timeout
+        )
+
+        result = []
+        for richmenu in response.json['richmenus']:
+            result.append(RichMenuResponse.new_from_json_dict(richmenu))
+
+        return result
+
+    def _get(self, path, params=None, headers=None, stream=False, timeout=None):
+        url = self.endpoint + path
+
+        if headers is None:
+            headers = {}
+        headers.update(self.headers)
+
+        response = self.http_client.get(
+            url, headers=headers, params=params, stream=stream, timeout=timeout
+        )
+
+        self.__check_error(response)
+        return response
+
+    def _post(self, path, data=None, headers=None, timeout=None):
+        url = self.endpoint + path
+
+        if headers is None:
+            headers = {'Content-Type': 'application/json'}
+        headers.update(self.headers)
+
+        response = self.http_client.post(
+            url, headers=headers, data=data, timeout=timeout
+        )
+
+        self.__check_error(response)
+        return response
+
+    def _delete(self, path, data=None, headers=None, timeout=None):
+        url = self.endpoint + path
+
+        if headers is None:
+            headers = {}
+        headers.update(self.headers)
+
+        response = self.http_client.delete(
+            url, headers=headers, data=data, timeout=timeout
+        )
+
+        self.__check_error(response)
+        return response
+
+    @staticmethod
+    def __check_error(response):
+        if 200 <= response.status_code < 300:
+            pass
+        else:
+            error = Error.new_from_json_dict(response.json)
+            raise LineBotApiError(response.status_code, error)
diff --git a/linebot/exceptions.py b/linebot/exceptions.py
new file mode 100644 (file)
index 0000000..58c9b84
--- /dev/null
@@ -0,0 +1,84 @@
+# -*- 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.
+
+"""linebot.exceptions module."""
+
+from __future__ import unicode_literals
+
+from abc import ABCMeta
+
+from future.utils import with_metaclass
+
+
+class BaseError(with_metaclass(ABCMeta, Exception)):
+    """Base Exception class."""
+
+    def __init__(self, message='-'):
+        """__init__ method.
+
+        :param str message: Human readable message
+        """
+        self.message = message
+
+    def __repr__(self):
+        """repr.
+
+        :return:
+        """
+        return str(self)
+
+    def __str__(self):
+        """str.
+
+        :rtype: str
+        :return:
+        """
+        return '<{0} [{1}]>'.format(
+            self.__class__.__name__, self.message)
+
+
+class InvalidSignatureError(BaseError):
+    """When Webhook signature does NOT match, this error will be raised."""
+
+    def __init__(self, message='-'):
+        """__init__ method.
+
+        :param str message: Human readable message
+        """
+        super(InvalidSignatureError, self).__init__(message)
+
+
+class LineBotApiError(BaseError):
+    """When LINE Messaging API response error, this error will be raised."""
+
+    def __init__(self, status_code, error=None):
+        """__init__ method.
+
+        :param int status_code: http status code
+        :param error: (optional) Error class object.
+        :type error: :py:class:`linebot.models.error.Error`
+        """
+        super(LineBotApiError, self).__init__(error.message)
+
+        self.status_code = status_code
+        self.error = error
+
+    def __str__(self):
+        """str.
+
+        :rtype: str
+        :return:
+        """
+        return '{0}: status_code={1}, error_response={2}'.format(
+            self.__class__.__name__, self.status_code, self.error)
diff --git a/linebot/http_client.py b/linebot/http_client.py
new file mode 100644 (file)
index 0000000..9a3f47d
--- /dev/null
@@ -0,0 +1,260 @@
+# -*- 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.
+
+"""linebot.http_client module."""
+
+from __future__ import unicode_literals
+
+from abc import ABCMeta, abstractmethod, abstractproperty
+
+import requests
+from future.utils import with_metaclass
+
+
+class HttpClient(with_metaclass(ABCMeta)):
+    """Abstract Base Classes of HttpClient."""
+
+    DEFAULT_TIMEOUT = 5
+
+    def __init__(self, timeout=DEFAULT_TIMEOUT):
+        """__init__ method.
+
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is :py:attr:`DEFAULT_TIMEOUT`
+        :type timeout: float | tuple(float, float)
+        :rtype: T <= :py:class:`HttpResponse`
+        :return: HttpResponse instance
+        """
+        self.timeout = timeout
+
+    @abstractmethod
+    def get(self, url, headers=None, params=None, stream=False, timeout=None):
+        """GET request.
+
+        :param str url: Request url
+        :param dict headers: (optional) Request headers
+        :param dict params: (optional) Request query parameter
+        :param bool stream: (optional) get content as stream
+        :param timeout: (optional), How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is :py:attr:`self.timeout`
+        :type timeout: float | tuple(float, float)
+        :rtype: T <= :py:class:`HttpResponse`
+        :return: HttpResponse instance
+        """
+        raise NotImplementedError
+
+    @abstractmethod
+    def post(self, url, headers=None, data=None, timeout=None):
+        """POST request.
+
+        :param str url: Request url
+        :param dict headers: (optional) Request headers
+        :param data: (optional) Dictionary, bytes, or file-like object to send in the body
+        :param timeout: (optional), How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is :py:attr:`self.timeout`
+        :type timeout: float | tuple(float, float)
+        :rtype: T <= :py:class:`HttpResponse`
+        :return: HttpResponse instance
+        """
+        raise NotImplementedError
+
+    @abstractmethod
+    def delete(self, url, headers=None, data=None, timeout=None):
+        """DELETE request.
+
+        :param str url: Request url
+        :param dict headers: (optional) Request headers
+        :param data: (optional) Dictionary, bytes, or file-like object to send in the body
+        :param timeout: (optional), How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is :py:attr:`self.timeout`
+        :type timeout: float | tuple(float, float)
+        :rtype: T <= :py:class:`HttpResponse`
+        :return: HttpResponse instance
+        """
+        raise NotImplementedError
+
+
+class RequestsHttpClient(HttpClient):
+    """HttpClient implemented by requests."""
+
+    def __init__(self, timeout=HttpClient.DEFAULT_TIMEOUT):
+        """__init__ method.
+
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is :py:attr:`DEFAULT_TIMEOUT`
+        :type timeout: float | tuple(float, float)
+        """
+        super(RequestsHttpClient, self).__init__(timeout)
+
+    def get(self, url, headers=None, params=None, stream=False, timeout=None):
+        """GET request.
+
+        :param str url: Request url
+        :param dict headers: (optional) Request headers
+        :param dict params: (optional) Request query parameter
+        :param bool stream: (optional) get content as stream
+        :param timeout: (optional), How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is :py:attr:`self.timeout`
+        :type timeout: float | tuple(float, float)
+        :rtype: :py:class:`RequestsHttpResponse`
+        :return: RequestsHttpResponse instance
+        """
+        if timeout is None:
+            timeout = self.timeout
+
+        response = requests.get(
+            url, headers=headers, params=params, stream=stream, timeout=timeout
+        )
+
+        return RequestsHttpResponse(response)
+
+    def post(self, url, headers=None, data=None, timeout=None):
+        """POST request.
+
+        :param str url: Request url
+        :param dict headers: (optional) Request headers
+        :param data: (optional) Dictionary, bytes, or file-like object to send in the body
+        :param timeout: (optional), How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is :py:attr:`self.timeout`
+        :type timeout: float | tuple(float, float)
+        :rtype: :py:class:`RequestsHttpResponse`
+        :return: RequestsHttpResponse instance
+        """
+        if timeout is None:
+            timeout = self.timeout
+
+        response = requests.post(
+            url, headers=headers, data=data, timeout=timeout
+        )
+
+        return RequestsHttpResponse(response)
+
+    def delete(self, url, headers=None, data=None, timeout=None):
+        """DELETE request.
+
+        :param str url: Request url
+        :param dict headers: (optional) Request headers
+        :param data: (optional) Dictionary, bytes, or file-like object to send in the body
+        :param timeout: (optional), How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is :py:attr:`self.timeout`
+        :type timeout: float | tuple(float, float)
+        :rtype: :py:class:`RequestsHttpResponse`
+        :return: RequestsHttpResponse instance
+        """
+        if timeout is None:
+            timeout = self.timeout
+
+        response = requests.delete(
+            url, headers=headers, data=data, timeout=timeout
+        )
+
+        return RequestsHttpResponse(response)
+
+
+class HttpResponse(with_metaclass(ABCMeta)):
+    """HttpResponse."""
+
+    @abstractproperty
+    def status_code(self):
+        """Get status code."""
+        raise NotImplementedError
+
+    @abstractproperty
+    def headers(self):
+        """Get headers."""
+        raise NotImplementedError
+
+    @abstractproperty
+    def text(self):
+        """Get request body as text-decoded."""
+        raise NotImplementedError
+
+    @abstractproperty
+    def content(self):
+        """Get request body as binary."""
+        raise NotImplementedError
+
+    @abstractproperty
+    def json(self):
+        """Get request body as json-decoded."""
+        raise NotImplementedError
+
+    @abstractmethod
+    def iter_content(self, chunk_size=1024, decode_unicode=False):
+        """Get request body as iterator content (stream).
+
+        :param int chunk_size:
+        :param bool decode_unicode:
+        """
+        raise NotImplementedError
+
+
+class RequestsHttpResponse(HttpResponse):
+    """HttpResponse implemented by requests lib's response."""
+
+    def __init__(self, response):
+        """__init__ method.
+
+        :param response: requests lib's response
+        """
+        self.response = response
+
+    @property
+    def status_code(self):
+        """Get status code."""
+        return self.response.status_code
+
+    @property
+    def headers(self):
+        """Get headers."""
+        return self.response.headers
+
+    @property
+    def text(self):
+        """Get request body as text-decoded."""
+        return self.response.text
+
+    @property
+    def content(self):
+        """Get request body as binary."""
+        return self.response.content
+
+    @property
+    def json(self):
+        """Get request body as json-decoded."""
+        return self.response.json()
+
+    def iter_content(self, chunk_size=1024, decode_unicode=False):
+        """Get request body as iterator content (stream).
+
+        :param int chunk_size:
+        :param bool decode_unicode:
+        """
+        return self.response.iter_content(chunk_size=chunk_size, decode_unicode=decode_unicode)
diff --git a/linebot/models/__init__.py b/linebot/models/__init__.py
new file mode 100644 (file)
index 0000000..819a993
--- /dev/null
@@ -0,0 +1,127 @@
+# -*- 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.
+
+"""linebot.models package."""
+
+from .actions import (  # noqa
+    Action,
+    PostbackAction,
+    MessageAction,
+    URIAction,
+    DatetimePickerAction,
+    CameraAction,
+    CameraRollAction,
+    LocationAction,
+    Action as TemplateAction,  # backward compatibility
+    PostbackAction as PostbackTemplateAction,  # backward compatibility
+    MessageAction as MessageTemplateAction,  # backward compatibility
+    URIAction as URITemplateAction,  # backward compatibility
+    DatetimePickerAction as DatetimePickerTemplateAction,  # backward compatibility
+)
+from .base import (  # noqa
+    Base,
+)
+from .error import (  # noqa
+    Error,
+    ErrorDetail,
+)
+from .events import (  # noqa
+    Event,
+    MessageEvent,
+    FollowEvent,
+    UnfollowEvent,
+    JoinEvent,
+    LeaveEvent,
+    PostbackEvent,
+    AccountLinkEvent,
+    BeaconEvent,
+    Postback,
+    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,
+    ImagemapAction,
+    URIImagemapAction,
+    MessageImagemapAction,
+    ImagemapArea,
+)
+from .messages import (  # noqa
+    Message,
+    TextMessage,
+    ImageMessage,
+    VideoMessage,
+    AudioMessage,
+    LocationMessage,
+    StickerMessage,
+    FileMessage,
+)
+from .responses import (  # noqa
+    Profile,
+    MemberIds,
+    Content,
+    RichMenuResponse,
+    Content as MessageContent,  # backward compatibility
+)
+from .rich_menu import (  # noqa
+    RichMenu,
+    RichMenuSize,
+    RichMenuArea,
+    RichMenuBounds,
+)
+from .send_messages import (  # noqa
+    SendMessage,
+    TextSendMessage,
+    ImageSendMessage,
+    VideoSendMessage,
+    AudioSendMessage,
+    LocationSendMessage,
+    StickerSendMessage,
+    QuickReply,
+    QuickReplyButton,
+)
+from .sources import (  # noqa
+    Source,
+    SourceUser,
+    SourceGroup,
+    SourceRoom,
+)
+from .template import (  # noqa
+    TemplateSendMessage,
+    Template,
+    ButtonsTemplate,
+    ConfirmTemplate,
+    CarouselTemplate,
+    CarouselColumn,
+    ImageCarouselTemplate,
+    ImageCarouselColumn,
+)
diff --git a/linebot/models/actions.py b/linebot/models/actions.py
new file mode 100644 (file)
index 0000000..07143dd
--- /dev/null
@@ -0,0 +1,247 @@
+# -*- 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.
+
+"""linebot.models.template module."""
+
+from __future__ import unicode_literals
+
+from abc import ABCMeta
+
+from future.utils import with_metaclass
+
+from .base import Base
+
+
+def get_action(action):
+    """Get action."""
+    action_obj = Base.get_or_new_from_json_dict_with_types(
+        action, {
+            'postback': PostbackAction,
+            'message': MessageAction,
+            'uri': URIAction,
+            'datetimepicker': DatetimePickerAction,
+            'camera': CameraAction,
+            'cameraRoll': CameraRollAction,
+            'location': LocationAction,
+        }
+    )
+    return action_obj
+
+
+def get_actions(actions):
+    """Get actions."""
+    new_actions = []
+    if actions:
+        for action in actions:
+            action_obj = get_action(action)
+            if action_obj:
+                new_actions.append(action_obj)
+
+    return new_actions
+
+
+class Action(with_metaclass(ABCMeta, Base)):
+    """Abstract base class of Action."""
+
+    def __init__(self, **kwargs):
+        """__init__ method.
+
+        :param kwargs:
+        """
+        super(Action, self).__init__(**kwargs)
+
+        self.type = None
+
+
+class PostbackAction(Action):
+    """PostbackAction.
+
+    https://developers.line.me/en/docs/messaging-api/reference/#postback-action
+
+    When a control associated with this action is tapped,
+    a postback event is returned via webhook with the specified string in the data property.
+    """
+
+    def __init__(self, label=None, data=None, display_text=None, text=None, **kwargs):
+        """__init__ method.
+
+        :param str label: Label for the action.
+        :param str data: String returned via webhook
+            in the postback.data property of the postback event.
+        :param str display_text: Text displayed in the chat as a message sent by
+            the user when the action is performed.
+        :param str text: Deprecated. Text displayed in the chat as a message sent by
+            the user when the action is performed. Returned from the server through a webhook.
+        :param kwargs:
+        """
+        super(PostbackAction, self).__init__(**kwargs)
+
+        self.type = 'postback'
+        self.label = label
+        self.data = data
+        self.display_text = display_text
+        self.text = text
+
+
+class MessageAction(Action):
+    """MessageAction.
+
+    https://developers.line.me/en/docs/messaging-api/reference/#message-action
+
+    When a control associated with this action is tapped,
+    the string in the text property is sent as a message from the user.
+    """
+
+    def __init__(self, label=None, text=None, **kwargs):
+        """__init__ method.
+
+        :param str label: Label for the action.
+        :param str text: Text sent when the action is performed.
+        :param kwargs:
+        """
+        super(MessageAction, self).__init__(**kwargs)
+
+        self.type = 'message'
+        self.label = label
+        self.text = text
+
+
+class URIAction(Action):
+    """URIAction.
+
+    https://developers.line.me/en/docs/messaging-api/reference/#uri-action
+
+    When a control associated with this action is tapped,
+    the URI specified in the uri property is opened.
+    """
+
+    def __init__(self, label=None, uri=None, **kwargs):
+        """__init__ method.
+
+        :param str label: Label for the action
+            Max: 20 characters
+        :param str uri: URI opened when the action is performed.
+        :param kwargs:
+        """
+        super(URIAction, self).__init__(**kwargs)
+
+        self.type = 'uri'
+        self.label = label
+        self.uri = uri
+
+
+class DatetimePickerAction(Action):
+    """DatetimePickerAction.
+
+    https://developers.line.me/en/docs/messaging-api/reference/#datetime-picker-action
+
+    When a control associated with this action is tapped,
+    a postback event is returned via webhook with the date and time
+    selected by the user from the date and time selection dialog.
+    The datetime picker action does not support time zones.
+    """
+
+    def __init__(self, label=None, data=None, mode=None,
+                 initial=None, max=None, min=None, **kwargs):
+        """__init__ method.
+
+        :param str label: Label for the action
+        :param str data: String returned via webhook
+            in the postback.data property of the postback event
+        :param str mode: Action mode
+            date: Pick date
+            time: Pick time
+            datetime: Pick date and time
+        :param str initial: Initial value of date or time
+        :param str max: Largest date or time value that can be selected.
+            Must be greater than the min value.
+        :param str min: Smallest date or time value that can be selected.
+            Must be less than the max value.
+        :param kwargs:
+        """
+        super(DatetimePickerAction, self).__init__(**kwargs)
+
+        self.type = 'datetimepicker'
+        self.label = label
+        self.data = data
+        self.mode = mode
+        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/base.py b/linebot/models/base.py
new file mode 100644 (file)
index 0000000..457ea1b
--- /dev/null
@@ -0,0 +1,154 @@
+# -*- 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.
+
+"""linebot.models.base module."""
+
+from __future__ import unicode_literals
+
+import json
+
+from .. import utils
+
+
+class Base(object):
+    """Base class of model.
+
+    Suitable for JSON base data.
+    """
+
+    def __init__(self, **kwargs):
+        """__init__ method.
+
+        :param kwargs:
+        """
+        pass
+
+    def __str__(self):
+        """__str__ method.
+
+        :return:
+        """
+        return self.as_json_string()
+
+    def __repr__(self):
+        """__repr__ method.
+
+        :return:
+        """
+        return str(self)
+
+    def __eq__(self, other):
+        """__eq__ method.
+
+        :param other:
+        :return:
+        """
+        return other and self.as_json_dict() == other.as_json_dict()
+
+    def __ne__(self, other):
+        """__ne__ method.
+
+        :param other:
+        :return:
+        """
+        return not self.__eq__(other)
+
+    def as_json_string(self):
+        """Return JSON string from this object.
+
+        :rtype: str
+        :return:
+        """
+        return json.dumps(self.as_json_dict(), sort_keys=True)
+
+    def as_json_dict(self):
+        """Return dictionary from this object.
+
+        :return: dict
+        """
+        data = {}
+        for key, value in self.__dict__.items():
+            camel_key = utils.to_camel_case(key)
+            if isinstance(value, (list, tuple, set)):
+                data[camel_key] = list()
+                for item in value:
+                    if hasattr(item, 'as_json_dict'):
+                        data[camel_key].append(item.as_json_dict())
+                    else:
+                        data[camel_key].append(item)
+
+            elif hasattr(value, 'as_json_dict'):
+                data[camel_key] = value.as_json_dict()
+            elif value is not None:
+                data[camel_key] = value
+
+        return data
+
+    @classmethod
+    def new_from_json_dict(cls, data):
+        """Create a new instance from a dict.
+
+        :param data: JSON dict
+        :rtype:
+        :return:
+        """
+        new_data = {utils.to_snake_case(key): value
+                    for key, value in data.items()}
+
+        return cls(**new_data)
+
+    @staticmethod
+    def get_or_new_from_json_dict(data, cls):
+        """Get `cls` object w/ deserialization from json if needed.
+
+        If data is instance of cls, return data.
+        Else if data is instance of dict, create instance from dict.
+        Else, return None.
+
+        :param data:
+        :param cls:
+        :rtype: object
+        :return:
+        """
+        if isinstance(data, cls):
+            return data
+        elif isinstance(data, dict):
+            return cls.new_from_json_dict(data)
+
+        return None
+
+    @staticmethod
+    def get_or_new_from_json_dict_with_types(
+            data, cls_map, type_key='type'
+    ):
+        """Get `cls` object w/ deserialization from json by using type key hint if needed.
+
+        If data is instance of one of cls, return data.
+        Else if data is instance of dict, create instance from dict.
+        Else, return None.
+
+        :param data:
+        :param cls_map:
+        :param type_key:
+        :rtype: object
+        :return:
+        """
+        if isinstance(data, tuple(cls_map.values())):
+            return data
+        elif isinstance(data, dict):
+            type_val = data[type_key]
+            if type_val in cls_map:
+                return cls_map[type_val].new_from_json_dict(data)
+
+        return None
diff --git a/linebot/models/error.py b/linebot/models/error.py
new file mode 100644 (file)
index 0000000..aab3a52
--- /dev/null
@@ -0,0 +1,68 @@
+# -*- 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.
+
+"""linebot.models.error module."""
+
+from __future__ import unicode_literals
+
+from .base import Base
+
+
+class Error(Base):
+    """Error response of LINE messaging API.
+
+    https://devdocs.line.me/en/#error-response
+    """
+
+    def __init__(self, message=None, details=None, **kwargs):
+        """__init__ method.
+
+        :param str message: Summary of the error
+        :param details: ErrorDetail instance list
+        :type details: list[T <= :py:class:`linebot.models.error.ErrorDetail`]
+        :type
+        :param kwargs:
+        """
+        super(Error, self).__init__(**kwargs)
+
+        self.message = message
+
+        new_details = []
+        if details:
+            for detail in details:
+                new_details.append(
+                    self.get_or_new_from_json_dict(detail, ErrorDetail)
+                )
+        self.details = new_details
+
+
+class ErrorDetail(Base):
+    """ErrorDetail response of LINE messaging API.
+
+    https://devdocs.line.me/en/#error-response
+    """
+
+    def __init__(self, message=None, property=None, **kwargs):
+        """__init__ method.
+
+        https://devdocs.line.me/en/#error-response
+
+        :param str message: Details of the error message
+        :param str property: Related property
+        :param kwargs:
+        """
+        super(ErrorDetail, self).__init__(**kwargs)
+
+        self.message = message
+        self.property = property
diff --git a/linebot/models/events.py b/linebot/models/events.py
new file mode 100644 (file)
index 0000000..bc7255d
--- /dev/null
@@ -0,0 +1,363 @@
+# -*- 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.
+
+"""linebot.models.events module."""
+
+from __future__ import unicode_literals
+
+from abc import ABCMeta
+
+from future.utils import with_metaclass
+
+from .base import Base
+from .messages import (
+    TextMessage,
+    ImageMessage,
+    VideoMessage,
+    AudioMessage,
+    LocationMessage,
+    StickerMessage,
+    FileMessage
+)
+from .sources import SourceUser, SourceGroup, SourceRoom
+
+
+class Event(with_metaclass(ABCMeta, Base)):
+    """Abstract Base Class of Webhook Event.
+
+    https://devdocs.line.me/en/#webhook-event-object
+    """
+
+    def __init__(self, timestamp=None, source=None, **kwargs):
+        """__init__ method.
+
+        :param long timestamp: Time of the event in milliseconds
+        :param source: Source object
+        :type source: T <= :py:class:`linebot.models.sources.Source`
+        :param kwargs:
+        """
+        super(Event, self).__init__(**kwargs)
+
+        self.type = None
+        self.timestamp = timestamp
+        self.source = self.get_or_new_from_json_dict_with_types(
+            source, {
+                'user': SourceUser,
+                'group': SourceGroup,
+                'room': SourceRoom,
+            }
+        )
+
+
+class MessageEvent(Event):
+    """Webhook MessageEvent.
+
+    https://devdocs.line.me/en/#message-event
+
+    Event object which contains the sent message.
+    The message field contains a message object which corresponds with the message type.
+    You can reply to message events.
+    """
+
+    def __init__(self, timestamp=None, source=None, reply_token=None, message=None, **kwargs):
+        """__init__ method.
+
+        :param long timestamp: Time of the event in milliseconds
+        :param source: Source object
+        :type source: T <= :py:class:`linebot.models.sources.Source`
+        :param str reply_token: Reply token
+        :param message: Message object
+        :type message: T <= :py:class:`linebot.models.messages.Message`
+        :param kwargs:
+        """
+        super(MessageEvent, self).__init__(
+            timestamp=timestamp, source=source, **kwargs
+        )
+
+        self.type = 'message'
+        self.reply_token = reply_token
+        self.message = self.get_or_new_from_json_dict_with_types(
+            message, {
+                'text': TextMessage,
+                'image': ImageMessage,
+                'video': VideoMessage,
+                'audio': AudioMessage,
+                'location': LocationMessage,
+                'sticker': StickerMessage,
+                'file': FileMessage
+            }
+        )
+
+
+class FollowEvent(Event):
+    """Webhook FollowEvent.
+
+    https://devdocs.line.me/en/#follow-event
+
+    Event object for when your account is added as a friend (or unblocked).
+    You can reply to follow events.
+    """
+
+    def __init__(self, timestamp=None, source=None, reply_token=None, **kwargs):
+        """__init__ method.
+
+        :param long timestamp: Time of the event in milliseconds
+        :param source: Source object
+        :type source: T <= :py:class:`linebot.models.sources.Source`
+        :param str reply_token: Reply token
+        :param kwargs:
+        """
+        super(FollowEvent, self).__init__(
+            timestamp=timestamp, source=source, **kwargs
+        )
+
+        self.type = 'follow'
+        self.reply_token = reply_token
+
+
+class UnfollowEvent(Event):
+    """Webhook UnfollowEvent.
+
+    https://devdocs.line.me/en/#unfollow-event
+
+    Event object for when your account is blocked.
+    """
+
+    def __init__(self, timestamp=None, source=None, **kwargs):
+        """__init__ method.
+
+        :param long timestamp: Time of the event in milliseconds
+        :param source: Source object
+        :type source: T <= :py:class:`linebot.models.sources.Source`
+        :param kwargs:
+        """
+        super(UnfollowEvent, self).__init__(
+            timestamp=timestamp, source=source, **kwargs
+        )
+
+        self.type = 'unfollow'
+
+
+class JoinEvent(Event):
+    """Webhook JoinEvent.
+
+    https://devdocs.line.me/en/#join-event
+
+    Event object for when your account joins a group or talk room.
+    You can reply to join events.
+    """
+
+    def __init__(self, timestamp=None, source=None, reply_token=None, **kwargs):
+        """__init__ method.
+
+        :param long timestamp: Time of the event in milliseconds
+        :param source: Source object
+        :type source: T <= :py:class:`linebot.models.sources.Source`
+        :param str reply_token: Reply token
+        :param kwargs:
+        """
+        super(JoinEvent, self).__init__(
+            timestamp=timestamp, source=source, **kwargs
+        )
+
+        self.type = 'join'
+        self.reply_token = reply_token
+
+
+class LeaveEvent(Event):
+    """Webhook LeaveEvent.
+
+    https://devdocs.line.me/en/#leave-event
+
+    Event object for when your account leaves a group.
+    """
+
+    def __init__(self, timestamp=None, source=None, **kwargs):
+        """__init__ method.
+
+        :param long timestamp: Time of the event in milliseconds
+        :param source: Source object
+        :type source: T <= :py:class:`linebot.models.sources.Source`
+        :param kwargs:
+        """
+        super(LeaveEvent, self).__init__(
+            timestamp=timestamp, source=source, **kwargs
+        )
+
+        self.type = 'leave'
+
+
+class PostbackEvent(Event):
+    """Webhook PostbackEvent.
+
+    https://devdocs.line.me/en/#postback-event
+
+    Event object for when a user performs an action on
+    a template message which initiates a postback.
+    You can reply to postback events.
+    """
+
+    def __init__(self, timestamp=None, source=None, reply_token=None, postback=None, **kwargs):
+        """__init__ method.
+
+        :param long timestamp: Time of the event in milliseconds
+        :param source: Source object
+        :type source: T <= :py:class:`linebot.models.sources.Source`
+        :param str reply_token: Reply token
+        :param postback: Postback object
+        :type postback: :py:class:`linebot.models.events.Postback`
+        :param kwargs:
+        """
+        super(PostbackEvent, self).__init__(
+            timestamp=timestamp, source=source, **kwargs
+        )
+
+        self.type = 'postback'
+        self.reply_token = reply_token
+        self.postback = self.get_or_new_from_json_dict(
+            postback, Postback
+        )
+
+
+class BeaconEvent(Event):
+    """Webhook BeaconEvent.
+
+    https://devdocs.line.me/en/#beacon-event
+
+    Event object for when a user detects a LINE Beacon. You can reply to beacon events.
+    """
+
+    def __init__(self, timestamp=None, source=None, reply_token=None,
+                 beacon=None, **kwargs):
+        """__init__ method.
+
+        :param long timestamp: Time of the event in milliseconds
+        :param source: Source object
+        :type source: T <= :py:class:`linebot.models.sources.Source`
+        :param str reply_token: Reply token
+        :param beacon: Beacon object
+        :type beacon: :py:class:`linebot.models.events.Beacon`
+        :param kwargs:
+        """
+        super(BeaconEvent, self).__init__(
+            timestamp=timestamp, source=source, **kwargs
+        )
+
+        self.type = 'beacon'
+        self.reply_token = reply_token
+        self.beacon = self.get_or_new_from_json_dict(
+            beacon, Beacon
+        )
+
+
+class AccountLinkEvent(Event):
+    """Webhook AccountLinkEvent.
+
+    https://developers.line.me/en/docs/messaging-api/reference/#account-link-event
+
+    Event object for when a user has linked his/her LINE account with a provider's service account.
+    You can reply to account link events.
+    If the link token has expired or has already been used,
+    no webhook event will be sent and the user will be shown an error.
+    """
+
+    def __init__(self, timestamp=None, source=None, reply_token=None, link=None, **kwargs):
+        """__init__ method.
+
+        :param long timestamp: Time of the event in milliseconds
+        :param source: Source object
+        :type source: T <= :py:class:`linebot.models.sources.Source`
+        :param str reply_token: Reply token
+        :param link: Link object
+        :type link: :py:class:`linebot.models.events.Link`
+        :param kwargs:
+        """
+        super(AccountLinkEvent, self).__init__(
+            timestamp=timestamp, source=source, **kwargs
+        )
+
+        self.type = 'accountLink'
+        self.reply_token = reply_token
+        self.link = self.get_or_new_from_json_dict(
+            link, Link
+        )
+
+
+class Postback(Base):
+    """Postback.
+
+    https://devdocs.line.me/en/#postback-event
+    """
+
+    def __init__(self, data=None, params=None, **kwargs):
+        """__init__ method.
+
+        :param str data: Postback data
+        :param dict params: JSON object with the date and time
+            selected by a user through a datetime picker action.
+            Only returned for postback actions via the datetime picker.
+        :param kwargs:
+        """
+        super(Postback, self).__init__(**kwargs)
+
+        self.data = data
+        self.params = params
+
+
+class Beacon(Base):
+    """Beacon.
+
+    https://devdocs.line.me/en/#beacon-event
+    """
+
+    def __init__(self, type=None, hwid=None, dm=None, **kwargs):
+        """__init__ method.
+
+        :param str type: Type of beacon event
+        :param str hwid: Hardware ID of the beacon that was detected
+        :param str dm: Optional. Device message of beacon which is hex string
+        :param kwargs:
+        """
+        super(Beacon, self).__init__(**kwargs)
+
+        self.type = type
+        self.hwid = hwid
+        self.dm = dm
+
+    @property
+    def device_message(self):
+        """Get dm(device_message) as bytearray.
+
+        :rtype: bytearray
+        :return:
+        """
+        return bytearray.fromhex(self.dm) if self.dm is not None else None
+
+
+class Link(Base):
+    """Link.
+
+    https://developers.line.me/en/docs/messaging-api/reference/#link-object
+    """
+
+    def __init__(self, result=None, nonce=None, **kwargs):
+        """__init__ method.
+
+        :param str result: Indicate whether the link was successful or not.
+        :param str nonce: Specified nonce when verifying the user ID.
+        """
+        super(Link, self).__init__(**kwargs)
+
+        self.result = result
+        self.nonce = nonce
diff --git a/linebot/models/flex_message.py b/linebot/models/flex_message.py
new file mode 100644 (file)
index 0000000..44a28d9
--- /dev/null
@@ -0,0 +1,462 @@
+# -*- 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.
+
+"""linebot.models.flex_message module."""
+
+from __future__ import unicode_literals
+
+from abc import ABCMeta
+
+from future.utils import with_metaclass
+
+from .actions import get_action
+from .base import Base
+from .send_messages import SendMessage
+
+
+class FlexSendMessage(SendMessage):
+    """FlexSendMessage.
+
+    https://developers.line.me/en/docs/messaging-api/reference/#flex-message
+
+    Flex Messages are messages with a customizable layout.
+    You can customize the layout freely by combining multiple elements.
+    """
+
+    def __init__(self, alt_text=None, contents=None, **kwargs):
+        """__init__ method.
+
+        :param str alt_text: Alternative text
+        :param contents: Flex Message container object
+        :type contents: :py:class:`linebot.models.flex_message.FlexContainer`
+        :param kwargs:
+        """
+        super(FlexSendMessage, self).__init__(**kwargs)
+
+        self.type = 'flex'
+        self.alt_text = alt_text
+        self.contents = contents
+        self.contents = self.get_or_new_from_json_dict_with_types(
+            contents, {
+                'bubble': BubbleContainer,
+                'carousel': CarouselContainer
+            }
+        )
+
+
+class FlexContainer(with_metaclass(ABCMeta, Base)):
+    """FlexContainer.
+
+    https://developers.line.me/en/docs/messaging-api/reference/#container
+
+    A container is the top-level structure of a Flex Message.
+    """
+
+    def __init__(self, **kwargs):
+        """__init__ method.
+
+        :param kwargs:
+        """
+        super(FlexContainer, self).__init__(**kwargs)
+
+        self.type = None
+
+
+class BubbleContainer(FlexContainer):
+    """BubbleContainer.
+
+    https://developers.line.me/en/docs/messaging-api/reference/#bubble-container
+
+    This is a container that contains one message bubble.
+    It can contain four blocks: header, hero, body, and footer.
+    """
+
+    def __init__(self, direction=None, header=None, hero=None, body=None, footer=None, styles=None,
+                 **kwargs):
+        """__init__ method.
+
+        :param str direction: Text directionality and the order of components
+            in horizontal boxes in the container
+        :param header: Header block
+        :type header: :py:class:`linebot.models.flex_message.BoxComponent`
+        :param hero: Hero block
+        :type hero: :py:class:`linebot.models.flex_message.ImageComponent`
+        :param body: Body block
+        :type body: :py:class:`linebot.models.flex_message.BoxComponent`
+        :param footer: Footer block
+        :type footer: :py:class:`linebot.models.flex_message.BoxComponent`
+        :param styles: Style of each block
+        :type styles: :py:class:`linebot.models.flex_message.BubbleStyle`
+        :param kwargs:
+        """
+        super(BubbleContainer, self).__init__(**kwargs)
+
+        self.type = 'bubble'
+        self.direction = direction
+        self.header = self.get_or_new_from_json_dict(header, BoxComponent)
+        self.hero = self.get_or_new_from_json_dict(hero, ImageComponent)
+        self.body = self.get_or_new_from_json_dict(body, BoxComponent)
+        self.footer = self.get_or_new_from_json_dict(footer, BoxComponent)
+        self.styles = self.get_or_new_from_json_dict(styles, BubbleStyle)
+
+
+class BubbleStyle(with_metaclass(ABCMeta, Base)):
+    """BubbleStyle.
+
+    https://developers.line.me/en/docs/messaging-api/reference/#objects-for-the-block-style
+    """
+
+    def __init__(self, header=None, hero=None, body=None, footer=None, **kwargs):
+        """__init__ method.
+
+        :param header: Style of the header block
+        :type header: :py:class:`linebot.models.flex_message.BlockStyle`
+        :param hero: Style of the hero block
+        :type hero: :py:class:`linebot.models.flex_message.BlockStyle`
+        :param body: Style of the body block
+        :type body: :py:class:`linebot.models.flex_message.BlockStyle`
+        :param footer: Style of the footer block
+        :type footer: :py:class:`linebot.models.flex_message.BlockStyle`
+        :param kwargs:
+        """
+        super(BubbleStyle, self).__init__(**kwargs)
+
+        self.header = self.get_or_new_from_json_dict(header, BlockStyle)
+        self.hero = self.get_or_new_from_json_dict(hero, BlockStyle)
+        self.body = self.get_or_new_from_json_dict(body, BlockStyle)
+        self.footer = self.get_or_new_from_json_dict(footer, BlockStyle)
+
+
+class BlockStyle(with_metaclass(ABCMeta, Base)):
+    """BlockStyle.
+
+    https://developers.line.me/en/docs/messaging-api/reference/#objects-for-the-block-style
+    """
+
+    def __init__(self, background_color=None, separator=None, separator_color=None, **kwargs):
+        """__init__ method.
+
+        :param str background_color: Background color of the block. Use a hexadecimal color code
+        :param bool separator: True to place a separator above the block
+            True will be ignored for the first block in a container
+            because you cannot place a separator above the first block.
+            The default value is False
+        :param str separator_color: Color of the separator. Use a hexadecimal color code
+        :param kwargs:
+        """
+        super(BlockStyle, self).__init__(**kwargs)
+        self.background_color = background_color
+        self.separator = separator
+        self.separator_color = separator_color
+
+
+class CarouselContainer(FlexContainer):
+    """CarouselContainer.
+
+    https://developers.line.me/en/docs/messaging-api/reference/#carousel-container
+
+    This is a container that contains multiple bubble containers, or message bubbles.
+    The bubbles will be shown in order by scrolling horizontally.
+    """
+
+    def __init__(self, contents=None, **kwargs):
+        """__init__ method.
+
+        :param contents: Array of bubble containers
+        :type contents: list[T <= :py:class:`linebot.models.flex_message.BubbleContainer`]
+        :param kwargs:
+        """
+        super(CarouselContainer, self).__init__(**kwargs)
+
+        self.type = 'carousel'
+
+        new_contents = []
+        if contents:
+            for it in contents:
+                new_contents.append(self.get_or_new_from_json_dict(
+                    it, BubbleContainer
+                ))
+        self.contents = new_contents
+
+
+class FlexComponent(with_metaclass(ABCMeta, Base)):
+    """FlexComponent.
+
+    https://developers.line.me/en/docs/messaging-api/reference/#component
+
+    Components are objects that compose a Flex Message container.
+    """
+
+    def __init__(self, **kwargs):
+        """__init__ method.
+
+        :param kwargs:
+        """
+        super(FlexComponent, self).__init__(**kwargs)
+
+        self.type = None
+
+
+class BoxComponent(FlexComponent):
+    """BoxComponent.
+
+    https://developers.line.me/en/docs/messaging-api/reference/#box-component
+
+    This is a component that defines the layout of child components.
+    You can also include a box in a box.
+    """
+
+    def __init__(self, layout=None, contents=None, flex=None, spacing=None, margin=None, **kwargs):
+        """__init__ method.
+
+        :param str layout: The placement style of components in this box
+        :param contents: Components in this box
+        :param float flex: The ratio of the width or height of this box within the parent box
+        :param str spacing: Minimum space between components in this box
+        :param str margin: Minimum space between this box
+            and the previous component in the parent box
+        :param kwargs:
+        """
+        super(BoxComponent, self).__init__(**kwargs)
+        self.type = 'box'
+        self.layout = layout
+        self.flex = flex
+        self.spacing = spacing
+        self.margin = margin
+
+        new_contents = []
+        if contents:
+            for it in contents:
+                new_contents.append(self.get_or_new_from_json_dict_with_types(
+                    it, {
+                        'box': BoxComponent,
+                        'button': ButtonComponent,
+                        'filler': FillerComponent,
+                        'icon': IconComponent,
+                        'image': ImageComponent,
+                        'separator': SeparatorComponent,
+                        'spacer': SpacerComponent,
+                        'text': TextComponent
+                    }
+                ))
+        self.contents = new_contents
+
+
+class ButtonComponent(FlexComponent):
+    """ButtonComponent.
+
+    https://developers.line.me/en/docs/messaging-api/reference/#button-component
+
+    This component draws a button.
+    When the user taps a button, a specified action is performed.
+    """
+
+    def __init__(self, action=None, flex=None, margin=None, height=None, style=None, color=None,
+                 gravity=None, **kwargs):
+        """__init__ method.
+
+        :param action: Action performed when this button is tapped
+        :type action: list[T <= :py:class:`linebot.models.actions.Action`]
+        :param float flex: The ratio of the width or height of this component within the parent box
+        :param str margin: Minimum space between this component
+            and the previous component in the parent box
+        :param str height: Height of the button
+        :param str style: Style of the button
+        :param str color: Character color when the style property is link.
+            Background color when the style property is primary or secondary.
+            Use a hexadecimal color code
+        :param str gravity: Vertical alignment style
+        :param kwargs:
+        """
+        super(ButtonComponent, self).__init__(**kwargs)
+        self.type = 'button'
+        self.action = get_action(action)
+        self.flex = flex
+        self.margin = margin
+        self.height = height
+        self.style = style
+        self.color = color
+        self.gravity = gravity
+
+
+class FillerComponent(FlexComponent):
+    """FillerComponent.
+
+    https://developers.line.me/en/docs/messaging-api/reference/#filler-component
+
+    This is an invisible component to fill extra space between components.
+    """
+
+    def __init__(self, **kwargs):
+        """__init__ method.
+
+        :param kwargs:
+        """
+        super(FillerComponent, self).__init__(**kwargs)
+        self.type = 'filler'
+
+
+class IconComponent(FlexComponent):
+    """IconComponent.
+
+    https://developers.line.me/en/docs/messaging-api/reference/#icon-component
+
+    This component draws an icon.
+    """
+
+    def __init__(self, url=None, margin=None, size=None, aspect_ratio=None, **kwargs):
+        """__init__ method.
+
+        :param str url: Image URL
+            Protocol: HTTPS
+            Image format: JPEG or PNG
+        :param str margin: Minimum space between this component
+            and the previous component in the parent box
+        :param str size: Maximum size of the icon width
+        :param str aspect_ratio: Aspect ratio of the icon
+        :param kwargs:
+        """
+        super(IconComponent, self).__init__(**kwargs)
+        self.type = 'icon'
+        self.url = url
+        self.margin = margin
+        self.size = size
+        self.aspect_ratio = aspect_ratio
+
+
+class ImageComponent(FlexComponent):
+    """ImageComponent.
+
+    https://developers.line.me/en/docs/messaging-api/reference/#image-component
+
+    This component draws an image.
+    """
+
+    def __init__(self, url=None, flex=None, margin=None, align=None, gravity=None, size=None,
+                 aspect_ratio=None, aspect_mode=None, background_color=None, action=None,
+                 **kwargs):
+        """__init__ method.
+
+        :param str url: Image URL
+            Protocol: HTTPS
+            Image format: JPEG or PNG
+        :param float flex: The ratio of the width or height of this component within the parent box
+        :param str margin: Minimum space between this component
+            and the previous component in the parent box
+        :param str align: Horizontal alignment style
+        :param str gravity: Vertical alignment style
+        :param str size: Maximum size of the image width
+        :param str aspect_ratio: Aspect ratio of the image
+        :param str aspect_mode: Style of the image
+        :param str background_color: Background color of the image. Use a hexadecimal color code.
+        :param action: Action performed when this image is tapped
+        :type action: list[T <= :py:class:`linebot.models.actions.Action`]
+        :param kwargs:
+        """
+        super(ImageComponent, self).__init__(**kwargs)
+        self.type = 'image'
+        self.url = url
+        self.flex = flex
+        self.margin = margin
+        self.align = align
+        self.gravity = gravity
+        self.size = size
+        self.aspect_ratio = aspect_ratio
+        self.aspect_mode = aspect_mode
+        self.background_color = background_color
+        self.action = get_action(action)
+
+
+class SeparatorComponent(FlexComponent):
+    """SeparatorComponent.
+
+    https://developers.line.me/en/docs/messaging-api/reference/#separator-component
+
+    This component draws a separator between components in the parent box.
+    """
+
+    def __init__(self, margin=None, color=None, **kwargs):
+        """__init__ method.
+
+        :param str margin: Minimum space between this component
+            and the previous component in the parent box
+        :param str color: Color of the separator. Use a hexadecimal color code
+        :param kwargs:
+        """
+        super(SeparatorComponent, self).__init__(**kwargs)
+        self.type = 'separator'
+        self.margin = margin
+        self.color = color
+
+
+class SpacerComponent(FlexComponent):
+    """SpacerComponent.
+
+    https://developers.line.me/en/docs/messaging-api/reference/#spacer-component
+
+    This is an invisible component that places a fixed-size space
+    at the beginning or end of the box
+    """
+
+    def __init__(self, size=None, **kwargs):
+        """__init__ method.
+
+        :param str size: Size of the space
+        :param kwargs:
+        """
+        super(SpacerComponent, self).__init__(**kwargs)
+        self.type = 'spacer'
+        self.size = size
+
+
+class TextComponent(FlexComponent):
+    """TextComponent.
+
+    https://developers.line.me/en/docs/messaging-api/reference/#text-component
+
+    This component draws text. You can format the text.
+    """
+
+    def __init__(self, text=None, flex=None, margin=None, size=None, align=None, gravity=None,
+                 wrap=None, weight=None,
+                 color=None, action=None, **kwargs):
+        r"""__init__ method.
+
+        :param str text: Text
+        :param float flex: The ratio of the width or height of this component within the parent box
+        :param str margin: Minimum space between this component
+            and the previous component in the parent box
+        :param str size: Font size
+        :param str align: Horizontal alignment style
+        :param str gravity: Vertical alignment style
+        :param bool wrap: rue to wrap text. The default value is False.
+            If set to True, you can use a new line character (\n) to begin on a new line.
+        :param str weight: Font weight
+        :param str color: Font color
+        :param action: Action performed when this image is tapped
+        :type action: list[T <= :py:class:`linebot.models.actions.Action`]
+        :param kwargs:
+        """
+        super(TextComponent, self).__init__(**kwargs)
+        self.type = 'text'
+        self.text = text
+        self.flex = flex
+        self.margin = margin
+        self.size = size
+        self.align = align
+        self.gravity = gravity
+        self.wrap = wrap
+        self.weight = weight
+        self.color = color
+        self.action = get_action(action)
diff --git a/linebot/models/imagemap.py b/linebot/models/imagemap.py
new file mode 100644 (file)
index 0000000..cd06f4b
--- /dev/null
@@ -0,0 +1,174 @@
+# -*- 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.
+
+"""linebot.models.imagemap module."""
+
+from __future__ import unicode_literals
+
+from abc import ABCMeta
+
+from future.utils import with_metaclass
+
+from .base import Base
+from .send_messages import SendMessage
+
+
+class ImagemapSendMessage(SendMessage):
+    """ImagemapSendMessage.
+
+    https://devdocs.line.me/en/#imagemap-message
+
+    Imagemaps are images with one or more links. You can assign one link for the entire image
+    or multiple links which correspond to different regions of the image.
+    """
+
+    def __init__(self, base_url=None, alt_text=None, base_size=None, actions=None, **kwargs):
+        """__init__ method.
+
+        :param str base_url: Base URL of image.
+            HTTPS
+        :param str alt_text: Alternative text
+        :param base_size: Width and height of base image
+        :type base_size: :py:class:`linebot.models.imagemap.BaseSize`
+        :param actions: Action when tapped
+        :type actions: list[T <= :py:class:`linebot.models.imagemap.ImagemapAction`]
+        :param kwargs:
+        """
+        super(ImagemapSendMessage, self).__init__(**kwargs)
+
+        self.type = 'imagemap'
+        self.base_url = base_url
+        self.alt_text = alt_text
+        self.base_size = self.get_or_new_from_json_dict(
+            base_size, BaseSize
+        )
+
+        new_actions = []
+        if actions:
+            for action in actions:
+                action_obj = self.get_or_new_from_json_dict_with_types(
+                    action, {
+                        'uri': URIImagemapAction,
+                        'message': MessageImagemapAction
+                    }
+                )
+                if action_obj:
+                    new_actions.append(action_obj)
+        self.actions = new_actions
+
+
+class BaseSize(Base):
+    """BaseSize.
+
+    https://devdocs.line.me/en/#imagemap-message
+    """
+
+    def __init__(self, width=None, height=None, **kwargs):
+        """__init__ method.
+
+        https://devdocs.line.me/en/#imagemap-message
+
+        :param int width: Width of base image (set to 1040px)
+        :param int height: Height of base image(set to the height
+            that corresponds to a width of 1040px
+        :param kwargs:
+        """
+        super(BaseSize, self).__init__(**kwargs)
+
+        self.width = width
+        self.height = height
+
+
+class ImagemapAction(with_metaclass(ABCMeta, Base)):
+    """ImagemapAction.
+
+    https://devdocs.line.me/en/#imagemap-message
+    """
+
+    def __init__(self, **kwargs):
+        """__init__ method.
+
+        :param kwargs:
+        """
+        super(ImagemapAction, self).__init__(**kwargs)
+
+        self.type = None
+
+
+class URIImagemapAction(ImagemapAction):
+    """URIImagemapAction.
+
+    https://devdocs.line.me/en/#imagemap-message
+    """
+
+    def __init__(self, link_uri=None, area=None, **kwargs):
+        """__init__ method.
+
+        :param str link_uri: Webpage URL
+        :param area: Defined tappable area
+        :type area: :py:class:`linebot.models.imagemap.ImagemapArea`
+        :param kwargs:
+        """
+        super(URIImagemapAction, self).__init__(**kwargs)
+
+        self.type = 'uri'
+        self.link_uri = link_uri
+        self.area = self.get_or_new_from_json_dict(area, ImagemapArea)
+
+
+class MessageImagemapAction(ImagemapAction):
+    """MessageImagemapAction.
+
+    https://devdocs.line.me/en/#imagemap-message
+    """
+
+    def __init__(self, text=None, area=None, **kwargs):
+        """__init__ method.
+
+        :param str text: Message to send
+        :param area: Defined tappable area
+        :type area: :py:class:`linebot.models.imagemap.ImagemapArea`
+        :param kwargs:
+        """
+        super(MessageImagemapAction, self).__init__(**kwargs)
+
+        self.type = 'message'
+        self.text = text
+        self.area = self.get_or_new_from_json_dict(area, ImagemapArea)
+
+
+class ImagemapArea(Base):
+    """ImagemapArea.
+
+    https://devdocs.line.me/en/#imagemap-area-object
+
+    Defines the size of the full imagemap with the width as 1040px.
+    The top left is used as the origin of the area.
+    """
+
+    def __init__(self, x=None, y=None, width=None, height=None, **kwargs):
+        """__init__ method.
+
+        :param int x: Horizontal position of the tappable area
+        :param int y: Vertical position of the tappable area
+        :param int width: Width of the tappable area
+        :param int height: Height of the tappable area
+        :param kwargs:
+        """
+        super(ImagemapArea, self).__init__(**kwargs)
+
+        self.x = x
+        self.y = y
+        self.width = width
+        self.height = height
diff --git a/linebot/models/messages.py b/linebot/models/messages.py
new file mode 100644 (file)
index 0000000..ace6e23
--- /dev/null
@@ -0,0 +1,193 @@
+# -*- 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.
+
+"""linebot.models.messages module."""
+
+from __future__ import unicode_literals
+
+from abc import ABCMeta
+
+from future.utils import with_metaclass
+
+from .base import Base
+
+
+class Message(with_metaclass(ABCMeta, Base)):
+    """Abstract Base Class of Message."""
+
+    def __init__(self, id=None, **kwargs):
+        """__init__ method.
+
+        :param str id: Message ID
+        :param kwargs:
+        """
+        super(Message, self).__init__(**kwargs)
+
+        self.type = None
+        self.id = id
+
+
+class TextMessage(Message):
+    """TextMessage.
+
+    https://devdocs.line.me/en/#text-message
+
+    Message object which contains the text sent from the source.
+    """
+
+    def __init__(self, id=None, text=None, **kwargs):
+        """__init__ method.
+
+        :param str id: Message ID
+        :param str text: Message text
+        :param kwargs:
+        """
+        super(TextMessage, self).__init__(id=id, **kwargs)
+
+        self.type = 'text'
+        self.text = text
+
+
+class ImageMessage(Message):
+    """ImageMessage.
+
+    https://devdocs.line.me/en/#image-message
+
+    Message object which contains the image content sent from the source.
+    The binary image data can be retrieved with the Content API.
+    """
+
+    def __init__(self, id=None, **kwargs):
+        """__init__ method.
+
+        :param str id: Message ID
+        :param kwargs:
+        """
+        super(ImageMessage, self).__init__(id=id, **kwargs)
+
+        self.type = 'image'
+
+
+class VideoMessage(Message):
+    """VideoMessage.
+
+    https://devdocs.line.me/en/#video-message
+
+    Message object which contains the video content sent from the source.
+    The binary video data can be retrieved with the Content API.
+    """
+
+    def __init__(self, id=None, **kwargs):
+        """__init__ method.
+
+        :param str id: Message ID
+        :param kwargs:
+        """
+        super(VideoMessage, self).__init__(id=id, **kwargs)
+
+        self.type = 'video'
+
+
+class AudioMessage(Message):
+    """AudioMessage.
+
+    https://devdocs.line.me/en/#audio-message
+
+    Message object which contains the audio content sent from the source.
+    The binary audio data can be retrieved with the Content API.
+    """
+
+    def __init__(self, id=None, **kwargs):
+        """__init__ method.
+
+        :param str id: Message ID
+        :param kwargs:
+        """
+        super(AudioMessage, self).__init__(id=id, **kwargs)
+
+        self.type = 'audio'
+
+
+class LocationMessage(Message):
+    """LocationMessage.
+
+    https://devdocs.line.me/en/#location-message
+    """
+
+    def __init__(self, id=None, title=None, address=None, latitude=None, longitude=None,
+                 **kwargs):
+        """__init__ method.
+
+        :param str id: Message ID
+        :param str title: Title
+        :param str address: Address
+        :param float latitude: Latitude
+        :param float longitude: Longitude
+        :param kwargs:
+        """
+        super(LocationMessage, self).__init__(id=id, **kwargs)
+
+        self.type = 'location'
+        self.title = title
+        self.address = address
+        self.latitude = latitude
+        self.longitude = longitude
+
+
+class StickerMessage(Message):
+    """StickerMessage.
+
+    https://devdocs.line.me/en/#sticker-message
+
+    Message object which contains the sticker data sent from the source.
+    For a list of basic LINE stickers and sticker IDs, see sticker list.
+    """
+
+    def __init__(self, id=None, package_id=None, sticker_id=None, **kwargs):
+        """__init__ method.
+
+        :param str id: Message ID
+        :param str package_id: Package ID
+        :param str sticker_id: Sticker ID
+        :param kwargs:
+        """
+        super(StickerMessage, self).__init__(id=id, **kwargs)
+
+        self.type = 'sticker'
+        self.package_id = package_id
+        self.sticker_id = sticker_id
+
+
+class FileMessage(Message):
+    """FileMessage.
+
+    https://devdocs.line.me/en/#file-message
+
+    Message object which contains the file content sent from the source.
+    The binary file data can be retrieved with the Content API.
+    """
+
+    def __init__(self, id=None, file_name=None, file_size=None, **kwargs):
+        """__init__ method.
+
+        :param str id: Message ID
+        :param str file_name: File Name
+        :param int file_size: File Size
+        :param kwargs:
+        """
+        super(FileMessage, self).__init__(id=id, **kwargs)
+
+        self.type = 'file'
+        self.file_size = file_size
+        self.file_name = file_name
diff --git a/linebot/models/responses.py b/linebot/models/responses.py
new file mode 100644 (file)
index 0000000..8cd6e08
--- /dev/null
@@ -0,0 +1,152 @@
+# -*- 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.
+
+"""linebot.models.responses module."""
+
+from __future__ import unicode_literals
+
+from .base import Base
+from .rich_menu import RichMenuSize, RichMenuArea
+
+
+class Profile(Base):
+    """Profile.
+
+    https://devdocs.line.me/en/#bot-api-get-profile
+    """
+
+    def __init__(self, display_name=None, user_id=None, picture_url=None,
+                 status_message=None, **kwargs):
+        """__init__ method.
+
+        :param str display_name: Display name
+        :param str user_id: User ID
+        :param str picture_url: Image URL
+        :param str status_message: Status message
+        :param kwargs:
+        """
+        super(Profile, self).__init__(**kwargs)
+
+        self.display_name = display_name
+        self.user_id = user_id
+        self.picture_url = picture_url
+        self.status_message = status_message
+
+
+class MemberIds(Base):
+    """MemberIds.
+
+    https://devdocs.line.me/en/#get-group-room-member-ids
+    """
+
+    def __init__(self, member_ids=None, next=None, **kwargs):
+        """__init__ method.
+
+        :param member_ids: List of user IDs of the members in the group or room.
+            Max: 100 user IDs
+        :type member_ids: list[str]
+        :param str next: continuationToken.
+            Only returned when there are more user IDs remaining in memberIds.
+        :param kwargs:
+        """
+        super(MemberIds, self).__init__(**kwargs)
+
+        self.member_ids = member_ids
+        self.next = next
+
+
+class Content(object):
+    """MessageContent.
+
+    https://devdocs.line.me/ja/#get-content
+    """
+
+    def __init__(self, response):
+        """__init__ method.
+
+        :param response: HttpResponse object
+        :type response: T <= :py:class:`linebot.http_client.HttpResponse`
+        """
+        self.response = response
+
+    @property
+    def content_type(self):
+        """Get Content-type header value.
+
+        :rtype: str
+        :return: content-type header value
+        """
+        return self.response.headers.get('content-type')
+
+    @property
+    def content(self):
+        """Get content.
+
+        If content size is large, should use iter_content.
+
+        :rtype: binary
+        """
+        return self.response.content
+
+    def iter_content(self, chunk_size=1024):
+        """Get content as iterator (stream).
+
+        If content size is large, should use this.
+
+        :param chunk_size: Chunk size
+        :rtype: iterator
+        :return:
+        """
+        return self.response.iter_content(chunk_size=chunk_size)
+
+
+class RichMenuResponse(Base):
+    """RichMenuResponse.
+
+    https://developers.line.me/en/docs/messaging-api/reference/#rich-menu-response-object
+    """
+
+    def __init__(self, rich_menu_id=None, size=None, selected=None, name=None,
+                 chat_bar_text=None, areas=None, **kwargs):
+        """__init__ method.
+
+        :param str id: Rich Menu ID
+        :param size: size object which describe the rich menu displayed in the chat.
+            Rich menu images must be one of the following sizes: 2500x1686, 2500x843.
+        :type size: :py:class:`linebot.models.rich_menu.RichMenuSize`
+        :param bool selected: true to display the rich menu by default. Otherwise, false.
+        :param str name: Name of the rich menu.
+            Maximum of 300 characters.
+        :param str chat_bar_text: Text displayed in the chat bar.
+            Maximum of 14 characters.
+        :param areas: Array of area objects which define coordinates and size of tappable areas.
+            Maximum of 20 area objects.
+        :type areas: list[T <= :py:class:`linebot.models.rich_menu.RichMenuArea`]
+        :param kwargs:
+        """
+        super(RichMenuResponse, self).__init__(**kwargs)
+
+        self.rich_menu_id = rich_menu_id
+        self.size = self.get_or_new_from_json_dict(size, RichMenuSize)
+        self.selected = selected
+        self.name = name
+        self.chat_bar_text = chat_bar_text
+
+        new_areas = []
+        if areas:
+            for area in areas:
+                new_areas.append(
+                    self.get_or_new_from_json_dict(area, RichMenuArea)
+                )
+        self.areas = new_areas
diff --git a/linebot/models/rich_menu.py b/linebot/models/rich_menu.py
new file mode 100644 (file)
index 0000000..c4db446
--- /dev/null
@@ -0,0 +1,126 @@
+# -*- 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.
+
+"""linebot.models.rich_menu module."""
+
+from __future__ import unicode_literals
+
+from abc import ABCMeta
+
+from future.utils import with_metaclass
+
+from .actions import get_action
+from .base import Base
+
+
+class RichMenu(with_metaclass(ABCMeta, Base)):
+    """RichMenu.
+
+    https://developers.line.me/en/docs/messaging-api/reference/#rich-menu-object
+    """
+
+    def __init__(self, size=None, selected=None, name=None, chat_bar_text=None,
+                 areas=None, **kwargs):
+        """__init__ method.
+
+        :param size: size object which describe the rich menu displayed in the chat.
+            Rich menu images must be one of the following sizes: 2500x1686, 2500x843.
+        :type size: :py:class:`linebot.models.rich_menu.RichMenuSize`
+        :param bool selected: true to display the rich menu by default. Otherwise, false.
+        :param str name: Name of the rich menu.
+            Maximum of 300 characters.
+        :param str chatBarText: Text displayed in the chat bar.
+                                Maximum of 14 characters.
+        :param areas: Array of area objects which define coordinates and size of tappable areas.
+                      Maximum of 20 area objects.
+        :type areas: list[T <= :py:class:`linebot.models.rich_menu.RichMenuArea`]
+        :param kwargs:
+        """
+        super(RichMenu, self).__init__(**kwargs)
+
+        self.size = self.get_or_new_from_json_dict(size, RichMenuSize)
+        self.selected = selected
+        self.name = name
+        self.chat_bar_text = chat_bar_text
+
+        new_areas = []
+        if areas:
+            for area in areas:
+                new_areas.append(
+                    self.get_or_new_from_json_dict(area, RichMenuArea)
+                )
+        self.areas = new_areas
+
+
+class RichMenuSize(with_metaclass(ABCMeta, Base)):
+    """RichMenuSize.
+
+    https://developers.line.me/en/docs/messaging-api/reference/#size-object
+    """
+
+    def __init__(self, width=None, height=None, **kwargs):
+        """__init__ method.
+
+        :param int width: Width of the rich menu. Must be 2500.
+        :param int height: Height of the rich menu. Possible values: 1686, 843.
+        :param kwargs:
+        """
+        super(RichMenuSize, self).__init__(**kwargs)
+
+        self.width = width
+        self.height = height
+
+
+class RichMenuArea(with_metaclass(ABCMeta, Base)):
+    """RichMenuArea.
+
+    https://developers.line.me/en/docs/messaging-api/reference/#area-object
+    """
+
+    def __init__(self, bounds=None, action=None, **kwargs):
+        """__init__ method.
+
+        :param bounds: Object describing the boundaries of the area in pixels. See bounds object.
+        :type bounds: :py:class:`linebot.models.rich_menu.RichMenuBound`
+        :param action: Action performed when the area is tapped. See action objects.
+        :type action: T <= :py:class:`linebot.models.actions.Action`
+        :param kwargs:
+        """
+        super(RichMenuArea, self).__init__(**kwargs)
+
+        self.bounds = self.get_or_new_from_json_dict(bounds, RichMenuBounds)
+        self.action = get_action(action)
+
+
+class RichMenuBounds(with_metaclass(ABCMeta, Base)):
+    """RichMenuBounds.
+
+    https://developers.line.me/en/docs/messaging-api/reference/#bounds-object
+    """
+
+    def __init__(self, x=None, y=None, width=None, height=None, **kwargs):
+        """__init__ method.
+
+        :param int x: Horizontal position relative to the top-left corner of the area.
+        :param int y: Vertical position relative to the top-left corner of the area.
+        :param int width: Width of the area.
+        :param int height: Height of the area.
+        :param kwargs:
+        """
+        super(RichMenuBounds, self).__init__(**kwargs)
+
+        self.x = x
+        self.y = y
+        self.width = width
+        self.height = height
diff --git a/linebot/models/send_messages.py b/linebot/models/send_messages.py
new file mode 100644 (file)
index 0000000..855e03d
--- /dev/null
@@ -0,0 +1,243 @@
+# -*- 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.
+
+"""linebot.models.send_messages module."""
+
+from __future__ import unicode_literals
+
+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, 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):
+    """TextSendMessage.
+
+    https://devdocs.line.me/en/#text
+    """
+
+    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__(quick_reply=quick_reply, **kwargs)
+
+        self.type = 'text'
+        self.text = text
+
+
+class ImageSendMessage(SendMessage):
+    """ImageSendMessage.
+
+    https://devdocs.line.me/en/#image
+    """
+
+    def __init__(self, original_content_url=None, preview_image_url=None,
+                 quick_reply=None, **kwargs):
+        """__init__ method.
+
+        :param str original_content_url: Image URL.
+            HTTPS
+            JPEG
+            Max: 1024 x 1024
+            Max: 1 MB
+        :param str preview_image_url: Preview image URL
+            HTTPS
+            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__(quick_reply=quick_reply, **kwargs)
+
+        self.type = 'image'
+        self.original_content_url = original_content_url
+        self.preview_image_url = preview_image_url
+
+
+class VideoSendMessage(SendMessage):
+    """VideoSendMessage.
+
+    https://devdocs.line.me/en/#video
+    """
+
+    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.
+            HTTPS
+            mp4
+            Less than 1 minute
+            Max: 10 MB
+        :param str preview_image_url: URL of preview image.
+            HTTPS
+            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__(quick_reply=quick_reply, **kwargs)
+
+        self.type = 'video'
+        self.original_content_url = original_content_url
+        self.preview_image_url = preview_image_url
+
+
+class AudioSendMessage(SendMessage):
+    """AudioSendMessage.
+
+    https://devdocs.line.me/en/#audio
+    """
+
+    def __init__(self, original_content_url=None, duration=None, quick_reply=None, **kwargs):
+        """__init__ method.
+
+        :param str original_content_url: URL of audio file.
+            HTTPS
+            m4a
+            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__(quick_reply=quick_reply, **kwargs)
+
+        self.type = 'audio'
+        self.original_content_url = original_content_url
+        self.duration = duration
+
+
+class LocationSendMessage(SendMessage):
+    """LocationSendMessage.
+
+    https://devdocs.line.me/en/#location
+    """
+
+    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__(quick_reply=quick_reply, **kwargs)
+
+        self.type = 'location'
+        self.title = title
+        self.address = address
+        self.latitude = latitude
+        self.longitude = longitude
+
+
+class StickerSendMessage(SendMessage):
+    """StickerSendMessage.
+
+    https://devdocs.line.me/en/#sticker
+    """
+
+    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__(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)
diff --git a/linebot/models/sources.py b/linebot/models/sources.py
new file mode 100644 (file)
index 0000000..c08a2c7
--- /dev/null
@@ -0,0 +1,152 @@
+# -*- 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.
+
+"""linebot.models.sources module."""
+
+from __future__ import unicode_literals
+
+import warnings
+from abc import ABCMeta, abstractproperty
+
+from future.utils import with_metaclass
+
+from .base import Base
+
+
+class Source(with_metaclass(ABCMeta, Base)):
+    """Abstract Base Class of Source."""
+
+    def __init__(self, **kwargs):
+        """__init__ method.
+
+        :param kwargs:
+        """
+        super(Source, self).__init__(**kwargs)
+        self.type = None
+
+    @abstractproperty
+    def sender_id(self):
+        """Abstract property of id to send a message.
+
+        If SourceUser, return user_id.
+        If SourceGroup, return group_id.
+        If SourceRoom, return room_id.
+
+        'sender_id' is deprecated.
+
+        :rtype: str
+        :return:
+        """
+        warnings.warn("'sender_id' is deprecated.", DeprecationWarning, stacklevel=2)
+        raise NotImplementedError
+
+
+class SourceUser(Source):
+    """SourceUser.
+
+    https://devdocs.line.me/en/#source-user
+
+    JSON object which contains the source user of the event.
+    """
+
+    def __init__(self, user_id=None, **kwargs):
+        """__init__ method.
+
+        :param str user_id: ID of the source user
+        :param kwargs:
+        """
+        super(SourceUser, self).__init__(**kwargs)
+
+        self.type = 'user'
+        self.user_id = user_id
+
+    @property
+    def sender_id(self):
+        """Alias of user_id.
+
+        'sender_id' is deprecated. Use 'user_id' instead.
+
+        :rtype: str
+        :return:
+        """
+        warnings.warn("'sender_id' is deprecated.", DeprecationWarning, stacklevel=2)
+        return self.user_id
+
+
+class SourceGroup(Source):
+    """SourceGroup.
+
+    https://devdocs.line.me/en/#source-group
+
+    JSON object which contains the source group of the event.
+    """
+
+    def __init__(self, group_id=None, user_id=None, **kwargs):
+        """__init__ method.
+
+        :param str group_id: ID of the source group
+        :param str user_id: ID of the source user
+        :param kwargs:
+        """
+        super(SourceGroup, self).__init__(**kwargs)
+
+        self.type = 'group'
+        self.group_id = group_id
+        self.user_id = user_id
+
+    @property
+    def sender_id(self):
+        """Alias of group_id.
+
+        'sender_id' is deprecated. Use 'group_id' instead.
+
+        :rtype: str
+        :return:
+        """
+        warnings.warn("'sender_id' is deprecated.", DeprecationWarning, stacklevel=2)
+        return self.group_id
+
+
+class SourceRoom(Source):
+    """SourceRoom.
+
+    https://devdocs.line.me/en/#source-room
+
+    JSON object which contains the source room of the event.
+    """
+
+    def __init__(self, room_id=None, user_id=None, **kwargs):
+        """__init__ method.
+
+        :param str room_id: ID of the source room
+        :param str user_id: ID of the source user
+        :param kwargs:
+        """
+        super(SourceRoom, self).__init__(**kwargs)
+
+        self.type = 'room'
+        self.room_id = room_id
+        self.user_id = user_id
+
+    @property
+    def sender_id(self):
+        """Alias of room_id.
+
+        'sender_id' is deprecated. Use 'room_id' instead.
+
+        :rtype: str
+        :return:
+        """
+        warnings.warn("'sender_id' is deprecated.", DeprecationWarning, stacklevel=2)
+        return self.room_id
diff --git a/linebot/models/template.py b/linebot/models/template.py
new file mode 100644 (file)
index 0000000..0e8efb5
--- /dev/null
@@ -0,0 +1,285 @@
+# -*- 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.
+
+"""linebot.models.template module."""
+
+from __future__ import unicode_literals
+
+from abc import ABCMeta
+
+from future.utils import with_metaclass
+
+from .actions import get_action, get_actions
+from .base import Base
+from .send_messages import SendMessage
+
+
+class TemplateSendMessage(SendMessage):
+    """TemplateSendMessage.
+
+    https://devdocs.line.me/en/#template-messages
+
+    Template messages are messages with predefined layouts which you can customize.
+    There are three types of templates available
+    that can be used to interact with users through your bot.
+    """
+
+    def __init__(self, alt_text=None, template=None, **kwargs):
+        """__init__ method.
+
+        :param str alt_text: Alternative text for unsupported devices.
+        :param template: Object with the contents of the template.
+        :type template: T <= :py:class:`linebot.models.template.Template`
+        :param kwargs:
+        """
+        super(TemplateSendMessage, self).__init__(**kwargs)
+
+        self.type = 'template'
+        self.alt_text = alt_text
+        self.template = self.get_or_new_from_json_dict_with_types(
+            template, {
+                'buttons': ButtonsTemplate,
+                'confirm': ConfirmTemplate,
+                'carousel': CarouselTemplate,
+                'image_carousel': ImageCarouselTemplate
+            }
+        )
+
+
+class Template(with_metaclass(ABCMeta, Base)):
+    """Abstract Base Class of Template."""
+
+    def __init__(self, **kwargs):
+        """__init__ method.
+
+        :param kwargs:
+        """
+        super(Template, self).__init__(**kwargs)
+
+        self.type = None
+
+
+class ButtonsTemplate(Template):
+    """ButtonsTemplate.
+
+    https://devdocs.line.me/en/#buttons
+
+    Template message with an image, title, text, and multiple action buttons.
+    """
+
+    def __init__(self, text=None, title=None, thumbnail_image_url=None,
+                 image_aspect_ratio=None,
+                 image_size=None, image_background_color=None,
+                 actions=None, **kwargs):
+        """__init__ method.
+
+        :param str text: Message text.
+            Max: 160 characters (no image or title).
+            Max: 60 characters (message with an image or title)
+        :param str title: Title.
+            Max: 40 characters
+        :param str thumbnail_image_url: Image URL.
+            HTTPS
+            JPEG or PNG
+            Aspect ratio: 1:1.51
+            Max width: 1024px
+            Max: 1 MB
+        :param str image_aspect_ratio: Aspect ratio of the image.
+            Specify one of the following values:
+            rectangle: 1.51:1
+            square: 1:1
+        :param str image_size: Size of the image.
+            Specify one of the following values:
+            cover: The image fills the entire image area.
+                Parts of the image that do not fit in the area are not displayed.
+            contain: The entire image is displayed in the image area.
+                A background is displayed in the unused areas to the left and right
+                of vertical images and in the areas above and below horizontal images.
+        :param str image_background_color: Background color of image.
+            Specify a RGB color value.
+        :param actions: Action when tapped.
+            Max: 4
+        :type actions: list[T <= :py:class:`linebot.models.actions.Action`]
+        :param kwargs:
+        """
+        super(ButtonsTemplate, self).__init__(**kwargs)
+
+        self.type = 'buttons'
+        self.text = text
+        self.title = title
+        self.thumbnail_image_url = thumbnail_image_url
+        self.image_aspect_ratio = image_aspect_ratio
+        self.image_size = image_size
+        self.image_background_color = image_background_color
+        self.actions = get_actions(actions)
+
+
+class ConfirmTemplate(Template):
+    """ConfirmTemplate.
+
+    https://devdocs.line.me/en/#confirm
+
+    Template message with two action buttons.
+    """
+
+    def __init__(self, text=None, actions=None, **kwargs):
+        """__init__ method.
+
+        :param str text: Message text.
+            Max: 240 characters
+        :param actions: Action when tapped.
+            Max: 2
+        :type actions: list[T <= :py:class:`linebot.models.actions.Action`]
+        :param kwargs:
+        """
+        super(ConfirmTemplate, self).__init__(**kwargs)
+
+        self.type = 'confirm'
+        self.text = text
+        self.actions = get_actions(actions)
+
+
+class CarouselTemplate(Template):
+    """CarouselTemplate.
+
+    https://devdocs.line.me/en/#carousel
+
+    Template message with multiple columns which can be cycled like a carousel.
+    """
+
+    def __init__(self, columns=None, image_aspect_ratio=None,
+                 image_size=None, **kwargs):
+        """__init__ method.
+
+        :param columns: Array of columns.
+            Max: 10
+        :type columns: list[T <= :py:class:`linebot.models.template.CarouselColumn`]
+        :param str image_aspect_ratio: Aspect ratio of the image.
+            Specify one of the following values:
+            rectangle: 1.51:1
+            square: 1:1
+        :param str image_size: Size of the image.
+            Specify one of the following values:
+            cover: The image fills the entire image area.
+                Parts of the image that do not fit in the area are not displayed.
+            contain: The entire image is displayed in the image area.
+                A background is displayed in the unused areas to the left and right
+                of vertical images and in the areas above and below horizontal images.
+        :param kwargs:
+        """
+        super(CarouselTemplate, self).__init__(**kwargs)
+
+        self.type = 'carousel'
+
+        new_columns = []
+        if columns:
+            for column in columns:
+                new_columns.append(self.get_or_new_from_json_dict(
+                    column, CarouselColumn
+                ))
+        self.columns = new_columns
+        self.image_aspect_ratio = image_aspect_ratio
+        self.image_size = image_size
+
+
+class ImageCarouselTemplate(Template):
+    """ImageCarouselTemplate.
+
+    https://devdocs.line.me/en/#image-carousel
+
+    Template message with multiple images columns which can be cycled like as carousel.
+    """
+
+    def __init__(self, columns=None, **kwargs):
+        """__init__ method.
+
+        :param columns: Array of columns.
+            Max: 10
+        :type columns: list[T <= :py:class:`linebot.models.template.ImageCarouselColumn`]
+        :param kwargs:
+        """
+        super(ImageCarouselTemplate, self).__init__(**kwargs)
+
+        self.type = 'image_carousel'
+
+        new_columns = []
+        if columns:
+            for column in columns:
+                new_columns.append(self.get_or_new_from_json_dict(
+                    column, ImageCarouselColumn
+                ))
+        self.columns = new_columns
+
+
+class CarouselColumn(Base):
+    """CarouselColumn.
+
+    https://devdocs.line.me/en/#column-object
+    """
+
+    def __init__(self, text=None, title=None, thumbnail_image_url=None,
+                 image_background_color=None, actions=None, **kwargs):
+        """__init__ method.
+
+        :param str text: Message text.
+            Max: 120 characters (no image or title)
+            Max: 60 characters (message with an image or title)
+        :param str title: Title.
+            Max: 40 characters
+        :param str thumbnail_image_url: Image URL.
+            HTTPS
+            JPEG or PNG
+            Aspect ratio: 1:1.51
+            Max width: 1024px
+            Max: 1 MB
+        :param str image_background_color: Background color of image.
+            Specify a RGB color value.
+        :param actions: Action when tapped.
+            Max: 3
+        :type actions: list[T <= :py:class:`linebot.models.actions.Action`]
+        :param kwargs:
+        """
+        super(CarouselColumn, self).__init__(**kwargs)
+
+        self.text = text
+        self.title = title
+        self.thumbnail_image_url = thumbnail_image_url
+        self.image_background_color = image_background_color
+        self.actions = get_actions(actions)
+
+
+class ImageCarouselColumn(Base):
+    """ImageCarouselColumn.
+
+    https://devdocs.line.me/en/#column-object-for-image-carousel
+    """
+
+    def __init__(self, image_url=None, action=None, **kwargs):
+        """__init__ method.
+
+        :param str image_url: Image URL.
+            HTTPS
+            JPEG or PNG
+            Aspect ratio: 1:1
+            Max width: 1024px
+            Max: 1 MB
+        :param action: Action when image is tapped
+            Max: 5
+        :type action: T <= :py:class:`linebot.models.actions.Action`
+        :param kwargs:
+        """
+        super(ImageCarouselColumn, self).__init__(**kwargs)
+
+        self.image_url = image_url
+        self.action = get_action(action)
diff --git a/linebot/utils.py b/linebot/utils.py
new file mode 100644 (file)
index 0000000..cc09699
--- /dev/null
@@ -0,0 +1,69 @@
+# -*- 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.
+
+"""linebot.http_client module."""
+
+from __future__ import unicode_literals
+
+import logging
+import re
+import sys
+
+LOGGER = logging.getLogger('linebot')
+
+PY3 = sys.version_info[0] == 3
+
+
+def to_snake_case(text):
+    """Convert to snake case.
+
+    :param str text:
+    :rtype: str
+    :return:
+    """
+    s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', text)
+    return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
+
+
+def to_camel_case(text):
+    """Convert to camel case.
+
+    :param str text:
+    :rtype: str
+    :return:
+    """
+    split = text.split('_')
+    return split[0] + "".join(x.title() for x in split[1:])
+
+
+def safe_compare_digest(val1, val2):
+    """safe_compare_digest method.
+
+    :param val1: string or bytes for compare
+    :type val1: str | bytes
+    :param val2: string or bytes for compare
+    :type val2: str | bytes
+    """
+    if len(val1) != len(val2):
+        return False
+
+    result = 0
+    if PY3 and isinstance(val1, bytes) and isinstance(val2, bytes):
+        for i, j in zip(val1, val2):
+            result |= i ^ j
+    else:
+        for i, j in zip(val1, val2):
+            result |= (ord(i) ^ ord(j))
+
+    return result == 0
diff --git a/linebot/webhook.py b/linebot/webhook.py
new file mode 100644 (file)
index 0000000..6253dcb
--- /dev/null
@@ -0,0 +1,250 @@
+# -*- 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.
+
+"""linebot.http_client webhook."""
+
+from __future__ import unicode_literals
+
+import base64
+import hashlib
+import hmac
+import inspect
+import json
+
+from .exceptions import InvalidSignatureError
+from .models.events import (
+    MessageEvent,
+    FollowEvent,
+    UnfollowEvent,
+    JoinEvent,
+    LeaveEvent,
+    PostbackEvent,
+    BeaconEvent,
+    AccountLinkEvent,
+)
+from .utils import LOGGER, PY3, safe_compare_digest
+
+
+if hasattr(hmac, "compare_digest"):
+    def compare_digest(val1, val2):
+        """compare_digest function.
+
+        If hmac module has compare_digest function, use it.
+        Or not, use linebot.utils.safe_compare_digest.
+
+        :param val1: string or bytes for compare
+        :type val1: str | bytes
+        :param val2: string or bytes for compare
+        :type val2: str | bytes
+        :rtype: bool
+        :return: result
+        """
+        return hmac.compare_digest(val1, val2)
+else:
+    def compare_digest(val1, val2):
+        """compare_digest function.
+
+        If hmac module has compare_digest function, use it.
+        Or not, use linebot.utils.safe_compare_digest.
+
+        :param val1: string or bytes for compare
+        :type val1: str | bytes
+        :param val2: string or bytes for compare
+        :type val2: str | bytes
+        :rtype: bool
+        :return: result
+        """
+        return safe_compare_digest(val1, val2)
+
+
+class SignatureValidator(object):
+    """Signature validator.
+
+    https://devdocs.line.me/en/#webhook-authentication
+    """
+
+    def __init__(self, channel_secret):
+        """__init__ method.
+
+        :param str channel_secret: Channel secret (as text)
+        """
+        self.channel_secret = channel_secret.encode('utf-8')
+
+    def validate(self, body, signature):
+        """Check signature.
+
+        https://devdocs.line.me/en/#webhook-authentication
+
+        :param str body: Request body (as text)
+        :param str signature: X-Line-Signature value (as text)
+        :rtype: bool
+        :return: result
+        """
+        gen_signature = hmac.new(
+            self.channel_secret,
+            body.encode('utf-8'),
+            hashlib.sha256
+        ).digest()
+
+        return compare_digest(
+                signature.encode('utf-8'), base64.b64encode(gen_signature)
+        )
+
+
+class WebhookParser(object):
+    """Webhook Parser."""
+
+    def __init__(self, channel_secret):
+        """__init__ method.
+
+        :param str channel_secret: Channel secret (as text)
+        """
+        self.signature_validator = SignatureValidator(channel_secret)
+
+    def parse(self, body, signature):
+        """Parse webhook request body as text.
+
+        :param str body: Webhook request body (as text)
+        :param str signature: X-Line-Signature value (as text)
+        :rtype: list[T <= :py:class:`linebot.models.events.Event`]
+        :return:
+        """
+        if not self.signature_validator.validate(body, signature):
+            raise InvalidSignatureError(
+                'Invalid signature. signature=' + signature)
+
+        body_json = json.loads(body)
+        events = []
+        for event in body_json['events']:
+            event_type = event['type']
+            if event_type == 'message':
+                events.append(MessageEvent.new_from_json_dict(event))
+            elif event_type == 'follow':
+                events.append(FollowEvent.new_from_json_dict(event))
+            elif event_type == 'unfollow':
+                events.append(UnfollowEvent.new_from_json_dict(event))
+            elif event_type == 'join':
+                events.append(JoinEvent.new_from_json_dict(event))
+            elif event_type == 'leave':
+                events.append(LeaveEvent.new_from_json_dict(event))
+            elif event_type == 'postback':
+                events.append(PostbackEvent.new_from_json_dict(event))
+            elif event_type == 'beacon':
+                events.append(BeaconEvent.new_from_json_dict(event))
+            elif event_type == 'accountLink':
+                events.append(AccountLinkEvent.new_from_json_dict(event))
+            else:
+                LOGGER.warn('Unknown event type. type=' + event_type)
+
+        return events
+
+
+class WebhookHandler(object):
+    """Webhook Handler."""
+
+    def __init__(self, channel_secret):
+        """__init__ method.
+
+        :param str channel_secret: Channel secret (as text)
+        """
+        self.parser = WebhookParser(channel_secret)
+        self._handlers = {}
+        self._default = None
+
+    def add(self, event, message=None):
+        """[Decorator] Add handler method.
+
+        :param event: Specify a kind of Event which you want to handle
+        :type event: T <= :py:class:`linebot.models.events.Event` class
+        :param message: (optional) If event is MessageEvent,
+            specify kind of Messages which you want to handle
+        :type: message: T <= :py:class:`linebot.models.messages.Message` class
+        :rtype: func
+        :return: decorator
+        """
+        def decorator(func):
+            if isinstance(message, (list, tuple)):
+                for it in message:
+                    self.__add_handler(func, event, message=it)
+            else:
+                self.__add_handler(func, event, message=message)
+
+            return func
+
+        return decorator
+
+    def default(self):
+        """[Decorator] Set default handler method.
+
+        :rtype: func
+        :return:
+        """
+        def decorator(func):
+            self._default = func
+            return func
+
+        return decorator
+
+    def handle(self, body, signature):
+        """Handle webhook.
+
+        :param str body: Webhook request body (as text)
+        :param str signature: X-Line-Signature value (as text)
+        """
+        events = self.parser.parse(body, signature)
+
+        for event in events:
+            func = None
+            key = None
+
+            if isinstance(event, MessageEvent):
+                key = self.__get_handler_key(
+                    event.__class__, event.message.__class__)
+                func = self._handlers.get(key, None)
+
+            if func is None:
+                key = self.__get_handler_key(event.__class__)
+                func = self._handlers.get(key, None)
+
+            if func is None:
+                func = self._default
+
+            if func is None:
+                LOGGER.info('No handler of ' + key + ' and no default handler')
+            else:
+                args_count = self.__get_args_count(func)
+                if args_count == 0:
+                    func()
+                else:
+                    func(event)
+
+    def __add_handler(self, func, event, message=None):
+        key = self.__get_handler_key(event, message=message)
+        self._handlers[key] = func
+
+    @staticmethod
+    def __get_args_count(func):
+        if PY3:
+            arg_spec = inspect.getfullargspec(func)
+            return len(arg_spec.args)
+        else:
+            arg_spec = inspect.getargspec(func)
+            return len(arg_spec.args)
+
+    @staticmethod
+    def __get_handler_key(event, message=None):
+        if message is None:
+            return event.__name__
+        else:
+            return event.__name__ + '_' + message.__name__
index a7d6aa1..a7e4e7a 100644 (file)
@@ -1,2 +1,3 @@
 tornado==4.4.2
-pymongo==3.4.0
\ No newline at end of file
+future
+requests
\ No newline at end of file
diff --git a/runtime.txt b/runtime.txt
deleted file mode 100644 (file)
index 02d0df5..0000000
+++ /dev/null
@@ -1 +0,0 @@
-python-3.6.3