Skip to content

Commit

Permalink
Merge pull request slackapi#15 from aoberoi/deployable
Browse files Browse the repository at this point in the history
Deployable
  • Loading branch information
aoberoi committed Mar 20, 2019
1 parent 5ccf69d commit 88c0238
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Describe your issue here.
Filling out the following details about bugs will help us solve your issue sooner.

#### Reproducible in:
slack-events version:
@slack/interactive-messages version:
node version:
OS version(s):

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ documentation is written manually in markdown in `docs/reference.md`.
* Tag the commit with the version number. For example `v1.0.8`.

2. Distribute the release
* Make sure the project has been built locally (the `dist` directory is up to date).
* Publish to the appropriate package manager. Once you have permission to publish on npm, you
can run `npm publish`.
* Create a GitHub Release. This will also serve as a Changelog for the project. Add a
Expand Down
1 change: 1 addition & 0 deletions packages/node-slack-interactive-messages/.travis.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
language: node_js
node_js:
- "8"
- "7"
- "6"
- "5"
Expand Down
100 changes: 53 additions & 47 deletions packages/node-slack-interactive-messages/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,22 @@ common tasks and best practices so that you don't need to.
## Installation

```
$ npm install --save @slack/interactive-messages express
$ npm install --save @slack/interactive-messages express body-parser
```

## Configuration

Before you can use [interactive messages](https://api.slack.com/interactive-messages) you must
[create a Slack App](https://api.slack.com/apps/new), and configure an interactive message request
URL. If your app will use dynamic menus, you also need to configure an options URL.
[create a Slack App](https://api.slack.com/apps/new). 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 Messages** feature, and enable it. Input your **Request URL**. If your app
will use dynamic menus, you also need to input a **Options URL**.

![Configuring a request URL](support/interactive-messages.gif)

### Getting a temporary URL for development
<details>
<summary>Getting a temporary Request URL for development</summary>

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](https://ngrok.com/) or
Expand All @@ -48,26 +52,30 @@ the base URL. This will depend on your app, but if you are using the built-in HT
path is `/slack/actions`. In this example the request URL would be
`https://d9f6dad3.ngrok.io/slack/actions`.

</details>

## Usage

The easiest way to start using interactive messages is by using the built-in HTTP server.
The easiest way to start responding to interactive message actions is by using the built-in HTTP
server.

```javascript
// Initialize using verification token from environment variables
const createMessageAdapter = require('@slack/events-api').createMessageAdapter;
const { createMessageAdapter } = require('@slack/interactive-messages');

// Initialize adapter using verification token from environment variables
const slackMessages = createMessageAdapter(process.env.SLACK_VERIFICATION_TOKEN);
const port = process.env.PORT || 3000;

// Attach action handlers by `callback_id` (See: https://api.slack.com/docs/interactive-message-field-guide#attachment_fields)
// Attach action handlers by `callback_id`
// (See: https://api.slack.com/docs/interactive-message-field-guide#attachment_fields)
slackMessages.action('welcome_button', (payload) => {
// `payload` is JSON that describes an interaction with a message.
console.log(`The user ${payload.user.name} in team ${payload.team.domain} pressed the welcome button`);

// The `actions` array contains details about the particular action (button press, menu selection, etc.)
// The `actions` array contains details about the specific action (button press, menu selection, etc.)
const action = payload.actions[0];
console.log(`The button had name ${action.name} and value ${action.value}`);

// You should return a value which describes a message to replace the original.
// You should return a JSON object which describes a message to replace the original.
// Note that the payload contains a copy of the original message (`payload.original_message`).
const replacement = payload.original_message;
// Typically, you want to acknowledge the action and remove the interactive elements from the message
Expand All @@ -76,30 +84,32 @@ slackMessages.action('welcome_button', (payload) => {
return replacement;
});

// Start a basic HTTP server
// Start the built-in HTTP server
const port = process.env.PORT || 3000;
slackMessages.start(port).then(() => {
console.log(`server listening on port ${port}`);
});
```

**NOTE**: To use the example above, your application must have already sent a message with an
attachment that contains a button whose `callback_id` is set to `'welcome_button'`. There are
multiple ways to produce these types of messages; including
[incoming webhooks](https://api.slack.com/incoming-webhooks), or the web API (`chat.postMessage`,
`chat.update`, or even `chat.unfurl`).
**NOTE**: To use the example above, your application must have already sent a message with a message
button whose `callback_id` is set to `'welcome_button'`. There are multiple ways to produce these
types of messages; including [incoming webhooks](https://api.slack.com/incoming-webhooks), or the
web API ([`chat.postMessage`](https://api.slack.com/methods/chat.postMessage),
[`chat.update`](https://api.slack.com/methods/chat.update), or
[`chat.unfurl`](https://api.slack.com/methods/chat.unfurl)).

### Using with Express
### Using as Express middleware

For usage within an existing Express application, you can route requests to the adapter's express
middleware by calling the `expressMiddleware()` method.
For usage within an existing Express application, call the `expressMiddleware()` method on the
adapter and it will return a middleware function which you can add to your app. Be sure to add
the `body-parser` middleware before the message adapter as shown below.

```javascript
const http = require('http');
const { createMessageAdapter } = require('@slack/interactive-messages');

// Initialize using verification token from environment variables
const createMessageAdapter = require('@slack/events-api').createMessageAdapter;
const slackMessages = createMessageAdapter(process.env.SLACK_VERIFICATION_TOKEN);
const port = process.env.PORT || 3000;

// Initialize an Express application
const express = require('express');
Expand All @@ -110,7 +120,7 @@ const app = express();
app.use(bodyParser.urlencoded({ extended: false }));

// Mount the event handler on a route
// NOTE: you must mount to a path that matches the Action URL or Options URL that was configured
// NOTE: you must mount to a path that matches the Request URL and/or Options URL that was configured
app.use('/slack/actions', slackMessages.expressMiddleware());

// Attach action handlers
Expand All @@ -119,17 +129,18 @@ slackMessages.action('welcome_button', (payload) => {
});

// Start the express application
const port = process.env.PORT || 3000;
http.createServer(app).listen(port, () => {
console.log(`server listening on port ${port}`);
});
```

**Pro-Tip**: You can use this technique to combine usage of this module with the
**Pro-Tip**: You can use this technique to combine usage of this adapter and the
[Events API adapter](https://github.com/slackapi/node-slack-events-api).

### Action matching

You can attach handlers to actions using more than just exact matches for the `"callback_id"` string.
You can attach handlers for actions using more than just exact matches for the `"callback_id"` string.

```javascript
// Regular expressions can be used to match a computed callback_id or family of callback_id's
Expand All @@ -138,24 +149,24 @@ slackMessages.action(/welcome_(\w+)/, (payload) => { /* ... */ });
// Action types can be used to separate interactions that originate from buttons or menus
slackMessages.action({ type: 'select' }, (payload) => { /* ... */ });

// Handle actions from unfurls separately, since properties in the payload can differ (e.g. no access to original_message content)
// Handle actions from unfurls separately, since properties in the payload can differ
// (e.g. no access to original_message content)
slackMessages.action({ unfurl: true }, (payload) => { /* ... */ });

// Combine multiple properties to match actions as specifically as you desire
slackMessages.action({ callbackId: 'welcome', type: 'button', unfurl: false }, (p) => { /* ... */ });
```

These options should allow you to match actions in any way that makes sense for your app. Keep in
mind that only the first handler to be registered that matches an incoming interaction will be
invoked.
mind that only the first handler that matches an action will be invoked.

### Asynchronous responses

You cannot always prepare a response to an action synchronously, especially in node where all I/O is
evented and you should not block. In these situations we recommend that you at least consider
updating the message to remove the interactive elements when the action shouldn't trigger more than
once. The adapter will invoke your handler with a `respond()` function as the second argument so you
can update the message again asynchronously.
Sometimes you can't prepare a response to an action synchronously, especially in node where all I/O
is evented and you should not block. In these situations we recommend that you at least consider
updating the message to remove the interactive elements (often times actions aren't meant to trigger
more than once for an app). The adapter will invoke your handler with a `respond()` function as the
second argument so you can update the message asynchronously.

```javascript
slackMessages.action('my_callback', (payload, respond) => {
Expand All @@ -176,21 +187,26 @@ slackMessages.action('my_callback', (payload, respond) => {
You may also return a promise for a JSON object that represents the replacement message from the
action handler. This is not recommended unless you know the promise will resolve within 3 seconds.

If you do not return a value or return a falsy value from the handler, the message is not replaced.
This is not recommended in most cases, because the interactive attachments will stay on the message
and might be used again by the same or a different user.

### Options handling and matching

The adapter can also deal with
[options requests from interactive menus](https://api.slack.com/docs/message-menus#menu_dynamic).
If you are using the built-in HTTP server, the URL for options requests will be the same as the
action URL. If you are using the adapter with Express, the options URL can be anywhere you mount
request URL. If you are using the adapter with Express, the options URL can be anywhere you mount
the express middleware.

Options handlers are matched using the menu's `'callback_id'` as a string or a regular expression.

```javascript
const messageMiddleware = slackMessages.expressMiddleware();
// `app` is an express application
// this is an example of choosing to use a separate action URL and options URL
app.use('/slack/actions', messageMiddleware)
// in this example, we create a separate Options URL (different from the above Request URL) and
// still use the same instance of the adapter's middleware
app.use('/slack/options', messageMiddleware);

slackMessages.options('my_dynamic_menu_callback', (selection, respond) => {
Expand All @@ -214,7 +230,7 @@ slackMessages
.action('make_order_1', orderStepOne)
.action('make_order_2', orderStepTwo)
.action('make_order_3', orderStepThree)
.options('make_order_3', workFlowStepThreeOptions);
.options('make_order_3', orderStepThreeOptions);
```

### Error handling
Expand All @@ -226,20 +242,10 @@ within the channel.
For asynchronous code, `respond()` will return a promise. On success, the promise resolves to a
response object. On errors, the promise rejects and you should handle it with a `.catch()`.

## Documentation

To learn more, see the [reference documentation](docs/reference.md).

## Support

Need help? Join the [Bot Developer Hangout](http://dev4slack.xoxco.com/) team and talk to us in
Need help? Join the [Bot Developer Hangout](https://community.botkit.ai/) team and talk to us in
[#slack-api](https://dev4slack.slack.com/messages/slack-api/).

You can also [create an Issue](https://github.com/slackapi/node-slack-events-api/issues/new)
right here on GitHub.

**TODO** mention falsy returns do no replacement in reference docs

**TODO** open an issue to discuss adding API for inspection or removal of action/option handlers

**TODO** open an issue to discuss if we need options for name or value matching/routing? slapp has these
10 changes: 8 additions & 2 deletions packages/node-slack-interactive-messages/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@slack/interactive-messages",
"version": "0.1.0",
"description": "Slack interactive messages module",
"version": "0.1.1",
"description": "Slack Interactive Messages module",
"main": "dist/index.js",
"repository": "https://github.com/slackapi/node-slack-interactive-messages.git",
"engines": {
Expand All @@ -26,18 +26,24 @@
"lodash.isregexp": "^4.0.1",
"lodash.isstring": "^4.0.1"
},
"optionalDependencies": {
"express": "^4.0.0",
"body-parser": "^1.4.3"
},
"devDependencies": {
"babel-cli": "^6.24.0",
"babel-eslint": "^7.2.0",
"babel-plugin-system-import-transformer": "^3.1.0",
"babel-preset-env": "^1.3.2",
"body-parser": "^1.17.2",
"codecov": "^2.1.0",
"eslint": "^3.18.0",
"eslint-config-airbnb": "^14.1.0",
"eslint-config-airbnb-base": "^11.1.1",
"eslint-plugin-import": "^2.2.0",
"eslint-plugin-jsx-a11y": "^4.0.0",
"eslint-plugin-react": "^6.10.3",
"express": "^4.15.3",
"get-random-port": "0.0.1",
"mocha": "^3.2.0",
"nop": "^1.0.0",
Expand Down
1 change: 1 addition & 0 deletions packages/node-slack-interactive-messages/src/adapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ export default class SlackMessageAdapter {

dispatch(payload) {
const action = payload.actions && payload.actions[0];
// The following result value represents "no replacement"
let result = { status: 200 };
const respond = (message) => {
debug('sending async response');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ var http = require('http');
var proxyquire = require('proxyquire');
var nop = require('nop');
var getRandomPort = require('get-random-port');
var isFunction = require('lodash.isfunction');
var systemUnderTest = require('../../dist/adapter');
var SlackMessageAdapter = systemUnderTest.default;

Expand Down Expand Up @@ -89,9 +90,56 @@ describe('SlackMessageAdapter', function () {
it('should return a Promise for a started http.Server', function () {
var self = this;
return this.adapter.start(self.portNumber).then(function (server) {
assert(server.listening);
// only works in node >= 5.7.0
// assert(server.listening);
assert.equal(server.address().port, self.portNumber);
});
});
});

describe('#stop()', function () {
beforeEach(function (done) {
var self = this;
self.adapter = new SlackMessageAdapter(workingVerificationToken);
getRandomPort(function (error, port) {
if (error) return done(error);
return self.adapter.start(port)
.then(function (server) {
self.server = server;
done();
})
.catch(done);
});
});
afterEach(function () {
return this.adapter.stop().catch(nop);
});
it('should return a Promise and the server should be stopped', function () {
var self = this;
return this.adapter.stop().then(function () {
assert(!self.server.listening);
});
});
});

describe('#expressMiddleware()', function () {
beforeEach(function () {
this.adapter = new SlackMessageAdapter(workingVerificationToken);
});
it('should return a function', function () {
var middleware = this.adapter.expressMiddleware();
assert(isFunction(middleware));
});
});

describe('#action()', function () {
beforeEach(function () {
this.adapter = new SlackMessageAdapter(workingVerificationToken);
});
it('should fail registration without handler', function () {
assert.throws(function () {
this.adapter.action('my_callback');
}, TypeError);
});
});
});

0 comments on commit 88c0238

Please sign in to comment.