From 35bd28a77e1b407a31487b8ca242c34d385b678c Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 19 Aug 2014 14:33:33 -0400 Subject: [PATCH] Add support for the Flowdock Team chat API: https://www.flowdock.com/api/push --- endpoints/notificationevent.py | 2 + endpoints/notificationmethod.py | 55 +++++++++++++++++- initdb.py | 2 + static/css/quay.css | 7 +++ .../create-external-notification-dialog.html | 8 ++- static/img/flowdock.ico | Bin 0 -> 5558 bytes static/js/app.js | 13 +++++ test/data/test.db | Bin 614400 -> 614400 bytes 8 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 static/img/flowdock.ico diff --git a/endpoints/notificationevent.py b/endpoints/notificationevent.py index f1cbec42c..e393dc134 100644 --- a/endpoints/notificationevent.py +++ b/endpoints/notificationevent.py @@ -184,6 +184,8 @@ class BuildFailureEvent(NotificationEvent): return 'build_failure' def get_sample_data(self, repository): + build_uuid = 'fake-build-id' + return build_event_data(repository, { 'build_id': build_uuid, 'build_name': 'some-fake-build', diff --git a/endpoints/notificationmethod.py b/endpoints/notificationmethod.py index b49055157..56adcc0a5 100644 --- a/endpoints/notificationmethod.py +++ b/endpoints/notificationmethod.py @@ -4,9 +4,10 @@ import os.path import tarfile import base64 import json +import requests from flask.ext.mail import Message -from app import mail, app +from app import mail, app, get_app_url from data import model logger = logging.getLogger(__name__) @@ -187,3 +188,55 @@ class WebhookMethod(NotificationMethod): return False return True + + +class FlowdockMethod(NotificationMethod): + """ Method for sending notifications to Flowdock via the Team Inbox API: + https://www.flowdock.com/api/team-inbox + """ + @classmethod + def method_name(cls): + return 'flowdock' + + def validate(self, repository, config_data): + token = config_data.get('flow_api_token', '') + if not token: + raise CannotValidateNotificationMethodException('Missing Flowdock API Token') + + def perform(self, notification, event_handler, notification_data): + config_data = json.loads(notification.config_json) + token = config_data.get('flow_api_token', '') + if not token: + return False + + owner = model.get_user(notification.repository.namespace) + if not owner: + # Something went wrong. + return False + + url = 'https://api.flowdock.com/v1/messages/team_inbox/%s' % token + headers = {'Content-type': 'application/json'} + payload = { + 'source': 'Quay', + 'from_address': 'support@quay.io', + 'subject': event_handler.get_summary(notification_data['event_data'], notification_data), + 'content': event_handler.get_message(notification_data['event_data'], notification_data), + 'from_name': owner.username, + 'project': notification.repository.namespace + ' ' + notification.repository.name, + 'tags': ['#' + event_handler.event_name()], + 'link': notification_data['event_data']['homepage'] + } + + try: + resp = requests.post(url, data=json.dumps(payload), headers=headers) + if resp.status_code/100 != 2: + logger.error('%s response for flowdock to url: %s' % (resp.status_code, + url)) + logger.error(resp.content) + return False + + except requests.exceptions.RequestException as ex: + logger.exception('Flowdock method was unable to be sent: %s' % ex.message) + return False + + return True diff --git a/initdb.py b/initdb.py index 7e48ae3af..cb56d987e 100644 --- a/initdb.py +++ b/initdb.py @@ -251,6 +251,8 @@ def initialize_database(): ExternalNotificationMethod.create(name='email') ExternalNotificationMethod.create(name='webhook') + ExternalNotificationMethod.create(name='flowdock') + NotificationKind.create(name='repo_push') NotificationKind.create(name='build_queued') NotificationKind.create(name='build_start') diff --git a/static/css/quay.css b/static/css/quay.css index 01fe84e60..a5cdf019b 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -4559,6 +4559,13 @@ i.quay-icon { height: 16px; } +i.flowdock-icon { + background-image: url(/static/img/flowdock.ico); + background-size: 16px; + width: 16px; + height: 16px; +} + .external-notification-view-element { margin: 10px; padding: 6px; diff --git a/static/directives/create-external-notification-dialog.html b/static/directives/create-external-notification-dialog.html index d384f3f59..ba78a4ac9 100644 --- a/static/directives/create-external-notification-dialog.html +++ b/static/directives/create-external-notification-dialog.html @@ -73,7 +73,7 @@
- {{ field.title }}: + {{ field.title }}:
@@ -86,7 +86,11 @@ current-entity="currentConfig[field.name]" ng-model="currentConfig[field.name]" allowed-entities="['user', 'team', 'org']" - ng-switch-when="entity"> + ng-switch-when="entity">
+ +
+ See: {{ field.help_url }} +
diff --git a/static/img/flowdock.ico b/static/img/flowdock.ico new file mode 100644 index 0000000000000000000000000000000000000000..e3d92799b7bfe01a0ae208fb9576467411a505d6 GIT binary patch literal 5558 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 K=eB?M|L{K?9?(|+ literal 0 HcmV?d00001 diff --git a/static/js/app.js b/static/js/app.js index ad6527fc1..8676d7d99 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -1076,6 +1076,19 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading 'title': 'Webhook URL' } ] + }, + { + 'id': 'flowdock', + 'title': 'Flowdock Team Notification', + 'icon': 'flowdock-icon', + 'fields': [ + { + 'name': 'flow_api_token', + 'type': 'string', + 'title': 'Flow API Token', + 'help_url': 'https://www.flowdock.com/account/tokens' + } + ] } ]; diff --git a/test/data/test.db b/test/data/test.db index 4d04283311e6feb845dff3fde9a4d370858119be..a947e59643b1155787f4970e641fb95bf1b9aef7 100644 GIT binary patch delta 6225 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