Skip to content

Instantly share code, notes, and snippets.

@devdazed
Created December 28, 2015 19:35
Show Gist options
  • Save devdazed/07c3437fbc76c999af4d to your computer and use it in GitHub Desktop.
Save devdazed/07c3437fbc76c999af4d 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()
@devdazed
Copy link
Author

Slack JIRA Link Responder

Create rich Slack messages containing vital information about a JIRA issue whenever it is mentioned.

screen shot 2015-12-30 at 12 17 16 pm

This app uses both outgoing and incoming web hooks in Slack alongside the JIRA REST API.

Running as a Standalone Server

To run this as a standalone server to accept incoming web-hooks, just set up the outgoing web hooks to point to the URL of the server and run.

./jira-slack.py -jira-domain acme.atlassian.net \
                      -jira-user my-user-name \
                      -jira-password my-password \
                      -slack-webhook-url 'https://hooks.slack.com/services/xxxxx/xxxxxxx'

Running as an AWS Lambda Function

You can use AWS Lambda to process messages from Slack. Either set up the AWS SQS Slack app and read messages from SQS to Lambda, or use the AWS API Gateway as the event source and set up the Slack outgoing web hooks to point to the API Gateway endpoint.

@jacoghi
Copy link

jacoghi commented May 2, 2024

Hi Russ, just came across this piece of code which seems very promising for my needs right now. I haven't set it up yet but am wondering if this still works for you in 2024 with all the changes Slack has implemented? Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment