Repository: line/line-bot-sdk-python
Pull request #385 (2022-05-15 05:56:00)

Support : "webhook redelivery"
# Overview
News
https://developers.line.biz/ja/news/2022/04/19/webhook-redelivery/

Added to two common properties
- create `DeliveryContext` class
- add two properties in `Event`
- add test case in `TestWebhookParser`

## Related Issue
- #380 


Patch:
diff --git a/linebot/models/delivery_context.py b/linebot/models/delivery_context.py
new file mode 100644
index 0000000..b140857
--- /dev/null
+++ b/linebot/models/delivery_context.py
@@ -0,0 +1,34 @@
+#  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.delivery_context module."""
+
+
+from abc import ABCMeta
+
+from future.utils import with_metaclass
+
+from .base import Base
+
+
+class DeliveryContext(with_metaclass(ABCMeta, Base)):
+    """Abstract Base Class of DeliveryContext."""
+
+    def __init__(self, is_redelivery=None, **kwargs):
+        """__init__ method.
+
+        :param bool is_redelivery: Whether the webhook event is a redelivered one or not
+        :param kwargs:
+        """
+        super(DeliveryContext, self).__init__(**kwargs)
+
+        self.is_redelivery = is_redelivery
diff --git a/linebot/models/events.py b/linebot/models/events.py
index a33c8a1..d0b1adb 100644
--- a/linebot/models/events.py
+++ b/linebot/models/events.py
@@ -20,6 +20,7 @@ from abc import ABCMeta
 from future.utils import with_metaclass
 
 from linebot.models.base import Base
+from linebot.models.delivery_context import DeliveryContext
 from linebot.models.messages import (
     TextMessage,
     ImageMessage,
@@ -46,7 +47,15 @@ class Event(with_metaclass(ABCMeta, Base)):
     https://developers.line.biz/en/reference/messaging-api/#webhook-event-objects
     """
 
-    def __init__(self, mode=None, timestamp=None, source=None, **kwargs):
+    def __init__(
+        self,
+        mode=None,
+        timestamp=None,
+        source=None,
+        webhook_event_id=None,
+        delivery_context=None,
+        **kwargs
+    ):
         """__init__ method.
 
         :param str mode: Channel state
@@ -67,6 +76,10 @@ class Event(with_metaclass(ABCMeta, Base)):
                 'room': SourceRoom,
             }
         )
+        self.webhook_event_id = webhook_event_id
+        self.delivery_context = self.get_or_new_from_json_dict(
+            delivery_context, DeliveryContext
+        )
 
 
 class MessageEvent(Event):


Test patch:
diff --git a/tests/test_webhook.py b/tests/test_webhook.py
index ad39638..6334555 100644
--- a/tests/test_webhook.py
+++ b/tests/test_webhook.py
@@ -30,6 +30,7 @@ from linebot.models import (
     LocationMessage, StickerMessage, FileMessage,
     SourceUser, SourceRoom, SourceGroup,
     DeviceLink, DeviceUnlink, ScenarioResult, ActionResult)
+from linebot.models.delivery_context import DeliveryContext
 from linebot.models.events import UnsendEvent, VideoPlayCompleteEvent
 from linebot.models.unsend import Unsend
 from linebot.models.video_play_complete import VideoPlayComplete
@@ -67,6 +68,9 @@ class TestWebhookParser(unittest.TestCase):
 
         events = self.parser.parse(body, 'channel_secret')
 
+        # events count
+        self.assertEqual(len(events), 29)
+
         # MessageEvent, SourceUser, TextMessage
         self.assertIsInstance(events[0], MessageEvent)
         self.assertEqual(events[0].reply_token, 'nHuyWiB7yP5Zw52FIkcQobQuGDXCTA')
@@ -76,6 +80,9 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[0].source, SourceUser)
         self.assertEqual(events[0].source.type, 'user')
         self.assertEqual(events[0].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
+        self.assertEqual(events[0].webhook_event_id, 'testwebhookeventid')
+        self.assertIsInstance(events[0].delivery_context, DeliveryContext)
+        self.assertFalse(events[0].delivery_context.is_redelivery)
         self.assertIsInstance(events[0].message, TextMessage)
         self.assertEqual(events[0].message.id, '325708')
         self.assertEqual(events[0].message.type, 'text')
@@ -91,6 +98,9 @@ class TestWebhookParser(unittest.TestCase):
         self.assertEqual(events[1].source.type, 'room')
         self.assertEqual(events[1].source.room_id, 'Ra8dbf4673c4c812cd491258042226c99')
         self.assertEqual(events[1].source.user_id, None)
+        self.assertEqual(events[1].webhook_event_id, 'testwebhookeventid')
+        self.assertIsInstance(events[1].delivery_context, DeliveryContext)
+        self.assertFalse(events[1].delivery_context.is_redelivery)
         self.assertIsInstance(events[1].message, ImageMessage)
         self.assertEqual(events[1].message.id, '325708')
         self.assertEqual(events[1].message.type, 'image')
@@ -109,6 +119,9 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[2].source, SourceUser)
         self.assertEqual(events[2].source.type, 'user')
         self.assertEqual(events[2].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
+        self.assertEqual(events[2].webhook_event_id, 'testwebhookeventid')
+        self.assertIsInstance(events[2].delivery_context, DeliveryContext)
+        self.assertFalse(events[2].delivery_context.is_redelivery)
         self.assertIsInstance(events[2].message, VideoMessage)
         self.assertEqual(events[2].message.id, '325708')
         self.assertEqual(events[2].message.type, 'video')
@@ -128,6 +141,9 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[3].source, SourceUser)
         self.assertEqual(events[3].source.type, 'user')
         self.assertEqual(events[3].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
+        self.assertEqual(events[3].webhook_event_id, 'testwebhookeventid')
+        self.assertIsInstance(events[3].delivery_context, DeliveryContext)
+        self.assertFalse(events[3].delivery_context.is_redelivery)
         self.assertIsInstance(events[3].message, AudioMessage)
         self.assertEqual(events[3].message.id, '325708')
         self.assertEqual(events[3].message.type, 'audio')
@@ -145,6 +161,9 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[4].source, SourceUser)
         self.assertEqual(events[4].source.type, 'user')
         self.assertEqual(events[4].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
+        self.assertEqual(events[4].webhook_event_id, 'testwebhookeventid')
+        self.assertIsInstance(events[4].delivery_context, DeliveryContext)
+        self.assertFalse(events[4].delivery_context.is_redelivery)
         self.assertIsInstance(events[4].message, LocationMessage)
         self.assertEqual(events[4].message.id, '325708')
         self.assertEqual(events[4].message.type, 'location')
@@ -162,6 +181,9 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[5].source, SourceUser)
         self.assertEqual(events[5].source.type, 'user')
         self.assertEqual(events[5].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
+        self.assertEqual(events[5].webhook_event_id, 'testwebhookeventid')
+        self.assertIsInstance(events[5].delivery_context, DeliveryContext)
+        self.assertFalse(events[5].delivery_context.is_redelivery)
         self.assertIsInstance(events[5].message, StickerMessage)
         self.assertEqual(events[5].message.id, '325708')
         self.assertEqual(events[5].message.type, 'sticker')
@@ -181,6 +203,9 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[6].source, SourceUser)
         self.assertEqual(events[6].source.type, 'user')
         self.assertEqual(events[6].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
+        self.assertEqual(events[6].webhook_event_id, 'testwebhookeventid')
+        self.assertIsInstance(events[6].delivery_context, DeliveryContext)
+        self.assertFalse(events[6].delivery_context.is_redelivery)
 
         # UnfollowEvent, SourceUser
         self.assertIsInstance(events[7], UnfollowEvent)
@@ -190,6 +215,9 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[7].source, SourceUser)
         self.assertEqual(events[7].source.type, 'user')
         self.assertEqual(events[7].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
+        self.assertEqual(events[7].webhook_event_id, 'testwebhookeventid')
+        self.assertIsInstance(events[7].delivery_context, DeliveryContext)
+        self.assertFalse(events[7].delivery_context.is_redelivery)
 
         # JoinEvent, SourceGroup
         self.assertIsInstance(events[8], JoinEvent)
@@ -201,6 +229,9 @@ class TestWebhookParser(unittest.TestCase):
         self.assertEqual(events[8].source.type, 'group')
         self.assertEqual(events[8].source.group_id, 'Ca56f94637cc4347f90a25382909b24b9')
         self.assertEqual(events[8].source.user_id, None)
+        self.assertEqual(events[8].webhook_event_id, 'testwebhookeventid')
+        self.assertIsInstance(events[8].delivery_context, DeliveryContext)
+        self.assertFalse(events[8].delivery_context.is_redelivery)
 
         # LeaveEvent, SourceGroup
         self.assertIsInstance(events[9], LeaveEvent)
@@ -211,6 +242,9 @@ class TestWebhookParser(unittest.TestCase):
         self.assertEqual(events[9].source.type, 'group')
         self.assertEqual(events[9].source.group_id, 'Ca56f94637cc4347f90a25382909b24b9')
         self.assertEqual(events[9].source.user_id, None)
+        self.assertEqual(events[9].webhook_event_id, 'testwebhookeventid')
+        self.assertIsInstance(events[9].delivery_context, DeliveryContext)
+        self.assertFalse(events[9].delivery_context.is_redelivery)
 
         # PostbackEvent, SourceUser
         self.assertIsInstance(events[10], PostbackEvent)
@@ -221,6 +255,9 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[10].source, SourceUser)
         self.assertEqual(events[10].source.type, 'user')
         self.assertEqual(events[10].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
+        self.assertEqual(events[10].webhook_event_id, 'testwebhookeventid')
+        self.assertIsInstance(events[10].delivery_context, DeliveryContext)
+        self.assertFalse(events[10].delivery_context.is_redelivery)
         self.assertEqual(events[10].postback.data, 'action=buyItem&itemId=123123&color=red')
         self.assertEqual(events[10].postback.params, None)
 
@@ -233,6 +270,9 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[11].source, SourceUser)
         self.assertEqual(events[11].source.type, 'user')
         self.assertEqual(events[11].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
+        self.assertEqual(events[11].webhook_event_id, 'testwebhookeventid')
+        self.assertIsInstance(events[11].delivery_context, DeliveryContext)
+        self.assertFalse(events[11].delivery_context.is_redelivery)
         self.assertEqual(events[11].beacon.hwid, 'd41d8cd98f')
         self.assertEqual(events[11].beacon.type, 'enter')
         self.assertEqual(events[11].beacon.dm, None)
@@ -247,6 +287,9 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[12].source, SourceUser)
         self.assertEqual(events[12].source.type, 'user')
         self.assertEqual(events[12].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
+        self.assertEqual(events[12].webhook_event_id, 'testwebhookeventid')
+        self.assertIsInstance(events[12].delivery_context, DeliveryContext)
+        self.assertFalse(events[12].delivery_context.is_redelivery)
         self.assertEqual(events[12].beacon.hwid, 'd41d8cd98f')
         self.assertEqual(events[12].beacon.type, 'enter')
         self.assertEqual(events[12].beacon.dm, '1234567890abcdef')
@@ -261,6 +304,9 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[13].source, SourceUser)
         self.assertEqual(events[13].source.type, 'user')
         self.assertEqual(events[13].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
+        self.assertEqual(events[13].webhook_event_id, 'testwebhookeventid')
+        self.assertIsInstance(events[13].delivery_context, DeliveryContext)
+        self.assertFalse(events[13].delivery_context.is_redelivery)
         self.assertEqual(events[13].link.result, 'ok')
         self.assertEqual(events[13].link.nonce, 'Vb771wDYtXuammLszK6h9A')
 
@@ -274,6 +320,9 @@ class TestWebhookParser(unittest.TestCase):
         self.assertEqual(events[14].source.type, 'group')
         self.assertEqual(events[14].source.group_id, 'Ca56f94637cc4347f90a25382909b24b9')
         self.assertEqual(events[14].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
+        self.assertEqual(events[14].webhook_event_id, 'testwebhookeventid')
+        self.assertIsInstance(events[14].delivery_context, DeliveryContext)
+        self.assertFalse(events[14].delivery_context.is_redelivery)
         self.assertIsInstance(events[14].message, TextMessage)
         self.assertEqual(events[14].message.id, '325708')
         self.assertEqual(events[14].message.type, 'text')
@@ -289,6 +338,9 @@ class TestWebhookParser(unittest.TestCase):
         self.assertEqual(events[15].source.type, 'room')
         self.assertEqual(events[15].source.room_id, 'Ra8dbf4673c4c812cd491258042226c99')
         self.assertEqual(events[15].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
+        self.assertEqual(events[15].webhook_event_id, 'testwebhookeventid')
+        self.assertIsInstance(events[15].delivery_context, DeliveryContext)
+        self.assertFalse(events[15].delivery_context.is_redelivery)
         self.assertIsInstance(events[15].message, TextMessage)
         self.assertEqual(events[15].message.id, '325708')
         self.assertEqual(events[15].message.type, 'text')
@@ -303,6 +355,9 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[16].source, SourceUser)
         self.assertEqual(events[16].source.type, 'user')
         self.assertEqual(events[16].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
+        self.assertEqual(events[16].webhook_event_id, 'testwebhookeventid')
+        self.assertIsInstance(events[16].delivery_context, DeliveryContext)
+        self.assertFalse(events[16].delivery_context.is_redelivery)
         self.assertEqual(events[16].postback.data, 'action=buyItem&itemId=123123&color=red')
         self.assertEqual(events[16].postback.params['date'], '2013-04-01')
 
@@ -315,6 +370,9 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[17].source, SourceUser)
         self.assertEqual(events[17].source.type, 'user')
         self.assertEqual(events[17].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
+        self.assertEqual(events[17].webhook_event_id, 'testwebhookeventid')
+        self.assertIsInstance(events[17].delivery_context, DeliveryContext)
+        self.assertFalse(events[17].delivery_context.is_redelivery)
         self.assertEqual(events[17].postback.data, 'action=buyItem&itemId=123123&color=red')
         self.assertEqual(events[17].postback.params['time'], '10:00')
 
@@ -327,6 +385,9 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[18].source, SourceUser)
         self.assertEqual(events[18].source.type, 'user')
         self.assertEqual(events[18].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
+        self.assertEqual(events[18].webhook_event_id, 'testwebhookeventid')
+        self.assertIsInstance(events[18].delivery_context, DeliveryContext)
+        self.assertFalse(events[18].delivery_context.is_redelivery)
         self.assertEqual(events[18].postback.data, 'action=buyItem&itemId=123123&color=red')
         self.assertEqual(events[18].postback.params['datetime'], '2013-04-01T10:00')
 
@@ -339,6 +400,9 @@ class TestWebhookParser(unittest.TestCase):
         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].webhook_event_id, 'testwebhookeventid')
+        self.assertIsInstance(events[19].delivery_context, DeliveryContext)
+        self.assertFalse(events[19].delivery_context.is_redelivery)
         self.assertIsInstance(events[19].things, DeviceLink)
         self.assertEqual(events[19].things.type, 'link')
         self.assertEqual(events[19].things.device_id, 't2c449c9d1')
@@ -352,6 +416,9 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[20].source, SourceGroup)
         self.assertEqual(events[20].source.type, 'group')
         self.assertEqual(events[20].source.group_id, 'C4af4980629...')
+        self.assertEqual(events[20].webhook_event_id, 'testwebhookeventid')
+        self.assertIsInstance(events[20].delivery_context, DeliveryContext)
+        self.assertFalse(events[20].delivery_context.is_redelivery)
         self.assertEqual(len(events[20].joined.members), 2)
         self.assertIsInstance(events[20].joined.members[0], SourceUser)
         self.assertEqual(events[20].joined.members[0].user_id, 'U4af4980629...')
@@ -365,6 +432,9 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[21].source, SourceGroup)
         self.assertEqual(events[21].source.type, 'group')
         self.assertEqual(events[21].source.group_id, 'C4af4980629...')
+        self.assertEqual(events[21].webhook_event_id, 'testwebhookeventid')
+        self.assertIsInstance(events[21].delivery_context, DeliveryContext)
+        self.assertFalse(events[21].delivery_context.is_redelivery)
         self.assertEqual(len(events[21].left.members), 2)
         self.assertIsInstance(events[21].left.members[0], SourceUser)
         self.assertEqual(events[21].left.members[0].user_id, 'U4af4980629...')
@@ -379,6 +449,9 @@ class TestWebhookParser(unittest.TestCase):
         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].webhook_event_id, 'testwebhookeventid')
+        self.assertIsInstance(events[22].delivery_context, DeliveryContext)
+        self.assertFalse(events[22].delivery_context.is_redelivery)
         self.assertIsInstance(events[22].things, DeviceUnlink)
         self.assertEqual(events[22].things.type, 'unlink')
         self.assertEqual(events[22].things.device_id, 't2c449c9d1')
@@ -392,6 +465,9 @@ 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].webhook_event_id, 'testwebhookeventid')
+        self.assertIsInstance(events[23].delivery_context, DeliveryContext)
+        self.assertFalse(events[23].delivery_context.is_redelivery)
         self.assertIsInstance(events[23].message, FileMessage)
         self.assertEqual(events[23].message.id, '325708')
         self.assertEqual(events[23].message.type, 'file')
@@ -407,6 +483,9 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[24].source, SourceUser)
         self.assertEqual(events[24].source.type, 'user')
         self.assertEqual(events[24].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
+        self.assertEqual(events[24].webhook_event_id, 'testwebhookeventid')
+        self.assertIsInstance(events[24].delivery_context, DeliveryContext)
+        self.assertFalse(events[24].delivery_context.is_redelivery)
         self.assertIsInstance(events[24].things, ScenarioResult)
         self.assertEqual(events[24].things.type, 'scenarioResult')
         self.assertEqual(events[24].things.device_id, 't2c449c9d1')
@@ -430,6 +509,9 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[25].source, SourceGroup)
         self.assertEqual(events[25].source.type, 'group')
         self.assertEqual(events[25].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
+        self.assertEqual(events[25].webhook_event_id, 'testwebhookeventid')
+        self.assertIsInstance(events[25].delivery_context, DeliveryContext)
+        self.assertFalse(events[25].delivery_context.is_redelivery)
         self.assertIsInstance(events[25].unsend, Unsend)
         self.assertEqual(events[25].unsend.message_id, '325708')
 
@@ -442,6 +524,9 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[26].source, SourceUser)
         self.assertEqual(events[26].source.type, 'user')
         self.assertEqual(events[26].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
+        self.assertEqual(events[26].webhook_event_id, 'testwebhookeventid')
+        self.assertIsInstance(events[26].delivery_context, DeliveryContext)
+        self.assertFalse(events[26].delivery_context.is_redelivery)
         self.assertIsInstance(events[26].video_play_complete, VideoPlayComplete)
         self.assertEqual(events[26].video_play_complete.tracking_id, 'track_id')
 
@@ -454,6 +539,9 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[27].source, SourceUser)
         self.assertEqual(events[27].source.type, 'user')
         self.assertEqual(events[27].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
+        self.assertEqual(events[27].webhook_event_id, 'testwebhookeventid')
+        self.assertIsInstance(events[27].delivery_context, DeliveryContext)
+        self.assertFalse(events[27].delivery_context.is_redelivery)
         self.assertIsInstance(events[27].message, ImageMessage)
         self.assertEqual(events[27].message.id, '354718705033693861')
         self.assertEqual(events[27].message.type, 'image')
@@ -462,6 +550,23 @@ class TestWebhookParser(unittest.TestCase):
         self.assertEqual(events[27].message.image_set.index, 2)
         self.assertEqual(events[27].message.image_set.total, 2)
 
+        # MessageEvent, SourceUser, TextMessage (Redeliveried)
+        self.assertIsInstance(events[28], MessageEvent)
+        self.assertEqual(events[28].reply_token, 'nHuyWiB7yP5Zw52FIkcQobQuGDXCTA')
+        self.assertEqual(events[28].type, 'message')
+        self.assertEqual(events[28].mode, 'active')
+        self.assertEqual(events[28].timestamp, 1462629479859)
+        self.assertIsInstance(events[28].source, SourceUser)
+        self.assertEqual(events[28].source.type, 'user')
+        self.assertEqual(events[28].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
+        self.assertEqual(events[28].webhook_event_id, 'testwebhookeventid')
+        self.assertIsInstance(events[28].delivery_context, DeliveryContext)
+        self.assertTrue(events[28].delivery_context.is_redelivery)
+        self.assertIsInstance(events[28].message, TextMessage)
+        self.assertEqual(events[28].message.id, '325708')
+        self.assertEqual(events[28].message.type, 'text')
+        self.assertEqual(events[28].message.text, 'Hello, world')
+
     def test_parse_webhook_req_without_destination(self):
         body = """
         {
diff --git a/tests/text/webhook.json b/tests/text/webhook.json
index fc40964..14ae819 100644
--- a/tests/text/webhook.json
+++ b/tests/text/webhook.json
@@ -10,6 +10,10 @@
         "type": "user",
         "userId": "U206d25c2ea6bd87c17655609a1c37cb8"
       },
+      "webhookEventId": "testwebhookeventid",
+      "deliveryContext": {
+        "isRedelivery": false
+      },
       "message": {
         "id": "325708",
         "type": "text",
@@ -25,6 +29,10 @@
         "type": "room",
         "roomId": "Ra8dbf4673c4c812cd491258042226c99"
       },
+      "webhookEventId": "testwebhookeventid",
+      "deliveryContext": {
+        "isRedelivery": false
+      },
       "message": {
         "id": "325708",
         "type": "image",
@@ -44,6 +52,10 @@
         "type": "user",
         "userId": "U206d25c2ea6bd87c17655609a1c37cb8"
       },
+      "webhookEventId": "testwebhookeventid",
+      "deliveryContext": {
+        "isRedelivery": false
+      },
       "message": {
         "id": "325708",
         "type": "video",
@@ -64,6 +76,10 @@
         "type": "user",
         "userId": "U206d25c2ea6bd87c17655609a1c37cb8"
       },
+      "webhookEventId": "testwebhookeventid",
+      "deliveryContext": {
+        "isRedelivery": false
+      },
       "message": {
         "id": "325708",
         "type": "audio",
@@ -83,6 +99,10 @@
         "type": "user",
         "userId": "U206d25c2ea6bd87c17655609a1c37cb8"
       },
+      "webhookEventId": "testwebhookeventid",
+      "deliveryContext": {
+        "isRedelivery": false
+      },
       "message": {
         "id": "325708",
         "type": "location",
@@ -101,6 +121,10 @@
         "type": "user",
         "userId": "U206d25c2ea6bd87c17655609a1c37cb8"
       },
+      "webhookEventId": "testwebhookeventid",
+      "deliveryContext": {
+        "isRedelivery": false
+      },
       "message": {
         "id": "325708",
         "type": "sticker",
@@ -119,6 +143,10 @@
       "source": {
         "type": "user",
         "userId": "U206d25c2ea6bd87c17655609a1c37cb8"
+      },
+      "webhookEventId": "testwebhookeventid",
+      "deliveryContext": {
+        "isRedelivery": false
       }
     },
     {
@@ -128,6 +156,10 @@
       "source": {
         "type": "user",
         "userId": "U206d25c2ea6bd87c17655609a1c37cb8"
+      },
+      "webhookEventId": "testwebhookeventid",
+      "deliveryContext": {
+        "isRedelivery": false
       }
     },
     {
@@ -138,6 +170,10 @@
       "source": {
         "type": "group",
         "groupId": "Ca56f94637cc4347f90a25382909b24b9"
+      },
+      "webhookEventId": "testwebhookeventid",
+      "deliveryContext": {
+        "isRedelivery": false
       }
     },
     {
@@ -147,6 +183,10 @@
       "source": {
         "type": "group",
         "groupId": "Ca56f94637cc4347f90a25382909b24b9"
+      },
+      "webhookEventId": "testwebhookeventid",
+      "deliveryContext": {
+        "isRedelivery": false
       }
     },
     {
@@ -160,6 +200,10 @@
       },
       "postback": {
         "data": "action=buyItem&itemId=123123&color=red"
+      },
+      "webhookEventId": "testwebhookeventid",
+      "deliveryContext": {
+        "isRedelivery": false
       }
     },
     {
@@ -171,6 +215,10 @@
         "type": "user",
         "userId": "U206d25c2ea6bd87c17655609a1c37cb8"
       },
+      "webhookEventId": "testwebhookeventid",
+      "deliveryContext": {
+        "isRedelivery": false
+      },
       "beacon": {
         "hwid": "d41d8cd98f",
         "type": "enter"
@@ -185,6 +233,10 @@
         "type": "user",
         "userId": "U206d25c2ea6bd87c17655609a1c37cb8"
       },
+      "webhookEventId": "testwebhookeventid",
+      "deliveryContext": {
+        "isRedelivery": false
+      },
       "beacon": {
         "hwid": "d41d8cd98f",
         "type": "enter",
@@ -200,6 +252,10 @@
         "type": "user",
         "userId": "U206d25c2ea6bd87c17655609a1c37cb8"
       },
+      "webhookEventId": "testwebhookeventid",
+      "deliveryContext": {
+        "isRedelivery": false
+      },
       "link": {
         "result": "ok",
         "nonce": "Vb771wDYtXuammLszK6h9A"
@@ -215,6 +271,10 @@
         "groupId": "Ca56f94637cc4347f90a25382909b24b9",
         "userId": "U206d25c2ea6bd87c17655609a1c37cb8"
       },
+      "webhookEventId": "testwebhookeventid",
+      "deliveryContext": {
+        "isRedelivery": false
+      },
       "message": {
         "id": "325708",
         "type": "text",
@@ -231,6 +291,10 @@
         "roomId": "Ra8dbf4673c4c812cd491258042226c99",
         "userId": "U206d25c2ea6bd87c17655609a1c37cb8"
       },
+      "webhookEventId": "testwebhookeventid",
+      "deliveryContext": {
+        "isRedelivery": false
+      },
       "message": {
         "id": "325708",
         "type": "text",
@@ -246,6 +310,10 @@
         "type": "user",
         "userId": "U206d25c2ea6bd87c17655609a1c37cb8"
       },
+      "webhookEventId": "testwebhookeventid",
+      "deliveryContext": {
+        "isRedelivery": false
+      },
       "postback": {
         "data": "action=buyItem&itemId=123123&color=red",
         "params": {
@@ -262,6 +330,10 @@
         "type": "user",
         "userId": "U206d25c2ea6bd87c17655609a1c37cb8"
       },
+      "webhookEventId": "testwebhookeventid",
+      "deliveryContext": {
+        "isRedelivery": false
+      },
       "postback": {
         "data": "action=buyItem&itemId=123123&color=red",
         "params": {
@@ -278,6 +350,10 @@
         "type": "user",
         "userId": "U206d25c2ea6bd87c17655609a1c37cb8"
       },
+      "webhookEventId": "testwebhookeventid",
+      "deliveryContext": {
+        "isRedelivery": false
+      },
       "postback": {
         "data": "action=buyItem&itemId=123123&color=red",
         "params": {
@@ -294,6 +370,10 @@
         "type": "user",
         "userId": "U206d25c2ea6bd87c17655609a1c37cb8"
       },
+      "webhookEventId": "testwebhookeventid",
+      "deliveryContext": {
+        "isRedelivery": false
+      },
       "things": {
         "deviceId": "t2c449c9d1",
         "type": "link"
@@ -308,6 +388,10 @@
         "type": "group",
         "groupId": "C4af4980629..."
       },
+      "webhookEventId": "testwebhookeventid",
+      "deliveryContext": {
+        "isRedelivery": false
+      },
       "joined": {
         "members": [
           {
@@ -329,6 +413,10 @@
         "type": "group",
         "groupId": "C4af4980629..."
       },
+      "webhookEventId": "testwebhookeventid",
+      "deliveryContext": {
+        "isRedelivery": false
+      },
       "left": {
         "members": [
           {
@@ -351,6 +439,10 @@
         "type": "user",
         "userId": "U206d25c2ea6bd87c17655609a1c37cb8"
       },
+      "webhookEventId": "testwebhookeventid",
+      "deliveryContext": {
+        "isRedelivery": false
+      },
       "things": {
         "deviceId": "t2c449c9d1",
         "type": "unlink"
@@ -365,6 +457,10 @@
         "type": "user",
         "userId": "U206d25c2ea6bd87c17655609a1c37cb8"
       },
+      "webhookEventId": "testwebhookeventid",
+      "deliveryContext": {
+        "isRedelivery": false
+      },
       "message": {
         "id": "325708",
         "type": "file",
@@ -380,6 +476,10 @@
         "type": "user",
         "userId": "U206d25c2ea6bd87c17655609a1c37cb8"
       },
+      "webhookEventId": "testwebhookeventid",
+      "deliveryContext": {
+        "isRedelivery": false
+      },
       "timestamp": 1547817848122,
       "things": {
         "type": "scenarioResult",
@@ -412,6 +512,10 @@
         "groupId": "Ca56f94637cc4347f90a25382909b24b9",
         "userId": "U206d25c2ea6bd87c17655609a1c37cb8"
       },
+      "webhookEventId": "testwebhookeventid",
+      "deliveryContext": {
+        "isRedelivery": false
+      },
       "unsend": {
         "messageId": "325708"
       }
@@ -425,6 +529,10 @@
         "type": "user",
         "userId": "U206d25c2ea6bd87c17655609a1c37cb8"
       },
+      "webhookEventId": "testwebhookeventid",
+      "deliveryContext": {
+        "isRedelivery": false
+      },
       "videoPlayComplete": {
         "trackingId": "track_id"
       }
@@ -448,8 +556,31 @@
         "type": "user",
         "userId": "U206d25c2ea6bd87c17655609a1c37cb8"
       },
+      "webhookEventId": "testwebhookeventid",
+      "deliveryContext": {
+        "isRedelivery": false
+      },
       "replyToken": "fbf94e269485410da6b7e3a5e33283e8",
       "mode": "active"
+    },
+    {
+      "replyToken": "nHuyWiB7yP5Zw52FIkcQobQuGDXCTA",
+      "type": "message",
+      "mode": "active",
+      "timestamp": 1462629479859,
+      "source": {
+        "type": "user",
+        "userId": "U206d25c2ea6bd87c17655609a1c37cb8"
+      },
+      "webhookEventId": "testwebhookeventid",
+      "deliveryContext": {
+        "isRedelivery": true
+      },
+      "message": {
+        "id": "325708",
+        "type": "text",
+        "text": "Hello, world"
+      }
     }
   ]
 }