Repository: line/line-bot-sdk-python
Pull request #284 (2020-08-11 06:46:32)

Support Unsend &  Video play complete event
Implemented #283 issues:
- Unsend event
- Video play complete event


Patch:
diff --git a/linebot/__about__.py b/linebot/__about__.py
index f40bf27..3d65e2d 100644
--- a/linebot/__about__.py
+++ b/linebot/__about__.py
@@ -16,7 +16,7 @@
 
 from __future__ import unicode_literals
 
-__version__ = '1.16.0'
+__version__ = '1.17.0'
 __author__ = 'LINE Corporation'
 __copyright__ = 'Copyright 2016, LINE Corporation'
 __license__ = 'Apache 2.0'
diff --git a/linebot/models/events.py b/linebot/models/events.py
index 439b341..2304d0c 100644
--- a/linebot/models/events.py
+++ b/linebot/models/events.py
@@ -37,6 +37,8 @@ from linebot.models.things import (
     ScenarioResult,
 )
 from linebot.models.things import Things  # noqa, backward compatibility
+from linebot.models.unsend import Unsend
+from linebot.models.video_play_complete import VideoPlayComplete
 
 
 class Event(with_metaclass(ABCMeta, Base)):
@@ -414,6 +416,68 @@ class ThingsEvent(Event):
         )
 
 
+class UnsendEvent(Event):
+    """Webhook UnsendEvent.
+
+    https://developers.line.biz/en/reference/messaging-api/#unsend-event
+
+    Event object for when the user unsends a message in a group or room.
+    """
+
+    def __init__(self, mode=None, timestamp=None, source=None, unsend=None, **kwargs):
+        """__init__ method.
+
+        :param str mode: Channel state
+        :param long timestamp: Time of the event in milliseconds
+        :param source: Source object
+        :type source: T <= :py:class:`linebot.models.sources.Source`
+        :param unsend: Unsend object
+        :type unsend: T <= :py:class:`linebot.models.unsend.Unsend`
+        :param kwargs:
+        """
+        super(UnsendEvent, self).__init__(
+            mode=mode, timestamp=timestamp, source=source, **kwargs
+        )
+
+        self.type = 'unsend'
+        self.unsend = self.get_or_new_from_json_dict(
+            unsend, Unsend
+        )
+
+
+class VideoPlayCompleteEvent(Event):
+    """Webhook VideoCompleteEvent.
+
+    https://developers.line.biz/en/reference/messaging-api/#video-viewing-complete
+
+    Event object Event for when a user finishes viewing a video at least once.
+    """
+
+    def __init__(self, mode=None, timestamp=None, source=None, reply_token=None,
+                 video_play_complete=None, **kwargs):
+        """__init__ method.
+
+        :param str mode: Channel state
+        :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 video_play_complete: VideoPlayComplete object
+        :type video_play_complete:
+            T <= :py:class:`linebot.models.video_play_complete.VideoPlayComplete`
+        :param kwargs:
+        """
+        super(VideoPlayCompleteEvent, self).__init__(
+            mode=mode, timestamp=timestamp, source=source, **kwargs
+        )
+
+        self.type = 'videoPlayComplete'
+        self.reply_token = reply_token
+        self.video_play_complete = self.get_or_new_from_json_dict(
+            video_play_complete, VideoPlayComplete
+        )
+
+
 class Postback(Base):
     """Postback.
 
diff --git a/linebot/models/send_messages.py b/linebot/models/send_messages.py
index d854cc1..dc9c534 100644
--- a/linebot/models/send_messages.py
+++ b/linebot/models/send_messages.py
@@ -101,13 +101,16 @@ class VideoSendMessage(SendMessage):
     """
 
     def __init__(self, original_content_url=None, preview_image_url=None,
-                 quick_reply=None, **kwargs):
+                 tracking_id=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 str tracking_id: the video viewing complete event occurs
+            when the user finishes watching the video.
+            Max character limit: 100.
         :param quick_reply: QuickReply object
         :type quick_reply: T <= :py:class:`linebot.models.send_messages.QuickReply`
         :param kwargs:
@@ -117,6 +120,7 @@ class VideoSendMessage(SendMessage):
         self.type = 'video'
         self.original_content_url = original_content_url
         self.preview_image_url = preview_image_url
+        self.tracking_id = tracking_id
 
 
 class AudioSendMessage(SendMessage):
diff --git a/linebot/models/unsend.py b/linebot/models/unsend.py
new file mode 100644
index 0000000..a764a2f
--- /dev/null
+++ b/linebot/models/unsend.py
@@ -0,0 +1,36 @@
+# -*- 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.unsend module."""
+
+from __future__ import unicode_literals
+
+from abc import ABCMeta
+
+from future.utils import with_metaclass
+
+from .base import Base
+
+
+class Unsend(with_metaclass(ABCMeta, Base)):
+    """Abstract Base Class of Unsend."""
+
+    def __init__(self, message_id=None, **kwargs):
+        """__init__ method.
+
+        :param str message_id: The message ID of the unsent message.
+        :param kwargs:
+        """
+        super(Unsend, self).__init__(**kwargs)
+        self.message_id = message_id
diff --git a/linebot/models/video_play_complete.py b/linebot/models/video_play_complete.py
new file mode 100644
index 0000000..f4cee5e
--- /dev/null
+++ b/linebot/models/video_play_complete.py
@@ -0,0 +1,38 @@
+# -*- 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.video_play_complete module."""
+
+from __future__ import unicode_literals
+
+from abc import ABCMeta
+
+from future.utils import with_metaclass
+
+from .base import Base
+
+
+class VideoPlayComplete(with_metaclass(ABCMeta, Base)):
+    """Abstract Base Class of VideoPlayComplete."""
+
+    def __init__(self, tracking_id=None, **kwargs):
+        """__init__ method.
+
+        :param str tracking_id: the video viewing complete event occurs
+            when the user finishes watching the video.
+            Max character limit: 100.
+        :param kwargs:
+        """
+        super(VideoPlayComplete, self).__init__(**kwargs)
+        self.tracking_id = tracking_id
diff --git a/linebot/webhook.py b/linebot/webhook.py
index eae6d23..7adc2be 100644
--- a/linebot/webhook.py
+++ b/linebot/webhook.py
@@ -34,7 +34,7 @@ from .models.events import (
     AccountLinkEvent,
     MemberJoinedEvent,
     MemberLeftEvent,
-    ThingsEvent,
+    ThingsEvent, UnsendEvent, VideoPlayCompleteEvent,
 )
 from .utils import LOGGER, PY3, safe_compare_digest
 
@@ -168,6 +168,10 @@ class WebhookParser(object):
                 events.append(MemberLeftEvent.new_from_json_dict(event))
             elif event_type == 'things':
                 events.append(ThingsEvent.new_from_json_dict(event))
+            elif event_type == 'unsend':
+                events.append(UnsendEvent.new_from_json_dict(event))
+            elif event_type == 'videoPlayComplete':
+                events.append(VideoPlayCompleteEvent.new_from_json_dict(event))
             else:
                 LOGGER.warn('Unknown event type. type=' + event_type)
 


Test patch:
diff --git a/tests/api/test_send_video_message.py b/tests/api/test_send_video_message.py
index e3c6e02..7d56815 100644
--- a/tests/api/test_send_video_message.py
+++ b/tests/api/test_send_video_message.py
@@ -33,13 +33,15 @@ class TestLineBotApi(unittest.TestCase):
 
         self.video_message = VideoSendMessage(
             original_content_url='https://example.com/original.mp4',
-            preview_image_url='https://example.com/preview.jpg'
+            preview_image_url='https://example.com/preview.jpg',
+            tracking_id='TrackId'
         )
 
         self.message = [{
             "type": "video",
             "originalContentUrl": "https://example.com/original.mp4",
             "previewImageUrl": "https://example.com/preview.jpg",
+            "trackingId": "TrackId"
         }]
 
     @responses.activate
diff --git a/tests/models/test_send_messages.py b/tests/models/test_send_messages.py
index 0e89911..8d944b7 100644
--- a/tests/models/test_send_messages.py
+++ b/tests/models/test_send_messages.py
@@ -72,6 +72,18 @@ class TestSendMessages(SerializeTestCase):
             VideoSendMessage(**arg).as_json_dict()
         )
 
+    def test_video_message_wtih_track(self):
+        arg = {
+            'type': 'video',
+            'original_content_url': 'https://example.com/original.mp4',
+            'preview_image_url': 'https://example.com/preview.jpg',
+            "tracking_id": "track_id"
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg, type=self.VIDEO),
+            VideoSendMessage(**arg).as_json_dict()
+        )
+
     def test_audio_message(self):
         arg = {
             'original_content_url': 'https://example.com/original.m4a',
diff --git a/tests/test_webhook.py b/tests/test_webhook.py
index 9839a65..e2d17c5 100644
--- a/tests/test_webhook.py
+++ b/tests/test_webhook.py
@@ -30,6 +30,9 @@ from linebot.models import (
     LocationMessage, StickerMessage, FileMessage,
     SourceUser, SourceRoom, SourceGroup,
     DeviceLink, DeviceUnlink, ScenarioResult, ActionResult)
+from linebot.models.events import UnsendEvent, VideoPlayCompleteEvent
+from linebot.models.unsend import Unsend
+from linebot.models.video_play_complete import VideoPlayComplete
 from linebot.utils import PY3
 
 
@@ -386,7 +389,6 @@ class TestWebhookParser(unittest.TestCase):
         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')
@@ -417,6 +419,29 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[24].things.result.action_results[1], ActionResult)
         self.assertEqual(events[24].things.result.action_results[1].type, 'void')
 
+        # UnsendEvent
+        self.assertIsInstance(events[25], UnsendEvent)
+        self.assertEqual(events[25].type, 'unsend')
+        self.assertEqual(events[25].mode, 'active')
+        self.assertEqual(events[25].timestamp, 1547817848122)
+        self.assertIsInstance(events[25].source, SourceGroup)
+        self.assertEqual(events[25].source.type, 'group')
+        self.assertEqual(events[25].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
+        self.assertIsInstance(events[25].unsend, Unsend)
+        self.assertEqual(events[25].unsend.message_id, '325708')
+
+        # VideoPlayCompleteEvent
+        self.assertIsInstance(events[26], VideoPlayCompleteEvent)
+        self.assertEqual(events[26].reply_token, 'nHuyWiB7yP5Zw52FIkcQobQuGDXCTA')
+        self.assertEqual(events[26].type, 'videoPlayComplete')
+        self.assertEqual(events[26].mode, 'active')
+        self.assertEqual(events[26].timestamp, 1462629479859)
+        self.assertIsInstance(events[26].source, SourceUser)
+        self.assertEqual(events[26].source.type, 'user')
+        self.assertEqual(events[26].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
+        self.assertIsInstance(events[26].video_play_complete, VideoPlayComplete)
+        self.assertEqual(events[26].video_play_complete.tracking_id, 'track_id')
+
     def test_parse_webhook_req_without_destination(self):
         body = """
         {
@@ -538,6 +563,7 @@ class TestInvokeWebhookHandler(unittest.TestCase):
                 else:
                     arg_spec = inspect.getargspec(func)
                 return func(*args[0:len(arg_spec.args)])
+
             return wrapper
 
         def func_with_0_args():
diff --git a/tests/text/webhook.json b/tests/text/webhook.json
index eb95618..d4ff25d 100644
--- a/tests/text/webhook.json
+++ b/tests/text/webhook.json
@@ -400,6 +400,32 @@
           ]
         }
       }
+    },
+    {
+      "type": "unsend",
+      "mode": "active",
+      "timestamp": 1547817848122,
+      "source": {
+        "type": "group",
+        "groupId": "Ca56f94637cc4347f90a25382909b24b9",
+        "userId": "U206d25c2ea6bd87c17655609a1c37cb8"
+      },
+      "unsend": {
+        "messageId": "325708"
+      }
+    },
+    {
+      "replyToken": "nHuyWiB7yP5Zw52FIkcQobQuGDXCTA",
+      "type": "videoPlayComplete",
+      "timestamp": 1462629479859,
+      "mode": "active",
+      "source": {
+        "type": "user",
+        "userId": "U206d25c2ea6bd87c17655609a1c37cb8"
+      },
+      "videoPlayComplete": {
+        "trackingId": "track_id"
+      }
     }
   ]
 }