Slack Interactive Messages for Node
![codecov](https://codecov.io/gh/slackapi/node-slack-interactive-messages/branch/master/graph/badge.svg)
Build your Slack Apps with rich and engaging user interactions using buttons, menus, and dialogs.
The package will help you start with sensible and secure defaults.
The adapter gives you a meaningful API to handle actions from all of Slack's interactive
message components (buttons,
menus, and dialogs).
Use it as an independent HTTP server or plug it into an existing server as
Express middleware.
This package does not help you compose messages with buttons, menus and dialogs to trigger the
actions. We recommend using the Message Builder to
design interactive messages. You can send these messages to Slack using the Web API, Incoming
Webhooks, and other parts of the platform.
Installation
$ npm install --save @slack/interactive-messages express body-parser
Configuration
Get started by creating a Slack App if you haven't already.
On the Basic Information page, in the section for App Credentials, note the
Verification Token. You will need it to initialize the adapter.
Select the Interactive Components feature, and enable it. Input a Request URL. If your
app will use dynamic message menus, you also need to input a Options Load URL.
![Configuring a request URL](https://github.com/slackapi/node-slack-sdk/raw/HEAD/support/interactive-components.gif)
What's a request URL? How can I get one for development?
Slack will send requests to your app server each time a button is clicked, a menu item is selected,
a dialog is submitted, and more. In order to reach your server, you have to tell Slack where your
app is listening for those requests. This location is the request URL.
If you're just getting started with development, you may not have a publicly accessible URL for
your app. We recommend using a development proxy, such as ngrok or
localtunnel, to generate a URL that can forward requests to
your local machine. Once you've installed the development proxy of your choice, run it to begin
forwarding requests to a specific port (for example, 3000).
ngrok: ngrok http 3000
localtunnel: lt --port 3000
![Starting a development proxy](https://github.com/slackapi/node-slack-sdk/raw/HEAD/support/ngrok.gif)
The output should show you a newly generated URL that you can use (ngrok will actually show you two
and we recommend the one that begins with "https"). Let's call this the base URL (for example,
https://e0e88971.ngrok.io
)
To create the request URL, we add the path where our app listens for message actions onto the end of
the base URL. If you are using the built-in HTTP server it is set to /slack/actions
. In this
example the request URL would be https://e0e88971.ngrok.io/slack/actions
. If you are using the
Express middlware, you can set whichever path you like, just remember to make the path you mount the
middleware into the application the same as the one you configure in Slack.
Usage
Starting a server
The adapter needs to be attached to an HTTP server. Either use the built-in HTTP server or attach
the adapter to an existing Express application as a middleware.
Built-in HTTP server
const { createMessageAdapter } = require('@slack/interactive-messages');
const slackInteractions = createMessageAdapter(process.env.SLACK_VERIFICATION_TOKEN);
const port = process.env.PORT || 3000;
slackInteractions.start(port).then(() => {
console.log(`server listening on port ${port}`);
});
Express application server
const { createMessageAdapter } = require('@slack/interactive-messages');
const http = require('http');
const express = require('express');
const bodyParser = require('body-parser');
const slackInteractions = createMessageAdapter(process.env.SLACK_VERIFICATION_TOKEN);
const app = express();
app.use(bodyParser.urlencoded({ extended: false }));
app.use('/slack/actions', slackInteractions.expressMiddleware());
const port = process.env.PORT || 3000;
http.createServer(app).listen(port, () => {
console.log(`server listening on port ${port}`);
});
Pro-Tip: You can combine this package and
@slack/events-api
by attaching each to the
same Express application.
Creating handlers
When a user interacts with one of the interactive components, this adapter will run a handler
function in response. Your app should create a handler for each type of interaction it expects.
There are two categories of interactions: actions and options requests. With either kind,
your app can describe which handler to run using one or many constraints.
Action matching
Use a string or RegExp as the first argument to the .action()
method to use a callback_id
constraint for the handler.
slackInteractions.action('welcome_button', handlerFunction);
slackInteractions.action(/welcome_(\w+)/, handlerFunction);
function handlerFunction() {
}
Use an object to describe other constraints, even combine multiple constraints to create more
specific handlers. The full set of constraint options are described in the
reference documentation.
slackInteractions.action({ type: 'button' }, handlerFunction)
slackInteractions.action({ callbackId: 'welcome', type: 'dialog_submission' }, handlerFunction);
slackInteractions.action({ callbackId: 'save', type: 'message_action' }, handlerFunction);
slackInteractions.action({ unfurl: true, type: 'select' }, handlerFunction);
function handlerFunction() {
}
Responding to actions
Slack requires your app to respond to actions in a timely manner so that the user isn't blocked.
The adapter helps your app respond correctly and on time.
For most actions (button presses, menu selections, and message actions), a response is simply an
updated message to replace the one where the interaction occurred. Your app can return a message
(or a Promise for a message) from the handler. We recommend that apps at least remove the
interactive elements from the message in the response so that users don't get confused (for
example, click the same button twice). Find details about the format for a message in the docs for
message.
The handler will receive a payload
which describes the interaction a user had with a message.
Find more details about the structure of payload
in the docs for
buttons and
menus.
If your app defers some work asynchronously (like querying another API or using a database), you can
continue to update the message using the respond()
function that is provided to your handler.
Example button handler:
slackInteractions.action('welcome_agree_button', (payload, respond) => {
console.log(`The user ${payload.user.name} in team ${payload.team.domain} pressed a button`);
users.findBySlackId(payload.user.id)
.then(user => user.acceptPolicyAndSave())
.then(() => {
const message = {
text: 'Thank you for agreeing to the team\'s policy.',
};
respond(message);
})
.catch((error) => {
console.error(error);
respond({
text: 'An error occurred while recording your agreement. Please contact an admin.'
});
});
const reply = payload.original_message;
delete reply.attachments[0].actions;
return reply;
});
NOTE: If you don't return any value, the adapter will respond with an OK response on your app's
behalf, which results in the message staying the same. If you return a Promise, and it resolves
after the timeout (2.5 seconds), then the adapter will also respond with an OK response and later
call respond()
with the eventual value. If you choose to use a Promise, remember to add a
.catch()
to handle rejections.
Dialog submission action handlers respond slightly differently from button presses and menu
selections.
The handler will receive a payload
which describes all the elements in the dialog. Find more
details about the structure of payload
in the docs for
dialogs.
Unlike with buttons and menus, the response does not replace the message (a dialog is not a message)
but rather the response tells Slack whether the inputs are valid and the dialog can be closed on
the user's screen. Your app returns a list of errors (or a Promise for a list of errors) from the
handler. If there are no errors, your app should return nothing from the handler. Find more details
on the structure of the list of errors in the docs for
input validation.
The handler will also receive a respond()
function, which can be used to send a message to the
conversation where the dialog was triggered. We recommend that apps use respond()
to notify the
user that the dialog submission was recieved and use it again to communicate updates such as
success or failure.
Example dialog submission handler:
slackInteractions.action('create_order_dialog', (payload, respond) => {
console.log(`The user ${payload.user.name} in team ${payload.team.domain} submitted a dialog`);
const errors = validateOrderSubmission(payload.submission);
if (errors) {
return errors;
} else {
setTimeout(() => {
respond({
text: `Thank you for completing an order, <@${payload.user.id}>. ` +
'Your order number will appear here shortly.',
});
orders.create(payload.submission)
.then((order) => {
const message = {
text: `Thank you for completing an order, <@${payload.user.id}>. ` +
`Your order number is ${order.number}`,
};
respond(message);
})
.catch((error) => {
console.error(error);
respond({ text: 'An error occurred while creating your order.' });
});
});
}
});
NOTE: If you return a Promise which takes longer than the timeout (2.5 seconds) to complete, the
adapter will continue to wait and the user will see an error.
Options request matching
Use a string or RegExp as the first argument to the .options()
method to use a callback_id
constraint for the handler.
slackInteractions.options('project_menu', handlerFunction);
slackInteractions.options(/(\w+)_menu/, handlerFunction);
function handlerFunction() {
}
Use an object to describe other constraints, even combine multiple constraints to create more
specific handlers. The full set of constraint options are described in the
reference documentation.
slackInteractions.options({ within: 'dialog' }, handlerFunction)
slackInteractions.options({ callbackId: 'project_menu', within: 'interactive_message' }, handlerFunction)
function handlerFunction() {
}
Responding to options requests
Slack requires your app to respond to options requests in a timely manner so that the user isn't
blocked. The adapter helps your app respond correctly and on time.
A response is a list of options or option groups that your app wants to populate into the menu.
Your app will return the list (or a Promise for the list) from the handler. However, if you use
a Promise which takes longer than the timeout (2.5 seconds) to resolve, the adapter will continue to
wait and the user will see an error. Find details on formatting the list in the docs for
options fields and
options groups.
The handler will receive a payload
which describes the current state of the menu. If the user
is typing into the field, the payload.value
property contains the value they have typed so far.
Find more details about the structure of payload
in the docs for
dynamic menus.
Example options request handler:
slackInteractions.options('project_menu', (payload) => {
console.log(`The user ${payload.user.name} in team ${payload.team.domain} is typing in a menu`);
return projects.fuzzyFind(payload.value)
.then(formatProjectsAsOptions)
.catch(error => {
console.error(error)
return { options: [] };
});
});
NOTE: Options request responses vary slightly depending on whether the menu is within a
message or within a dialog. When the options request is from within a menu, the fields for each
option are text
and value
. When the options request is from within a dialog, the fields for each
option are label
and value
.
Chaining
The .action()
and .options()
methods return the adapter object, which means the API supports
chaining.
slackInteractions
.action('make_order_1', orderStepOne)
.action('make_order_2', orderStepTwo)
.action('make_order_3', orderStepThree)
.options('make_order_3', orderStepThreeOptions);
Examples
- Express All Interactions - A ready to run sample app that
creates and responds to buttons, menus, and dialogs. It also demonstrates a menu with dynamic
options. It is built on top of the Express web framework.
Reference Documentation
See the reference documentation a more formal description of this pacakge's
objects and functions.
Support
Need help? Join the Bot Developer Hangout team and talk to us in
#slack-api.
You can also create an Issue
right here on GitHub.