Created
July 17, 2018 16:26
-
-
Save TheSharpieOne/407894097830371dc2cee2e7cc3570d4 to your computer and use it in GitHub Desktop.
A rocket.chat outgoing webhook integration which allow the user to create and manage on-prem/private JIRA instance tickets via rocket.chat messages.
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 characters
/* exported Script */ | |
/* globals console, _, s, HTTP */ | |
const username = 'JIRA_SERVICE_ACCOUNT_USERNAME'; | |
const password = 'JIRA_SERVICE_ACCOUNT_PASSWORD'; | |
const baseJiraUrl = 'ON_PREM_JIRA_DOMAIN'; | |
const baseApiUrl = `${baseJiraUrl}/rest/api/2`; | |
const apiTicketBase = `${baseApiUrl}/issue`; | |
const browseTicketBase = `${baseJiraUrl}/browse`; | |
// If behind corp proxy | |
// const npmRequestOptions = { | |
// rejectUnauthorized: false, | |
// strictSSL:false | |
// }; | |
const auth = `${username}:${password}`; | |
// May need to be updated if even needed, maybe JIRA's API can take the string name directly? | |
const issueTypeMap = { | |
bug: '1', | |
task: '3', | |
feature: '2', | |
improvement: '4', | |
story: '7' | |
} | |
/** Global Helpers | |
* | |
* console - A normal console instance | |
* _ - An underscore instance | |
* s - An underscore string instance | |
* HTTP - The Meteor HTTP object to do sync http calls | |
*/ | |
function getTicketLinkMarkup(ticket) { | |
return `[${ticket.toUpperCase()}](${browseTicketBase}/${ticket.toUpperCase()})`; | |
} | |
function formatApiError({response}) { | |
return `${response.data.errorMessages.length ? response.data.errorMessages.join('\n') : `\`\`\`${JSON.stringify(response.data.errors, null, 2)}\n\`\`\``}`; | |
} | |
function updateTicket(ticket, action, type, values) { | |
const ucTicket = ticket.toUpperCase(); | |
if (!Array.isArray(values)) { | |
values = [values] | |
} | |
const valuesToAdd = values.map(value => ({[action]: value})); | |
if(valuesToAdd.length > 0) { | |
const data = { update: { [type]: valuesToAdd } }; | |
const response = HTTP('PUT',`${apiTicketBase}/${ucTicket}`, {data, auth, npmRequestOptions}); | |
const statusCode = response.error ? response.error.response.statusCode : response.result.statusCode; | |
if (response.error) console.log(response.error); | |
switch (statusCode) { | |
case 204: return `${type} ${action.substr(-1) === 'e' ? 'd' : 'ed'} to ${getTicketLinkMarkup(ucTicket)}.` | |
case 403: return `Service user does not have permission to ${action} ${type} to ${getTicketLinkMarkup(ucTicket)}.` | |
case 400: return `The bot had an issue and could not ${action} ${type}.` | |
default: return `Got ${statusCode} and the bot cannot handle that.` | |
} | |
} | |
} | |
function addStringItems(ticket, type, items) { | |
const ucTicket = ticket.toUpperCase(); | |
if (!Array.isArray(items)) { | |
items = items.split(/,|\s/) | |
} | |
updateTicket(ticket, 'add', type, items.map(item => item.replace(/\s/g, '')).filter(item => !!item)); | |
} | |
function removeStringItems(ticket, type, items) { | |
const ucTicket = ticket.toUpperCase(); | |
if (!Array.isArray(items)) { | |
items = items.split(/,|\s/) | |
} | |
updateTicket(ticket, 'remove', type, items.map(item => item.replace(/\s/g, '')).filter(item => !!item)); | |
} | |
function addWatchers(ticket, watchers){ | |
const ucTicket = ticket.toUpperCase(); | |
if (!Array.isArray(watchers)) { | |
watchers = watchers.split(/,|\s/) | |
} | |
const watchersToAdd = watchers.map(watcher => watcher.trim()).filter(watcher => !!watcher); | |
if(watchersToAdd.length > 0) { | |
const retVal = watchersToAdd.map(watcher => { | |
const response = HTTP('POST',`${apiTicketBase}/${ucTicket}/watchers`, {data: watcher, auth, npmRequestOptions}); | |
if (response.error) { | |
console.log(response.error); | |
retVal.push(`The bot had an issue and could not add watchers.`); | |
} | |
const statusCode = response.error ? response.error.response.statusCode : response.result.statusCode; | |
switch (statusCode) { | |
case 204: return `${watcher} is now watching ${getTicketLinkMarkup(ucTicket)}.` | |
case 401: return `Service user does not have permission to add watcher to ${getTicketLinkMarkup(ucTicket)}.` | |
case 404: return `User \`${watcher}\` doesn't appear to exist.` | |
case 400: return `The bot had an issue and could not add watchers:\n${formatApiError(response.error)}` | |
default: return `Got ${statusCode} and the bot cannot handle that.` | |
} | |
}); | |
return retVal.join('\n'); | |
} | |
} | |
function removeWatchers(ticket, watchers){ | |
const ucTicket = ticket.toUpperCase(); | |
if (!Array.isArray(watchers)) { | |
watchers = watchers.split(/,|\s/) | |
} | |
const watchersToAdd = watchers.map(watcher => watcher.trim()).filter(watcher => !!watcher); | |
if(watchersToAdd.length > 0) { | |
const retVal = watchersToAdd.map(watcher => { | |
const response = HTTP('DELETE',`${apiTicketBase}/${ucTicket}/watchers?username=${watcher}`, {auth, npmRequestOptions}); | |
if (response.error) { | |
console.log(response.error); | |
retVal.push(`The bot had an issue and could not remove watchers.`); | |
} | |
const statusCode = response.error ? response.error.response.statusCode : response.result.statusCode; | |
switch (statusCode) { | |
case 204: return `${watcher} is no longer watching ${getTicketLinkMarkup(ucTicket)}.` | |
case 401: return `Service user does not have permission to remove watcher from ${getTicketLinkMarkup(ucTicket)}.` | |
case 404: return `User \`${watcher}\` doesn't appear to exist.` | |
case 400: return `The bot had an issue and could not remove watchers:\n${formatApiError(response.error)}` | |
default: return `Got ${statusCode} and the bot cannot handle that.` | |
} | |
}); | |
return retVal.join('\n'); | |
} | |
} | |
function addComment(ticket, comment, request){ | |
const ucTicket = ticket.toUpperCase(); | |
if(comment) { | |
const data = { body: `[~${request.data.user_name}] via Rocket.Chat:\n${comment}` }; | |
const response = HTTP('POST',`${apiTicketBase}/${ucTicket}/comment`, {data, auth, npmRequestOptions}); | |
const statusCode = response.error ? response.error.response.statusCode : response.result.statusCode; | |
switch (statusCode) { | |
case 201: return `Comment added to ${getTicketLinkMarkup(ucTicket)}.` | |
case 403: return `Service user does not have permission to add comments to ${getTicketLinkMarkup(ucTicket)}.` | |
case 400: return `The bot had an issue and could not add the comment:\n${formatApiError(response.error)}` | |
default: return `Got ${statusCode} and the bot cannot handle that.` | |
} | |
} | |
} | |
function add(request) { | |
const [text, ticket, item, value] = request.data.text.match(/\s+([A-Za-z]{2,10}-\d+)\s+add\s+(labels?|comment|watchers?|time|worklog|components?)\s+(.*)/); | |
switch (item.toLowerCase()) { | |
case 'label': | |
case 'labels': return addStringItems(ticket, 'labels', value); | |
break; | |
case 'component': | |
case 'components': return addStringItems(ticket, 'components', value); | |
break; | |
case 'comment': return addComment(ticket, value, request); | |
break; | |
case 'watcher': | |
case 'watchers': return addWatchers(ticket, value); | |
break; | |
default: return `The ability to add ${item} has not been implemented _yet_.`; | |
} | |
} | |
function remove(request) { | |
const [text, ticket, item, value] = request.data.text.match(/\s+([A-Za-z]{2,10}-\d+)\s+remove\s+(labels?|watchers?|components?)\s+(.*)/); | |
switch (item.toLowerCase()) { | |
case 'label': | |
case 'labels': return removeStringItems(ticket, 'labels', value); | |
break; | |
case 'component': | |
case 'components': return removeStringItems(ticket, 'components', value); | |
break; | |
case 'watcher': | |
case 'watchers': return removeWatchers(ticket, value); | |
break; | |
default: return `The ability to add ${item} has not been implemented _yet_.`; | |
} | |
} | |
function getProjectId(projectKey) { | |
const response = HTTP('GET', `${baseApiUrl}/project/${projectKey.toUpperCase()}`, {auth, npmRequestOptions}); | |
if (response.error) throw response.error; | |
return response.result.data.id; | |
} | |
function createTicket(request) { | |
const [text, projectKey, issuetypeKey = 'task', assignTo, summary, description] = request.data.text.match(/create\s+([^\s]+)\s+(bug|task|feature|improvement|story)?\s*(?:~([^\s]+))?\s*([^;]+);?\s*(.*)?/); | |
console.log(projectKey, assignTo, summary, description); | |
const project = {key: projectKey.toUpperCase()}; | |
const fields = { | |
project, | |
summary, | |
description, | |
issuetype: { | |
id: issueTypeMap[issuetypeKey.toLowerCase()] | |
}, | |
labels: ['from-rocketchat'] | |
} | |
if (issuetypeKey.toLowerCase() !== 'task' && projectKey.toLowerCase() !== 'cm') { | |
fields.reporter = {name: request.data.user_name}; | |
} | |
if (assignTo) { | |
fields.assignee = {name: assignTo}; | |
} | |
const response = HTTP('POST',`${apiTicketBase}`, {data: {fields}, auth, npmRequestOptions}); | |
const statusCode = response.error ? response.error.response.statusCode : response.result.statusCode; | |
switch (statusCode) { | |
case 201: return response.result.data.key | |
case 401: return `Service user does not have permission to create tickets for the \`${project.key}\` project.` | |
case 403: return `Service user does not have permission to create tickets for the \`${project.key}\` project.` | |
case 400: return `The bot had an issue and could not create the ticket:\n${formatApiError(response.error)}` | |
default: return `Got ${statusCode} and the bot cannot handle that.` | |
} | |
} | |
function help(request) { | |
const commands = request.data.text.split(' '); | |
let command; | |
if (commands.length > 2) { | |
command = commands[2]; | |
} | |
switch(command){ | |
case 'details': return [ | |
'Get details about a specific ticket', | |
'**Usage**', | |
'```bash', | |
'jira <ticket-number>', | |
'jira details <ticket-number>', | |
'```', | |
'**Parameters**', | |
'• ticket-number: JIRA ticket number; e.g. `av-123`. Case insensitive; required', | |
'**Example**', | |
'`jira av-123`', | |
].join('\n'); | |
case 'add': return [ | |
'Add information to a specific ticket', | |
'**Usage**', | |
'```bash', | |
'jira <ticket-number> add <type> <value>', | |
'```', | |
'**Parameters**', | |
'• ticket-number: JIRA ticket number; e.g. `av-123`. Case insensitive; required', | |
'• type: Type of information you want to add to the ticket; e.g. Case insensitive; required; can be one of the following:', | |
'├ label', | |
'├ labels', | |
'├ component', | |
'├ components', | |
'├ watcher', | |
'├ watchers', | |
'└ comment', | |
'• value: The information you want to add. Depending on the type, this can be a comma/space separated list. Case insensitive; required', | |
'**Example**', | |
'`jira av-123 add label this-is-a-label, this-is-another-label this-is-a-third-label`', | |
'`jira av-123 add comment Testing completed in QA`', | |
'`jira av-123 add watcher esharp`', | |
].join('\n'); | |
case 'remove': return [ | |
'Remove information from a specific ticket', | |
'**Usage**', | |
'```bash', | |
'jira <ticket-number> remove <type> <value>', | |
'```', | |
'**Parameters**', | |
'• ticket-number: JIRA ticket number; e.g. `av-123`. Case insensitive; required', | |
'• type: Type of information you want to remove from the ticket; e.g. Case insensitive; required; can be one of the following:', | |
'├ label', | |
'├ labels', | |
'├ component', | |
'├ components', | |
'├ watcher', | |
'└ watchers', | |
'• value: The information you want to remove. Comma/space separated list. Case insensitive; required', | |
'**Example**', | |
'`jira av-123 remove label this-is-a-label, this-is-another-label this-is-a-third-label`', | |
'`jira av-123 remove watcher esharp`', | |
].join('\n'); | |
case 'create': return [ | |
'Create a new ticket', | |
'**Usage**', | |
'```bash', | |
'jira create <projectKey> <issueType> ~<assignTo> <title>; <description>', | |
'```', | |
'**Parameters**', | |
'• `projectKey`: JIRA project key; e.g. `av`. Case insensitive; required', | |
'• `issueType`: JIRA project issue type; e.g. `bug`. Case insensitive; optional, defaults to `task`. Must be one of the follow:', | |
'├ bug', | |
'├ task', | |
'├ feature', | |
'├ improvement', | |
'└ story', | |
'• `assignTo`: JIRA username to assign the ticket to; optional; If provided: Must start with `~`; Username must exist in JIRA.', | |
'• `title`: Title / summary of the ticket; required.', | |
'• `description`: Description / details of the ticket; optional; If provided: Must separate title from description using `;`.', | |
'**Examples**', | |
'`jira create av story ~esharp Add new thing; Add this thing to this place`', | |
'`jira create av Do this; This thing needs to be done`', | |
].join('\n'); | |
default: return [ | |
'**List of command**', | |
'• `details <ticket-number>`: Get ticket details', | |
'• `<ticket-number>`: alias for details', | |
'• `<ticket-number> add`: Add information to a specific ticket', | |
'• `<ticket-number> remove`: Remove information from a specific ticket', | |
'• `create <projectKey> <issueType> ~<assignTo> <title>; <description>`: create new ticket', | |
'• `help`: This help information', | |
'• `help <command>`: Get help for a specific command', | |
'**Note**', | |
`In order for any of these commands to work, the service user \`${username}\` will need permissions to the project associated with the ticket you are trying to gets details about or create.`, | |
].join('\n'); | |
} | |
} | |
class Script { | |
/** | |
* @params {object} request | |
*/ | |
prepare_outgoing_request({ request }) { | |
// request.params {object} | |
// request.method {string} | |
// request.url {string} | |
// request.auth {string} | |
// request.headers {object} | |
// request.data.token {string} | |
// request.data.channel_id {string} | |
// request.data.channel_name {string} | |
// request.data.timestamp {date} | |
// request.data.user_id {string} | |
// request.data.user_name {string} | |
// request.data.text {string} | |
// request.data.trigger_word {string} | |
// request.data.bot {boolean} | |
try { | |
if (request.data.text && request.data.user_name.toLowerCase() !== 'jira' && !request.data.bot && username && password) { | |
const commands = request.data.text.split(' '); | |
const command = commands[1] || ''; | |
let ticket | |
switch(command.toLowerCase()) { | |
case "create": ticket = createTicket(request); | |
break; | |
case "help": ticket = help(request); | |
break; | |
case 'detail': | |
case 'details': | |
ticket = commands[2].match(/\b([A-Za-z]{2,10}-\d+)\b/g); | |
ticket = ticket && ticket[0]; | |
break; | |
default: | |
const match = command.match(/\b([A-Za-z]{2,10}-\d+)\b/g); | |
ticket = match && match[0]; | |
if (commands.length > 3) { | |
switch (commands[2]) { | |
case 'add': ticket = add(request); | |
break; | |
case 'remove': ticket = remove(request); | |
break; | |
} | |
} | |
break; | |
} | |
if (ticket && typeof ticket === 'string' && ticket.indexOf(' ') === -1) { | |
return { | |
command, | |
ticket, | |
url: `${apiTicketBase}/${ticket}`, | |
auth, | |
method: 'GET', | |
type: 'issue', | |
npmRequestOptions, | |
}; | |
} | |
if (ticket && typeof ticket === 'string') { | |
return { message: { text: ticket } }; | |
} | |
return ticket; | |
} | |
} catch(e) { | |
console.log('jiraticket error', e); | |
return { | |
error: { | |
success: false, | |
message: `${e.message || e} ${JSON.stringify(request.data)}` | |
} | |
}; | |
} | |
} | |
/** | |
* @params {object} request, response | |
*/ | |
process_outgoing_response({ request, response }) { | |
// request {object} - the object returned by prepare_outgoing_request | |
// response.error {object} | |
// response.status_code {integer} | |
// response.content {object} | |
// response.content_raw {string/object} | |
// response.headers {object} | |
if (response.error) { | |
let msg; | |
switch (response.status_code) { | |
case 403: msg = `Service user does not have permission to view ${getTicketLinkMarkup(request.ticket)}.`; break; | |
case 400: msg = `The bot had an issue and could not look up ${getTicketLinkMarkup(request.ticket)}:\n${formatApiError(response.error)}`; break; | |
case 400: msg = `JIRA returned a 500... not sure what to do with that.`; break; | |
default: msg = `Got ${response.status_code} and the bot cannot handle that.`; break; | |
} | |
return { message: { text: msg } }; | |
} | |
if (request.type === 'issue') { | |
const issue = response.content; | |
issue.fields.priority = issue.fields.priority || {name: 'Unknown'}; | |
const assignedTo = (issue.fields.assignee && issue.fields.assignee.displayName) ? issue.fields.assignee.displayName : 'unassigned'; | |
const status = issue.fields.status && `${issue.fields.status.name}${issue.fields.status.statusCategory ? ` (${issue.fields.status.statusCategory.name})` : ''}`; | |
const message = { | |
icon_url: (issue.fields.project && issue.fields.project.avatarUrls && issue.fields.project.avatarUrls['48x48']) || undefined, | |
attachments: [] | |
}; | |
message.attachments.push({ | |
author_name: `${issue.key}${issue.fields.issuetype ? ` (${issue.fields.issuetype.name})` : ''}`, | |
author_link: `${browseTicketBase}/${issue.key}`, | |
author_icon: issue.fields.issuetype && issue.fields.issuetype.iconUrl && issue.fields.issuetype.iconUrl.replace(/\.svg$/, '.png'), | |
title: issue.fields.summary, | |
thumb_url: issue.fields.priority.iconUrl && issue.fields.priority.iconUrl.replace(/\.svg$/, '.png') || undefined, | |
text: issue.fields.description || '_no description_', | |
fields: [ | |
{ | |
title: 'Priority', | |
value: issue.fields.priority.name, | |
short: true | |
}, | |
{ | |
title: 'Assigned to', | |
value: assignedTo, | |
short: true | |
}, | |
{ | |
title: 'Status', | |
value: status, | |
short: true | |
}, | |
{ | |
title: 'Comments', | |
value: issue.fields.comment.total, | |
short: true | |
}, | |
] | |
}); | |
return {content: message}; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment