From 6ec89bb17982d7e301df6518dd394d20b84fee48 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 26 Aug 2014 22:09:56 -0400 Subject: [PATCH] Add Slack notification support --- endpoints/notificationmethod.py | 78 ++++++++++++++++++++++++++++++++ initdb.py | 1 + static/css/quay.css | 7 +++ static/img/slack.ico | Bin 0 -> 22486 bytes static/js/app.js | 18 ++++++++ test/data/test.db | Bin 614400 -> 614400 bytes 6 files changed, 104 insertions(+) create mode 100644 static/img/slack.ico diff --git a/endpoints/notificationmethod.py b/endpoints/notificationmethod.py index a6d958037..9650e79f6 100644 --- a/endpoints/notificationmethod.py +++ b/endpoints/notificationmethod.py @@ -5,6 +5,7 @@ import tarfile import base64 import json import requests +import re from flask.ext.mail import Message from app import mail, app, get_app_url @@ -302,3 +303,80 @@ class HipchatMethod(NotificationMethod): return False return True + + +class SlackMethod(NotificationMethod): + """ Method for sending notifications to Slack via the API: + https://api.slack.com/docs/attachments + """ + @classmethod + def method_name(cls): + return 'slack' + + def validate(self, repository, config_data): + if not config_data.get('token', ''): + raise CannotValidateNotificationMethodException('Missing Slack Token') + + if not config_data.get('subdomain', '').isalnum(): + raise CannotValidateNotificationMethodException('Missing Slack Subdomain Name') + + def formatForSlack(self, message): + message = message.replace('\n', '') + message = re.sub(r'\s+', ' ', message) + message = message.replace('
', '\n') + message = re.sub(r'(.+)', '<\\1|\\2>', message) + return message + + def perform(self, notification, event_handler, notification_data): + config_data = json.loads(notification.config_json) + + token = config_data.get('token', '') + subdomain = config_data.get('subdomain', '') + + if not token or not subdomain: + return False + + owner = model.get_user(notification.repository.namespace) + if not owner: + # Something went wrong. + return False + + url = 'https://%s.slack.com/services/hooks/incoming-webhook?token=%s' % (subdomain, token) + + level = event_handler.get_level(notification_data['event_data'], notification_data) + color = { + 'info': '#ffffff', + 'warning': 'warning', + 'error': 'danger', + 'primary': 'good' + }.get(level, '#ffffff') + + summary = event_handler.get_summary(notification_data['event_data'], notification_data) + message = event_handler.get_message(notification_data['event_data'], notification_data) + + headers = {'Content-type': 'application/json'} + payload = { + 'text': summary, + 'username': 'quayiobot', + 'attachments': [ + { + 'fallback': summary, + 'text': self.formatForSlack(message), + 'color': color + } + ] + } + + try: + resp = requests.post(url, data=json.dumps(payload), headers=headers) + if resp.status_code/100 != 2: + logger.error('%s response for Slack to url: %s' % (resp.status_code, + url)) + logger.error(resp.content) + return False + + except requests.exceptions.RequestException as ex: + logger.exception('Slack method was unable to be sent: %s' % ex.message) + return False + + return True diff --git a/initdb.py b/initdb.py index 6b68cee85..7cadbee87 100644 --- a/initdb.py +++ b/initdb.py @@ -253,6 +253,7 @@ def initialize_database(): ExternalNotificationMethod.create(name='flowdock') ExternalNotificationMethod.create(name='hipchat') + ExternalNotificationMethod.create(name='slack') NotificationKind.create(name='repo_push') NotificationKind.create(name='build_queued') diff --git a/static/css/quay.css b/static/css/quay.css index bd8f571e8..2a4696551 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -4573,6 +4573,13 @@ i.hipchat-icon { height: 16px; } +i.slack-icon { + background-image: url(/static/img/slack.ico); + background-size: 16px; + width: 16px; + height: 16px; +} + .external-notification-view-element { margin: 10px; padding: 6px; diff --git a/static/img/slack.ico b/static/img/slack.ico new file mode 100644 index 0000000000000000000000000000000000000000..d32c2528026b0950f2efe73a9b96e2092f013000 GIT binary patch literal 22486 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 literal 0 HcmV?d00001 diff --git a/static/js/app.js b/static/js/app.js index 3394543e7..c272b7662 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -1141,6 +1141,24 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading 'title': 'Notification Token' } ] + }, + { + 'id': 'slack', + 'title': 'Slack Room Notification', + 'icon': 'slack-icon', + 'fields': [ + { + 'name': 'subdomain', + 'type': 'string', + 'title': 'Slack Subdomain' + }, + { + 'name': 'token', + 'type': 'string', + 'title': 'Token', + 'help_url': 'https://{subdomain}.slack.com/services/new/outgoing-webhook' + } + ] } ]; diff --git a/test/data/test.db b/test/data/test.db index a947e59643b1155787f4970e641fb95bf1b9aef7..ddfe7166b2af30d01966351fdc308ff0061f4e25 100644 GIT binary patch delta 6169 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