This content originally appeared on JavaScript January and was authored by Emily Freeman
Many thanks to Daria Caraway for this article!
I recently tried to build a Slack App to learn about the process and technology and I very quickly found myself a bit overwhelmed. The Slack API is full-featured and heavily documented, which means it can be easy to get lost wandering through the massive amounts of information. This article pools together many different docs and blogs to hopefully build a single coherent resource.
In this post, I walk through how to develop a custom Slack Bot Application with the Slack Node SDK and Express. We will:
- Register and configure a Slack Application from the Slack Developer site.
- Create an Express Server and register it with your Slack App.
- Use the Node SDK Events API to listen to app mentions.
- Use the Node SDK Web Client to send messages to a channel from our bot.
- Use Slack Block Kit and Block Kit Builder to create interactive messages.
- Use Node SDK Interactive Messages API to listen to the interactive actions.
- User Slack Block Kit and Block Kit Builder to create a modal with an interactive form with validation rules.
This tutorial builds a demo application that goes through the following flow:
- The App Bot listens for any mentions of itself.
- Once mentioned, the bot responds to the mention with a prompt.
- If accepted, the prompt opens a modal with a form. The form will contain a dropdown and a text input. On submit, the form runs against validation.
- The submitted valid form data logs to the console! YAY!
*Note: The Slack API is going through revisions and is often changing, so do notice of the date of this writing.
Setup and Configuration
Slack Apps are highly configurable and customizable, so they take a bit of initial bootstrapping to get going.
Creating a Slack Application & Bot
The first thing you need to do is to register your app with Slack at the Slack Developer Site. Once you've created your Slack developer login, navigate to Create New App
in the Your Apps
section.
You will be prompted to enter a Slack Workspace that you have access to as a development space. This workspace represents your dev environment; your bot will be deployed here for testing and experimenting.
Next, we need to create a Bot User for our App.
- Go to
Bot Users
- Click
Add Bot User
- Create a display name and user name for you Slack bot
- Click
Add Bot User
.
Getting Your Bot User Installed in a Slack Workspace
To install a bot into a Workspace, the app must be enabled for at least one permissions scope. To set these scopes, go to OAuth & Permissions
, then scroll down to Scopes
.
Our demo application will need three scopes: bot
chat:write:bot
and users:read
With these permissions, you can scroll back up to OAuth Tokens & Redirect URLs
and click Install App to Workspace
. Once accepted, your bot is installed, and you receive an OAuth Access Token
and a Bot User OAuth Access Token
. We will use these tokens later on.
Introducing the Node SDK
Slack has a multitude of SDKs available for developers, and I personally found it tricky to figure out which did what and why I might use them.
One of the developer tools includes Bolt, a lightweight framework that allows you to build Slack apps with limited functionality quickly. Users also develop with Bolt in JavasScript, so it can be easy to get the documentation confused with building a custom Node server. But, in this tutorial, we will focus solely on the [Slack Node SDK.] (https://github.com/slackapi/node-slack-sdk).
The Slack Node SDK is a collection of packages that give you the ability to interact with just about everything in the Slack platform, without having to build out RESTful capabilities explicitly. For our purposes, we need 3 of the 5 available packages:
- Events API
@slack/events-api
- Web Client API
@slack/web-api
- Interactive Messages API
@slack/interactive-messages
Starting our Node Server
Once you have registered a Slack App, let's begin building out our Node server. As we add functionality to our server, we will adjust the configuration for our app.
To start, initialize a new npm repository and create an app.js file
. In this file, we are going to spin up a lightweight Express server.
const express = require('express') const bodyParser = require('body-parser') const port = process.env.PORT || 3000 const app = express() app.use(bodyParser.urlencoded({extended: true})) app.use(bodyParser.json()) // Starts server app.listen(port, function() { console.log('Bot is listening on port ' + port) })
Listening to App Mentions
The first piece of functionality we are going to build is the bot's ability to listen for mentions.
Introducing the Node SDK Events API
For this: we need to set up our server to receive events with the Slack Events API. To use the Node SDK Slack Events API, install @slack/events-api
:
npm add @slack/events-api
Creating a URL for our Node Server to Accept Events
For our Slack development workspace to talk to our Node server, we have to provide a URL where it can be accessed. There are many ways to do this; the easiest I found was to use ngrok.
The Slack blog has a great post about using ngrok for local development
Once ngrok is installedvand on your $PATH
, we are going to tunnel our development port. In this case, I have chosen port 3000.
ngrok http 3000
daria$ ngrok http 3000 ngrok by @inconshreveable (Ctrl+C to quit) Session Status online Session Expired Restart ngrok or upgrade: ngrok.com/upgrade Version 2.3.35 Region United States (us) Web Interface http://127.0.0.1:4040 Forwarding http://13605143.ngrok.io -> http://localhost:3000 Forwarding https://13605143.ngrok.io -> http://localhost:3000 Connections ttl opn rt1 rt5 p50 p90 105 0 0.00 0.00 5.01 5.34
Note: the forwarded URL is dynamic and changes every time you run the ngrok command.
Next, we need to create a path to be the location that accepts the events on our Express server. Many of the Slack docs recommend using /slack/events
. So in this example, my full URL would be http://13605143.ngrok.io/slack/events
.
Responding to the Slack Challenge
To make sure you own the URL you provide when registering it with Slack, Slack sends a challenge request. To accept the challenge, your server must respond with a specific reply.
Luckily, instead of building this functionality into our server, we can use a command-line utility built into the @slack/events-api
package. To use it, we need our signing secret, which is available in the Basic Information
section of the developer configuration. We also need the name of our events path.
$ ./node_modules/.bin/slack-verify --secret <signing_secret> [--path=/slack/events] [--port=3000]\
When successfully run, you should see The verification server is now listening at the URL: http://:::3000/slack/events
.
This command starts a server that correctly accepts the Slack Events API challenge.
Now we can provide our events URL to our Slack App configuration:
- Go to
Event Subscriptions
- Turn on
Enable Events
- Enter the URL we built above. In my case,
http://13605143.ngrok.io/slack/events
As soon as you enter this URL, Slack sends the challenge message. Our running verification server should correctly accept the challenge.
Once this URL is verified, we can stop the ./node_modules/.bin/slack-verify
server.
Configuring Bot Event Subscriptions
Inside the Event Subscriptions
section, you should now have a verified Request URL
. Next, we need to register to which events we want our bot to have access.
- Click Into
Subscribe to bot events
- Click
Add Bot User Event
- Search for
app_mention
and add it - Save your changes at the bottom of the page!!!!
Great! Our bot has permissions to listen to any @ mentions of itself.
Woah! We Can Finally Write Some Code
With all this configuration out of the way, We can consume the Slack Events API to listen to app mentions for our bot in our app.js file.
To listen to Slack events, we need to use the Node SDK Events Adapter
provided by the Events API and install it as an Express middleware. The Events Adapter requires the Signing Secret
that can be found in Basic Information
and passed to Node via an environment variable.
The path given to app.use
needs to correspond to the path given to ngrok and registered as our Verified Request URL
path.
const = require('@slack/events-api') const slackEvents = createEventAdapter(process.env.SLACK_SIGNING_SECRET) app.use('/slack/events', slackEvents.expressMiddleware())
Note: Make sure to install this middleware above the body-parser middleware.
With slackEvents installed, we are free to use it to listen to bot mentions with the app mention event.
slackEvents.on('app_mention', async (event) => { try { console.log("I got a mention in this channel", event.channel) } catch (e) })
Next, let's start our server with the environment variables we need to run:
SLACK_SIGNING_SECRET=<signing_secret> SLACK_BOT_TOKEN=<bot_user_oAuth_access_token> node app.js
Now, when you go to a channel in your development workspace, add your bot to the channel, and @ mention it, you should see some console messages! WOOO WE DID SOMETHING!
Responding to the App Mention
Next, let's add some logic to have our bot respond to the mention.
Introducing the Node SDK Web Client
To emit event messages back to Slack, we need to use the Slack Node SDK Web API.
npm add @slack/web-api
To initialize a Web Client, you need the Bot User OAuth Access Token
from the OAuth & Permissions
section of the developer config. To keep this token secret, I also pass it in through an environment variable.
const = require('@slack/web-api') const token = process.env.SLACK_BOT_TOKEN const webClient = new WebClient(token)
This webClient instance has a chat object, which allows you to post a message block event back to slack.
webClient.chat.postMessage(messageBlock)
The docs for the generic (non-Node specific) Web API are here and contain a list of available methods.
Introducing Slack Block Kit
What exactly is this messageBlock
I am posting? Well... Slack provides a component framework called Block Kit. Block Kit allows you to put together JSON "blocks" that represent visual and interactive components. Blocks are made up of Block Elements. You can find the full list of blocks here.
Slack also provides a handy tool called the Block Kit Builder This builder provides a drag and drop UI that generates JSON blocks from a live example.
It is important to note that at the time of this writing, some block elements can only be used in particular contexts. For example, input blocks are only available inside of modals, but not messages. Be aware of which context you are building, and which blocks are available to you, or you will receive an API error.
For our bot message response, I want it to be a simple message that contains a button that launches a modal:
Using the Block Kit Builder, I've translated this design into the following JSON:
const messageJsonBlock = {
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Hello, thanks for calling me. Would you like to launch a modal?"
},
"accessory": {
"type": "button",
"action_id": "open_modal_button", // We need to add this
"text": {
"type": "plain_text",
"text": "Launch",
"emoji": true
},
"value": "launch_button_click"
}
}
]
}
In order to identify our button on click, we need to make sure to add an action_id
to its JSON block. Block Kit Builder does not provide this key; you have to add it.
In my messageJsonBlock
i've added the action_id
of open_modal_button
.
Note: In general, the Block Kit Builder output does not include any identifying or interaction specific JSON, and the developer is responsible for managing ids and callbacks.
Sending Back a Message Block
Now that we have our message blocks. We can send them through the postMessage
function. To correctly post a message, we need to include the channel to target. We can get that information from the mention event.
slackEvents.on('app_mention', async (event) => { try { const mentionResponseBlock = { messageJsonBlock, {channel: event.channel}} const res = await webClient.chat.postMessage(mentionResponseBlock) console.log('Message sent: ', res.ts) } catch (e) })
Introducing Node SDK Slack Interactive Messages API
We are making progress!!
Now that our bot can send back a message that contains a button, we need to handle the behavior for when the user clicks it.
This of course requires... another package. This time we need the interactions adapters.
npm add @slack/interactive-messages
Similarly to the events adapter, we need to provide a server path where our server accepts interactive actions. Many Slack docs suggest using /slack/actions
, so that is what I've used here.
const = require('@slack/interactive-messages') const slackInteractions = createMessageAdapter(process.env.SLACK_SIGNING_SECRET) app.use('/slack/actions', slackInteractions.expressMiddleware())
Registering Your Actions Route with Slack
Much like configuring events, we need to tell Slack about our path for accepting interactive actions and configure the available actions:
- In your App configuration, navigate to
Interactive Components
- Turn on interactivity
- Enter your
Request URL
. In my case, it would behttp://13605143.ngrok.io/slack/actions
. - Save your changes at the bottom of the page!!!!
Unlike your events request URL, this one does not need to be verified.
Listening to an Interaction
Once we have the slackInteraction middleware installed and our path registered with Slack, we can use the action_id
on our button to listen to interaction events.
slackInteractions.action({ actionId: 'open_modal_button' }, async (payload) => { try { console.log("button click recieved", payload) } catch (e) return { text: 'Processing...', } })
According to the Node SDK Docs, "The return value is used to update the message where the action occurred immediately. Use [it with] items like buttons and menus that you only want a user to interact with once."
Once we implement this action, you can restart your Node server and @ mention you bot again. Click the Launch
button in the bot response, and you should see the button click received
console message!
Building a Modal
Woohoo! We have all the packages we need moving forward, and are ready to build our modal.
Note: Slack recently deprecated their old dialog blocks called dialog
and replaced them with their new blocks, modals
. Some of their documentation has yet to be updated, so make sure if you are looking for information about modals, you are reading about modals
and not dialogs
.
Again, using the Block Kit Builder, I've designed this modal:
It allows the user to choose a cute animal category from a static list, and create a name for that cute animal.
The builder gives me the following JSON:
const modalJsonBlock = { "type": "modal", "callback_id": "cute_animal_modal_submit", // We need to add this "title": { "type": "plain_text", "text": "My App", "emoji": true }, "submit": { "type": "plain_text", "text": "Done", "emoji": true }, "close": { "type": "plain_text", "text": "Cancel", "emoji": true }, "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": "Thanks for openeing this modal!" } }, { "type": "input", "block_id": "cute_animal_selection_block", // put this here to identify the selection block "element": { "type": "static_select", "action_id": "cute_animal_selection_element", // put this here to identify the selection element "placeholder": { "type": "plain_text", "text": "Select a cute animal", "emoji": true }, "options": [ { "text": { "type": "plain_text", "text": "Puppy", "emoji": true }, "value": "puppy" }, { "text": { "type": "plain_text", "text": "Kitten", "emoji": true }, "value": "kitten" }, { "text": { "type": "plain_text", "text": "Bunny", "emoji": true }, "value": "bunny" } ] }, "label": { "type": "plain_text", "text": "Choose a cute pet:", "emoji": true } }, { "type": "input", "block_id": "cute_animal_name_block", // put this here to identify the input. "element": { "type": "plain_text_input", "action_id": "cute_animal_name_element" // put this here to identify the selection element }, "label": { "type": "plain_text", "text": "Give it a cute name:", "emoji": true } } ] }
Like before, the Block Kit Builder doesn't put any identifying information on our blocks. To handle a modal submit, we must add a callback
to our payload. We also need to put a block_id
on any block we want to get input data out of later, and an action_id
that matches to the input element within the block.
I have added the following ids:
- Modal callback:
cute_animal_modal_submit
- Select Dropdown Block:
cute_animal_selection_block
- Select Dropdown Element:
cute_animal_selection_element
- Input Block:
cute_animal_name_block
- Input Element:
cute_animal_name_element
Opening a Modal
Our action callback can now return this modal to the user. For this, we need the views.open
function on the Web Client.
slackInteractions.action({ actionId: 'open_modal_button' }, async (payload) => { try { await webClient.views.open({ trigger_id: payload.trigger_id, view: modalJsonBlock } ) } catch (e) // The return value is used to update the message where the action occurred immediately. // Use this to items like buttons and menus that you only want a user to interact with once. return { text: 'Processing...', } })
Note: Do not use dialog.open
. This function is for the old dialog implementation and does not support Block Kit blocks.
Handling Modal Submit
With our callback on the block payload, we can again use the Interactive Messages API to listen for a modal submission.
slackInteractions.viewSubmission('cute_animal_modal_submit' , async (payload) => { const blockData = payload.view.state const cuteAnimalSelection = blockData.values.cute_animal_selection_block.cute_animal_selection_element.selected_option.value const nameInput = blockData.values.cute_animal_name_block.cute_animal_name_element.value console.log(cuteAnimalSelection, nameInput) return { response_action: "clear" } }))
The return value above tells the modal to close.
Validation on the Modal
Slack inputs are handy because they have some validation already built-in. By default, inputs are required, and if a user submits an empty input, they get a validation error.
To perform custom validation, you return an error from the viewSubmission
event.
Here, I invalidate the cute animal name input if the name has only one character.
slackInteractions.viewSubmission('cute_animal_modal_submit' , async (payload) => { const cuteAnimalSelection = blockData.values.cute_animal_selection_block.cute_animal_selection_element.value const nameInput = blockData.values.cute_animal_name_block.cute_animal_name_element.value if (nameInput.length < 2) { return { "response_action": "errors", "errors": { "cute_animal_name_block": "Cute animal names must have more than one letter." } } } return { response_action: "clear" } })
Conclusion
Wow... we made it, what a wild journey. Overall, I've found the Slack Developer API to be quite powerful, and I love the ability to build metadata-driven views with Block Kit. With great power, comes great documentation, and digging through all of the potential functionality can be a little daunting. I hope this article was able to consolidate some of the critical pieces of information and makes it easier for you to start your own Slack App with the Node SDK and Express!
Check our the entire code example on my github
This content originally appeared on JavaScript January and was authored by Emily Freeman
Emily Freeman | Sciencx (2020-01-27T13:43:00+00:00) Building a Slack App with Express and the Node SDK. Retrieved from https://www.scien.cx/2020/01/27/building-a-slack-app-with-express-and-the-node-sdk/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.