Created
December 28, 2015 19:35
-
-
Save devdazed/07c3437fbc76c999af4d to your computer and use it in GitHub Desktop.
Revisions
-
Russ Bradberry created this gist
Dec 28, 2015 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,276 @@ #!/usr/bin/env python from __future__ import print_function import json import logging import re from base64 import b64decode, b64encode from urllib2 import Request, urlopen, URLError, HTTPError log = logging.getLogger(__name__) class SlackJiraLinkBot(object): """ A Bot that scans slack messages for mentions of potential JIRA Issues and responds with information about the Issue """ # The domain for JIRA (eg. https://example.atlassian.net) jira_domain = None # The JIRA username to use for JIRA Basic Authentication. jira_user = None # The JIRA password to user for JIRA Basic Authentication jira_password = None # The REST API base path for JIRA Issues # Default: /rest/api/2/issue/ jira_issue_path = '/rest/api/2/issue/' # Regex used to detect when JIRA Issues are mentioned in a Slack Message # Default: [A-Z]{2,}-\d+ jira_issue_regex = '[A-Z]{2,}-\d+' # The Slack Incoming WebHook URL for sending messages slack_webhook_url = None # A list of valid slack tokens that come from any Slack Outgoing WebHooks # An empty list will accept any token. # Default: () slack_valid_tokens = () # Username to post Slack messages under. Note: does not need to be a real user. # Default: JIRA slack_user = 'JIRA' # Icon URL for the Slack user. # Default: (JIRA icon) https://slack.global.ssl.fastly.net/66f9/img/services/jira_128.png slack_user_icon = 'https://slack.global.ssl.fastly.net/66f9/img/services/jira_128.png' # Colors to use for JIRA issues. # These should match the issue types you have set up in JIRA. colors = { 'New Feature': '#65AC43', # Apple 'Bug': '#D24331', # Valencia 'Task': '#377DC6', # Tufts Blue 'Sub-Task': '#377DC6', # Tufts Blue 'Epic': '#654783', # Gigas 'Question': '#707070', # Dove Grey 'DEFAULT': '#F5F5F5' # Wild Sand } def __init__(self, jira_domain, jira_user, jira_password, slack_webhook_url, slack_valid_tokens=slack_valid_tokens, jira_issue_path=jira_issue_path, jira_issue_regex=jira_issue_regex, slack_user=slack_user, slack_user_icon=slack_user_icon, colors=colors, log_level='INFO'): self.jira_domain = jira_domain self.jira_user = jira_user self.jira_password = jira_password self.slack_webhook_url = slack_webhook_url self.slack_valid_tokens = slack_valid_tokens self.jira_issue_path = jira_issue_path self.jira_issue_regex = re.compile(jira_issue_regex) self.slack_user = slack_user self.slack_user_icon = slack_user_icon self.colors = colors log.setLevel(log_level) @staticmethod def _make_request(url, body=None, headers={}): if 'https://' not in url: url = 'https://' + url req = Request(url, body, headers) log.info('Making request to %s', url) try: response = urlopen(req) body = response.read() try: return json.loads(body) except ValueError: return body except HTTPError as e: log.error("Request failed: %d %s", e.code, e.reason) except URLError as e: log.error("Server connection failed: %s", e.reason) def _process_issue(self, key, channel): """ Gets information for a JIRA issue and sends a message to Slack with information about the issue. :param key: The JIRA issue key (eg. JIRA-1234) """ issue = self.jira_issue(key) if issue is not None: self.send_message(issue, channel) def on_message(self, event): """ Parses a message even coming from a Slack outgoing webhook then determines if any JIRA issues exist in the message and send a Slack notification to the channel with information about the issue :param event: The Slack outgoing webhook payload """ log.info('Processing Event: %s', event) # Validate Slack Tokens if self.slack_valid_tokens and event['token'] not in self.slack_valid_tokens: log.error('Request token (%s) is invalid', event['token']) raise Exception('Invalid request token') # Find all JIRA issues in a message message = event.get('text', '') matches = self.jira_issue_regex.findall(message) if len(matches) == 0: log.info('No issues found in (%s)', message) return for key in matches: self._process_issue(key, event['channel_name']) def jira_issue(self, key): """ Makes a call to the JIRA REST API to retrieve JIRA Issue information :param key: The JIRA Issue key (eg. JIRA-1234) :return: dict """ url = self.jira_domain + self.jira_issue_path + key auth = 'Basic ' + b64encode(self.jira_user + ':' + self.jira_password) return self._make_request(url, headers={ 'Authorization': auth, 'Content-Type': 'application/json' }) def send_message(self, issue, channel): """ Sends a Slack message with information about the JIRA Issue :param issue: The JIRA Issue dict :param channel: The Slack channel to send the message """ # Ensure there is a '#' prepended to the Slack channel channel = '#' + channel # The color of the post, to match the issue type color = self.colors.get(issue['fields']['issuetype']['name'], self.colors['DEFAULT']) # The Title to the JIRA issue, (eg. JIRA-1234 - Add JIRA Slack Integration) title = issue['key'] + ' - ' + issue['fields']['summary'] # The link to the JIRA issue show page title_link = self.jira_domain + '/browse/' + issue['key'] # Text sent to Slack, replaces JIRA code blocks with Slack code blocks # As a side effect this also replaces color blocks with Slack code blocks text = re.sub('{.*}', '```', issue['fields']['description']) # The priority name of the issue priority = issue['fields']['priority']['name'] # The name of the person assigned to the issue assignee = issue['fields']['assignee']['displayName'] # The status of the issue status = issue['fields']['status']['name'] # create the body of the request. body = json.dumps({ 'channel': channel, 'username': self.slack_user, 'icon_url': self.slack_user_icon, 'attachments': [ { 'fallback': title, 'mrkdwn_in': ['text', 'pretext', 'fields'], 'color': color, 'title': title, 'title_link': title_link, 'text': text, 'fields': [ {'title': 'Priority', 'value': priority, 'short': True}, {'title': 'Assignee', 'value': assignee, 'short': True}, {'title': 'Status', 'value': status, 'short': True}, ] } ] }) self._make_request(self.slack_webhook_url, body) def lambda_handler(event, _): """ Main entry point for AWS Lambda. Variables can not be passed in to AWS Lambda, the configuration parameters below are encrypted using AWS IAM Keys. :param event: The event as it is received from AWS Lambda """ # Boto is always available in AWS lambda, but may not be available in # standalone mode import boto3 # To generate the encrypted values, go to AWS IAM Keys and Generate a key # Then grant decryption using the key to the IAM Role used for your lambda # function. # # Then use the command `aws kms encrypt --key-id alias/<key-alias> --plaintext <value-to-encrypt> # Put the encrypted value in the configuration dictionary below encrypted_config = { 'jira_domain': '<ENCRYPTED JIRA DOMAIN>', 'jira_user': '<ENCRYPTED JIRA USER>', 'jira_password': '<ENCRYPTED JIRA PASSWORD>', 'slack_webhook_url': '<ENCRYPTED WEBHOOK URL>' } kms = boto3.client('kms') config = {x: kms.decrypt(CiphertextBlob=b64decode(y))['Plaintext'] for x, y in encrypted_config.iteritems()} bot = SlackJiraLinkBot(**config) return bot.on_message(event) def main(): """ Runs the SlackJIRA bot as a standalone server. """ from argparse import ArgumentParser from flask import Flask, request parser = ArgumentParser(usage=main.__doc__) parser.add_argument("-p", "--port", dest="port", default=8675, help="Port to run bot server") parser.add_argument("-jd", "--jira-domain", required=True, dest="jira_domain", help="Domain where your JIRA is located") parser.add_argument("-ju", "--jira-user", required=True, dest="jira_user", help="The JIRA username for authenticating to the JIRA REST API") parser.add_argument("-jp", "--jira-password", required=True, dest="jira_password", help="The JIRA password for authenticating to the JIRA REST API") parser.add_argument("-su", "--slack-webhook-url", dest="slack_webhook_url", help="URL for incoming Slack WebHook") app = Flask(__name__) app.config['PROPAGATE_EXCEPTIONS'] = True args = vars(parser.parse_args()) port = args.pop('port') bot = SlackJiraLinkBot(**args) log.addHandler(logging.StreamHandler()) @app.route('/', methods=('POST',)) def handle(): bot.on_message(request.form.to_dict()) return 'OK' app.run(port=port) if __name__ == '__main__': main()