Make your Slack app accessible directly from a message

By Tomomi Imura

Published: November 22, 2019
Updated: January 21, 2020

Actions let Slack users interact directly with your app from a message, similar to built-in actions like adding reactions or message sharing. When your app registers an action, users can interact directly with it from a Slack conversation, making it easier to create a task in a project management app, report an issue into a bug tracker, make a request to your IT helpdesk, or anything else you can dream up, all from a message. As a developer, adding actions not only makes your app more interactive but also makes it more discoverable by users who wouldn't otherwise know about your app.

This tutorial will walk you through how to use the API to make your app actionable.

Building "ClipIt! for Slack"

You are going to create "ClipIt! for Slack", a Slack app for a fictional service called ClipIt!. Let’s pretend you run a web service which allows users to “clip” some content of a web page and save it to a database. Your users can access the saved content from web or mobile, as the data is synchronized across platforms. You are going to build the companion app that runs on the Slack client, where your users can clip text from Slack messages using Actions.

This GIF shows how the Slack app works:

demo gif

Here’s a walkthrough of how a user interacts with ClipIt from Slack:

  1. A user hovers a message and chooses Clip the message from the menu
  2. A modal opens to let the user give your app more information
  3. The user submits the modal form
  4. ClipIt! for Slack exports the message to the ClipIt database
  5. ClipIt! for Slack DMs the user with a confirmation message

This tutorial is meant to be helpful for everyone who wants to learn about Slack APIs. These instructions use Node.js, so if you’d like to follow the how-to, make sure Node.js is installed on your machine. If you wish to keep developing Slack apps with Node.js afterward, check out Slack’s official Node SDK!

🐙🐱 The original source is on GitHub but I have also created its simplified version, which is used in this tutorial on Glitch.

🎏🍴 “Remix” the simplified code sample here https://glitch.com/edit/#!/remix/slack-clipit-simplified to get started this tutorial!

⚙️ Configuring Your App

Sign in to your Slack account and go to Slack App Management to create an app by clicking the button:

Create a Slack app

Enter an App Name and select your development workspace, then click the Create App button.

screenshot - create an app

Next, at Basic Information, scroll down to find your App Credentials.

screenshot - app credentials

You need to reveal the hidden Signing Secret and copy the code to keep it as an environmental variable: SLACK_SIGNING_SECRET in your .env file in the root of your Node app. I will explain what it is and how to use it at Verifying the Requests section in this tutorial.

SLACK_SIGNING_SECRET=15770a…

Scroll a bit more down to fill out the Display Information with your app icon and description any time.

Now, go to Interactive Components, and turn on Interactivity. Once it is enabled, you will see more fields appear on the page.

screenshot - actions

Then enter your Request URL, which is the URL where Slack sends the data payload to when the action is invoked by a user.

Note: This should be your server URL, where your application code runs. For example, if you host on Glitch during development, your URL should look something like https://project-name.glitch.me/actions. If you tunnel your localhost with service like ngrok, use the URL (e.g. https://example.ngrok.io), then add /actions). If you would like to set up ngrok, you can follow the instructions for using ngrok to develop locally for Slack.

Once you enter the Request URL, scroll down to Actions, and click Create New Action button. Fill the form out:

screenshot - actions

Click Create, followed by Save Changes.

Next, go to OAuth & Permissions and

Click Install App to Workspace. Once you’ve installed it to your development space the installation page will take you back to the OAuth & Permission page with access tokens. Copy the Bot user token and keep it in your .env file.

SLACK_ACCESS_TOKEN=xoxb-214…

Also, on the same page, you need to enable permission scopes. Scroll down on the page to Scopes, and add the following bot scopes:

  • commands, which is required to enable the message actions
  • chat:write, which is required to post messages

screenshot - scopes

Whew! You’ve taken care of the configuration for your app and you are ready to build.

☕️ Building an App

As I mentioned in the intro, I am using Node.js and ExpressJS instead of our SDK in this tutorial to show in more depth how to use the Slack API.

First, install the dependencies. ExpressJS and a middleware, bodyParser, are used to handle POST requests. Also, I am using the axios HTTP request client with qs, a querystring parser:

$ npm install express body-parser axios qs dotenv --save

Let’s start with the core parts. We will modify the code later as we add more functions. First, create an index.js file. In this file, instantiate an Express app and make the server listen to an appropriate port:

/* Snippet 1 */

require('dotenv').config(); // To grab env vers from the .env file

const express = require('express');
const bodyParser = require('body-parser');
const axios = require('axios');
const qs = require('qs');
const app = express();

// The next two lines will be modified later
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

const server = app.listen(5000); // port

Before you proceed, let’s take a look at this diagram to see how the app works.

app flow diagram

Each flow is initiated when a user uses the action from a message menu. As soon as the message_action event is triggered, Slack sends your app an event payload to the endpoint you specified earlier as a Request URL.

app flow diagram

The endpoint can be written like this:

/* Snippet 2 */

app.post('/actions', (req, res) => {
  const payload = JSON.parse(req.body.payload);
  const { type, user, view } = payload; 

  // Verifying the request. I'll explain this later in this tutorial!
  if (!signature.isVerified(req)) {
    res.sendStatus(404);
    return;
  }

  if(type === 'message_action') {
    // open a modal here
  } else if (type === 'view_submission') {
    // the modal is submitted
  }
});

If the event type is message_action (= when the event came as a result of executing an action), your app opens a modal.

In code snippet 2, where it says “open a modal here", include this code that defines the modal content structure using Block Kit. You can customize the UI component using Block Kit Builder.

Then open it in Slack client using the [views.open](https://api.slack.com/methods/view.open) method:

/* Snippet 2.1 */
const viewData = {
  token: process.env.SLACK_ACCESS_TOKEN,
  trigger_id: payload.trigger_id,
  view: JSON.stringify({
    type: 'modal',
    title: {
      type: 'plain_text',
      text: 'Save it to ClipIt!'
    },
    callback_id: 'clipit',
    submit: {
      type: 'plain_text',
      text: 'ClipIt'
    },
    blocks: [ // Block Kit
      {
        block_id: 'message',
        type: 'input',
        element: {
          action_id: 'message_id',
          type: 'plain_text_input',
          multiline: true,
          initial_value: payload.message.text
        },
        label: {
          type: 'plain_text',
          text: 'Message Text'
        }
      },
      {
        block_id: 'importance',
        type: 'input',
        element: {
          action_id: 'importance_id',
          type: 'static_select',
          placeholder: {
            type: 'plain_text',
            text: 'Select importance',
            emoji: true
          },
          options: [
            {
              text: {
                type: 'plain_text',
                text: 'High 💎💎✨',
                emoji: true
              },
              value: 'high'
            },
            {
              text: {
                type: 'plain_text',
                text: 'Medium 💎',
                emoji: true
              },
              value: 'medium'
            },
            {
              text: {
                type: 'plain_text',
                text: 'Low ⚪️',
                emoji: true
              },
              value: 'low'
            }
          ]
        },
        label: {
          type: 'plain_text',
          text: 'Importance'
        }
      }
    ]
  });
};

axios.post('https://slack.com/api/views.open', qs.stringify(viewData))
  .then((result) => {
    res.sendStatus(200);
  });

Here, use the axios module to perform a POST request to Slack, and when the views.open method successfully opens a modal, send a HTTP status 200.

app

This endpoint is also called when the modal is submitted by a user. In code snippet 2, where it says Modal is submitted”, you need to respond with an empty HTTP 200 response to let Slack knows the submission is received.

Then, let’s assume you are storing the “clipped” message in your app database, and finally send the user a confirmation message using the chat.postMessage method:

/* Snippet 2.2 */
} else if(type === 'view_submission') {
  res.send(''); // Make sure to respond immediately to the Slack server to avoid an error

  // Save the data in DB 
  db.set(user.id, payload); // this is a pseudo-code! 

  // DM the user a confirmation message
  let values = view.state.values;

  let blocks = [
    {
      type: 'section',
      text: {
        type: 'mrkdwn',
        text: 'Message clipped!\n\n'
      }
    },
    {
      type: 'section',
      text: {
        type: 'mrkdwn',
        text: `*Message*\n${values.message.message_id.value}`
      }
    },
    {
      type: 'section',
      fields: [
        {
          type: 'mrkdwn',
          text: `*Importance:*\n${values.importance.importance_id.selected_option.text.text}`
        },
        {
          type: 'mrkdwn',
          text: `*Link:*\nhttp://example.com/${user.id}/clip`
        }
      ]
    }
  ];

  let message = {
    token: process.env.SLACK_ACCESS_TOKEN,
    channel: userId,
    blocks: JSON.stringify(blocks)
  };

  axios.post(`${apiUrl}/chat.postMessage`, qs.stringify(message));
} 

As you may have noticed, the message sent to the user is composed with Block Kit again and sent via a HTTP POST request.

Sending a confirmation is an important part for a good end-user experience. Always keep in mind to close the loop!

Okay, let’s run the code and try your app on the Slack client. You should be able to view your app action in the message menu, and the app should work as expected besides the database part (unless you actually set it up!).

There’s just one more thing you need to do to make your app secure .

🔐 Verifying the Requests

Now you need to make your app more secure: use signing secrets to verify if the requests received are authentically from Slack. Always verify any request from Slack (for example, the request notifying your app that a user has used an action) before acting on it.

Signing secrets replace the old verification tokens, which you were using before if you have previously worked with Slack APIs, or followed my old tutorial. To make the verification process more secure, Slack sends an additional X-Slack-Signature HTTP header on each HTTP request.

The X-Slack-Signature is just the hash of the raw request payload (HMAC SHA256, to be precise), keyed by your app’s Signing Secret.

When you are using ExpressJS with body-parser, use the body-parser verify function option to get the raw request payload.

Let’s go back to code snippet 1, the top of your code. Replace the body-parser middleware settings where it says //The next two lines will be modified later to these:

/* Snippet 3 */

const rawBodyBuffer = (req, res, buf, encoding) => {
 if (buf && buf.length) {
   req.rawBody = buf.toString(encoding || 'utf8');
 }
};

app.use(bodyParser.urlencoded({verify: rawBodyBuffer, extended: true }));
app.use(bodyParser.json({ verify: rawBodyBuffer }));

I already have provided the cryptographic functionality in verifySignature.js, so just include the function in the beginning of your index.js:

const signature = require('./verifySignature');

Then, verify the request by comparing your signing secret against the hash at the event endpoint (See code snippet 2):

if(!signature.isVerified(req)) { // when the request is NOT coming from Slack!
   res.sendStatus(404); // a good idea to just make it “not found” to the potential attacker!
   return;
}

In this example, you need to verify only at this event endpoint. However, when you write any Slack apps in future, you must verify each time you receive payloads from Slack. (To learn more about signing secret, read Verifying requests from Slack.)

Run your Node code again, and if everything works fine, you’re done with your Action-able app! Congrats!


I hope this tutorial helped you to get some ideas on what to build, or modify your existing Slack app!

If you run into any trouble, find us on Twitter, or reach out our developer support team at feedback@slack.com. Happy developing! 🤖

Related documentation

Was this page helpful?