Skip to content

Commit

Permalink
New field 'destination' in Webhook request (#178)
Browse files Browse the repository at this point in the history
* add destination property

* Update document
  • Loading branch information
okue committed Jun 18, 2019
1 parent a0c2872 commit 4d1d425
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 63 deletions.
67 changes: 49 additions & 18 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -737,18 +737,25 @@ WebhookParser
parser = linebot.WebhookParser('YOUR_CHANNEL_SECRET')
parse(self, body, signature)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
parse(self, body, signature, as_payload=False)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

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

.. code:: python
events = parser.parse(body, signature)
for event in events:
# Do something
do_something(event)
.. code:: python
payload = parser.parse(body, signature, as_payload=True)
for event in payload.events:
do_something(payload.event, payload.destination)
WebhookHandler
~~~~~~~~~~~~~~
Expand All @@ -765,19 +772,18 @@ WebhookHandler
handle(self, body, signature)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

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

.. code:: python
handler.handle(body, signature)
Add handler method
^^^^^^^^^^^^^^^^^^
add(self, event, message=None)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

You can add a handler method by using the ``add`` decorator.

``add(self, event, message=None)``
Add a **handler** method by using this decorator.

.. code:: python
Expand All @@ -787,15 +793,30 @@ You can add a handler method by using the ``add`` decorator.
event.reply_token,
TextSendMessage(text=event.message.text))
When the event is an instance of MessageEvent and event.message is an instance of
TextMessage, this handler method is called.
When the event is an instance of MessageEvent and event.message is an instance of TextMessage,
this handler method is called.

Set default handler method
^^^^^^^^^^^^^^^^^^^^^^^^^^
.. code:: python
You can set the default handler method by using the ``default`` decorator.
@handler.add(MessageEvent)
def handle_message(event, destination):
# do something
``default(self)``
If the arity of the handler method is more than one,
a destination property in a webhook request is passed to it as the second argument.

.. code:: python
@handler.add(FollowEvent)
def handle_follow():
# do something
If the arity of the handler method is zero, the handler method is called with no arguments.

default(self)
^^^^^^^^^^^^^

Set the default **handler** method by using this decorator.

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

WebhookPayload
~~~~~~~~~~~~~~~

https://developers.line.biz/en/reference/messaging-api/#request-body

- WebhookPayload
- destination
- events: list[`Event <#event>`__]

Webhook event object
~~~~~~~~~~~~~~~~~~~~

Expand Down Expand Up @@ -977,6 +1007,7 @@ Test by using tox. We test against the following versions.
- 3.4
- 3.5
- 3.6
- 3.7

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

Expand Down
1 change: 1 addition & 0 deletions linebot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,5 @@
SignatureValidator,
WebhookParser,
WebhookHandler,
WebhookPayload,
)
46 changes: 36 additions & 10 deletions linebot/webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
)
from .utils import LOGGER, PY3, safe_compare_digest


if hasattr(hmac, "compare_digest"):
def compare_digest(val1, val2):
"""compare_digest function.
Expand Down Expand Up @@ -101,10 +100,27 @@ def validate(self, body, signature):
).digest()

return compare_digest(
signature.encode('utf-8'), base64.b64encode(gen_signature)
signature.encode('utf-8'), base64.b64encode(gen_signature)
)


class WebhookPayload(object):
"""Webhook Payload.
https://developers.line.biz/en/reference/messaging-api/#request-body
"""

def __init__(self, events=None, destination=None):
"""__init__ method.
:param events: Information about the events.
:type events: list[T <= :py:class:`linebot.models.events.Event`]
:param str destination: User ID of a bot that should receive webhook events.
"""
self.events = events
self.destination = destination


class WebhookParser(object):
"""Webhook Parser."""

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

def parse(self, body, signature):
def parse(self, body, signature, as_payload=False):
"""Parse webhook request body as text.
:param str body: Webhook request body (as text)
:param str signature: X-Line-Signature value (as text)
:param bool as_payload: (optional) True to return WebhookPayload object.
:rtype: list[T <= :py:class:`linebot.models.events.Event`]
:return:
| :py:class:`linebot.webhook.WebhookPayload`
:return: Events list, or WebhookPayload instance
"""
if not self.signature_validator.validate(body, signature):
raise InvalidSignatureError(
Expand Down Expand Up @@ -156,11 +174,17 @@ def parse(self, body, signature):
else:
LOGGER.warn('Unknown event type. type=' + event_type)

return events
if as_payload:
return WebhookPayload(events=events, destination=body_json['destination'])
else:
return events


class WebhookHandler(object):
"""Webhook Handler."""
"""Webhook Handler.
Please read https://github.com/line/line-bot-sdk-python#webhookhandler
"""

def __init__(self, channel_secret):
"""__init__ method.
Expand Down Expand Up @@ -197,7 +221,7 @@ def default(self):
"""[Decorator] Set default handler method.
:rtype: func
:return:
:return: decorator
"""
def decorator(func):
self._default = func
Expand All @@ -211,9 +235,9 @@ def handle(self, body, signature):
:param str body: Webhook request body (as text)
:param str signature: X-Line-Signature value (as text)
"""
events = self.parser.parse(body, signature)
payload = self.parser.parse(body, signature, as_payload=True)

for event in events:
for event in payload.events:
func = None
key = None

Expand All @@ -235,8 +259,10 @@ def handle(self, body, signature):
args_count = self.__get_args_count(func)
if args_count == 0:
func()
else:
elif args_count == 1:
func(event)
else:
func(event, payload.destination)

def __add_handler(self, func, event, message=None):
key = self.__get_handler_key(event, message=message)
Expand Down
64 changes: 29 additions & 35 deletions tests/test_webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,48 +391,62 @@ def test_parse(self):
class TestWebhookHandler(unittest.TestCase):
def setUp(self):
self.handler = WebhookHandler('channel_secret')
self.calls = []

@self.handler.add(MessageEvent, message=TextMessage)
def message_text(event):
self.calls.append('1 ' + event.type + '_' + event.message.type)
def message_text(event, destination):
self.assertEqual('message', event.type)
self.assertEqual('text', event.message.type)
self.assertEqual('U123', destination)

@self.handler.add(
MessageEvent, message=(ImageMessage, VideoMessage, AudioMessage))
@self.handler.add(MessageEvent,
message=(ImageMessage, VideoMessage, AudioMessage))
def message_content(event):
self.calls.append('2 ' + event.type + '_' + event.message.type)
self.assertEqual('message', event.type)
self.assertIn(
event.message.type,
['image', 'video', 'audio']
)

@self.handler.add(MessageEvent, message=StickerMessage)
def message_sticker(event):
self.calls.append('3 ' + event.type + '_' + event.message.type)
self.assertEqual('message', event.type)
self.assertEqual('sticker', event.message.type)

@self.handler.add(MessageEvent)
def message(event):
self.calls.append(event.type + '_' + event.message.type)
self.assertEqual('message', event.type)
self.assertNotIn(
event.message.type,
['text', 'image', 'video', 'audio', 'sticker']
)

@self.handler.add(FollowEvent)
def follow(event):
self.calls.append('4 ' + event.type)
def follow(event, destination):
self.assertEqual('follow', event.type)
self.assertEqual('U123', destination)

@self.handler.add(JoinEvent)
def join(event):
self.calls.append('5 ' + event.type)
self.assertEqual('join', event.type)

@self.handler.add(PostbackEvent)
def postback(event):
self.calls.append('6 ' + event.type)
self.assertEqual('postback', event.type)

@self.handler.add(BeaconEvent)
def beacon(event):
self.calls.append('7 ' + event.type)
self.assertEqual('beacon', event.type)

@self.handler.add(AccountLinkEvent)
def account_link(event):
self.calls.append('8 ' + event.type)
self.assertEqual('accountLink', event.type)

@self.handler.default()
def default(event):
self.calls.append('default ' + event.type)
self.assertNotIn(
event.type,
['message', 'follow', 'join', 'postback', 'beacon', 'accountLink']
)

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

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

self.assertEqual(self.calls[0], '1 message_text')
self.assertEqual(self.calls[1], '2 message_image')
self.assertEqual(self.calls[2], '2 message_video')
self.assertEqual(self.calls[3], '2 message_audio')
self.assertEqual(self.calls[4], 'message_location')
self.assertEqual(self.calls[5], '3 message_sticker')
self.assertEqual(self.calls[6], '4 follow')
self.assertEqual(self.calls[7], 'default unfollow')
self.assertEqual(self.calls[8], '5 join')
self.assertEqual(self.calls[9], 'default leave')
self.assertEqual(self.calls[10], '6 postback')
self.assertEqual(self.calls[11], '7 beacon')
self.assertEqual(self.calls[12], '7 beacon')
self.assertEqual(self.calls[13], '8 accountLink')
self.assertEqual(self.calls[14], '1 message_text')
self.assertEqual(self.calls[15], '1 message_text')
self.assertEqual(self.calls[16], '6 postback')
self.assertEqual(self.calls[17], '6 postback')
self.assertEqual(self.calls[18], '6 postback')


if __name__ == '__main__':
unittest.main()
1 change: 1 addition & 0 deletions tests/text/webhook.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"destination": "U123",
"events": [
{
"replyToken": "nHuyWiB7yP5Zw52FIkcQobQuGDXCTA",
Expand Down

0 comments on commit 4d1d425

Please sign in to comment.