diff --git a/platypush/backend/http/app/routes/hook.py b/platypush/backend/http/app/routes/hook.py index 5243039c..bcf33c50 100644 --- a/platypush/backend/http/app/routes/hook.py +++ b/platypush/backend/http/app/routes/hook.py @@ -1,9 +1,11 @@ import json -from flask import Blueprint, abort, request, Response +from flask import Blueprint, abort, request +from flask.wrappers import Response from platypush.backend.http.app import template_folder from platypush.backend.http.app.utils import logger, send_message +from platypush.config import Config from platypush.message.event.http.hook import WebhookEvent @@ -15,9 +17,11 @@ __routes__ = [ ] -@hook.route('/hook/', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS']) -def _hook(hook_name): - """ Endpoint for custom webhooks """ +@hook.route( + '/hook/', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'] +) +def hook_route(hook_name): + """Endpoint for custom webhooks""" event_args = { 'hook': hook_name, @@ -28,20 +32,36 @@ def _hook(hook_name): } if event_args['data']: - # noinspection PyBroadException try: event_args['data'] = json.loads(event_args['data']) except Exception as e: - logger().warning('Not a valid JSON string: {}: {}'.format(event_args['data'], str(e))) + logger().warning( + 'Not a valid JSON string: %s: %s', event_args['data'], str(e) + ) event = WebhookEvent(**event_args) + matching_hooks = [ + hook + for hook in Config.get_event_hooks().values() + if hook.condition.type == WebhookEvent + and hook.condition.args.get('hook') == hook_name + and request.method.lower() + == hook.condition.args.get('method', request.method).lower() + ] try: send_message(event) - return Response(json.dumps({'status': 'ok', **event_args}), mimetype='application/json') + + # If there are matching hooks, wait for their completion before returning + if matching_hooks: + return event.wait_response(timeout=60) + + return Response( + json.dumps({'status': 'ok', **event_args}), mimetype='application/json' + ) except Exception as e: logger().exception(e) - logger().error('Error while dispatching webhook event {}: {}'.format(event, str(e))) + logger().error('Error while dispatching webhook event %s: %s', event, str(e)) abort(500, str(e)) diff --git a/platypush/event/hook.py b/platypush/event/hook.py index 7dc3140a..35cb052d 100644 --- a/platypush/event/hook.py +++ b/platypush/event/hook.py @@ -15,10 +15,10 @@ logger = logging.getLogger('platypush') def parse(msg): - """ Builds a dict given another dictionary or - a JSON UTF-8 encoded string/bytearray """ + """Builds a dict given another dictionary or + a JSON UTF-8 encoded string/bytearray""" - if isinstance(msg, bytes) or isinstance(msg, bytearray): + if isinstance(msg, (bytes, bytearray)): msg = msg.decode('utf-8') if isinstance(msg, str): try: @@ -30,8 +30,8 @@ def parse(msg): return msg -class EventCondition(object): - """ Event hook condition class """ +class EventCondition: + """Event hook condition class""" def __init__(self, type=Event.__class__, priority=None, **kwargs): """ @@ -55,8 +55,8 @@ class EventCondition(object): @classmethod def build(cls, rule): - """ Builds a rule given either another EventRule, a dictionary or - a JSON UTF-8 encoded string/bytearray """ + """Builds a rule given either another EventRule, a dictionary or + a JSON UTF-8 encoded string/bytearray""" if isinstance(rule, cls): return rule @@ -64,8 +64,7 @@ class EventCondition(object): rule = parse(rule) assert isinstance(rule, dict), f'Not a valid rule: {rule}' - type = get_event_class_by_type( - rule.pop('type') if 'type' in rule else 'Event') + type = get_event_class_by_type(rule.pop('type') if 'type' in rule else 'Event') args = {} for (key, value) in rule.items(): @@ -75,8 +74,8 @@ class EventCondition(object): class EventAction(Request): - """ Event hook action class. It is a special type of runnable request - whose fields can be configured later depending on the event context """ + """Event hook action class. It is a special type of runnable request + whose fields can be configured later depending on the event context""" def __init__(self, target=None, action=None, **args): if target is None: @@ -99,16 +98,16 @@ class EventAction(Request): return super().build(action) -class EventHook(object): - """ Event hook class. It consists of one conditions and - one or multiple actions to be executed """ +class EventHook: + """Event hook class. It consists of one conditions and + one or multiple actions to be executed""" def __init__(self, name, priority=None, condition=None, actions=None): - """ Constructor. Takes a name, a EventCondition object and an event action - procedure as input. It may also have a priority attached - as a positive number. If multiple hooks match against an event, - only the ones that have either the maximum match score or the - maximum pre-configured priority will be run. """ + """Constructor. Takes a name, a EventCondition object and an event action + procedure as input. It may also have a priority attached + as a positive number. If multiple hooks match against an event, + only the ones that have either the maximum match score or the + maximum pre-configured priority will be run.""" self.name = name self.condition = EventCondition.build(condition or {}) @@ -118,8 +117,8 @@ class EventHook(object): @classmethod def build(cls, name, hook): - """ Builds a rule given either another EventRule, a dictionary or - a JSON UTF-8 encoded string/bytearray """ + """Builds a rule given either another EventRule, a dictionary or + a JSON UTF-8 encoded string/bytearray""" if isinstance(hook, cls): return hook @@ -146,14 +145,14 @@ class EventHook(object): return cls(name=name, condition=condition, actions=actions, priority=priority) def matches_event(self, event): - """ Returns an EventMatchResult object containing the information - about the match between the event and this hook """ + """Returns an EventMatchResult object containing the information + about the match between the event and this hook""" return event.matches_condition(self.condition) def run(self, event): - """ Checks the condition of the hook against a particular event and - runs the hook actions if the condition is met """ + """Checks the condition of the hook against a particular event and + runs the hook actions if the condition is met""" def _thread_func(result): set_thread_name('Event-' + self.name) @@ -163,7 +162,9 @@ class EventHook(object): if result.is_match: logger.info('Running hook {} triggered by an event'.format(self.name)) - threading.Thread(target=_thread_func, name='Event-' + self.name, args=(result,)).start() + threading.Thread( + target=_thread_func, name='Event-' + self.name, args=(result,) + ).start() def hook(event_type=Event, **condition): @@ -172,8 +173,14 @@ def hook(event_type=Event, **condition): f.condition = EventCondition(type=event_type, **condition) @wraps(f) - def wrapped(*args, **kwargs): - return exec_wrapper(f, *args, **kwargs) + def wrapped(event, *args, **kwargs): + from platypush.message.event.http.hook import WebhookEvent + + response = exec_wrapper(f, event, *args, **kwargs) + if isinstance(event, WebhookEvent): + event.send_response(response) + + return response return wrapped diff --git a/platypush/message/event/http/hook.py b/platypush/message/event/http/hook.py index b423e844..1c1690d9 100644 --- a/platypush/message/event/http/hook.py +++ b/platypush/message/event/http/hook.py @@ -1,11 +1,26 @@ +import json +import uuid + from platypush.message.event import Event +from platypush.utils import get_redis + class WebhookEvent(Event): """ Event triggered when a custom webhook is called. """ - def __init__(self, *argv, hook, method, data=None, args=None, headers=None, **kwargs): + def __init__( + self, + *argv, + hook, + method, + data=None, + args=None, + headers=None, + response=None, + **kwargs, + ): """ :param hook: Name of the invoked web hook, from http://host:port/hook/ :type hook: str @@ -21,10 +36,35 @@ class WebhookEvent(Event): :param headers: Request headers :type args: dict - """ - super().__init__(hook=hook, method=method, data=data, args=args or {}, - headers=headers or {}, *argv, **kwargs) + :param response: Response returned by the hook. + :type args: dict | list | str + """ + # This queue is used to synchronize with the hook and wait for its completion + kwargs['response_queue'] = kwargs.get( + 'response_queue', f'platypush/webhook/{str(uuid.uuid1())}' + ) + + super().__init__( + *argv, + hook=hook, + method=method, + data=data, + args=args or {}, + headers=headers or {}, + response=response, + **kwargs, + ) + + def send_response(self, response): + output = response.output + if isinstance(output, (dict, list)): + output = json.dumps(output) + + get_redis().rpush(self.args['response_queue'], output) + + def wait_response(self, timeout=None): + return get_redis().blpop(self.args['response_queue'], timeout=timeout)[1] # vim:sw=4:ts=4:et: