Skip to content

Commit

Permalink
Merge pull request #18 from shaunburdick/feature/dm-to-get-full
Browse files Browse the repository at this point in the history
Feature/dm to get full
shaunburdick committed May 18, 2016

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
2 parents 5608e25 + 4d5670b commit 68eb770
Showing 8 changed files with 80 additions and 85 deletions.
4 changes: 0 additions & 4 deletions .eslintignore

This file was deleted.

10 changes: 0 additions & 10 deletions .eslintrc

This file was deleted.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Slack Bot for JIRA
[![Build Status](https://travis-ci.org/shaunburdick/slack-jirabot.svg)](https://travis-ci.org/shaunburdick/slack-jirabot) [![Coverage Status](https://coveralls.io/repos/shaunburdick/slack-jirabot/badge.svg?branch=master&service=github)](https://coveralls.io/github/shaunburdick/slack-jirabot?branch=master) [![Docker Pulls](https://img.shields.io/docker/pulls/shaunburdick/slack-jirabot.svg?maxAge=2592000)](https://hub.docker.com/r/shaunburdick/slack-jirabot/)
[![Build Status](https://travis-ci.org/shaunburdick/slack-jirabot.svg)](https://travis-ci.org/shaunburdick/slack-jirabot) [![Coverage Status](https://coveralls.io/repos/shaunburdick/slack-jirabot/badge.svg?branch=master&service=github)](https://coveralls.io/github/shaunburdick/slack-jirabot?branch=master) [![Docker Pulls](https://img.shields.io/docker/pulls/shaunburdick/slack-jirabot.svg?maxAge=2592000)](https://hub.docker.com/r/shaunburdick/slack-jirabot/) [![js-semistandard-style](https://img.shields.io/badge/code%20style-semistandard-brightgreen.svg)](https://github.com/Flet/semistandard)

This slack bot will listen on any channel it's on for JIRA tickets. It will lookup the ticket and respond with some information about it.

6 changes: 3 additions & 3 deletions config.default.js
Original file line number Diff line number Diff line change
@@ -15,12 +15,12 @@ const config = {
customFields: {

},
response: 'full', // full or minimal
response: 'full' // full or minimal
},
slack: {
token: 'xoxb-Your-Token',
autoReconnect: true,
autoReconnect: true
},
usermap: {},
usermap: {}
};
module.exports = config;
67 changes: 36 additions & 31 deletions lib/bot.js
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@ class Bot {
* @constructor
* @param {Config} config The final configuration for the bot
*/
constructor(config) {
constructor (config) {
this.config = config;
/* hold tickets and last time responded to */
this.ticketBuffer = new Map();
@@ -26,7 +26,7 @@ class Bot {
this.TICKET_BUFFER_LENGTH = 300000;

this.controller = Botkit.slackbot({
logger,
logger
});

this.ticketRegExp = new RegExp(config.jira.regex, 'g');
@@ -40,19 +40,21 @@ class Bot {
password: config.jira.pass,
apiVersion: config.jira.apiVersion,
strictSSL: config.jira.strictSSL,
base: config.jira.base,
base: config.jira.base
});
}

/**
* Build a response string about an issue.
*
* @param {Issue} issue the issue object returned by JIRA
* @param {Issue} issue the issue object returned by JIRA
* @param {string} usrFormat the format to respond with
* @return {Attachment} The response attachment.
*/
issueResponse(issue) {
issueResponse (issue, usrFormat) {
const format = usrFormat || this.config.jira.response;
const response = {
fallback: `No summary found for ${issue.key}`,
fallback: `No summary found for ${issue.key}`
};
const created = moment(issue.fields.created);
const updated = moment(issue.fields.updated);
@@ -64,31 +66,31 @@ class Bot {
response.title = issue.fields.summary;
response.title_link = this.buildIssueLink(issue.key);
response.fields = [];
if (this.config.jira.response === RESPONSE_FULL) {
if (format === RESPONSE_FULL) {
response.fields.push({
title: 'Created',
value: created.calendar(),
short: true,
short: true
});
response.fields.push({
title: 'Updated',
value: updated.calendar(),
short: true,
short: true
});
response.fields.push({
title: 'Status',
value: issue.fields.status.name,
short: true,
short: true
});
response.fields.push({
title: 'Priority',
value: issue.fields.priority.name,
short: true,
short: true
});
response.fields.push({
title: 'Reporter',
value: (this.jira2Slack(issue.fields.reporter.name) || issue.fields.reporter.displayName),
short: true,
short: true
});
let assignee = 'Unassigned';
if (issue.fields.assignee) {
@@ -98,14 +100,14 @@ class Bot {
response.fields.push({
title: 'Assignee',
value: assignee,
short: true,
short: true
});
// Sprint fields
if (this.config.jira.sprintField) {
response.fields.push({
title: 'Sprint',
value: (this.parseSprint(issue.fields[this.config.jira.sprintField]) || 'Not Assigned'),
short: false,
short: false
});
}
// Custom fields
@@ -115,6 +117,7 @@ class Bot {
// Do some simple guarding before eval
if (!/[;&\|\(\)]/.test(customField)) {
try {
/* eslint no-eval: 0*/
fieldVal = eval(`issue.fields.${customField}`);
} catch (e) {
fieldVal = `Error while reading ${customField}`;
@@ -126,7 +129,7 @@ class Bot {
return response.fields.push({
title: this.config.jira.customFields[customField],
value: fieldVal,
short: false,
short: false
});
});
}
@@ -144,7 +147,7 @@ class Bot {
* @param {string} description The raw description
* @return {string} the formatted description
*/
formatIssueDescription(description) {
formatIssueDescription (description) {
const desc = description || 'Ticket does not contain a description';
let depths = [];
let lastDepth = 0;
@@ -238,7 +241,7 @@ class Bot {
* @param {string} issueKey The issueKey for the issue
* @return {string} The constructed link
*/
buildIssueLink(issueKey) {
buildIssueLink (issueKey) {
let base = '/browse/';
if (this.config.jira.base) {
// Strip preceeding and trailing forward slash
@@ -256,7 +259,7 @@ class Bot {
* @param {string[]} customField The contents of the greenhopper custom field
* @return {string} The name of the sprint or ''
*/
parseSprint(customField) {
parseSprint (customField) {
let retVal = '';
if (customField && customField.length > 0) {
const sprintString = customField.pop();
@@ -275,7 +278,7 @@ class Bot {
* @param {string} username the JIRA username
* @return {string} The slack username or ''
*/
jira2Slack(username) {
jira2Slack (username) {
let retVal = '';
if (this.config.usermap[username]) {
retVal = `@${this.config.usermap[username]}`;
@@ -292,7 +295,7 @@ class Bot {
* @param {string} message the message to search in
* @return {string[]} an array of tickets, empty if none found
*/
parseTickets(channel, message) {
parseTickets (channel, message) {
const retVal = [];
if (!channel || !message) {
return retVal;
@@ -305,8 +308,8 @@ class Bot {
found.forEach((ticket) => {
ticketHash = this.hashTicket(channel, ticket);
if (
!uniques.hasOwnProperty(ticket)
&& (now - (this.ticketBuffer.get(ticketHash) || 0) > this.TICKET_BUFFER_LENGTH)
!uniques.hasOwnProperty(ticket) &&
(now - (this.ticketBuffer.get(ticketHash) || 0) > this.TICKET_BUFFER_LENGTH)
) {
retVal.push(ticket);
uniques[ticket] = 1;
@@ -324,7 +327,7 @@ class Bot {
* @param {string} ticket The name of the ticket
* @return {string} The unique hash
*/
hashTicket(channel, ticket) {
hashTicket (channel, ticket) {
return `${channel}-${ticket}`;
}

@@ -333,7 +336,7 @@ class Bot {
*
* @return {null} nada
*/
cleanupTicketBuffer() {
cleanupTicketBuffer () {
const now = Date.now();
logger.debug('Cleaning Ticket Buffer');
this.ticketBuffer.forEach((time, key) => {
@@ -350,7 +353,7 @@ class Bot {
* @param {object} payload Connection payload
* @return {Bot} returns itself
*/
slackOpen(payload) {
slackOpen (payload) {
const channels = [];
const groups = [];
const mpims = [];
@@ -391,10 +394,10 @@ class Bot {
* @param {object} message The incoming message from Slack
* @returns {null} nada
*/
handleMessage(message) {
handleMessage (message) {
const response = {
as_user: true,
attachments: [],
attachments: []
};

if (message.type === 'message' && message.text) {
@@ -404,7 +407,9 @@ class Bot {
found.forEach((issueId) => {
this.jira.findIssue(issueId)
.then((issue) => {
response.attachments = [this.issueResponse(issue)];
// If direct mention, use full format
const responseFormat = message.event === 'direct_mention' ? RESPONSE_FULL : null;
response.attachments = [this.issueResponse(issue, responseFormat)];
this.bot.reply(message, response, (err) => {
if (err) {
logger.error('Unable to respond', err);
@@ -430,7 +435,7 @@ class Bot {
*
* @return {Bot} returns itself
*/
start() {
start () {
this.controller.on(
'direct_mention,mention,ambient,direct_message',
(bot, message) => {
@@ -459,10 +464,10 @@ class Bot {
* Connect to the RTM
* @return {Bot} this
*/
connect() {
connect () {
this.bot = this.controller.spawn({
token: this.config.slack.token,
retry: this.config.slack.autoReconnect ? Infinity : 0,
retry: this.config.slack.autoReconnect ? Infinity : 0
}).startRTM((err, bot, payload) => {
if (err) {
logger.error('Error starting bot!', err);
6 changes: 3 additions & 3 deletions lib/logger.js
Original file line number Diff line number Diff line change
@@ -13,9 +13,9 @@ module.exports = () => {
new (winston.transports.Console)({
timestamp: true,
prettyPrint: true,
handleExceptions: true,
}),
],
handleExceptions: true
})
]
});
logger.cli();

20 changes: 12 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"name": "slack-jirabot",
"version": "2.2.2",
"version": "2.3.0",
"description": "Slackbot for interacting with JIRA",
"main": "app.js",
"private": true,
"scripts": {
"start": "node app.js",
"test": "npm run lint && npm run unit",
"unit": "nyc --all tape ./test/*.test.js",
"lint": "eslint .",
"unit": "nyc --all tape ./test/*.test.js | faucet && nyc report",
"lint": "semistandard --verbose | snazzy",
"coverage": "nyc report --reporter=text-lcov | coveralls"
},
"author": "Shaun Burdick <[email protected]>",
@@ -19,7 +19,7 @@
},
"license": "ISC",
"engine": {
"node": "^5.1.0"
"node": "^4.0.0"
},
"dependencies": {
"botkit": "^0.1.1",
@@ -29,14 +29,18 @@
"winston": "^2.1.1"
},
"devDependencies": {
"babel-eslint": "^6.0.3",
"coveralls": "^2.11.9",
"eslint": "^2.8.0",
"eslint-config-airbnb-base": "^3.0.1",
"eslint-plugin-import": "^1.8.0",
"faucet": "0.0.1",
"nyc": "^6.4.0",
"semistandard": "^8.0.0",
"snazzy": "^4.0.0",
"tape": "^4.5.1"
},
"semistandard": {
"ignore": [
"coverage"
]
},
"nyc": {
"include": [
"lib/**/*.js"
50 changes: 25 additions & 25 deletions test/bot.test.js
Original file line number Diff line number Diff line change
@@ -32,7 +32,7 @@ test('Bot: parse a sprint name from greenhopper field', (assert) => {
const bot = new Bot(configDist);
const sprintName = 'TEST';
const exampleSprint = [
`derpry-derp-derp,name=${sprintName},foo`,
`derpry-derp-derp,name=${sprintName},foo`
];

assert.equal(bot.parseSprint(exampleSprint), sprintName);
@@ -46,7 +46,7 @@ test('Bot: parse a sprint name from the last sprint in the greenhopper field', (
const exampleSprint = [
`derpry-derp-derp,name=${sprintName}1,foo`,
`derpry-derp-derp,name=${sprintName}2,foo`,
`derpry-derp-derp,name=${sprintName}3,foo`,
`derpry-derp-derp,name=${sprintName}3,foo`
];

assert.equal(bot.parseSprint(exampleSprint), `${sprintName}3`);
@@ -57,7 +57,7 @@ test('Bot: translate a jira username to a slack username', (assert) => {
configDist.usermap = {
foo: 'bar',
fizz: 'buzz',
ping: 'pong',
ping: 'pong'
};

const bot = new Bot(configDist);
@@ -154,24 +154,24 @@ test('Bot: show custom fields', (assert) => {
summary: 'Blarty',
description: 'Foo foo foo foo foo foo',
status: {
name: 'Open',
name: 'Open'
},
priority: {
name: 'Low',
name: 'Low'
},
reporter: {
name: 'bob',
displayName: 'Bob',
displayName: 'Bob'
},
assignee: {
name: 'fred',
displayName: 'Fred',
displayName: 'Fred'
},
customfield_10000: 'Fizz',
customfield_10001: [
{ value: 'Buzz' },
],
},
{ value: 'Buzz' }
]
}
};

// Add some custom fields
@@ -219,24 +219,24 @@ test('Bot: show minimal response', (assert) => {
summary: 'Blarty',
description: 'Foo foo foo foo foo foo',
status: {
name: 'Open',
name: 'Open'
},
priority: {
name: 'Low',
name: 'Low'
},
reporter: {
name: 'bob',
displayName: 'Bob',
displayName: 'Bob'
},
assignee: {
name: 'fred',
displayName: 'Fred',
displayName: 'Fred'
},
customfield_10000: 'Fizz',
customfield_10001: [
{ value: 'Buzz' },
],
},
{ value: 'Buzz' }
]
}
};

// Add some custom fields
@@ -254,7 +254,7 @@ test('Bot: show minimal response', (assert) => {
assert.end();
});

test('Bot: show minimal response', (assert) => {
test('Bot: Check formatting', (assert) => {
const issue = {
key: 'TEST-1',
fields: {
@@ -294,24 +294,24 @@ test('Bot: show minimal response', (assert) => {
'Color: {color:white}This is white text{color}\n' +
'Panel: {panel:title=foo}Panel Contents{panel}\n',
status: {
name: 'Open',
name: 'Open'
},
priority: {
name: 'Low',
name: 'Low'
},
reporter: {
name: 'bob',
displayName: 'Bob',
displayName: 'Bob'
},
assignee: {
name: 'fred',
displayName: 'Fred',
displayName: 'Fred'
},
customfield_10000: 'Fizz',
customfield_10001: [
{ value: 'Buzz' },
],
},
{ value: 'Buzz' }
]
}
};

const expectedText = '\n *Heading*\n\nFoo foo _foo_ foo foo foo\n' +

0 comments on commit 68eb770

Please sign in to comment.