-
Notifications
You must be signed in to change notification settings - Fork 87
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Webhooks #2624
base: master
Are you sure you want to change the base?
Webhooks #2624
Changes from all commits
5887bdc
1b7902c
9c087da
3f40bfe
b922e42
b9c5e34
bfec774
1b715be
971891e
ec349a4
d23b140
07e48ac
9efc812
f0e876d
a0a99d2
e341560
57a398b
60905db
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -58,6 +58,7 @@ exclude = | |
specs, | ||
udata/static, | ||
udata/templates | ||
docstring-quotes = ''' | ||
|
||
[wheel] | ||
universal = 1 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
def init_app(app): | ||
from udata.features.webhooks import triggers # noqa |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import json | ||
import requests | ||
|
||
from flask import current_app | ||
|
||
from udata.tasks import get_logger, task | ||
from udata.features.webhooks.utils import sign | ||
|
||
log = get_logger(__name__) | ||
|
||
# number of time we should retry | ||
DEFAULT_RETRIES = 5 | ||
# exponentional backoff factor (in seconds) | ||
# https://docs.celeryproject.org/en/v4.3.0/userguide/tasks.html#Task.retry_backoff | ||
DEFAULT_BACKOFF = 30 | ||
# timeout for a single request | ||
DEFAULT_TIMEOUT = 30 | ||
|
||
|
||
def dispatch(event, payload): | ||
webhooks = current_app.config['WEBHOOKS'] | ||
for wh in webhooks: | ||
if event in wh.get('events', []): | ||
_dispatch.delay(event, payload, wh) | ||
|
||
|
||
@task( | ||
autoretry_for=(requests.exceptions.HTTPError,), retry_backoff=DEFAULT_BACKOFF, | ||
retry_kwargs={'max_retries': DEFAULT_RETRIES} | ||
) | ||
def _dispatch(event, event_payload, wh): | ||
url = wh['url'] | ||
log.debug(f'Dispatching {event} to {url}') | ||
|
||
event_payload = event_payload if not type(event_payload) is str else json.loads(event_payload) | ||
payload = { | ||
'event': event, | ||
'payload': event_payload, | ||
} | ||
|
||
r = requests.post(url, json=payload, headers={ | ||
'x-hook-signature': sign(payload, wh.get('secret')) | ||
}, timeout=DEFAULT_TIMEOUT) | ||
|
||
if not r.ok: | ||
log.error(f'Failed dispatching webhook {event} to {url}') | ||
|
||
r.raise_for_status() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import json | ||
|
||
from udata.core.discussions.signals import ( | ||
on_new_discussion, on_new_discussion_comment, on_discussion_closed, | ||
) | ||
|
||
from udata.features.webhooks.tasks import dispatch | ||
from udata.models import Dataset, Organization, Reuse, CommunityResource | ||
|
||
|
||
@Dataset.on_create.connect | ||
def on_dataset_create(dataset): | ||
if not dataset.private: | ||
dispatch('datagouvfr.dataset.created', dataset.to_json()) | ||
|
||
|
||
@Dataset.on_delete.connect | ||
def on_dataset_delete(dataset): | ||
if not dataset.private: | ||
dispatch('datagouvfr.dataset.deleted', dataset.to_json()) | ||
|
||
|
||
@Dataset.on_update.connect | ||
def on_dataset_update(dataset): | ||
updates, _ = dataset._delta() | ||
if dataset.private: | ||
# Notify if newly private but not otherwise | ||
if 'private' in updates: | ||
# Do we want to send the full dataset (because the private update can add private information?) | ||
dispatch('datagouvfr.dataset.deleted', dataset.to_json()) | ||
else: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm confused by the logic on dataset privacy here. If an update is made on a private dataset, we're going to dispatch the updated signal? |
||
if 'private' in updates: | ||
dispatch('datagouvfr.dataset.created', dataset.to_json()) | ||
else: | ||
dispatch('datagouvfr.dataset.updated', dataset.to_json()) | ||
|
||
|
||
@on_new_discussion.connect | ||
def on_new_discussion(discussion): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same question on discussion events, don't we want to add the |
||
dispatch('datagouvfr.discussion.created', discussion.to_json()) | ||
|
||
|
||
@on_new_discussion_comment.connect | ||
def on_new_discussion_comment(discussion, message=None): | ||
dispatch('datagouvfr.discussion.commented', { | ||
'message_id': message, | ||
'discussion': json.loads(discussion.to_json()), | ||
}) | ||
|
||
|
||
@on_discussion_closed.connect | ||
def on_discussion_closed(discussion, message=None): | ||
dispatch('datagouvfr.discussion.closed', { | ||
'message_id': message, | ||
'discussion': json.loads(discussion.to_json()), | ||
}) | ||
|
||
|
||
@Organization.on_create.connect | ||
def on_organization_created(organization): | ||
dispatch('datagouvfr.organization.created', organization.to_json()) | ||
|
||
|
||
@Organization.on_update.connect | ||
def on_organization_updated(organization): | ||
dispatch('datagouvfr.organization.updated', organization.to_json()) | ||
|
||
|
||
@Reuse.on_create.connect | ||
def on_reuse_created(reuse): | ||
dispatch('datagouvfr.reuse.created', reuse.to_json()) | ||
|
||
|
||
@Reuse.on_update.connect | ||
def on_reuse_updated(reuse): | ||
dispatch('datagouvfr.reuse.updated', reuse.to_json()) | ||
|
||
|
||
@CommunityResource.on_create.connect | ||
def on_community_resource_created(community_resource): | ||
dispatch('datagouvfr.community_resource.created', community_resource.to_json()) | ||
|
||
|
||
@CommunityResource.on_update.connect | ||
def on_community_resource_updated(community_resource): | ||
dispatch('datagouvfr.community_resource.updated', community_resource.to_json()) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import json | ||
import hmac | ||
|
||
|
||
def sign(msg, secret): | ||
if isinstance(secret, str): | ||
secret = secret.encode('utf-8') | ||
if isinstance(msg, (dict, tuple, list)): | ||
msg = json.dumps(msg).encode('utf-8') | ||
return hmac.new(secret, msg, 'sha256').hexdigest() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't we want to add the
if not dataset.private
condition on dataset deletion ?