├── .env-sample ├── .gitignore ├── LICENSE.md ├── Procfile ├── README.md ├── diff.md ├── how_it_works.md ├── images ├── actions_and_dialogs.png ├── actions_and_modals.png ├── icon.png ├── icon_small.png └── screen.gif ├── package.json └── src ├── api.js ├── confirmation.js ├── exportNote.js ├── index.js ├── payloads.js └── verifySignature.js /.env-sample: -------------------------------------------------------------------------------- 1 | SLACK_SIGNING_SECRET= 2 | SLACK_ACCESS_TOKEN= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | package-lock.json 4 | test 5 | temp* 6 | 7 | .env 8 | 9 | ### https://raw.github.com/github/gitignore/b304edf487ce607174e188712225b5269d43f279/Global/OSX.gitignore 10 | 11 | .DS_Store 12 | .AppleDouble 13 | .LSOverride 14 | 15 | ### IDE Settings (EditorConfig/Sublime) 16 | .editorconfig 17 | 18 | ### IDE Settings (VSCode) 19 | .vscode 20 | 21 | clip* 22 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Slack Technologies 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node src/index.js 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blueprint: Message Action and ~~Dialog~~ Modal (NEW!) 2 | ​ 3 | 4 | > :sparkles: *Updated January 2020: As we have introduced some new features, this tutorial and the code samples have been updated! All the changes from the previous version of this example, read the [DIFF.md](DIFF.md)* 5 | 6 | 7 | By registering your app's capabilities as message actions, users can pick and choose messages to send to your app so you can do something useful with them. Store these messages in a virtual trapper keeper, feed them to your internal markov chain bot, or file away information about an important lead. 8 | 9 | ## Creating "ClipIt" app using an action and a dialog 10 | 11 | ![App icon](images/icon_small.png) This fictional Slack app, "ClipIt" allows users to "clip" a message posted on Slack by using the actions to export the message to JSON to be used in the external app/service, let's say, "ClipIt web app". 12 | 13 | ### Developer Use-Cases 14 | 15 | If you are developing apps like memo / note-taking app, collaborative doc app, this sample use-case would be a nice addition to your Slack app. 16 | 17 | Also, the message action would be great for: 18 | 19 | - Bug / issue tracking app (*e.g.* "Create a ticket from the message") 20 | - To-Do app (*e.g.* "Create a to-do") 21 | - Project management app (*e.g.* "Attach to task") 22 | - Social media (*e.g.* "Post it to [my social media] App") 23 | 24 | ### User Work Flow 25 | 26 | When a user hover a message then choose "Clip the message" from the action menu, a dialog pops open. 27 | The message text is pre-populated into the modal box, but the user can edit before submitting it too. 28 | Once a user finalize the form and submit, the app DMs the user with the confirmation. 29 | 30 | ​ 31 | ![ClipIt](https://cdn.glitch.com/441299e3-79ff-44b2-9688-4ade057797c8%2Fscreen_actions_dialogs_demo.gif?1526686807617) 32 | 33 | ### App Flow 34 | ![diagram](https://cdn.glitch.com/802be3e8-445a-4f15-9fb4-966573ebed75%2Factions_and_modals.png?v=1571270384477) 35 | 36 | ## Setup 37 | 38 | ### Create a Slack app 39 | 40 | 1. Create an app at https://api.slack.com/apps 41 | 2. Navigate to the OAuth & Permissions page and add the following bot token scopes: 42 | * `commands` (required for Actions) 43 | * `chat:write` (required to send messages as a bot user) 44 | * `im:write` (required to open a DM channel between your bot and a user) 45 | 3. Click 'Save Changes' and install the app 46 | 47 | ​ 48 | #### Run locally or [![Remix on Glitch](https://cdn.glitch.com/2703baf2-b643-4da7-ab91-7ee2a2d00b5b%2Fremix-button.svg)](https://glitch.com/edit/#!/remix/slack-action-and-modal-blueprint) 49 | 50 | 1. Get the code 51 | * Either clone this repo and run `npm install` 52 | * Or visit https://glitch.com/~slack-action-and-modal-blueprint 53 | 2. Set the following environment variables to `.env` with your API credentials (see `.env.sample`): 54 | * `SLACK_ACCESS_TOKEN`: Your app's bot token, `xoxb-` token (available on the Install App page, after you install the app to a workspace once.) 55 | * `SLACK_SIGNING_SECRET`: Your app's Signing Secret (available on the **Basic Information** page)to a workspace) 56 | 3. If you're running the app locally: 57 | 1. Start the app (`npm start`) 58 | 1. In another window, start ngrok on the same port as your webserver 59 | ​ 60 | #### Add a Action 61 | 1. Go back to the app settings and click on **Interactive Components**. 62 | 2. Click "Enable Interactive Components" button: 63 | * Request URL: Your ngrok or Glitch URL + `/actions` in the end (e.g. `https://example.ngrok.io/actions`) 64 | * Under **Actions**, click "Create New Action" button 65 | * Action Name: `Clip the message` 66 | * Description: `Save this message to ClipIt! app` 67 | * Callback ID: `clipit` 68 | 3. Save 69 | ​ 70 | 71 | ​ 72 | -------------------------------------------------------------------------------- /diff.md: -------------------------------------------------------------------------------- 1 | # What's New? - Updates from the Previous Example 2 | 3 | Now all the Blueprints examples have been updated with new Slack platform features. So what are the *diffs* in this updated example? 4 | 5 | 6 | --- 7 | ## Changes made in January 2020 8 | 9 | ### OAuth Scopes and Permission 10 | 11 | We’ve made major improvements to the way scopes work for apps. The bot scope used to be very broad and permissive, but now you can request more specific and granular permissions for your app. 12 | 13 | This sample app used to need only bot scope, but now you need the `chat:write` to allow the bot to post messages in channels. But no other actions. 14 | 15 | We recommend selecting only the scopes your app needs. Requesting too many scopes can cause your app to be restricted by a team’s Admin or App Manager. 16 | 17 | Please read [Scopes and permissions](https://api.slack.com/scopes) to figure out which scopes you need. 18 | 19 | 20 | --- 21 | ## Changes made in October 2019 22 | 23 | ### Modals 24 | 25 | *Major updates!: This requires to update your code!* 26 | 27 | We released [Modals](https://api.slack.com/block-kit/surfaces/modals), which is replacing the existing Dialogs, with more powerful features. 28 | 29 | Now, instead of calling an API to open a dialog is replaced with the new view API to open a modal with Block Kit in the code sample. 30 | 31 | ### Block Kit 32 | We introduced Block Kit UI framework that allows you to create messages with the components called blocks. If you have been creating messages with the legacy "attatchment", please consider switching to Blcok Kit! 33 | 34 | Read more at: [Block Kit](https://api.slack.com/block-kit) 35 | 36 | --- 37 | ## Changes made in October 2018 38 | 39 | ### OAuth Token 40 | 41 | Your OAuth access token should begins with `-xoxb` instead of `-xoxp`. The bot tokens will be the default token in future. 42 | 43 | 44 | ### Sigining Secret 45 | 46 | *This requires to update your code!* 47 | 48 | Previously, you needed to verify a *verificatin token* to see if a request was coming from Slack, not from some malicious place by simply comparing a string with the legacy token with a token received with a payload. But now you must use more secure *sigining secrets*. 49 | 50 | Basically, you need to compare the value of the `X-Slack-Signature`, the HMAC-SHA256 keyed hash of the raw request payload, with a hashed string containing your Slack signin secret code, combined with the version and `X-Slack-Request-Timestamp`. 51 | 52 | Learn more at [Verifying requests from Slack](https://api.slack.com/docs/verifying-requests-from-slack). 53 | 54 | ### Token rotation 55 | 56 | OAuth refresh tokens are also introduced as a security feature, which allows the app owners to proactively rotate tokens when the tokens are compromised. 57 | 58 | Your workspace app can use the new `apps.uninstall` method to uninstall itself from a single workspace, revoking all tokens associated with it. To revoke a workspace token without uninstalling the app, use `auth.revoke`. 59 | 60 | Although the example of using the short-lived refresh token is *not* included in this Blurprints example since this tutorial is written for internal integration, if you are distributing your app, use a short-lived OAuth Refresh token. Implementing token rotation is required for all apps that are distributed, whether submitted for the App Directory or not. 61 | 62 | To lean more, read [Token rotation for workspace apps](https://api.slack.com/docs/rotating-and-refreshing-credentials). 63 | 64 | 65 | :gift: If you are using the [Node SDK](https://github.com/slackapi/node-slack-sdk/issues/617), the token refresh feature is available for you already! 66 | -------------------------------------------------------------------------------- /how_it_works.md: -------------------------------------------------------------------------------- 1 | # Message Action and Modal Blueprint 2 | 3 | This demo app allow users to export a message in Slack from the message action menu to a 3rd party system (let's call the fictional app *ClipIt*) using 4 | a [message actions](https://api.slack.com/actions) and a [Dialog](https://api.slack.com/dialogs). 5 | 6 | Assumeing you have your 3rd party note-keeping app with database setup already — 7 | To just give you a quick idea, in this code sample each selected message is added in a JSON to be exported to your external app. 8 | 9 | 10 | ## How it works 11 | 12 | #### 1. Receive action events from Slack 13 | 14 | When a user executes the message action associated with the app, Slack will send a POST request to the request URL provided in the app settings. This request will include the message text in the payload. The `command` scope is used for the message action. 15 | 16 | Payload example: 17 | ```JSON 18 | { 19 | "token": "Nj2rfC2hU8mAfgaJLemZgO7H", 20 | "callback_id": "clipit", 21 | "type": "message_action", 22 | "trigger_id": "13345224609.8534564800.6f8ab1f53e13d0cd15f96106292d5536", 23 | "response_url": "https://hooks.dev.slack.com/app-actions/T0MJR11A4/21974584944/yk1S9ndf35Q1flupVG5JbpM6", 24 | "team": {...}, 25 | "channel": {...}, 26 | "user": { 27 | "id": "U0D15K92L", 28 | "name": "dr_meow" 29 | }, 30 | "message": { 31 | "type": "message", 32 | "user": "U0MJRG1AL", 33 | "ts": "1516229207.000133", 34 | "text": "Can you order a tuna with cheese and lactose-free milk for me, please?" 35 | } 36 | } 37 | ``` 38 | 39 | The payload also include the user ID of the person who originally posted the message. This example app uses the ID to call `users.info` method to get the person's full name. You can obtain more info of the user like avatar, if you wish. See [`users.info`](https://api.slack.com/methods/users.info). 40 | 41 | #### 2. Open a modal 42 | 43 | In order to let the user to edit the message to be saved in the 3rd party app, the app will open a modal in Slack. When the user submits the modal, Slack will send a POST request to the same request URL used for the message action. To differentiate which action triggers the event, parse the payload and check the `type` (`type="view_submission"`). 44 | 45 | #### 3. Confirm the user 46 | 47 | Once the user submit the modal, this example app export the message in a JSON (where you probably want to do something else to work with your own app and service, such as save in a DB). In the meantime, the app notifies the user by sending a DM using `chat.postMessage` method. To do so, you need to enable the `users:read` scope. 48 | 49 | ## App Flow Diagram 50 | 51 | ![Dialog](https://cdn.glitch.com/802be3e8-445a-4f15-9fb4-966573ebed75%2Factions_and_modals.png?v=1571270384477) 52 | -------------------------------------------------------------------------------- /images/actions_and_dialogs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slack-samples/javascript-message-clippings/51625d0418b34d4e717f22d68f17fb6715febc8a/images/actions_and_dialogs.png -------------------------------------------------------------------------------- /images/actions_and_modals.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slack-samples/javascript-message-clippings/51625d0418b34d4e717f22d68f17fb6715febc8a/images/actions_and_modals.png -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slack-samples/javascript-message-clippings/51625d0418b34d4e717f22d68f17fb6715febc8a/images/icon.png -------------------------------------------------------------------------------- /images/icon_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slack-samples/javascript-message-clippings/51625d0418b34d4e717f22d68f17fb6715febc8a/images/icon_small.png -------------------------------------------------------------------------------- /images/screen.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slack-samples/javascript-message-clippings/51625d0418b34d4e717f22d68f17fb6715febc8a/images/screen.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "template-action-and-dialog", 3 | "version": "1.5.0", 4 | "description": "Sample Slack app that responds to an action with an interactive message", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node src/index.js", 8 | "dev": "nodemon src/index.js", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "keywords": [ 12 | "bot", 13 | "slack", 14 | "chatbot", 15 | "slack API" 16 | ], 17 | "authors": [ 18 | "Tomomi Imura ", 19 | "David Pichsenmeister " 20 | ], 21 | "license": "MIT", 22 | "dependencies": { 23 | "axios": ">=0.18.1", 24 | "body-parser": "^1.18.2", 25 | "express": "^4.16.2", 26 | "qs": "^6.5.1", 27 | "tsscmp": "^1.0.6", 28 | "dotenv": "^8.1.0" 29 | }, 30 | "devDependencies": { 31 | "nodemon": "^1.19.3" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/api.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const apiUrl = 'https://slack.com/api'; 3 | 4 | const callAPIMethod = async (method, payload) => { 5 | let result = await axios.post(`${apiUrl}/${method}`, payload, { 6 | headers: { Authorization: "Bearer " + process.env.SLACK_ACCESS_TOKEN } 7 | }); 8 | return result.data; 9 | } 10 | 11 | module.exports = { 12 | callAPIMethod 13 | } -------------------------------------------------------------------------------- /src/confirmation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const payloads = require('./payloads'); 4 | const api = require('./api'); 5 | 6 | /* 7 | * Send confirmation via chat.postMessage to the user who clipped it 8 | */ 9 | 10 | const sendConfirmation = async (userId, view) => { 11 | let values = view.state.values; 12 | 13 | // open a DM channel with the user to receive the channel ID 14 | let user = await api.callAPIMethod('im.open', { 15 | user: userId 16 | }); 17 | 18 | const messageData = payloads.confirmation({ 19 | channel_id: user.channel.id, 20 | user_id: userId, 21 | selected_user_id: values.user.user_id.selected_user, 22 | message_id: values.message.message_id.value, 23 | importance: values.importance.importance_id.selected_option.text.text 24 | }); 25 | 26 | await api.callAPIMethod('chat.postMessage', messageData); 27 | } 28 | 29 | module.exports = { sendConfirmation }; 30 | -------------------------------------------------------------------------------- /src/exportNote.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | 5 | /* 6 | * Create a json file that stored the copied Slack message for each user 7 | * Ideally, you should be using a DB. 8 | */ 9 | 10 | const exportToJson = (userId, view) => { 11 | const fileName = `clip_${userId}.json`; 12 | 13 | fs.open(fileName, 'r', (err, data) => { 14 | if (err) { 15 | let obj = { 16 | messages: [] 17 | }; 18 | obj.messages.push(view); 19 | fs.writeFile(fileName, JSON.stringify(obj, null, 2), 'utf8', (err) => { 20 | if (err) throw err; 21 | console.log(`${fileName} has been created`); 22 | }); 23 | } else { 24 | fs.readFile(fileName, 'utf8', (err, data) => { 25 | if (err) throw err; 26 | 27 | let obj = JSON.parse(data); // Object 28 | obj.messages.push(view); 29 | 30 | fs.writeFile(fileName, JSON.stringify(obj, null, 2), 'utf8', (err) => { 31 | if (err) throw err; 32 | console.log(`New data added to ${fileName}`); 33 | }); 34 | }); 35 | } 36 | 37 | }) 38 | }; 39 | 40 | module.exports = { exportToJson }; 41 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* ************************************************************** 2 | * Slack Demo: Message clipping app using an action and a dialog 3 | * 4 | * Tomomi Imura (@girlie_mac) 5 | * **************************************************************/ 6 | 7 | 'use strict'; 8 | 9 | require('dotenv').config(); 10 | 11 | const express = require('express'); 12 | const bodyParser = require('body-parser'); 13 | const confirmation = require('./confirmation'); 14 | const exportNote = require('./exportNote'); 15 | const signature = require('./verifySignature'); 16 | const payloads = require('./payloads'); 17 | const api = require('./api'); 18 | const app = express(); 19 | 20 | /* 21 | * Parse application/x-www-form-urlencoded && application/json 22 | * Use body-parser's `verify` callback to export a parsed raw body 23 | * that you need to use to verify the signature 24 | */ 25 | 26 | const rawBodyBuffer = (req, res, buf, encoding) => { 27 | if (buf && buf.length) { 28 | req.rawBody = buf.toString(encoding || 'utf8'); 29 | } 30 | }; 31 | 32 | app.use(bodyParser.urlencoded({ verify: rawBodyBuffer, extended: true })); 33 | app.use(bodyParser.json({ verify: rawBodyBuffer })); 34 | 35 | /* 36 | /* Endpoint to receive an action and a dialog submission from Slack. 37 | /* To use actions and dialogs, enable the Interactive Components in your dev portal. 38 | /* Scope: `command` to enable actions 39 | */ 40 | 41 | app.post('/actions', async (req, res) => { 42 | if (!signature.isVerified(req)) return res.status(404).send(); 43 | 44 | const payload = JSON.parse(req.body.payload); 45 | const { type, user, view } = payload; 46 | 47 | switch (type) { 48 | case 'message_action': 49 | let result = await openModal(payload) 50 | if (result.error) { 51 | console.log(result.error); 52 | return res.status(500).send(); 53 | } 54 | return res.status(200).send(); 55 | case 'view_submission': 56 | // immediately respond with a empty 200 response to let 57 | // Slack know the command was received 58 | res.send(''); 59 | // create a ClipIt and prepare to export it to the theoritical external app 60 | exportNote.exportToJson(user.id, view); 61 | // DM the user a confirmation message 62 | confirmation.sendConfirmation(user.id, view); 63 | break; 64 | } 65 | }); 66 | 67 | // open the dialog by calling dialogs.open method and sending the payload 68 | const openModal = async (payload) => { 69 | 70 | const viewData = payloads.openModal({ 71 | trigger_id: payload.trigger_id, 72 | user_id: payload.message.user, 73 | text: payload.message.text 74 | }) 75 | 76 | return await api.callAPIMethod('views.open', viewData) 77 | }; 78 | 79 | 80 | const server = app.listen(process.env.PORT || 5000, () => { 81 | console.log('Express server listening on port %d in %s mode', server.address().port, app.settings.env); 82 | }); 83 | -------------------------------------------------------------------------------- /src/payloads.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | openModal: context => { 3 | return { 4 | token: process.env.SLACK_ACCESS_TOKEN, 5 | trigger_id: context.trigger_id, 6 | view: { 7 | type: 'modal', 8 | title: { 9 | type: 'plain_text', 10 | text: 'Save it to ClipIt!' 11 | }, 12 | callback_id: 'clipit', 13 | submit: { 14 | type: 'plain_text', 15 | text: 'ClipIt' 16 | }, 17 | blocks: [ 18 | { 19 | block_id: 'message', 20 | type: 'input', 21 | element: { 22 | action_id: 'message_id', 23 | type: 'plain_text_input', 24 | multiline: true, 25 | initial_value: context.text 26 | }, 27 | label: { 28 | type: 'plain_text', 29 | text: 'Message Text' 30 | } 31 | }, 32 | { 33 | block_id: 'user', 34 | type: 'input', 35 | element: { 36 | action_id: 'user_id', 37 | type: 'users_select', 38 | initial_user: context.user_id 39 | }, 40 | label: { 41 | type: 'plain_text', 42 | text: 'Posted by' 43 | } 44 | }, 45 | { 46 | block_id: 'importance', 47 | type: 'input', 48 | element: { 49 | action_id: 'importance_id', 50 | type: 'static_select', 51 | placeholder: { 52 | type: 'plain_text', 53 | text: 'Select importance', 54 | emoji: true 55 | }, 56 | options: [ 57 | { 58 | text: { 59 | type: 'plain_text', 60 | text: 'High 💎💎✨', 61 | emoji: true 62 | }, 63 | value: 'high' 64 | }, 65 | { 66 | text: { 67 | type: 'plain_text', 68 | text: 'Medium 💎', 69 | emoji: true 70 | }, 71 | value: 'medium' 72 | }, 73 | { 74 | text: { 75 | type: 'plain_text', 76 | text: 'Low ⚪️', 77 | emoji: true 78 | }, 79 | value: 'low' 80 | } 81 | ] 82 | }, 83 | label: { 84 | type: 'plain_text', 85 | text: 'Importance' 86 | } 87 | } 88 | ] 89 | } 90 | } 91 | }, 92 | confirmation: context => { 93 | return { 94 | channel: context.channel_id, 95 | blocks: [ 96 | { 97 | type: 'section', 98 | text: { 99 | type: 'mrkdwn', 100 | text: '*Message clipped!*' 101 | } 102 | }, 103 | { 104 | type: 'divider' 105 | }, 106 | { 107 | type: 'section', 108 | text: { 109 | type: 'mrkdwn', 110 | text: `*Message*\n${context.message_id}` 111 | } 112 | }, 113 | { 114 | type: 'context', 115 | elements: [ 116 | { 117 | type: 'mrkdwn', 118 | text: `*Posted by* <@${context.selected_user_id}>` 119 | }, 120 | { 121 | type: 'mrkdwn', 122 | text: `*Importance:* ${context.importance}` 123 | }, 124 | { 125 | type: 'mrkdwn', 126 | // This should be the link in the ClipIt web app 127 | text: `*Link:* http://example.com/${context.user_id}/clip` 128 | } 129 | ] 130 | } 131 | ] 132 | } 133 | } 134 | } -------------------------------------------------------------------------------- /src/verifySignature.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | const timingSafeCompare = require('tsscmp'); 3 | 4 | const isVerified = (req) => { 5 | const signature = req.headers['x-slack-signature']; 6 | const timestamp = req.headers['x-slack-request-timestamp']; 7 | const hmac = crypto.createHmac('sha256', process.env.SLACK_SIGNING_SECRET); 8 | const [version, hash] = signature.split('='); 9 | 10 | // Check if the timestamp is too old 11 | const fiveMinutesAgo = ~~(Date.now() / 1000) - (60 * 5); 12 | if (timestamp < fiveMinutesAgo) return false; 13 | 14 | hmac.update(`${version}:${timestamp}:${req.rawBody}`); 15 | 16 | // check that the request signature matches expected value 17 | return timingSafeCompare(hmac.digest('hex'), hash); 18 | }; 19 | 20 | module.exports = { isVerified }; --------------------------------------------------------------------------------