Repository: line/line-bot-sdk-python (python)

Issue:
Support Redelivery object for narrowcast messages
News: https://developers.line.biz/en/news/2020/09/15/messaging-api-narrowcast-requestId/
Docs: [Redelivery object](https://developers.line.biz/en/docs/messaging-api/sending-messages/#redelivery-object)

Pull request description:
Support Redelivery object
Closed #292

Patch:
diff --git a/README.rst b/README.rst
index 368f99a..eff72b9 100644
--- a/README.rst
+++ b/README.rst
@@ -590,6 +590,25 @@ https://developers.line.biz/en/reference/messaging-api/#get-message-event
     broadcast_response = line_bot_api.broadcast(TextSendMessage(text='Hello World!'))
     insight = line_bot_api.get_insight_message_event(broadcast_response.request_id)
     print(insight.overview)
+    
+get\_bot_info(self, timeout=None)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Get bot's basic information.
+
+https://developers.line.biz/en/reference/messaging-api/#get-bot-info
+
+.. code:: python
+
+    bot_info = line_bot_api.get_bot_info()
+
+    print(bot_info.display_name)
+    print(bot_info.user_id)
+    print(bot_info.basic_id)
+    print(bot_info.premium_id)
+    print(bot_info.picture_url)
+    print(bot_info.chat_mode)
+    print(bot_info.mark_as_read_mode)
 
 ※ Error handling
 ^^^^^^^^^^^^^^^^^
diff --git a/linebot/api.py b/linebot/api.py
index 85bee50..7edb8d9 100644
--- a/linebot/api.py
+++ b/linebot/api.py
@@ -28,7 +28,7 @@ from .models import (
     MessageDeliveryPushResponse, MessageDeliveryReplyResponse,
     InsightMessageDeliveryResponse, InsightFollowersResponse, InsightDemographicResponse,
     InsightMessageEventResponse, BroadcastResponse, NarrowcastResponse,
-    MessageProgressNarrowcastResponse,
+    MessageProgressNarrowcastResponse, BotInfo,
 )
 from .models.responses import Group
 
@@ -1129,6 +1129,24 @@ class LineBotApi(object):
 
         return InsightMessageEventResponse.new_from_json_dict(response.json)
 
+    def get_bot_info(self, timeout=None):
+        """Get a bot's basic information.
+
+        https://developers.line.biz/en/reference/messaging-api/#get-bot-info
+        :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.BotInfo`
+        """
+        response = self._get(
+            '/v2/bot/info',
+            timeout=timeout
+        )
+
+        return BotInfo.new_from_json_dict(response.json)
+
     def _get(self, path, endpoint=None, params=None, headers=None, stream=False, timeout=None):
         url = (endpoint or self.endpoint) + path
 
diff --git a/linebot/models/__init__.py b/linebot/models/__init__.py
index f321a37..ce135c3 100644
--- a/linebot/models/__init__.py
+++ b/linebot/models/__init__.py
@@ -54,7 +54,7 @@ from .events import (  # noqa
     Beacon,
     Link,
 )
-from .filter import(  # noqa
+from .filter import (  # noqa
     Filter,
     DemographicFilter,
     GenderFilter,
@@ -126,7 +126,8 @@ from .operator import (  # noqa
 )
 
 from .recipient import (  # noqa
-    AudienceRecipient
+    AudienceRecipient,
+    RedeliveryRecipient
 )
 
 from .responses import (  # noqa
@@ -150,6 +151,7 @@ from .responses import (  # noqa
     BroadcastResponse,
     NarrowcastResponse,
     MessageProgressNarrowcastResponse,
+    BotInfo,
 )
 from .rich_menu import (  # noqa
     RichMenu,
diff --git a/linebot/models/recipient.py b/linebot/models/recipient.py
index 4855d91..07fe77b 100644
--- a/linebot/models/recipient.py
+++ b/linebot/models/recipient.py
@@ -56,3 +56,19 @@ class AudienceRecipient(Recipient):
 
         self.type = "audience"
         self.audience_group_id = group_id
+
+
+class RedeliveryRecipient(Recipient):
+    """RedeliveryRecipient."""
+
+    def __init__(self, request_id=None, **kwargs):
+        """__init__ method.
+
+        :param str request_id: The request ID of the narrowcast message previously sent.
+            The request IDs is an ID issued for each Messaging API request.
+        :param kwargs:
+        """
+        super(RedeliveryRecipient, self).__init__(**kwargs)
+
+        self.type = "redelivery"
+        self.request_id = request_id
diff --git a/linebot/models/responses.py b/linebot/models/responses.py
index 936c60a..aa4613a 100644
--- a/linebot/models/responses.py
+++ b/linebot/models/responses.py
@@ -496,3 +496,27 @@ class NarrowcastResponse(Base):
         super(NarrowcastResponse, self).__init__(**kwargs)
 
         self.request_id = request_id
+
+
+class BotInfo(Base):
+    """Response of `linebot.get_bot_info()` .
+
+    https://developers.line.biz/en/reference/messaging-api/#get-bot-info
+    """
+
+    def __init__(self, user_id=None, basic_id=None, premium_id=None,
+                 display_name=None, picture_url=None, chat_mode=None,
+                 mark_as_read_mode=None, **kwargs):
+        """__init__ method.
+
+        :param kwargs:
+        """
+        super(BotInfo, self).__init__(**kwargs)
+
+        self.user_id = user_id
+        self.basic_id = basic_id
+        self.premium_id = premium_id
+        self.display_name = display_name
+        self.picture_url = picture_url
+        self.chat_mode = chat_mode
+        self.mark_as_read_mode = mark_as_read_mode


Test patch:
diff --git a/tests/api/test_get_bot_info.py b/tests/api/test_get_bot_info.py
new file mode 100644
index 0000000..b1f9c89
--- /dev/null
+++ b/tests/api/test_get_bot_info.py
@@ -0,0 +1,64 @@
+# -*- 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
+)
+
+
+class TestLineBotApi(unittest.TestCase):
+    def setUp(self):
+        self.tested = LineBotApi('channel_secret')
+
+    @responses.activate
+    def test_get_bot_info(self):
+        responses.add(
+            responses.GET,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/info',
+            json={
+                "userId": "Ub9952f8...",
+                "basicId": "@216ru...",
+                "premiumId": "@example",
+                "displayName": "Example name",
+                "pictureUrl": "https://obs.line-apps.com/...",
+                "chatMode": "chat",
+                "markAsReadMode": "manual",
+            },
+            status=200
+        )
+
+        bot = self.tested.get_bot_info()
+
+        request = responses.calls[0].request
+        self.assertEqual(request.method, 'GET')
+        self.assertEqual(
+            request.url,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/info')
+        self.assertEqual(bot.user_id, 'Ub9952f8...')
+        self.assertEqual(bot.basic_id, '@216ru...')
+        self.assertEqual(bot.premium_id, '@example')
+        self.assertEqual(bot.display_name, 'Example name')
+        self.assertEqual(bot.picture_url, 'https://obs.line-apps.com/...')
+        self.assertEqual(bot.chat_mode, 'chat')
+        self.assertEqual(bot.mark_as_read_mode, 'manual')
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/api/test_narrowcast_message.py b/tests/api/test_narrowcast_message.py
index 3e48c12..99417cc 100644
--- a/tests/api/test_narrowcast_message.py
+++ b/tests/api/test_narrowcast_message.py
@@ -35,6 +35,7 @@ from linebot.models import (
     AgeFilter,
     AudienceRecipient,
     SubscriptionPeriodFilter,
+    RedeliveryRecipient,
 )
 
 
@@ -92,6 +93,66 @@ class TestNarrowcastMessage(unittest.TestCase):
             }
         )
 
+    @responses.activate
+    def test_narrowcast_redelivery_recipient_text_message(self):
+        responses.add(
+            responses.POST,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/message/narrowcast',
+            json={}, status=200,
+        )
+
+        self.tested.narrowcast(
+            self.text_message,
+            recipient=And(
+                AudienceRecipient(group_id=5614991017776),
+                Not(
+                    RedeliveryRecipient(request_id='request_id_test')
+                )
+            ),
+            filter=Filter(demographic=AgeFilter(gte="age_35", lt="age_40")),
+            limit=Limit(max=10),
+        )
+
+        request = responses.calls[0].request
+        self.assertEqual(
+            request.url,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/message/narrowcast')
+        self.assertEqual(request.method, 'POST')
+        self.assertEqual(
+            json.loads(request.body),
+            {
+                "messages": self.message,
+                "recipient": {
+                    "type": "operator",
+                    "and": [
+                        {
+                            "type": "audience",
+                            "audienceGroupId": 5614991017776
+                        },
+                        {
+                            "type": "operator",
+                            "not": {
+                                "type": "redelivery",
+                                "requestId": "request_id_test"
+                            }
+                        }
+                    ]
+                },
+                "filter": {
+                    "demographic": {
+                        "type": "age",
+                        "gte": "age_35",
+                        "lt": "age_40"
+                    }
+                },
+                "limit": {
+                    "max": 10,
+                    "upToRemainingQuota": False,
+                },
+                "notificationDisabled": False,
+            }
+        )
+
     @responses.activate
     def test_narrowcast_text_message(self):
         responses.add(