diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 366d4ba..0000000 --- a/.eslintignore +++ /dev/null @@ -1,4 +0,0 @@ -.git/* -coverage/* -doc/* -node_modules/* diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 3090874..0000000 --- a/.eslintrc +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "airbnb-base", - "rules": { - "strict": 0, - "valid-jsdoc" : 2, - "no-eval": 0, - "global-require": 0, - "import/no-unresolved": 0 - } -} diff --git a/README.md b/README.md index 0889edd..d02c4e2 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/config.default.js b/config.default.js index beb3028..6f39648 100644 --- a/config.default.js +++ b/config.default.js @@ -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; diff --git a/lib/bot.js b/lib/bot.js index c68d99c..e57747a 100644 --- a/lib/bot.js +++ b/lib/bot.js @@ -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); diff --git a/lib/logger.js b/lib/logger.js index 59cb50b..c36548e 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -13,9 +13,9 @@ module.exports = () => { new (winston.transports.Console)({ timestamp: true, prettyPrint: true, - handleExceptions: true, - }), - ], + handleExceptions: true + }) + ] }); logger.cli(); diff --git a/package.json b/package.json index 64abecf..8b9d1fd 100644 --- a/package.json +++ b/package.json @@ -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 ", @@ -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" diff --git a/test/bot.test.js b/test/bot.test.js index 817ff91..55673df 100644 --- a/test/bot.test.js +++ b/test/bot.test.js @@ -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' +