Repository: line/line-bot-sdk-python
Pull request #85 (2017-12-17 03:50:06)

Add API for RichMenu Control
* Add create rich menu function and function will return a rich menu id

* Add function to delete rich menu

* Add function to get rich menu by rich menu id

* Add comments for new functions

* add  function link_rich_menu_to_user but not test yet

* add function to get rich menu list

* add function to upload rich menu image

* add function to get rich menu id of user

* add function to unlink rich menu from user

* fix typo in comment

* Fix for flake8 check result

* Add test case for get_rich_menu_id_of_user, unlink_rich_menu_from_user, link_rich_menu_to_user, delete_rich_menu

* add api to download_rich_menu_image

* add test case for create_rich_menu and get_rich_menu_list

* Update README.rst for Rich menu apis

* Update README.rst

* Update URL

quts: It looks like that pytest not support python 3.3 currently. 
It should be the reason why python 3.3 checks fail message appear in every pull request.

Should we remove python 3.3 from the test environment, such as .travis.yml and tox.ini?

```
Collecting six

  Using cached six-1.11.0-py2.py3-none-any.whl

Collecting requests>=2.0 (from -r /home/travis/build/line/line-bot-sdk-python/requirements.txt (line 1))

  Downloading requests-2.18.4-py2.py3-none-any.whl (88kB)

Collecting future (from -r /home/travis/build/line/line-bot-sdk-python/requirements.txt (line 2))

  Downloading future-0.16.0.tar.gz (824kB)

Collecting pytest (from -r /home/travis/build/line/line-bot-sdk-python/requirements-test.txt (line 1))

  Downloading pytest-3.3.1-py2.py3-none-any.whl (184kB)

pytest requires Python '>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*' but the running Python is 3.3.6


ERROR: could not install deps [six, -r/home/travis/build/line/line-bot-sdk-python/requirements.txt, -r/home/travis/build/line/line-bot-sdk-python/requirements-test.txt]; v = InvocationError('/home/travis/build/line/line-bot-sdk-python/.tox/py33/bin/pip install six -r/home/travis/build/line/line-bot-sdk-python/requirements.txt -r/home/travis/build/line/line-bot-sdk-python/requirements-test.txt (see /home/travis/build/line/line-bot-sdk-python/.tox/py33/log/py33-1.log)', 1)
```
quts: merged PR#88 to fix the issue about check fail
quts: Thanks for reviewing, and I made some modification. Please advise.
be-hase: Thanks !
I will prepare release in a few days.

Patch:
diff --git a/README.rst b/README.rst
index dd750a6..379a5d7 100644
--- a/README.rst
+++ b/README.rst
@@ -241,6 +241,135 @@ https://developers.line.me/en/docs/messaging-api/reference/#leave-room
 
     line_bot_api.leave_room(room_id)
 
+create\_rich\_menu(self, data, timeout=None)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Create a rich menu object through a group of given data and return rich menu id.
+The data is an rich menu object to create.
+
+https://developers.line.me/en/docs/messaging-api/reference/#create-rich-menu
+
+.. code:: python
+    
+    rich_menu_to_create = RichMenu(
+                                size=RichMenuBound(
+                                    width=2500,
+                                    height=1686
+                                ),
+                                selected= False,
+                                name="nice richmenu",
+                                chatBarText="touch me",
+                                areas=[
+                                    RichMenuArea(
+                                        RichMenuBound(
+                                            x=0,
+                                            y=0,
+                                            width=2500,
+                                            height=1686 
+                                        ),
+                                        URITemplateAction(
+                                            uri='line://nv/location'
+                                        )                    
+                                    )
+                                ]
+                            )
+    rich_menu_id = line_bot_api.create_rich_menu(data=rich_menu_to_create)
+    print(rich_menu_id)
+
+delete\_rich\_menu(self, rich\_menu\_id, timeout=None)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Delete rich menu object through a given rich_menu_id.
+
+https://developers.line.me/en/docs/messaging-api/reference/#delete-rich-menu
+        
+.. code:: python
+
+    line_bot_api.delete_rich_menu(rich_menu_id)
+
+set\_rich\_menu\_image(self, rich\_menu\_id, content\_type, content, timeout=None)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Uploads and attaches an image to a rich menu through id and image path.
+
+https://developers.line.me/en/docs/messaging-api/reference/#upload-rich-menu-image
+
+.. code:: python
+
+    line_bot_api.set_rich_menu_image(rich_menu_id, content_type, content)
+
+link\_rich\_menu\_to\_user(self, user\_id, rich\_menu\_id, timeout=None)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Links a rich menu to a user. Only one rich menu can be linked to a user at one time.
+
+https://developers.line.me/en/docs/messaging-api/reference/#link-rich-menu-to-user
+
+.. code:: python
+
+    line_bot_api.link_rich_menu_to_user(user_id, rich_menu_id)
+
+get\_rich\_menu\_list(self, timeout=None)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Gets a list of all uploaded rich menus.
+
+https://developers.line.me/en/docs/messaging-api/reference/#get-rich-menu-list
+
+.. code:: python
+
+    lst_rich_menu_obj = line_bot_api.get_rich_menu_list()
+    for rich_menu_obj in lst_rich_menu_obj:
+        print(rich_menu_obj.rich_menu_id)
+
+get\_rich\_menu(self, rich\_menu\_id, timeout=None)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Get rich menu object through a given Rich menu ID.
+
+https://developers.line.me/en/docs/messaging-api/reference/#get-rich-menu
+
+.. code:: python
+
+    rich_menu_object = line_bot_api.get_rich_menu(rich_menu_id)
+    print(rich_menu_obj.rich_menu_id)
+
+get\_rich\_menu\_id\_of\_user(self, user\_id, timeout=None)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Gets the ID of the rich menu linked to a user.
+
+https://developers.line.me/en/docs/messaging-api/reference/#get-rich-menu-id-of-user
+
+.. code:: python
+
+    rich_menu_object = ine_bot_api.get_rich_menu_id_of_user(user_id)
+    print(rich_menu_object.rich_menu_id)
+
+unlink\_rich\_menu\_from\_user(self, user\_id, timeout=None)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Unlinks a rich menu from a user.
+
+https://developers.line.me/en/docs/messaging-api/reference/#unlink-rich-menu-from-user
+
+.. code:: python
+
+    line_bot_api.unlink_rich_menu_from_user(user_id)
+
+get_rich_menu_image(self, rich\_menu\_id, timeout=None)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Downloads an image associated with a rich menu.
+
+https://developers.line.me/en/docs/messaging-api/reference/#download-rich-menu-image
+
+.. code:: python
+
+    message_content = line_bot_api.get_rich_menu_image(rich_menu_id)
+    with open(file_path, 'wb') as fd:
+        for chunk in message_content.iter_content():
+            fd.write(chunk)
+
 ※ Error handling
 ^^^^^^^^^^^^^^^^
 
diff --git a/linebot/api.py b/linebot/api.py
index 8babcd2..f894169 100644
--- a/linebot/api.py
+++ b/linebot/api.py
@@ -22,7 +22,7 @@ from .__about__ import __version__
 from .exceptions import LineBotApiError
 from .http_client import HttpClient, RequestsHttpClient
 from .models.error import Error
-from .models.responses import Profile, MemberIds, MessageContent
+from .models.responses import Profile, MemberIds, MessageContent, RichMenuResponse
 
 
 class LineBotApi(object):
@@ -124,6 +124,206 @@ class LineBotApi(object):
             '/v2/bot/message/push', data=json.dumps(data), 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
+
+        Get rich menu object through a given rich_menu_id
+
+        :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)
+        :return: RichMenuResponse instance
+        :type RichMenuResponse: T <= :py:class:`linebot.models.reponse.RichMenuResponse`
+        """
+        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 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
+
+        Delete rich menu object through a given rich_menu_id
+
+        :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)
+        """
+        self._delete(
+            '/v2/bot/richmenu/{rich_menu_id}'.format(rich_menu_id=rich_menu_id),
+            timeout=timeout
+        )
+
+    def create_rich_menu(self, rich_menu, timeout=None):
+        """Call delete rich menu API.
+
+        https://developers.line.me/en/docs/messaging-api/reference/#create-rich-menu
+
+        Create a rich menu object through a group of given rich_menu data
+
+        :param rich_menu: Inquired to create a rich menu.
+        :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)
+        :return: rich menu id
+        """
+        response = self._post(
+            '/v2/bot/richmenu', data=rich_menu.as_json_string(), 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
+
+        Links a rich menu to a user. Only one rich menu can be linked to a user at one time.
+
+        :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 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
+
+        Gets a list of all uploaded rich menus.
+
+        :return: An list of RichMenuResponse instances
+        :type RichMenuResponse: T <= :py:class:`linebot.models.reponse.RichMenuResponse`
+        :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)
+        """
+        response = self._get(
+            '/v2/bot/richmenu/list',
+            timeout=timeout
+        )
+
+        lst_result = []
+        for richmenu in response.json['richmenus']:
+            lst_result.append(RichMenuResponse.new_from_json_dict(richmenu))
+
+        return lst_result
+
+    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 rechmenu
+        :param str content_type: image/jpeg or image/png
+        :param str content: image content as request body in bytes
+        :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.headers['Content-Type'] = content_type
+
+        # send request
+        self._post(
+            '/v2/bot/richmenu/{rich_menu_id}/content'.format(rich_menu_id=rich_menu_id),
+            data=content,
+            timeout=timeout
+        )
+
+        # remove added header to avoid side effect in other apis
+        del self.headers['Content-Type']
+
+    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
+
+        Gets the ID of the rich menu linked to a 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)
+        """
+        response = self._get(
+            '/v2/bot/user/{user_id}/richmenu'.format(user_id=user_id),
+            timeout=timeout
+        )
+
+        return response.json.get('richMenuId')
+
+    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
+
+        Unlinks a rich menu from a 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
+
+        Downloads an image associated with a rich menu.
+
+        :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.MessageContent`
+        :return: MessageContent instance
+        """
+        response = self._get(
+            '/v2/bot/richmenu/{rich_menu_id}/content'.format(rich_menu_id=rich_menu_id),
+            timeout=timeout
+        )
+
+        return MessageContent(response)
+
     def multicast(self, to, messages, timeout=None):
         """Call multicast API.
 
@@ -372,6 +572,18 @@ class LineBotApi(object):
         self.__check_error(response)
         return response
 
+    def _delete(self, path, data=None, timeout=None):
+        url = self.endpoint + path
+        headers = {'Content-Type': 'application/json'}
+        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:
diff --git a/linebot/http_client.py b/linebot/http_client.py
index a83cba0..9a3f47d 100644
--- a/linebot/http_client.py
+++ b/linebot/http_client.py
@@ -75,6 +75,23 @@ class HttpClient(with_metaclass(ABCMeta)):
         """
         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."""
@@ -137,6 +154,29 @@ class RequestsHttpClient(HttpClient):
 
         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."""
diff --git a/linebot/models/__init__.py b/linebot/models/__init__.py
index 19c0865..505eab8 100644
--- a/linebot/models/__init__.py
+++ b/linebot/models/__init__.py
@@ -84,3 +84,8 @@ from .template import (  # noqa
     ImageCarouselTemplate,
     ImageCarouselColumn,
 )
+from .rich_menu import ( # noqa
+    RichMenu,
+    RichMenuBound,
+    RichMenuArea,
+)
diff --git a/linebot/models/responses.py b/linebot/models/responses.py
index 4bdd136..bea4be0 100644
--- a/linebot/models/responses.py
+++ b/linebot/models/responses.py
@@ -19,6 +19,40 @@ from __future__ import unicode_literals
 from .base import Base
 
 
+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, chatBarText=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: T <= :py:class:`linebot.models.rich_menu.RichMenuBound`
+        :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: T <= :py:class:`linebot.models.rich_menu.RichMenuArea`
+        :param kwargs:
+        """
+        super(RichMenuResponse, self).__init__(**kwargs)
+
+        self.rich_menu_id = rich_menu_id
+        self.size = size
+        self.selected = selected
+        self.name = name
+        self.chatBarText = chatBarText
+        self.areas = areas
+
+
 class Profile(Base):
     """Profile.
 
diff --git a/linebot/models/rich_menu.py b/linebot/models/rich_menu.py
new file mode 100644
index 0000000..8ebf3fa
--- /dev/null
+++ b/linebot/models/rich_menu.py
@@ -0,0 +1,115 @@
+# -*- 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 .base import Base
+
+
+class RichMenu(with_metaclass(ABCMeta, Base)):
+    """RichMenu.
+
+    https://developers.line.me/en/docs/messaging-api/reference/#rich-menu-object
+
+    This is the template to create a richmenu before posting to remote server,
+    or a template to receive rich menu query result from remote server.
+    """
+
+    def __init__(self, size=None, selected=None, name=None, chatBarText=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: T <= :py:class:`linebot.models.rich_menu.RichMenuBound`
+        :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: T <= :py:class:`linebot.models.rich_menu.RichMenuArea`
+        :param kwargs:
+        """
+        super(RichMenu, self).__init__(**kwargs)
+
+        self.size = size  # RichMenuBound
+        self.selected = selected
+        self.name = name
+        self.chatBarText = chatBarText
+        self.areas = []
+        if not isinstance(areas, (list, tuple)):
+            areas = [areas]
+
+        self.areas = [area.as_json_dict() for area in areas]  # RichMenuArea
+
+
+class RichMenuBound(with_metaclass(ABCMeta, Base)):
+    """RichMenuBound.
+
+    https://developers.line.me/en/docs/messaging-api/reference/#bounds-object
+    https://developers.line.me/en/docs/messaging-api/reference/#size-object
+
+    This is the template to describe the boundaries of the area in pixels.
+    If x and y are equal to default value, the bound object is equal to size 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(RichMenuBound, self).__init__(id=id, **kwargs)
+
+        self.x = x
+        self.y = y
+        self.width = width
+        self.height = height
+
+
+class RichMenuArea(with_metaclass(ABCMeta, Base)):
+    """RichMenuArea.
+
+    https://developers.line.me/en/docs/messaging-api/reference/#area-object
+
+    Array of area objects which define the coordinates and size of tappable areas.
+    """
+
+    def __init__(self, bounds=None, action=None, **kwargs):
+        """__init__ method.
+
+        :param bounds: Object describing the boundaries of the area in pixels.
+        :type areas: T <= :py:class:`linebot.models.template.RichMenuBound`
+        :param action: Action performed when the area is tapped.
+        :type action: T <= :py:class:`linebot.models.template.PostbackTemplateAction` |
+                      T <= :py:class:`linebot.models.template.MessageTemplateAction` |
+                      T <= :py:class:`linebot.models.template.URITemplateAction` |
+                      T <= :py:class:`linebot.models.template.DatetimePickerTemplateAction`
+        :param kwargs:
+        """
+        super(RichMenuArea, self).__init__(id=id, **kwargs)
+
+        self.action = action
+        self.bounds = bounds  # RichMenuBound


Test patch:
diff --git a/tests/api/test_rich_menu.py b/tests/api/test_rich_menu.py
new file mode 100644
index 0000000..a7cc8c8
--- /dev/null
+++ b/tests/api/test_rich_menu.py
@@ -0,0 +1,182 @@
+# -*- coding: utf-8 -*-
+
+#  Licensed under the Apache License, Version 2.0 (the "License"); you may
+#  not use this file except in compliance with the License. You may obtain
+#  a copy of the License at
+#
+#       https://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#  License for the specific language governing permissions and limitations
+#  under the License.
+
+from __future__ import unicode_literals, absolute_import
+
+import unittest
+
+import responses
+
+from linebot import (
+    LineBotApi
+)
+from linebot.models import (
+    URITemplateAction,
+    RichMenu, RichMenuBound, RichMenuArea
+)
+
+
+class TestLineBotApi(unittest.TestCase):
+
+    maxDiff = None
+
+    def setUp(self):
+        self.tested = LineBotApi('channel_secret')
+
+        self.rich_menu_id = 'richmenu-0000000000'
+        self.user_id = 'userid'
+        self.rich_menu = RichMenu(
+                size=RichMenuBound(
+                    width=2500,
+                    height=1686
+                ),
+                selected=False,
+                name="nice richmenu",
+                chatBarText="touch me",
+                areas=[
+                    RichMenuArea(
+                        RichMenuBound(
+                            x=0,
+                            y=0,
+                            width=833,
+                            height=843
+                        ),
+                        URITemplateAction(
+                            uri='line://nv/location'
+                        )
+                    )
+                ]
+            )
+
+    @responses.activate
+    def test_delete_rich_menu(self):
+        responses.add(
+            responses.DELETE,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/richmenu/{rich_menu_id}'.format(
+                rich_menu_id=self.rich_menu_id
+            ),
+            json={}, status=200
+        )
+
+        self.tested.delete_rich_menu(self.rich_menu_id)
+
+        request = responses.calls[0].request
+        self.assertEqual(request.method, 'DELETE')
+        self.assertEqual(
+            request.url,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/richmenu/{rich_menu_id}'.format(
+                rich_menu_id=self.rich_menu_id
+            )
+        )
+
+    @responses.activate
+    def test_link_rich_menu_to_user(self):
+        responses.add(
+            responses.POST,
+            LineBotApi.DEFAULT_API_ENDPOINT +
+            '/v2/bot/user/{user_id}/richmenu/{rich_menu_id}'.format(
+                user_id=self.user_id,
+                rich_menu_id=self.rich_menu_id
+            ),
+            json={}, status=200
+        )
+
+        self.tested.link_rich_menu_to_user(self.user_id, self.rich_menu_id)
+
+        request = responses.calls[0].request
+        self.assertEqual(request.method, 'POST')
+        self.assertEqual(
+            request.url,
+            LineBotApi.DEFAULT_API_ENDPOINT +
+            '/v2/bot/user/{user_id}/richmenu/{rich_menu_id}'.format(
+                user_id=self.user_id,
+                rich_menu_id=self.rich_menu_id
+            )
+        )
+
+    @responses.activate
+    def test_unlink_rich_menu_from_user(self):
+        responses.add(
+            responses.DELETE,
+            LineBotApi.DEFAULT_API_ENDPOINT +
+            '/v2/bot/user/{user_id}/richmenu'.format(user_id=self.user_id),
+            json={}, status=200
+        )
+
+        self.tested.unlink_rich_menu_from_user(self.user_id)
+
+        request = responses.calls[0].request
+        self.assertEqual(request.method, 'DELETE')
+        self.assertEqual(
+            request.url,
+            LineBotApi.DEFAULT_API_ENDPOINT +
+            '/v2/bot/user/{user_id}/richmenu'.format(user_id=self.user_id)
+        )
+
+    @responses.activate
+    def test_get_rich_menu_id_of_user(self):
+        responses.add(
+            responses.GET,
+            LineBotApi.DEFAULT_API_ENDPOINT +
+            '/v2/bot/user/{user_id}/richmenu'.format(user_id=self.user_id),
+            json={}, status=200
+        )
+
+        self.tested.get_rich_menu_id_of_user(self.user_id)
+
+        request = responses.calls[0].request
+        self.assertEqual(request.method, 'GET')
+        self.assertEqual(
+            request.url,
+            LineBotApi.DEFAULT_API_ENDPOINT +
+            '/v2/bot/user/{user_id}/richmenu'.format(user_id=self.user_id)
+        )
+
+    @responses.activate
+    def test_get_rich_menu_list(self):
+        responses.add(
+            responses.GET,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/richmenu/list',
+            json={'richmenus': []}, status=200
+        )
+
+        self.tested.get_rich_menu_list()
+
+        request = responses.calls[0].request
+        self.assertEqual(request.method, 'GET')
+        self.assertEqual(
+            request.url,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/richmenu/list'
+        )
+
+    @responses.activate
+    def test_create_rich_menu(self):
+        responses.add(
+            responses.POST,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/richmenu',
+            json={}, status=200
+        )
+
+        self.tested.create_rich_menu(self.rich_menu)
+
+        request = responses.calls[0].request
+        self.assertEqual(request.method, 'POST')
+        self.assertEqual(
+            request.url,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/richmenu'
+        )
+
+
+if __name__ == '__main__':
+    unittest.main()