Slack is a virtual office. Businesses, teams, and companies rely on it for communications and for managing daily activities. Activities such as placing orders for lunch, or contacting the front desk team or placing any form of request on the company's website can be carried out via the Slack workspace using Slack integrations via the Slack API.

In this article, I will be showing how you can build an integration for sending requests from the Slack workspace to a given website. We will build a simple Nodejs server to test the integration.

Requirements

  • Knowledge of Node.js/Express
  • Slack workspace: If you don't have a Slack workspace, you can create one for test purposes.
  • Ngrok: Since we will be working with a local development server, we will need ngrok to provision a public URL from the API in order to be able to communicate with Slack. Visit the link to install it or you can install it globally via npm here

With those requirements above met, we can proceed to get the stuff done following the steps below:

Create Slack App

In order to interact with the Slack API, you need a Slack app. To create a Slack app, you need a Slack account (a workspace). Click here to create a Slack app.

Create Slack app

Next up is to generate an access token for authorizing requests between the website and the Slack workspace. You will get the token by clicking OAuth & Permissions

Next up, we need to create a slash command (/frontdesk). The slash command would pop up a modal when a user invokes it anywhere on the Slack workspace. But before creating the slash command, let's spin up an Express server. If you already have a server running you can skip to Creating Slash Command. The reason for having a running server at this point is that the slash command requires the endpoint for sending payloads to the website when invoked.

Clone the project used in this article here to follow along

In server/index.js, I set up a simple Express server as shown in the snippet below:

const express = require('express');
const bodyParser = require('body-parser');
const routes = require('../routes/');

const app = express();

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

app.get('/', (req, res) => {
    res.json({ message: 'The app is running...'})
})

app.use('/api', routes);

module.exports = app;

Note: You must use the bodyParser.urlencoded() middle. This is needed to parse the url-encoded payload from Slack.

Slash command

Slash command is a means to trigger an action from the Slack workspace. In our case, we want a user to be able to contact the front desk by invoking the /frontdesk command on Slack. When a user types the command /frontdesk and hits enter; an action is triggered and Slack will notify your website through an endpoint you provided while creating the slash command. In our case, the endpoint simply returns a modal with a form for the user to submit his/her request.

Creating the slash command endpoint

The code for the slash command endpoint is as shown below:

router.post('/slack/frontdesk', (req, res) => {
  const { trigger_id: triggerId } = req.body;

  res.status(200).send('');
  (async () => {
    // Open a modal.
    await web.views.open({
      trigger_id: triggerId,
      view: {
        type: 'modal',
        title: {
          type: 'plain_text',
          text: 'Contact Front Desk',
        },
        submit: {
          type: 'plain_text',
          text: 'Submit',
        },
        callback_id: 'frontdesk',
        blocks: [
          {
            type: 'section',
            text: {
              type: 'plain_text',
              text: ':wave: We will get back to you as soon as possible',
              emoji: true,
            },
          },
          {
            type: 'divider',
          },

          {
            type: 'input',
            block_id: 'title',
            label: {
              type: 'plain_text',
              text: 'Title',
              emoji: true,
            },
            element: {
              type: 'plain_text_input',
              multiline: false,
              action_id: 'title',
            },
          },
          {
            type: 'input',
            block_id: 'description',
            label: {
              type: 'plain_text',
              text: 'Description',
              emoji: true,
            },
            element: {
              type: 'plain_text_input',
              multiline: true,
              action_id: 'description',
            },
            optional: true,
          },
        ],
      },
    });
  })();
});

Things to note from the slash command endpoint

  • The req.body contains among other payloads the trigger_id, the trigger_id is an identifier for the source of the command. In order to send the modal form to the user that invoked the command, we need the trigger_id so that the modal pops up at the exact channel.
  • I used the @slack/web-api which makes sending HTTP request to the Slack API seamless.
  • I used the Slack modal, read more about it.
  • I used the Block Kit to generate the modal form, read more about it.
  • Note that each block has block_id and action_id, you'll find the reason shortly.
  • The line res.status(200).send(''); is a way to tell slack that the trigger was received successfully. Failure to return a response of 200 would cause Slack trigger an error.

Handling Interactions

When the user submits the form, Slack sends the payload to an endpoint that you specified for it. This means that we need another endpoint for this purpose. The code for the interactions endpoint is as shown below:

router.post('/slack/interactions', (req, res) => {
  res.status(200).send('');

  const payload = JSON.parse(req.body.payload);

  // view the payload on console
  console.log(payload);

  if (
    payload.type === 'view_submission' &&
    payload.view.callback_id === 'frontdesk'
  ) {
    const { values } = payload.view.state;
    const title = values.title.title.value;
    const description = values.description.description.value;

    console.log(`title ----->${title}`, `description---->${description}`);

  // Save the title and description to the database or handle it as you may deem fit.
  }
});

The form payload is located at the payload object of the req.body. The form values are located in the payload.view.state object. Notice the long chain in order to extract the form input value, that is due to the block_id and action_id used in building the form, without it, the form values would not be deterministic as the block_id and action_id would be random values.

Lastly, we need to create the actual slash command on the slack app we created earlier and also specify the interactions endpoint. To do so, we need to start the server and provision a public URL using ngrok.

npm start

Next up, navigate to the folder where the ngrok was unzipped and run the command below:

./ngrok http 3000

If you installed ngrok globally, you can run the command below anywhere on the terminal

ngrok http 3000 

Note that 3000 is the port the server is listening, you can change it to the port yours is running. The screenshot below shows the commands above in action:

Image showing running server and ngrok

Now that we have provisioned a public URL from the local server, the slash command and the interactions endpoints would be https://38a73f75.ngrok.io/api/slack/frontdesk and https://38a73f75.ngrok.io/api/slack/interactions respectively. Let's proceed to create the slash command.

Head to the Slack app you created earlier as shown below:

Create slash command page

Fill the form as shown below:

Create a slash command form

Next up, click the Interactive Components to add the interactions endpoint as shown below:

Interation components page

It is time to test it! Head over to the Slack workspace and invoke the /frontdesk command, you'll see a modal as shown below:

Modal popped up when `/frontdesk` is invoked

The result of submitting the form is shown below as logged on the console:

The console showing the output of the interaction endpoint

Conclusion

I tried to make this article as simple as possible, hence, the snippets may not have followed best practices, feel free to refactor it to suit your needs.

If you have some suggestions that would further make the article easier to the readers, you are welcome to do so. Gracias!