Repository: line/line-bot-sdk-python

Issue:
Support Rich menu aliases Rich menu switch action apis
News: https://developers.line.biz/en/news/2021/06/21/switch-between-multiple-rich-menus/

Discussion:

Patch:
diff --git a/README.rst b/README.rst
index 5bce98b..1308679 100644
--- a/README.rst
+++ b/README.rst
@@ -698,7 +698,7 @@ TextSendMessage-Emoji
             "emojiId": "002"
         }
     ]
-    text_message = TextSendMessage(text='$ LINE emoji $', emojis=[emoji])
+    text_message = TextSendMessage(text='$ LINE emoji $', emojis=emoji)
 
 ImageSendMessage
 ^^^^^^^^^^^^^^^^
diff --git a/linebot/api.py b/linebot/api.py
index a8d54fa..94c9c7a 100644
--- a/linebot/api.py
+++ b/linebot/api.py
@@ -32,7 +32,7 @@ from .models import (
     AudienceGroup, ClickAudienceGroup, ImpAudienceGroup, GetAuthorityLevel, Audience,
     CreateAudienceGroup
 )
-from .models.responses import Group, UserIds
+from .models.responses import Group, UserIds, RichMenuAliasResponse, RichMenuAliasListResponse
 
 
 class LineBotApi(object):
@@ -657,7 +657,7 @@ class LineBotApi(object):
     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
+        https://developers.line.biz/en/reference/messaging-api/#get-rich-menu
 
         :param str rich_menu_id: ID of the rich menu
         :param timeout: (optional) How long to wait for the server
@@ -675,10 +675,49 @@ class LineBotApi(object):
 
         return RichMenuResponse.new_from_json_dict(response.json)
 
+    def get_rich_menu_alias(self, rich_menu_alias_id=None, timeout=None):
+        """Call get rich menu alias API.
+
+        https://developers.line.biz/en/reference/messaging-api/#get-rich-menu-alias-by-id
+
+        :param str rich_menu_alias_id: ID of an uploaded rich menu alias.
+        :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.RichMenuAliasResponse`
+        :return: RichMenuAliasResponse instance
+        """
+        response = self._get(
+            '/v2/bot/richmenu/alias/{rich_menu_id}'.format(rich_menu_id=rich_menu_alias_id),
+            timeout=timeout
+        )
+        return RichMenuAliasResponse.new_from_json_dict(response.json)
+
+    def get_rich_menu_alias_list(self, timeout=None):
+        """Call get rich menu alias list API.
+
+        https://developers.line.biz/en/reference/messaging-api/#update-rich-menu-alias
+
+        :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.RichMenuAliasListResponse`
+        :return: RichMenuAliasListResponse instance
+        """
+        response = self._get(
+            '/v2/bot/richmenu/alias/list',
+            timeout=timeout
+        )
+        return RichMenuAliasListResponse.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
+        https://developers.line.biz/en/reference/messaging-api/#create-rich-menu
 
         :param rich_menu: Inquired to create a rich menu object.
         :type rich_menu: T <= :py:class:`linebot.models.rich_menu.RichMenu`
@@ -696,10 +735,51 @@ class LineBotApi(object):
 
         return response.json.get('richMenuId')
 
+    def create_rich_menu_alias(self, rich_menu_alias, timeout=None):
+        """Call create rich menu alias API.
+
+        https://developers.line.biz/en/reference/messaging-api/#create-rich-menu-alias
+
+        :param rich_menu_alias: Inquired to create a rich menu alias object.
+        :type rich_menu_alias: T <= :py:class:`linebot.models.rich_menu.RichMenuAlias`
+        :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
+        """
+        self._post(
+            '/v2/bot/richmenu/alias', data=rich_menu_alias.as_json_string(), timeout=timeout
+        )
+
+    def update_rich_menu_alias(self, rich_menu_alias_id, rich_menu_alias, timeout=None):
+        """Call update rich menu alias API.
+
+        https://developers.line.biz/en/reference/messaging-api/#update-rich-menu-alias
+
+        :param str rich_menu_alias_id: ID of an uploaded rich menu alias.
+        :param rich_menu_alias: Inquired to create a rich menu alias object.
+        :type rich_menu_alias: T <= :py:class:`linebot.models.rich_menu.RichMenuAlias`
+        :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
+        """
+        self._post(
+            '/v2/bot/richmenu/alias/{rich_menu_id}'.format(rich_menu_id=rich_menu_alias_id),
+            data=rich_menu_alias.as_json_string(),
+            timeout=timeout
+        )
+
     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
+        https://developers.line.biz/en/reference/messaging-api/#delete-rich-menu
 
         :param str rich_menu_id: ID of an uploaded rich menu
         :param timeout: (optional) How long to wait for the server
@@ -713,10 +793,28 @@ class LineBotApi(object):
             timeout=timeout
         )
 
+    def delete_rich_menu_alias(self, rich_menu_alias_id, timeout=None):
+        """Call delete rich menu alias API.
+
+        https://developers.line.biz/en/reference/messaging-api/#delete-rich-menu-alias
+
+        :param str rich_menu_alias_id: ID of an uploaded rich menu alias.
+        :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/alias/{rich_menu_alias_id}'.format(
+                rich_menu_alias_id=rich_menu_alias_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
+        https://developers.line.biz/en/reference/messaging-api/#get-rich-menu-id-of-user
 
         :param str user_id: IDs of the user
         :param timeout: (optional) How long to wait for the server
@@ -737,7 +835,7 @@ class LineBotApi(object):
     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
+        https://developers.line.biz/en/reference/messaging-api/#link-rich-menu-to-user
 
         :param str user_id: ID of the user
         :param str rich_menu_id: ID of an uploaded rich menu
@@ -782,7 +880,7 @@ class LineBotApi(object):
     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
+        https://developers.line.biz/en/reference/messaging-api#unlink-rich-menu-from-user
 
         :param str user_id: ID of the user
         :param timeout: (optional) How long to wait for the server
@@ -821,7 +919,7 @@ class LineBotApi(object):
     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
+        https://developers.line.biz/en/reference/messaging-api#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
@@ -1300,14 +1398,14 @@ class LineBotApi(object):
         if audiences:
             audiences = [Audience.new_from_json_dict(audience) for audience in audiences]
         response = self._put(
-                '/v2/bot/audienceGroup/upload',
-                data=json.dumps({
-                    "audienceGroupId": audience_group_id,
-                    "audiences": [audience.as_json_dict() for audience in audiences],
-                    "uploadDescription": upload_description,
-                }),
-                timeout=timeout
-            )
+            '/v2/bot/audienceGroup/upload',
+            data=json.dumps({
+                "audienceGroupId": audience_group_id,
+                "audiences": [audience.as_json_dict() for audience in audiences],
+                "uploadDescription": upload_description,
+            }),
+            timeout=timeout
+        )
 
         return response.json
 
diff --git a/linebot/models/actions.py b/linebot/models/actions.py
index 6b0ab9a..5f1b0f6 100644
--- a/linebot/models/actions.py
+++ b/linebot/models/actions.py
@@ -34,6 +34,7 @@ def get_action(action):
             'camera': CameraAction,
             'cameraRoll': CameraRollAction,
             'location': LocationAction,
+            'richmenuswitch': RichMenuSwitchAction,
         }
     )
     return action_obj
@@ -270,3 +271,33 @@ class LocationAction(Action):
 
         self.type = 'location'
         self.label = label
+
+
+class RichMenuSwitchAction(Action):
+    """RichMenuSwitchAction.
+
+    https://developers.line.biz/en/reference/messaging-api/#richmenu-switch-action
+
+    This action can be configured only with rich menus.
+    It can't be used for Flex Messages or quick replies.
+    When you tap a rich menu associated with this action,
+    you can switch between rich menus,
+    and a postback event including the rich menu alias ID selected
+     by the user is returned via a webhook.
+    """
+
+    def __init__(self, label=None, rich_menu_alias_id=None, data=None, **kwargs):
+        """__init__ method.
+
+        :param str label: Label for the action
+        :param str rich_menu_alias_id: Rich menu alias ID to switch to.
+        :param str data: String returned by the postback.data property
+         of the postback event via a webhook
+        :param kwargs:
+        """
+        super(RichMenuSwitchAction, self).__init__(**kwargs)
+
+        self.type = 'richmenuswitch'
+        self.label = label
+        self.rich_menu_alias_id = rich_menu_alias_id
+        self.data = data
diff --git a/linebot/models/responses.py b/linebot/models/responses.py
index 5227ed0..dff63f5 100644
--- a/linebot/models/responses.py
+++ b/linebot/models/responses.py
@@ -22,7 +22,7 @@ from .insight import (
     GenderInsight, AreaInsight, MessageInsight, ClickInsight,
     MessageStatistics, JobInsight,
 )
-from .rich_menu import RichMenuSize, RichMenuArea
+from .rich_menu import RichMenuSize, RichMenuArea, RichMenuAlias
 
 
 class BroadcastResponse(object):
@@ -194,6 +194,48 @@ class RichMenuResponse(Base):
         self.areas = new_areas
 
 
+class RichMenuAliasResponse(Base):
+    """RichMenuAliasResponse.
+
+    https://developers.line.biz/en/reference/messaging-api/#get-rich-menu-alias-by-id-response
+    """
+
+    def __init__(self, rich_menu_alias_id=None, rich_menu_id=None, **kwargs):
+        """__init__ method.
+
+        :param str rich_menu_alias_id: Rich menu alias ID.
+        :param str rich_menu_id: Rich menu ID.
+        :param kwargs:
+        """
+        super(RichMenuAliasResponse, self).__init__(**kwargs)
+
+        self.rich_menu_alias_id = rich_menu_alias_id
+        self.rich_menu_id = rich_menu_id
+
+
+class RichMenuAliasListResponse(Base):
+    """RichMenuAliasListResponse.
+
+    https://developers.line.biz/en/reference/messaging-api/#get-rich-menu-alias-list-response
+    """
+
+    def __init__(self, aliases=None, **kwargs):
+        """__init__ method.
+
+        :param aliases: Array of rich menu alias objects
+        :type areas: list[T <= :py:class:`linebot.models.RichMenuAlias`]
+        :param kwargs:
+        """
+        super(RichMenuAliasListResponse, self).__init__(**kwargs)
+        new_aliases = []
+        if aliases:
+            for alias in aliases:
+                new_aliases.append(
+                    self.get_or_new_from_json_dict(alias, RichMenuAlias)
+                )
+        self.aliases = new_aliases
+
+
 class MessageQuotaResponse(Base):
     """MessageQuotaResponse.
 
diff --git a/linebot/models/rich_menu.py b/linebot/models/rich_menu.py
index c4db446..6c9fc69 100644
--- a/linebot/models/rich_menu.py
+++ b/linebot/models/rich_menu.py
@@ -124,3 +124,23 @@ class RichMenuBounds(with_metaclass(ABCMeta, Base)):
         self.y = y
         self.width = width
         self.height = height
+
+
+class RichMenuAlias(with_metaclass(ABCMeta, Base)):
+    """RichMenuAlias.
+
+    https://developers.line.biz/en/reference/messaging-api/#create-rich-menu-alias
+    """
+
+    def __init__(self, rich_menu_alias_id=None, rich_menu_id=None, **kwargs):
+        """__init__ method.
+
+        :param string rich_menu_alias_id: Rich menu alias ID,
+         which can be any ID, unique for each channel.
+        :param string rich_menu_id: The rich menu ID to be associated with the rich menu alias.
+        :param kwargs:
+        """
+        super(RichMenuAlias, self).__init__(**kwargs)
+
+        self.rich_menu_alias_id = rich_menu_alias_id
+        self.rich_menu_id = rich_menu_id


Test patch:
diff --git a/tests/api/test_rich_menu.py b/tests/api/test_rich_menu.py
index a4b71b9..6462a40 100644
--- a/tests/api/test_rich_menu.py
+++ b/tests/api/test_rich_menu.py
@@ -26,6 +26,7 @@ from linebot.models import (
     URITemplateAction,
     RichMenu, RichMenuSize, RichMenuArea, RichMenuBounds
 )
+from linebot.models.actions import RichMenuSwitchAction
 
 
 class TestLineBotApi(unittest.TestCase):
@@ -92,7 +93,6 @@ class TestLineBotApi(unittest.TestCase):
         )
 
         rich_menu = self.tested.get_rich_menu('rich_menu_id')
-        print(rich_menu)
 
         request = responses.calls[0].request
         self.assertEqual(request.method, 'GET')
@@ -438,6 +438,74 @@ class TestLineBotApi(unittest.TestCase):
             request.url
         )
 
+    @responses.activate
+    def test_rich_menu_with_switch_action(self):
+        rich_menu = RichMenu(
+            size=RichMenuSize(
+                width=2500,
+                height=1686
+            ),
+            selected=False,
+            name="nice richmenu",
+            chatBarText="touch me",
+            areas=[
+                RichMenuArea(
+                    RichMenuBounds(
+                        x=0,
+                        y=0,
+                        width=833,
+                        height=843
+                    ),
+                    RichMenuSwitchAction(
+                        rich_menu_alias_id="richmenu-alias-a",
+                        data="richmenu-changed-to-a"
+                    )
+                )
+            ]
+        )
+        responses.add(
+            responses.POST,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/richmenu',
+            json={
+                "richMenuId": "rich_menu_id",
+                "size": {
+                    "width": 2500,
+                    "height": 1686
+                },
+                "selected": False,
+                "name": "name",
+                "chatBarText": "chatBarText",
+                "areas": [
+                    {
+                        "bounds": {
+                            "x": 0,
+                            "y": 0,
+                            "width": 2500,
+                            "height": 1686
+                        },
+                        "action": {
+                            "type": "richmenuswitch",
+                            "richMenuAliasId": "richmenu-alias-a",
+                            "data": "richmenu-changed-to-a"
+                        }
+                    }
+                ]
+            },
+            status=200
+        )
+
+        result = self.tested.create_rich_menu(rich_menu)
+        print(rich_menu)
+
+        request = responses.calls[0].request
+        self.assertEqual(request.method, 'POST')
+        self.assertEqual(
+            request.url,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/richmenu'
+        )
+
+        self.assertEqual(result, "rich_menu_id")
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/api/test_rich_menu_alias.py b/tests/api/test_rich_menu_alias.py
new file mode 100644
index 0000000..6c57748
--- /dev/null
+++ b/tests/api/test_rich_menu_alias.py
@@ -0,0 +1,178 @@
+# -*- 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.rich_menu import RichMenuAlias
+
+
+class TestLineBotApi(unittest.TestCase):
+    maxDiff = None
+
+    def setUp(self):
+        self.tested = LineBotApi('channel_secret')
+
+        self.alias_id_a = 'richmenu-alias-a'
+        self.alias_id_b = 'richmenu-alias-b'
+        self.rich_menu_id_a = 'richmenu-0000000'
+        self.rich_menu_id_b = 'richmenu-1111111'
+
+    @responses.activate
+    def test_create_rich_menu_alias(self):
+        responses.add(
+            responses.POST,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/richmenu/alias',
+            json={
+                "richMenuAliasId": self.alias_id_a,
+                "richMenuId": self.rich_menu_id_a
+            }, status=200
+        )
+        alias = RichMenuAlias(
+            rich_menu_alias_id=self.alias_id_a,
+            rich_menu_id=self.rich_menu_id_a
+        )
+        result = self.tested.create_rich_menu_alias(alias)
+
+        request = responses.calls[0].request
+        self.assertEqual(request.method, 'POST')
+        self.assertEqual(
+            request.url,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/richmenu/alias'
+        )
+        self.assertEqual(result, None)
+
+    @responses.activate
+    def test_delete_rich_menu(self):
+        responses.add(
+            responses.DELETE,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/richmenu/alias/' + self.alias_id_a,
+            json={}, status=200
+        )
+
+        result = self.tested.delete_rich_menu_alias(self.alias_id_a)
+
+        request = responses.calls[0].request
+        self.assertEqual(request.method, 'DELETE')
+        self.assertEqual(
+            request.url,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/richmenu/alias/richmenu-alias-a'
+        )
+        self.assertEqual(result, None)
+
+    @responses.activate
+    def test_update_rich_menu_alias(self):
+        responses.add(
+            responses.POST,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/richmenu/alias/' + self.alias_id_a,
+            json={}, status=200
+        )
+
+        alias = RichMenuAlias(rich_menu_id=self.rich_menu_id_a)
+        result = self.tested.update_rich_menu_alias(
+            rich_menu_alias_id=self.alias_id_a, rich_menu_alias=alias
+        )
+
+        request = responses.calls[0].request
+        self.assertEqual(request.method, 'POST')
+        self.assertEqual(
+            request.url,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/richmenu/alias/richmenu-alias-a'
+        )
+        self.assertEqual(result, None)
+
+    @responses.activate
+    def test_get_rich_menu_alias(self):
+        responses.add(
+            responses.GET,
+            LineBotApi.DEFAULT_API_ENDPOINT +
+            '/v2/bot/richmenu/alias/' + self.alias_id_a,
+            json={
+                "richMenuAliasId": self.alias_id_a,
+                "richMenuId": self.rich_menu_id_a
+            }, status=200
+        )
+
+        result = self.tested.get_rich_menu_alias(rich_menu_alias_id=self.alias_id_a)
+        request = responses.calls[0].request
+        self.assertEqual(request.method, 'GET')
+        self.assertEqual(
+            request.url,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/richmenu/alias/richmenu-alias-a'
+        )
+        self.assertEqual(result.rich_menu_alias_id, 'richmenu-alias-a')
+        self.assertEqual(result.rich_menu_id, 'richmenu-0000000')
+
+    @responses.activate
+    def test_get_rich_menu_alias_list(self):
+        responses.add(
+            responses.GET,
+            LineBotApi.DEFAULT_API_ENDPOINT +
+            '/v2/bot/richmenu/alias/list',
+            json={
+                "aliases": [{
+                    "richMenuAliasId": self.alias_id_a,
+                    "richMenuId": self.rich_menu_id_a
+                }, {
+                    "richMenuAliasId": self.alias_id_b,
+                    "richMenuId": self.rich_menu_id_b
+                }]
+            }, status=200
+        )
+
+        result = self.tested.get_rich_menu_alias_list()
+        request = responses.calls[0].request
+        self.assertEqual(request.method, 'GET')
+        self.assertEqual(
+            request.url,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/richmenu/alias/list'
+        )
+        self.assertEqual(type(result.aliases), list)
+        self.assertEqual(result.aliases[0].rich_menu_alias_id, "richmenu-alias-a")
+        self.assertEqual(
+            result.aliases[0].rich_menu_id, "richmenu-0000000"
+        )
+        self.assertEqual(result.aliases[1].rich_menu_alias_id, "richmenu-alias-b")
+        self.assertEqual(
+            result.aliases[1].rich_menu_id, "richmenu-1111111"
+        )
+
+    @responses.activate
+    def test_get_rich_menu_alias_empty_list(self):
+        responses.add(
+            responses.GET,
+            LineBotApi.DEFAULT_API_ENDPOINT +
+            '/v2/bot/richmenu/alias/list',
+            json={"aliases": []},
+            status=200
+        )
+
+        result = self.tested.get_rich_menu_alias_list()
+        request = responses.calls[0].request
+        self.assertEqual(request.method, 'GET')
+        self.assertEqual(
+            request.url,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/richmenu/alias/list'
+        )
+        self.assertEqual(type(result.aliases), list)
+        self.assertEqual(len(result.aliases), 0)
+
+
+if __name__ == '__main__':
+    unittest.main()