diff --git a/README.md b/README.md index 02b1261..c0572b2 100644 --- a/README.md +++ b/README.md @@ -14,12 +14,14 @@ Use a slash command and a dialog to create a helpdesk ticket in a 3rd-party syst 1. Create an app at [https://api.slack.com/apps](https://api.slack.com/apps) 2. Add a Slash command (See *Add a Slash Command* section below) -3. Navigate to **Bot Users** and click "Add a Bot User" to create one. -4. Enable Interactive components (See *Enable Interactive Components* below) -5. Navigate to the **OAuth & Permissions** page and make sure the following scopes are pre-selected: +3. Enable Interactive components (See *Enable Interactive Components* below) +4. Navigate to the **OAuth & Permissions** page and select the following bot token scopes: * `commands` - * `bot` -6. Click 'Save Changes' and install the app (You should get an OAuth access token after the installation) + * `chat:write` + * `users:read` + * `users:read.email` + * `im:write` +5. Click 'Save Changes' and install the app (You should get an OAuth access token after the installation) #### Add a Slash Command 1. Go back to the app settings and click on Slash Commands. diff --git a/src/api.js b/src/api.js new file mode 100644 index 0000000..2cb9558 --- /dev/null +++ b/src/api.js @@ -0,0 +1,13 @@ +const axios = require('axios'); +const qs = require('querystring'); +const apiUrl = 'https://slack.com/api'; + +const callAPIMethod = async (method, payload) => { + let data = Object.assign({ token: process.env.SLACK_ACCESS_TOKEN }, payload); + let result = await axios.post(`${apiUrl}/${method}`, qs.stringify(data)); + return result.data; +} + +module.exports = { + callAPIMethod +} \ No newline at end of file diff --git a/src/index.js b/src/index.js index 5ddcb3e..1fccf93 100644 --- a/src/index.js +++ b/src/index.js @@ -1,15 +1,13 @@ require('dotenv').config(); -const axios = require('axios'); const express = require('express'); const bodyParser = require('body-parser'); -const qs = require('querystring'); const ticket = require('./ticket'); const signature = require('./verifySignature'); +const api = require('./api'); +const payloads = require('./payloads'); const debug = require('debug')('slash-command-template:index'); -const apiUrl = 'https://slack.com/api'; - const app = express(); /* @@ -24,126 +22,38 @@ const rawBodyBuffer = (req, res, buf, encoding) => { } }; -app.use(bodyParser.urlencoded({verify: rawBodyBuffer, extended: true })); +app.use(bodyParser.urlencoded({ verify: rawBodyBuffer, extended: true })); app.use(bodyParser.json({ verify: rawBodyBuffer })); app.get('/', (req, res) => { res.send('
Follow the' + - ' instructions in the README to configure the Slack App and your environment variables.
'); + ' instructions in the README to configure the Slack App and your environment variables.'); }); /* * Endpoint to receive /helpdesk slash command from Slack. * Checks verification token and opens a dialog to capture more info. */ -app.post('/command', (req, res) => { +app.post('/command', async (req, res) => { + // Verify the signing secret + if (!signature.isVerified(req)) { + debug('Verification token mismatch'); + return res.status(404).send(); + } + // extract the slash command text, and trigger ID from payload - const { text, trigger_id } = req.body; + const { trigger_id } = req.body; - // Verify the signing secret - if (signature.isVerified(req)) { - // create the dialog payload - includes the dialog structure, Slack API token, - // and trigger ID - const view = { - token: process.env.SLACK_ACCESS_TOKEN, - trigger_id, - view: JSON.stringify({ - type: 'modal', - title: { - type: 'plain_text', - text: 'Submit a helpdesk ticket' - }, - callback_id: 'submit-ticket', - submit: { - type: 'plain_text', - text: 'Submit' - }, - blocks: [ - { - block_id: 'title_block', - type: 'input', - label: { - type: 'plain_text', - text: 'Title' - }, - element: { - action_id: 'title', - type: 'plain_text_input' - }, - hint: { - type: 'plain_text', - text: '30 second summary of the problem' - } - }, - { - block_id: 'description_block', - type: 'input', - label: { - type: 'plain_text', - text: 'Description' - }, - element: { - action_id: 'description', - type: 'plain_text_input', - multiline: true - }, - optional: true - }, - { - block_id: 'urgency_block', - type: 'input', - label: { - type: 'plain_text', - text: 'Importance' - }, - element: { - action_id: 'urgency', - type: 'static_select', - options: [ - { - text: { - type: "plain_text", - text: "High" - }, - value: "high" - }, - { - text: { - type: "plain_text", - text: "Medium" - }, - value: "medium" - }, - { - text: { - type: "plain_text", - text: "Low" - }, - value: "low" - } - ] - }, - optional: true - } - ] - }) - }; + // create the modal payload - includes the dialog structure, Slack API token, + // and trigger ID + let view = payloads.modal({ + trigger_id + }); - console.log('open view') + let result = await api.callAPIMethod('views.open', view); - // open the dialog by calling dialogs.open method and sending the payload - axios.post(`${apiUrl}/views.open`, qs.stringify(view)) - .then((result) => { - debug('views.open: %o', result.data); - res.send(''); - }).catch((err) => { - debug('views.open call failed: %o', err); - res.sendStatus(500); - }); - } else { - debug('Verification token mismatch'); - res.sendStatus(404); - } + debug('views.open: %o', result); + return res.send(''); }); /* @@ -151,22 +61,15 @@ app.post('/command', (req, res) => { * and creates a Helpdesk ticket */ app.post('/interactive', (req, res) => { - const body = JSON.parse(req.body.payload); - - // check that the verification token matches expected value - if (signature.isVerified(req)) { - debug(`Form submission received: ${body.view}`); - - // immediately respond with a empty 200 response to let - // Slack know the command was received - res.send(''); - - // create Helpdesk ticket - ticket.create(body.user.id, body.view); - } else { - debug('Token mismatch'); - res.sendStatus(404); + // Verify the signing secret + if (!signature.isVerified(req)) { + debug('Verification token mismatch'); + return res.status(404).send(); } + + const body = JSON.parse(req.body.payload); + res.send(''); + ticket.create(body.user.id, body.view); }); const server = app.listen(process.env.PORT || 5000, () => { diff --git a/src/payloads.js b/src/payloads.js new file mode 100644 index 0000000..90b4e73 --- /dev/null +++ b/src/payloads.js @@ -0,0 +1,121 @@ +module.exports = { + confirmation: context => { + return { + channel: context.channel_id, + text: 'Helpdesk ticket created!', + blocks: JSON.stringify([ + { + type: 'section', + text: { + type: 'mrkdwn', + text: '*Helpdesk ticket created!*' + } + }, + { + type: 'divider' + }, + { + type: 'section', + text: { + type: 'mrkdwn', + text: `*Title*\n${context.title}\n\n*Description*\n${context.description}` + } + }, + { + type: 'context', + elements: [ + { + type: 'mrkdwn', + text: `*Urgency*: ${context.urgency}` + } + ] + } + ]) + } + }, + modal: context => { + return { + trigger_id: context.trigger_id, + view: JSON.stringify({ + type: 'modal', + title: { + type: 'plain_text', + text: 'Submit a helpdesk ticket' + }, + callback_id: 'submit-ticket', + submit: { + type: 'plain_text', + text: 'Submit' + }, + blocks: [ + { + block_id: 'title_block', + type: 'input', + label: { + type: 'plain_text', + text: 'Title' + }, + element: { + action_id: 'title', + type: 'plain_text_input' + }, + hint: { + type: 'plain_text', + text: '30 second summary of the problem' + } + }, + { + block_id: 'description_block', + type: 'input', + label: { + type: 'plain_text', + text: 'Description' + }, + element: { + action_id: 'description', + type: 'plain_text_input', + multiline: true + }, + optional: true + }, + { + block_id: 'urgency_block', + type: 'input', + label: { + type: 'plain_text', + text: 'Importance' + }, + element: { + action_id: 'urgency', + type: 'static_select', + options: [ + { + text: { + type: "plain_text", + text: "High" + }, + value: "high" + }, + { + text: { + type: "plain_text", + text: "Medium" + }, + value: "medium" + }, + { + text: { + type: "plain_text", + text: "Low" + }, + value: "low" + } + ] + }, + optional: true + } + ] + }) + } + } +} \ No newline at end of file diff --git a/src/ticket.js b/src/ticket.js index 80d0eb2..067e9ea 100644 --- a/src/ticket.js +++ b/src/ticket.js @@ -1,77 +1,44 @@ -const axios = require('axios'); const debug = require('debug')('slash-command-template:ticket'); -const qs = require('querystring'); -const users = require('./users'); +const api = require('./api'); +const payloads = require('./payloads'); /* * Send ticket creation confirmation via * chat.postMessage to the user who created it */ -const sendConfirmation = (ticket) => { - axios.post('https://slack.com/api/chat.postMessage', qs.stringify({ - token: process.env.SLACK_ACCESS_TOKEN, - channel: ticket.userId, - as_user: true, - text: 'Helpdesk ticket created!', - blocks: JSON.stringify([ - { - type: 'section', - text: { - type: 'mrkdwn', - text: '*Helpdesk ticket created!*' - } - }, - { - type: 'divider' - }, - { - type: 'section', - text: { - type: 'mrkdwn', - text: `*Title*\n${ticket.title}\n\n*Description*\n${ticket.description}` - } - }, - { - type: 'context', - elements: [ - { - type: 'mrkdwn', - text: `*Urgency*: ${ticket.urgency}` - } - ] - } - ]), - })).then((result) => { - debug('sendConfirmation: %o', result.data); - }).catch((err) => { - debug('sendConfirmation error: %o', err); - console.error(err); +const sendConfirmation = async (ticket) => { + // open a DM channel for that user + let channel = await api.callAPIMethod('im.open', { + user: ticket.userId + }) + + let message = payloads.confirmation({ + channel_id: channel.channel.id, + title: ticket.title, + description: ticket.description, + urgency: ticket.urgency }); + + let result = await api.callAPIMethod('chat.postMessage', message) + debug('sendConfirmation: %o', result); }; // Create helpdesk ticket. Call users.find to get the user's email address // from their user ID -const create = (userId, view) => { - let values = view.state.values - const ticket = {}; +const create = async (userId, view) => { + let values = view.state.values; - const fetchUserEmail = new Promise((resolve, reject) => { - users.find(userId).then((result) => { - debug(`Find user: ${userId}`); - resolve(result.data.user.profile.email); - }).catch((err) => { reject(err); }); + let result = await api.callAPIMethod('users.info', { + user: userId }); - fetchUserEmail.then((result) => { - ticket.userId = userId; - ticket.userEmail = result; - ticket.title = values.title_block.title.value; - ticket.description = values.description_block.description.value; - ticket.urgency = values.urgency_block.urgency.selected_option.text.text; - sendConfirmation(ticket); - - return ticket; - }).catch((err) => { console.error(err); }); + await sendConfirmation({ + userId, + userEmail: result.user.profile.email, + title: values.title_block.title.value, + description: values.description_block.description.value || '_empty_', + urgency: values.urgency_block.urgency.selected_option && values.urgency_block.urgency.selected_option.text.text || 'not assigned' + }); }; module.exports = { create, sendConfirmation }; diff --git a/src/users.js b/src/users.js deleted file mode 100644 index 8a3a827..0000000 --- a/src/users.js +++ /dev/null @@ -1,10 +0,0 @@ -const qs = require('querystring'); -const axios = require('axios'); - -const find = (slackUserId) => { - const body = { token: process.env.SLACK_ACCESS_TOKEN, user: slackUserId }; - const promise = axios.post('https://slack.com/api/users.info', qs.stringify(body)); - return promise; -}; - -module.exports = { find }; diff --git a/src/verifySignature.js b/src/verifySignature.js index ffd308a..457e62b 100644 --- a/src/verifySignature.js +++ b/src/verifySignature.js @@ -1,7 +1,7 @@ const crypto = require('crypto'); const timingSafeCompare = require('tsscmp'); -const isVerified = (req) => { +const isVerified = (req) => { const signature = req.headers['x-slack-signature']; const timestamp = req.headers['x-slack-request-timestamp']; const hmac = crypto.createHmac('sha256', process.env.SLACK_SIGNING_SECRET); @@ -15,7 +15,6 @@ const isVerified = (req) => { // check that the request signature matches expected value return timingSafeCompare(hmac.digest('hex'), hash); -}; - +}; + module.exports = { isVerified }; - \ No newline at end of file