Skip to content

Commit 4d1d425

Browse files
authored
New field 'destination' in Webhook request (#178)
* add destination property * Update document
1 parent a0c2872 commit 4d1d425

File tree

5 files changed

+116
-63
lines changed

5 files changed

+116
-63
lines changed

README.rst

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -737,18 +737,25 @@ WebhookParser
737737
738738
parser = linebot.WebhookParser('YOUR_CHANNEL_SECRET')
739739
740-
parse(self, body, signature)
741-
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
740+
parse(self, body, signature, as_payload=False)
741+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
742742

743-
Parses the webhook body and builds an event object list. If the signature does NOT
744-
match, InvalidSignatureError is raised.
743+
Parses the webhook body, and returns a list of Event objects or a WebhookPayload object (depending on as_payload).
744+
If the signature does NOT match, ``InvalidSignatureError`` is raised.
745745

746746
.. code:: python
747747
748748
events = parser.parse(body, signature)
749749
750750
for event in events:
751-
# Do something
751+
do_something(event)
752+
753+
.. code:: python
754+
755+
payload = parser.parse(body, signature, as_payload=True)
756+
757+
for event in payload.events:
758+
do_something(payload.event, payload.destination)
752759
753760
WebhookHandler
754761
~~~~~~~~~~~~~~
@@ -765,19 +772,18 @@ WebhookHandler
765772
handle(self, body, signature)
766773
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
767774

768-
Handles webhooks. If the signature does NOT match,
769-
InvalidSignatureError is raised.
775+
Handles webhooks with **handlers** added
776+
by the decorators `add <#add-self-event-message-none>`__ and `default <#default-self>`__.
777+
If the signature does NOT match, ``InvalidSignatureError`` is raised.
770778

771779
.. code:: python
772780
773781
handler.handle(body, signature)
774782
775-
Add handler method
776-
^^^^^^^^^^^^^^^^^^
783+
add(self, event, message=None)
784+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
777785

778-
You can add a handler method by using the ``add`` decorator.
779-
780-
``add(self, event, message=None)``
786+
Add a **handler** method by using this decorator.
781787

782788
.. code:: python
783789
@@ -787,15 +793,30 @@ You can add a handler method by using the ``add`` decorator.
787793
event.reply_token,
788794
TextSendMessage(text=event.message.text))
789795
790-
When the event is an instance of MessageEvent and event.message is an instance of
791-
TextMessage, this handler method is called.
796+
When the event is an instance of MessageEvent and event.message is an instance of TextMessage,
797+
this handler method is called.
792798

793-
Set default handler method
794-
^^^^^^^^^^^^^^^^^^^^^^^^^^
799+
.. code:: python
795800
796-
You can set the default handler method by using the ``default`` decorator.
801+
@handler.add(MessageEvent)
802+
def handle_message(event, destination):
803+
# do something
797804
798-
``default(self)``
805+
If the arity of the handler method is more than one,
806+
a destination property in a webhook request is passed to it as the second argument.
807+
808+
.. code:: python
809+
810+
@handler.add(FollowEvent)
811+
def handle_follow():
812+
# do something
813+
814+
If the arity of the handler method is zero, the handler method is called with no arguments.
815+
816+
default(self)
817+
^^^^^^^^^^^^^
818+
819+
Set the default **handler** method by using this decorator.
799820

800821
.. code:: python
801822
@@ -805,6 +826,15 @@ You can set the default handler method by using the ``default`` decorator.
805826
806827
If there is no handler for an event, this default handler method is called.
807828

829+
WebhookPayload
830+
~~~~~~~~~~~~~~~
831+
832+
https://developers.line.biz/en/reference/messaging-api/#request-body
833+
834+
- WebhookPayload
835+
- destination
836+
- events: list[`Event <#event>`__]
837+
808838
Webhook event object
809839
~~~~~~~~~~~~~~~~~~~~
810840

@@ -977,6 +1007,7 @@ Test by using tox. We test against the following versions.
9771007
- 3.4
9781008
- 3.5
9791009
- 3.6
1010+
- 3.7
9801011

9811012
To run all tests and to run ``flake8`` against all versions, use:
9821013

linebot/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,5 @@
3131
SignatureValidator,
3232
WebhookParser,
3333
WebhookHandler,
34+
WebhookPayload,
3435
)

linebot/webhook.py

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@
3838
)
3939
from .utils import LOGGER, PY3, safe_compare_digest
4040

41-
4241
if hasattr(hmac, "compare_digest"):
4342
def compare_digest(val1, val2):
4443
"""compare_digest function.
@@ -101,10 +100,27 @@ def validate(self, body, signature):
101100
).digest()
102101

103102
return compare_digest(
104-
signature.encode('utf-8'), base64.b64encode(gen_signature)
103+
signature.encode('utf-8'), base64.b64encode(gen_signature)
105104
)
106105

107106

107+
class WebhookPayload(object):
108+
"""Webhook Payload.
109+
110+
https://developers.line.biz/en/reference/messaging-api/#request-body
111+
"""
112+
113+
def __init__(self, events=None, destination=None):
114+
"""__init__ method.
115+
116+
:param events: Information about the events.
117+
:type events: list[T <= :py:class:`linebot.models.events.Event`]
118+
:param str destination: User ID of a bot that should receive webhook events.
119+
"""
120+
self.events = events
121+
self.destination = destination
122+
123+
108124
class WebhookParser(object):
109125
"""Webhook Parser."""
110126

@@ -115,13 +131,15 @@ def __init__(self, channel_secret):
115131
"""
116132
self.signature_validator = SignatureValidator(channel_secret)
117133

118-
def parse(self, body, signature):
134+
def parse(self, body, signature, as_payload=False):
119135
"""Parse webhook request body as text.
120136
121137
:param str body: Webhook request body (as text)
122138
:param str signature: X-Line-Signature value (as text)
139+
:param bool as_payload: (optional) True to return WebhookPayload object.
123140
:rtype: list[T <= :py:class:`linebot.models.events.Event`]
124-
:return:
141+
| :py:class:`linebot.webhook.WebhookPayload`
142+
:return: Events list, or WebhookPayload instance
125143
"""
126144
if not self.signature_validator.validate(body, signature):
127145
raise InvalidSignatureError(
@@ -156,11 +174,17 @@ def parse(self, body, signature):
156174
else:
157175
LOGGER.warn('Unknown event type. type=' + event_type)
158176

159-
return events
177+
if as_payload:
178+
return WebhookPayload(events=events, destination=body_json['destination'])
179+
else:
180+
return events
160181

161182

162183
class WebhookHandler(object):
163-
"""Webhook Handler."""
184+
"""Webhook Handler.
185+
186+
Please read https://github.com/line/line-bot-sdk-python#webhookhandler
187+
"""
164188

165189
def __init__(self, channel_secret):
166190
"""__init__ method.
@@ -197,7 +221,7 @@ def default(self):
197221
"""[Decorator] Set default handler method.
198222
199223
:rtype: func
200-
:return:
224+
:return: decorator
201225
"""
202226
def decorator(func):
203227
self._default = func
@@ -211,9 +235,9 @@ def handle(self, body, signature):
211235
:param str body: Webhook request body (as text)
212236
:param str signature: X-Line-Signature value (as text)
213237
"""
214-
events = self.parser.parse(body, signature)
238+
payload = self.parser.parse(body, signature, as_payload=True)
215239

216-
for event in events:
240+
for event in payload.events:
217241
func = None
218242
key = None
219243

@@ -235,8 +259,10 @@ def handle(self, body, signature):
235259
args_count = self.__get_args_count(func)
236260
if args_count == 0:
237261
func()
238-
else:
262+
elif args_count == 1:
239263
func(event)
264+
else:
265+
func(event, payload.destination)
240266

241267
def __add_handler(self, func, event, message=None):
242268
key = self.__get_handler_key(event, message=message)

tests/test_webhook.py

Lines changed: 29 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -391,48 +391,62 @@ def test_parse(self):
391391
class TestWebhookHandler(unittest.TestCase):
392392
def setUp(self):
393393
self.handler = WebhookHandler('channel_secret')
394-
self.calls = []
395394

396395
@self.handler.add(MessageEvent, message=TextMessage)
397-
def message_text(event):
398-
self.calls.append('1 ' + event.type + '_' + event.message.type)
396+
def message_text(event, destination):
397+
self.assertEqual('message', event.type)
398+
self.assertEqual('text', event.message.type)
399+
self.assertEqual('U123', destination)
399400

400-
@self.handler.add(
401-
MessageEvent, message=(ImageMessage, VideoMessage, AudioMessage))
401+
@self.handler.add(MessageEvent,
402+
message=(ImageMessage, VideoMessage, AudioMessage))
402403
def message_content(event):
403-
self.calls.append('2 ' + event.type + '_' + event.message.type)
404+
self.assertEqual('message', event.type)
405+
self.assertIn(
406+
event.message.type,
407+
['image', 'video', 'audio']
408+
)
404409

405410
@self.handler.add(MessageEvent, message=StickerMessage)
406411
def message_sticker(event):
407-
self.calls.append('3 ' + event.type + '_' + event.message.type)
412+
self.assertEqual('message', event.type)
413+
self.assertEqual('sticker', event.message.type)
408414

409415
@self.handler.add(MessageEvent)
410416
def message(event):
411-
self.calls.append(event.type + '_' + event.message.type)
417+
self.assertEqual('message', event.type)
418+
self.assertNotIn(
419+
event.message.type,
420+
['text', 'image', 'video', 'audio', 'sticker']
421+
)
412422

413423
@self.handler.add(FollowEvent)
414-
def follow(event):
415-
self.calls.append('4 ' + event.type)
424+
def follow(event, destination):
425+
self.assertEqual('follow', event.type)
426+
self.assertEqual('U123', destination)
416427

417428
@self.handler.add(JoinEvent)
418429
def join(event):
419-
self.calls.append('5 ' + event.type)
430+
self.assertEqual('join', event.type)
420431

421432
@self.handler.add(PostbackEvent)
422433
def postback(event):
423-
self.calls.append('6 ' + event.type)
434+
self.assertEqual('postback', event.type)
424435

425436
@self.handler.add(BeaconEvent)
426437
def beacon(event):
427-
self.calls.append('7 ' + event.type)
438+
self.assertEqual('beacon', event.type)
428439

429440
@self.handler.add(AccountLinkEvent)
430441
def account_link(event):
431-
self.calls.append('8 ' + event.type)
442+
self.assertEqual('accountLink', event.type)
432443

433444
@self.handler.default()
434445
def default(event):
435-
self.calls.append('default ' + event.type)
446+
self.assertNotIn(
447+
event.type,
448+
['message', 'follow', 'join', 'postback', 'beacon', 'accountLink']
449+
)
436450

437451
def test_handler(self):
438452
file_dir = os.path.dirname(__file__)
@@ -445,26 +459,6 @@ def test_handler(self):
445459

446460
self.handler.handle(body, 'signature')
447461

448-
self.assertEqual(self.calls[0], '1 message_text')
449-
self.assertEqual(self.calls[1], '2 message_image')
450-
self.assertEqual(self.calls[2], '2 message_video')
451-
self.assertEqual(self.calls[3], '2 message_audio')
452-
self.assertEqual(self.calls[4], 'message_location')
453-
self.assertEqual(self.calls[5], '3 message_sticker')
454-
self.assertEqual(self.calls[6], '4 follow')
455-
self.assertEqual(self.calls[7], 'default unfollow')
456-
self.assertEqual(self.calls[8], '5 join')
457-
self.assertEqual(self.calls[9], 'default leave')
458-
self.assertEqual(self.calls[10], '6 postback')
459-
self.assertEqual(self.calls[11], '7 beacon')
460-
self.assertEqual(self.calls[12], '7 beacon')
461-
self.assertEqual(self.calls[13], '8 accountLink')
462-
self.assertEqual(self.calls[14], '1 message_text')
463-
self.assertEqual(self.calls[15], '1 message_text')
464-
self.assertEqual(self.calls[16], '6 postback')
465-
self.assertEqual(self.calls[17], '6 postback')
466-
self.assertEqual(self.calls[18], '6 postback')
467-
468462

469463
if __name__ == '__main__':
470464
unittest.main()

tests/text/webhook.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
2+
"destination": "U123",
23
"events": [
34
{
45
"replyToken": "nHuyWiB7yP5Zw52FIkcQobQuGDXCTA",

0 commit comments

Comments
 (0)