Repository: line/line-bot-sdk-python
Pull request #164 (2019-05-21 01:21:34)

Add notificationDisabled to send message APIs
- refer to #162 
- also add test to broadcast


Patch:
diff --git a/linebot/api.py b/linebot/api.py
index dbafedc..ce2e11f 100644
--- a/linebot/api.py
+++ b/linebot/api.py
@@ -58,7 +58,7 @@ class LineBotApi(object):
         else:
             self.http_client = RequestsHttpClient(timeout=timeout)
 
-    def reply_message(self, reply_token, messages, timeout=None):
+    def reply_message(self, reply_token, messages, notification_disabled=False, timeout=None):
         """Call reply message API.
 
         https://devdocs.line.me/en/#reply-message
@@ -78,6 +78,8 @@ class LineBotApi(object):
             Max: 5
         :type messages: T <= :py:class:`linebot.models.send_messages.SendMessage` |
             list[T <= :py:class:`linebot.models.send_messages.SendMessage`]
+        :param bool notification_disabled: (optional) True to disable push notification
+            when the message is sent. The default value is False.
         :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.
@@ -89,14 +91,15 @@ class LineBotApi(object):
 
         data = {
             'replyToken': reply_token,
-            'messages': [message.as_json_dict() for message in messages]
+            'messages': [message.as_json_dict() for message in messages],
+            'notificationDisabled': notification_disabled,
         }
 
         self._post(
             '/v2/bot/message/reply', data=json.dumps(data), timeout=timeout
         )
 
-    def push_message(self, to, messages, timeout=None):
+    def push_message(self, to, messages, notification_disabled=False, timeout=None):
         """Call push message API.
 
         https://devdocs.line.me/en/#push-message
@@ -108,6 +111,8 @@ class LineBotApi(object):
             Max: 5
         :type messages: T <= :py:class:`linebot.models.send_messages.SendMessage` |
             list[T <= :py:class:`linebot.models.send_messages.SendMessage`]
+        :param bool notification_disabled: (optional) True to disable push notification
+            when the message is sent. The default value is False.
         :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.
@@ -119,14 +124,15 @@ class LineBotApi(object):
 
         data = {
             'to': to,
-            'messages': [message.as_json_dict() for message in messages]
+            'messages': [message.as_json_dict() for message in messages],
+            'notificationDisabled': notification_disabled,
         }
 
         self._post(
             '/v2/bot/message/push', data=json.dumps(data), timeout=timeout
         )
 
-    def multicast(self, to, messages, timeout=None):
+    def multicast(self, to, messages, notification_disabled=False, timeout=None):
         """Call multicast API.
 
         https://devdocs.line.me/en/#multicast
@@ -140,6 +146,8 @@ class LineBotApi(object):
             Max: 5
         :type messages: T <= :py:class:`linebot.models.send_messages.SendMessage` |
             list[T <= :py:class:`linebot.models.send_messages.SendMessage`]
+        :param bool notification_disabled: (optional) True to disable push notification
+            when the message is sent. The default value is False.
         :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.
@@ -151,14 +159,15 @@ class LineBotApi(object):
 
         data = {
             'to': to,
-            'messages': [message.as_json_dict() for message in messages]
+            'messages': [message.as_json_dict() for message in messages],
+            'notificationDisabled': notification_disabled,
         }
 
         self._post(
             '/v2/bot/message/multicast', data=json.dumps(data), timeout=timeout
         )
 
-    def broadcast(self, messages, timeout=None):
+    def broadcast(self, messages, notification_disabled=False, timeout=None):
         """Call broadcast API.
 
         https://developers.line.biz/en/reference/messaging-api/#send-broadcast-message
@@ -169,6 +178,8 @@ class LineBotApi(object):
             Max: 5
         :type messages: T <= :py:class:`linebot.models.send_messages.SendMessage` |
             list[T <= :py:class:`linebot.models.send_messages.SendMessage`]
+        :param bool notification_disabled: (optional) True to disable push notification
+            when the message is sent. The default value is False.
         :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.
@@ -179,7 +190,8 @@ class LineBotApi(object):
             messages = [messages]
 
         data = {
-            'messages': [message.as_json_dict() for message in messages]
+            'messages': [message.as_json_dict() for message in messages],
+            'notificationDisabled': notification_disabled,
         }
 
         self._post(
diff --git a/linebot/models/__init__.py b/linebot/models/__init__.py
index cb7e941..e7233e8 100644
--- a/linebot/models/__init__.py
+++ b/linebot/models/__init__.py
@@ -48,6 +48,7 @@ from .events import (  # noqa
     MemberJoinedEvent,
     MemberLeftEvent,
     BeaconEvent,
+    ThingsEvent,
     Postback,
     Beacon,
     Link,
diff --git a/linebot/models/events.py b/linebot/models/events.py
index 2f570b9..4783251 100644
--- a/linebot/models/events.py
+++ b/linebot/models/events.py
@@ -356,6 +356,37 @@ class AccountLinkEvent(Event):
         )
 
 
+class ThingsEvent(Event):
+    """Webhook ThingsEvent.
+
+    https://developers.line.biz/en/reference/messaging-api/#device-link-event
+
+    Indicates that a LINE Things-compatible device has been linked with LINE by
+    a user operation.
+    """
+
+    def __init__(self, timestamp=None, source=None, reply_token=None, things=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 things: Things object
+        :type things: :py:class:`linebot.models.events.Things`
+        :param kwargs:
+        """
+        super(ThingsEvent, self).__init__(
+            timestamp=timestamp, source=source, **kwargs
+        )
+
+        self.type = 'things'
+        self.reply_token = reply_token
+        self.things = self.get_or_new_from_json_dict(
+            things, Things
+        )
+
+
 class Postback(Base):
     """Postback.
 
@@ -467,3 +498,21 @@ class Link(Base):
 
         self.result = result
         self.nonce = nonce
+
+
+class Things(Base):
+    """Things.
+
+    https://developers.line.biz/en/docs/line-things/develop-bot/#link-event
+    """
+
+    def __init__(self, device_id=None, type=None, **kwargs):
+        """__init__ method.
+
+        :param str device_id: Device ID of the device that was linked with LINE.
+        :param str type: link or unlink
+        """
+        super(Things, self).__init__(**kwargs)
+
+        self.device_id = device_id
+        self.type = type
diff --git a/linebot/webhook.py b/linebot/webhook.py
index 415ceb1..1fed867 100644
--- a/linebot/webhook.py
+++ b/linebot/webhook.py
@@ -34,6 +34,7 @@ from .models.events import (
     AccountLinkEvent,
     MemberJoinedEvent,
     MemberLeftEvent,
+    ThingsEvent,
 )
 from .utils import LOGGER, PY3, safe_compare_digest
 
@@ -150,6 +151,8 @@ class WebhookParser(object):
                 events.append(MemberJoinedEvent.new_from_json_dict(event))
             elif event_type == 'memberLeft':
                 events.append(MemberLeftEvent.new_from_json_dict(event))
+            elif event_type == 'things':
+                events.append(ThingsEvent.new_from_json_dict(event))
             else:
                 LOGGER.warn('Unknown event type. type=' + event_type)
 


Test patch:
diff --git a/tests/api/test_send_audio_message.py b/tests/api/test_send_audio_message.py
index 9db1e52..24888a3 100644
--- a/tests/api/test_send_audio_message.py
+++ b/tests/api/test_send_audio_message.py
@@ -62,6 +62,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": "to",
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
@@ -85,6 +86,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "replyToken": "replyToken",
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
@@ -108,6 +110,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": ['to1', 'to2'],
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
diff --git a/tests/api/test_send_image_message.py b/tests/api/test_send_image_message.py
index 0ef0115..1b59e3f 100644
--- a/tests/api/test_send_image_message.py
+++ b/tests/api/test_send_image_message.py
@@ -61,6 +61,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": "to",
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
@@ -84,6 +85,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "replyToken": "replyToken",
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
@@ -107,6 +109,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": ['to1', 'to2'],
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
diff --git a/tests/api/test_send_imagemap_message.py b/tests/api/test_send_imagemap_message.py
index b26a107..bbac91d 100644
--- a/tests/api/test_send_imagemap_message.py
+++ b/tests/api/test_send_imagemap_message.py
@@ -129,6 +129,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": "to",
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
@@ -152,6 +153,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "replyToken": "replyToken",
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
@@ -175,6 +177,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": ['to1', 'to2'],
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
diff --git a/tests/api/test_send_location_message.py b/tests/api/test_send_location_message.py
index 03fa5c9..c7f5d0c 100644
--- a/tests/api/test_send_location_message.py
+++ b/tests/api/test_send_location_message.py
@@ -65,6 +65,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": "to",
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
@@ -88,6 +89,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "replyToken": "replyToken",
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
@@ -111,6 +113,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": ['to1', 'to2'],
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
diff --git a/tests/api/test_send_sticker_message.py b/tests/api/test_send_sticker_message.py
index 940c59e..323d9aa 100644
--- a/tests/api/test_send_sticker_message.py
+++ b/tests/api/test_send_sticker_message.py
@@ -60,6 +60,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": "to",
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
@@ -83,6 +84,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "replyToken": "replyToken",
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
@@ -106,6 +108,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": ['to1', 'to2'],
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
diff --git a/tests/api/test_send_template_message.py b/tests/api/test_send_template_message.py
index c0e2e81..de8f840 100644
--- a/tests/api/test_send_template_message.py
+++ b/tests/api/test_send_template_message.py
@@ -380,6 +380,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": "to",
+                'notificationDisabled': False,
                 "messages": self.button_message
             }
         )
@@ -403,6 +404,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "replyToken": "replyToken",
+                'notificationDisabled': False,
                 "messages": self.button_message
             }
         )
@@ -426,6 +428,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": "to",
+                'notificationDisabled': False,
                 "messages": self.confirm_message
             }
         )
@@ -450,6 +453,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "replyToken": "replyToken",
+                'notificationDisabled': False,
                 "messages": self.confirm_message
             }
         )
@@ -473,6 +477,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": "to",
+                'notificationDisabled': False,
                 "messages": self.carousel_message
             }
         )
@@ -496,6 +501,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "replyToken": "replyToken",
+                'notificationDisabled': False,
                 "messages": self.carousel_message
             }
         )
@@ -519,6 +525,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": ['to1', 'to2'],
+                'notificationDisabled': False,
                 "messages": self.carousel_message
             }
         )
@@ -542,6 +549,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": "to",
+                'notificationDisabled': False,
                 "messages": self.image_carousel_message
             }
         )
@@ -565,6 +573,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "replyToken": "replyToken",
+                'notificationDisabled': False,
                 "messages": self.image_carousel_message
             }
         )
@@ -588,6 +597,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": ['to1', 'to2'],
+                'notificationDisabled': False,
                 "messages": self.image_carousel_message
             }
         )
diff --git a/tests/api/test_send_text_message.py b/tests/api/test_send_text_message.py
index f64921d..1256324 100644
--- a/tests/api/test_send_text_message.py
+++ b/tests/api/test_send_text_message.py
@@ -54,6 +54,7 @@ class TestSendTestMessage(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": "to",
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
@@ -77,6 +78,7 @@ class TestSendTestMessage(unittest.TestCase):
             json.loads(request.body),
             {
                 "replyToken": "replyToken",
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
@@ -100,6 +102,46 @@ class TestSendTestMessage(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": ['to1', 'to2'],
+                'notificationDisabled': False,
+                "messages": self.message
+            }
+        )
+
+    @responses.activate
+    def test_broadcast_text_message(self):
+        responses.add(
+            responses.POST,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/message/broadcast',
+            json={}, status=200
+        )
+
+        self.tested.broadcast(self.text_message)
+
+        request = responses.calls[0].request
+        self.assertEqual(
+            request.url,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/message/broadcast')
+        self.assertEqual(request.method, 'POST')
+        self.assertEqual(
+            json.loads(request.body),
+            {
+                'notificationDisabled': False,
+                "messages": self.message
+            }
+        )
+
+        # call with notification_disable=True
+        self.tested.broadcast(self.text_message, notification_disabled=True)
+
+        request = responses.calls[1].request
+        self.assertEqual(
+            request.url,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/message/broadcast')
+        self.assertEqual(request.method, 'POST')
+        self.assertEqual(
+            json.loads(request.body),
+            {
+                'notificationDisabled': True,
                 "messages": self.message
             }
         )
diff --git a/tests/api/test_send_text_message_with_quick_reply.py b/tests/api/test_send_text_message_with_quick_reply.py
index 1ff8f5e..ae50260 100644
--- a/tests/api/test_send_text_message_with_quick_reply.py
+++ b/tests/api/test_send_text_message_with_quick_reply.py
@@ -81,6 +81,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": "to",
+                'notificationDisabled': False,
                 "messages": [
                     {
                         "type": "text",
diff --git a/tests/api/test_send_video_message.py b/tests/api/test_send_video_message.py
index 217b583..e3c6e02 100644
--- a/tests/api/test_send_video_message.py
+++ b/tests/api/test_send_video_message.py
@@ -61,6 +61,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": "to",
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
@@ -84,7 +85,8 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "replyToken": "replyToken",
-                "messages": self.message
+                "messages": self.message,
+                'notificationDisabled': False,
             }
         )
 
@@ -107,7 +109,8 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": ['to1', 'to2'],
-                "messages": self.message
+                "messages": self.message,
+                "notificationDisabled": False,
             }
         )
 
diff --git a/tests/test_webhook.py b/tests/test_webhook.py
index 980b5a5..a6e3b5f 100644
--- a/tests/test_webhook.py
+++ b/tests/test_webhook.py
@@ -24,7 +24,7 @@ from linebot import (
 from linebot.models import (
     MessageEvent, FollowEvent, UnfollowEvent, JoinEvent,
     LeaveEvent, PostbackEvent, BeaconEvent, AccountLinkEvent,
-    MemberJoinedEvent, MemberLeftEvent,
+    MemberJoinedEvent, MemberLeftEvent, ThingsEvent,
     TextMessage, ImageMessage, VideoMessage, AudioMessage,
     LocationMessage, StickerMessage, FileMessage,
     SourceUser, SourceRoom, SourceGroup
@@ -304,20 +304,16 @@ class TestWebhookParser(unittest.TestCase):
         self.assertEqual(events[18].postback.data, 'action=buyItem&itemId=123123&color=red')
         self.assertEqual(events[18].postback.params['datetime'], '2013-04-01T10:00')
 
-        # MessageEvent, SourceUser, FileMessage
-        self.assertIsInstance(events[19], MessageEvent)
+        # ThingsEvent, SourceUser, link
+        self.assertIsInstance(events[19], ThingsEvent)
         self.assertEqual(events[19].reply_token, 'nHuyWiB7yP5Zw52FIkcQobQuGDXCTA')
-        self.assertEqual(events[19].type, 'message')
+        self.assertEqual(events[19].type, 'things')
         self.assertEqual(events[19].timestamp, 1462629479859)
         self.assertIsInstance(events[19].source, SourceUser)
         self.assertEqual(events[19].source.type, 'user')
         self.assertEqual(events[19].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
-        self.assertEqual(events[19].source.sender_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
-        self.assertIsInstance(events[19].message, FileMessage)
-        self.assertEqual(events[19].message.id, '325708')
-        self.assertEqual(events[19].message.type, 'file')
-        self.assertEqual(events[19].message.file_name, "file.txt")
-        self.assertEqual(events[19].message.file_size, 2138)
+        self.assertEqual(events[19].things.device_id, 't2c449c9d1')
+        self.assertEqual(events[19].things.type, 'link')
 
         # MemberJoinedEvent
         self.assertIsInstance(events[20], MemberJoinedEvent)
@@ -344,6 +340,32 @@ class TestWebhookParser(unittest.TestCase):
         self.assertEqual(events[21].left.members[0].user_id, 'U4af4980629...')
         self.assertEqual(events[21].left.members[1].user_id, 'U91eeaf62d9...')
 
+        # ThingsEvent, SourceUser, unlink
+        self.assertIsInstance(events[22], ThingsEvent)
+        self.assertEqual(events[22].reply_token, 'nHuyWiB7yP5Zw52FIkcQobQuGDXCTA')
+        self.assertEqual(events[22].type, 'things')
+        self.assertEqual(events[22].timestamp, 1462629479859)
+        self.assertIsInstance(events[22].source, SourceUser)
+        self.assertEqual(events[22].source.type, 'user')
+        self.assertEqual(events[22].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
+        self.assertEqual(events[22].things.device_id, 't2c449c9d1')
+        self.assertEqual(events[22].things.type, 'unlink')
+
+        # MessageEvent, SourceUser, FileMessage
+        self.assertIsInstance(events[23], MessageEvent)
+        self.assertEqual(events[23].reply_token, 'nHuyWiB7yP5Zw52FIkcQobQuGDXCTA')
+        self.assertEqual(events[23].type, 'message')
+        self.assertEqual(events[23].timestamp, 1462629479859)
+        self.assertIsInstance(events[23].source, SourceUser)
+        self.assertEqual(events[23].source.type, 'user')
+        self.assertEqual(events[23].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
+        self.assertEqual(events[23].source.sender_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
+        self.assertIsInstance(events[23].message, FileMessage)
+        self.assertEqual(events[23].message.id, '325708')
+        self.assertEqual(events[23].message.type, 'file')
+        self.assertEqual(events[23].message.file_name, "file.txt")
+        self.assertEqual(events[23].message.file_size, 2138)
+
 
 class TestWebhookHandler(unittest.TestCase):
     def setUp(self):
diff --git a/tests/text/webhook.json b/tests/text/webhook.json
index 09ab5a9..bfa54ec 100644
--- a/tests/text/webhook.json
+++ b/tests/text/webhook.json
@@ -248,17 +248,15 @@
     },
     {
       "replyToken": "nHuyWiB7yP5Zw52FIkcQobQuGDXCTA",
-      "type": "message",
+      "type": "things",
       "timestamp": 1462629479859,
       "source": {
         "type": "user",
         "userId": "U206d25c2ea6bd87c17655609a1c37cb8"
       },
-      "message": {
-        "id": "325708",
-        "type": "file",
-        "fileName": "file.txt",
-        "fileSize": 2138
+      "things": {
+        "deviceId": "t2c449c9d1",
+        "type": "link"
       }
     },
     {
@@ -301,6 +299,34 @@
           }
         ]
       }
+    },
+    {
+      "replyToken": "nHuyWiB7yP5Zw52FIkcQobQuGDXCTA",
+      "type": "things",
+      "timestamp": 1462629479859,
+      "source": {
+        "type": "user",
+        "userId": "U206d25c2ea6bd87c17655609a1c37cb8"
+      },
+      "things": {
+        "deviceId": "t2c449c9d1",
+        "type": "unlink"
+      }
+    },
+    {
+      "replyToken": "nHuyWiB7yP5Zw52FIkcQobQuGDXCTA",
+      "type": "message",
+      "timestamp": 1462629479859,
+      "source": {
+        "type": "user",
+        "userId": "U206d25c2ea6bd87c17655609a1c37cb8"
+      },
+      "message": {
+        "id": "325708",
+        "type": "file",
+        "fileName": "file.txt",
+        "fileSize": 2138
+      }
     }
   ]
 }