Skip to content

Instantly share code, notes, and snippets.

@vvalorous
Forked from devdazed/jira-slack.py
Created March 26, 2018 23:15
Show Gist options
  • Save vvalorous/aa9d8c6c66f50684f3d383d7d62c5438 to your computer and use it in GitHub Desktop.
Save vvalorous/aa9d8c6c66f50684f3d383d7d62c5438 to your computer and use it in GitHub Desktop.
#!/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()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment