@slack/interactive-messages
Advanced tools
Comparing version 1.3.0 to 1.4.0
@@ -14,3 +14,3 @@ /// <reference types="node" /> | ||
/** | ||
* The number of milliseconds to wait before flushing a syncrhonous response to an incoming request and falling back | ||
* The number of milliseconds to wait before flushing a synchronous response to an incoming request and falling back | ||
* to an asynchronous response. | ||
@@ -31,3 +31,3 @@ */ | ||
* @param signingSecret - Slack app signing secret used to authenticate request | ||
* @param options.syncResponseTimeout - number of milliseconds to wait before flushing a syncrhonous response to an | ||
* @param options.syncResponseTimeout - number of milliseconds to wait before flushing a synchronous response to an | ||
* incoming request and falling back to an asynchronous response. | ||
@@ -80,5 +80,5 @@ * @param options.lateResponseFallbackEnabled - whether or not promises that resolve after the syncResponseTimeout can | ||
* :-----:|:-----:|:-----:|:-----:|:-----:|:-----: | ||
* **Button Press**| Message in response | When resolved before `syncResposeTimeout` or `lateResponseFallbackEnabled: false`, message in response<br />When resolved after `syncResponseTimeout` and `lateResponseFallbackEnabled: true`, message in request to `response_url` | Empty response | Message in request to `response_url` | Create a new message instead of replacing using `replace_original: false` | ||
* **Menu Selection**| Message in response | When resolved before `syncResposeTimeout` or `lateResponseFallbackEnabled: false`, message in response<br />When resolved after `syncResponseTimeout` and `lateResponseFallbackEnabled: true`, message in request to `response_url` | Empty response | Message in request to `response_url` | Create a new message instead of replacing using `replace_original: false` | ||
* **Message Action** | Message in response | When resolved before `syncResposeTimeout` or `lateResponseFallbackEnabled: false`, message in response<br />When resolved after `syncResponseTimeout` and `lateResponseFallbackEnabled: true`, message in request to `response_url` | Empty response | Message in request to `response_url` | | ||
* **Button Press**| Message in response | When resolved before `syncResponseTimeout` or `lateResponseFallbackEnabled: false`, message in response<br />When resolved after `syncResponseTimeout` and `lateResponseFallbackEnabled: true`, message in request to `response_url` | Empty response | Message in request to `response_url` | Create a new message instead of replacing using `replace_original: false` | ||
* **Menu Selection**| Message in response | When resolved before `syncResponseTimeout` or `lateResponseFallbackEnabled: false`, message in response<br />When resolved after `syncResponseTimeout` and `lateResponseFallbackEnabled: true`, message in request to `response_url` | Empty response | Message in request to `response_url` | Create a new message instead of replacing using `replace_original: false` | ||
* **Message Action** | Message in response | When resolved before `syncResponseTimeout` or `lateResponseFallbackEnabled: false`, message in response<br />When resolved after `syncResponseTimeout` and `lateResponseFallbackEnabled: true`, message in request to `response_url` | Empty response | Message in request to `response_url` | | ||
* **Dialog Submission**| Error list in response | Error list in response | Empty response | Message in request to `response_url` | Returning a Promise that takes longer than 3 seconds to resolve can result in the user seeing an error. Warning logged if a promise isn't completed before `syncResponseTimeout`. | ||
@@ -109,2 +109,27 @@ * | ||
/** | ||
* Add a handler for view submission. | ||
* | ||
* The value returned from the `callback` determines the response sent back to Slack. The handler can return a plain | ||
* object with a `response_action` property to dismiss the modal, push a view into the modal, display validation | ||
* errors, or update the view. Alternatively, the handler can return a Promise for this kind of object, which resolves | ||
* before `syncResponseTimeout` or `lateResponseFallbackEnabled: false`, to perform the same response actions. If the | ||
* Promise resolves afterwards or `lateResponseFallbackEnabled: true` then the modal will be dismissed. If the handler | ||
* returns `undefined` the modal will be dismissed. | ||
* | ||
* @param matchingConstraints - the callback ID (as a string or RegExp) or an object describing the constraints to | ||
* match view submissions for the handler. | ||
* @param callback - the function to run when an view submission is matched | ||
* @returns this instance (for chaining) | ||
*/ | ||
viewSubmission(matchingConstraints: string | RegExp | ViewConstraints, callback: ViewSubmissionHandler): this; | ||
/** | ||
* Add a handler for view closed interaction. The handler should not return a value. | ||
* | ||
* @param matchingConstraints - the callback ID (as a string or RegExp) or an object describing the constraints to | ||
* match view closed interactions for the handler. | ||
* @param callback - the function to run when an view closed interaction is matched | ||
* @returns this instance (for chaining) | ||
*/ | ||
viewClosed(matchingConstraints: string | RegExp | ViewConstraints, callback: ViewClosedHandler): this; | ||
/** | ||
* Dispatches the contents of an HTTP request to the registered handlers. | ||
@@ -193,2 +218,19 @@ * | ||
/** | ||
* Constraints on when to call a view submission or view closed handler. | ||
*/ | ||
export interface ViewConstraints { | ||
/** | ||
* A string or RegExp to match against the `callback_id` | ||
*/ | ||
callbackId?: string | RegExp; | ||
/** | ||
* A string to match against the `external_id` | ||
*/ | ||
externalId?: string | RegExp; | ||
/** | ||
* A string to match against the `view_id` | ||
*/ | ||
viewId?: string; | ||
} | ||
/** | ||
* A function used to send message updates after an action is handled. This function can be used | ||
@@ -221,3 +263,3 @@ * up to 5 times in 30 minutes. | ||
* this object is a list of [validation errors](https://api.slack.com/dialogs#input_validation). It may also be a | ||
* Promise for a list of validation errors, and if so and the Promise takes longer than the `syncReponseTimeout` to | ||
* Promise for a list of validation errors, and if so and the Promise takes longer than the `syncResponseTimeout` to | ||
* complete, Slack will display an error to the user. If there is no return value, then button presses and menu | ||
@@ -241,2 +283,14 @@ * selections do not update the message and dialog submissions will validate and dismiss. | ||
declare type OptionsHandler = (payload: any) => any | Promise<any> | undefined; | ||
/** | ||
* A handler function for view submission requests. | ||
* | ||
* TODO: describe the payload and return values more specifically? | ||
*/ | ||
declare type ViewSubmissionHandler = (payload: any) => any | Promise<any> | undefined; | ||
/** | ||
* A handler function for view closed requests. | ||
* | ||
* TODO: describe the payload and return values more specifically? | ||
*/ | ||
declare type ViewClosedHandler = (payload: any) => void; | ||
//# sourceMappingURL=adapter.d.ts.map |
"use strict"; | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
return new (P || (P = Promise))(function (resolve, reject) { | ||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } | ||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
@@ -82,2 +83,5 @@ }); | ||
} | ||
if (!hasBlockRelatedConstraints(matchingConstraints)) { | ||
return false; | ||
} | ||
if (!util_1.isFalsy(matchingConstraints.blockId) && | ||
@@ -105,6 +109,24 @@ !(lodash_isstring_1.default(matchingConstraints.blockId) || lodash_isregexp_1.default(matchingConstraints.blockId))) { | ||
} | ||
// We don't need to validate unfurl, we'll just cooerce it to a boolean | ||
// We don't need to validate unfurl, we'll just coerce it to a boolean | ||
return false; | ||
} | ||
/** | ||
* Validates properties of a matching constraints object specific to registering a view submission or view closed | ||
* request | ||
* @param viewConstraints - object describing the constraints on a view submission or view closed handler | ||
* @returns `false` represents successful validation, an error represents failure and describes why validation failed. | ||
*/ | ||
function validateViewConstraints(viewConstraints) { | ||
if (viewConstraints.externalId === null || | ||
((!util_1.isFalsy(viewConstraints.externalId) && | ||
!(lodash_isstring_1.default(viewConstraints.externalId) || lodash_isregexp_1.default(viewConstraints.externalId))))) { | ||
return new TypeError('External ID must be a string or RegExp'); | ||
} | ||
if (viewConstraints.viewId === null || | ||
(!util_1.isFalsy(viewConstraints.viewId) && !lodash_isstring_1.default(viewConstraints.viewId))) { | ||
return new TypeError('View ID must be a string'); | ||
} | ||
return false; | ||
} | ||
/** | ||
* An adapter for Slack's interactive message components such as buttons, menus, and dialogs. | ||
@@ -118,3 +140,3 @@ * @typicalname slackInteractions | ||
* @param signingSecret - Slack app signing secret used to authenticate request | ||
* @param options.syncResponseTimeout - number of milliseconds to wait before flushing a syncrhonous response to an | ||
* @param options.syncResponseTimeout - number of milliseconds to wait before flushing a synchronous response to an | ||
* incoming request and falling back to an asynchronous response. | ||
@@ -230,5 +252,5 @@ * @param options.lateResponseFallbackEnabled - whether or not promises that resolve after the syncResponseTimeout can | ||
* :-----:|:-----:|:-----:|:-----:|:-----:|:-----: | ||
* **Button Press**| Message in response | When resolved before `syncResposeTimeout` or `lateResponseFallbackEnabled: false`, message in response<br />When resolved after `syncResponseTimeout` and `lateResponseFallbackEnabled: true`, message in request to `response_url` | Empty response | Message in request to `response_url` | Create a new message instead of replacing using `replace_original: false` | ||
* **Menu Selection**| Message in response | When resolved before `syncResposeTimeout` or `lateResponseFallbackEnabled: false`, message in response<br />When resolved after `syncResponseTimeout` and `lateResponseFallbackEnabled: true`, message in request to `response_url` | Empty response | Message in request to `response_url` | Create a new message instead of replacing using `replace_original: false` | ||
* **Message Action** | Message in response | When resolved before `syncResposeTimeout` or `lateResponseFallbackEnabled: false`, message in response<br />When resolved after `syncResponseTimeout` and `lateResponseFallbackEnabled: true`, message in request to `response_url` | Empty response | Message in request to `response_url` | | ||
* **Button Press**| Message in response | When resolved before `syncResponseTimeout` or `lateResponseFallbackEnabled: false`, message in response<br />When resolved after `syncResponseTimeout` and `lateResponseFallbackEnabled: true`, message in request to `response_url` | Empty response | Message in request to `response_url` | Create a new message instead of replacing using `replace_original: false` | ||
* **Menu Selection**| Message in response | When resolved before `syncResponseTimeout` or `lateResponseFallbackEnabled: false`, message in response<br />When resolved after `syncResponseTimeout` and `lateResponseFallbackEnabled: true`, message in request to `response_url` | Empty response | Message in request to `response_url` | Create a new message instead of replacing using `replace_original: false` | ||
* **Message Action** | Message in response | When resolved before `syncResponseTimeout` or `lateResponseFallbackEnabled: false`, message in response<br />When resolved after `syncResponseTimeout` and `lateResponseFallbackEnabled: true`, message in request to `response_url` | Empty response | Message in request to `response_url` | | ||
* **Dialog Submission**| Error list in response | Error list in response | Empty response | Message in request to `response_url` | Returning a Promise that takes longer than 3 seconds to resolve can result in the user seeing an error. Warning logged if a promise isn't completed before `syncResponseTimeout`. | ||
@@ -284,2 +306,49 @@ * | ||
}; | ||
/** | ||
* Add a handler for view submission. | ||
* | ||
* The value returned from the `callback` determines the response sent back to Slack. The handler can return a plain | ||
* object with a `response_action` property to dismiss the modal, push a view into the modal, display validation | ||
* errors, or update the view. Alternatively, the handler can return a Promise for this kind of object, which resolves | ||
* before `syncResponseTimeout` or `lateResponseFallbackEnabled: false`, to perform the same response actions. If the | ||
* Promise resolves afterwards or `lateResponseFallbackEnabled: true` then the modal will be dismissed. If the handler | ||
* returns `undefined` the modal will be dismissed. | ||
* | ||
* @param matchingConstraints - the callback ID (as a string or RegExp) or an object describing the constraints to | ||
* match view submissions for the handler. | ||
* @param callback - the function to run when an view submission is matched | ||
* @returns this instance (for chaining) | ||
*/ | ||
SlackMessageAdapter.prototype.viewSubmission = function (matchingConstraints, callback) { | ||
var viewConstraints = formatMatchingConstraints(matchingConstraints); | ||
var error = validateConstraints(viewConstraints) || validateViewConstraints(viewConstraints); | ||
if (error) { | ||
debug('view submission could not be registered: %s', error.message); | ||
throw error; | ||
} | ||
var storableConstraints = Object.assign(viewConstraints, { | ||
handlerType: "view_submission" /* ViewSubmission */, | ||
}); | ||
return this.registerCallback(storableConstraints, callback); | ||
}; | ||
/** | ||
* Add a handler for view closed interaction. The handler should not return a value. | ||
* | ||
* @param matchingConstraints - the callback ID (as a string or RegExp) or an object describing the constraints to | ||
* match view closed interactions for the handler. | ||
* @param callback - the function to run when an view closed interaction is matched | ||
* @returns this instance (for chaining) | ||
*/ | ||
SlackMessageAdapter.prototype.viewClosed = function (matchingConstraints, callback) { | ||
var viewConstraints = formatMatchingConstraints(matchingConstraints); | ||
var error = validateConstraints(viewConstraints) || validateViewConstraints(viewConstraints); | ||
if (error) { | ||
debug('view closed could not be registered: %s', error.message); | ||
throw error; | ||
} | ||
var storableConstraints = Object.assign(viewConstraints, { | ||
handlerType: "view_closed" /* ViewClosed */, | ||
}); | ||
return this.registerCallback(storableConstraints, callback); | ||
}; | ||
/* Interface for HTTP servers (like express middleware) */ | ||
@@ -351,4 +420,5 @@ /** | ||
// * "no replacement" for message actions | ||
// * "submission is valid" for dialog submissions | ||
// * "submission is valid" for dialog submissions and view submissions | ||
// * "no suggestions" for menu options TODO: check that this is true | ||
// * "ack" for view closed | ||
return Promise.resolve({ status: 200 }); | ||
@@ -370,6 +440,9 @@ }; | ||
if (!util_1.isFalsy(constraints.callbackId)) { | ||
if (lodash_isstring_1.default(constraints.callbackId) && payload.callback_id !== constraints.callbackId) { | ||
// The callback ID is located at a different path in the payload for view submission and view closed | ||
var callbackId = (constraints.handlerType === "view_submission" /* ViewSubmission */ || | ||
constraints.handlerType === "view_closed" /* ViewClosed */) ? payload.view.callback_id : payload.callback_id; | ||
if (lodash_isstring_1.default(constraints.callbackId) && callbackId !== constraints.callbackId) { | ||
return false; | ||
} | ||
if (lodash_isregexp_1.default(constraints.callbackId) && !constraints.callbackId.test(payload.callback_id)) { | ||
if (lodash_isregexp_1.default(constraints.callbackId) && !constraints.callbackId.test(callbackId)) { | ||
return false; | ||
@@ -466,2 +539,29 @@ } | ||
} | ||
if (constraints.handlerType === "view_submission" /* ViewSubmission */ || | ||
constraints.handlerType === "view_closed" /* ViewClosed */) { | ||
// a payload that represents a view submission always has a type property set to view_submission, | ||
// a payload that represents a view closed interaction always has a type property set to view_closed | ||
if (!util_1.isFalsy(payload.type) && | ||
(constraints.handlerType === "view_submission" /* ViewSubmission */ && payload.type !== 'view_submission') || | ||
(constraints.handlerType === "view_closed" /* ViewClosed */ && payload.type !== 'view_closed')) { | ||
return false; | ||
} | ||
// if there's no view in this payload, this payload is malformed - abort matching. | ||
if (util_1.isFalsy(payload.view)) { | ||
return false; | ||
} | ||
// if the view ID constraint is specified, only continue if it matches | ||
if (!util_1.isFalsy(constraints.viewId) && payload.view.id !== constraints.viewId) { | ||
return false; | ||
} | ||
// if the external ID constraint is specified, only continue if it matches | ||
if (!util_1.isFalsy(constraints.externalId)) { | ||
if (lodash_isstring_1.default(constraints.externalId) && payload.view.external_id !== constraints.externalId) { | ||
return false; | ||
} | ||
if (lodash_isregexp_1.default(constraints.externalId) && !constraints.externalId.test(payload.view.external_id)) { | ||
return false; | ||
} | ||
} | ||
} | ||
// if there's no reason to eliminate this callback, then its a match! | ||
@@ -481,2 +581,6 @@ return true; | ||
})(ResponseStatus || (ResponseStatus = {})); | ||
function hasBlockRelatedConstraints(constraints) { | ||
var asBlockRelatedConstraints = constraints; | ||
return !(util_1.isFalsy(asBlockRelatedConstraints.blockId) && util_1.isFalsy(asBlockRelatedConstraints.actionId)); | ||
} | ||
//# sourceMappingURL=adapter.js.map |
{ | ||
"name": "@slack/interactive-messages", | ||
"version": "1.3.0", | ||
"version": "1.4.0", | ||
"description": "Official library for using the Slack Platform's Interactive Buttons, Menus, Dialogs, Actions, and Block Actions", | ||
@@ -86,3 +86,3 @@ "author": "Slack Technologies, Inc.", | ||
}, | ||
"gitHead": "3a0e06381d632bb6aae74016c45158e962d15ea5" | ||
"gitHead": "1503609d79abf035e9e21bad7360e124e4211594" | ||
} |
129
README.md
@@ -360,2 +360,113 @@ # Slack Interactive Messages for Node | ||
### Handling view submission and view closed interactions | ||
View submissions are generated when a user clicks on the submission button of a | ||
[Modal](https://api.slack.com/surfaces/modals). View closed interactions are generated when a user clicks on the cancel | ||
button of a Modal, or dismisses the modal using the `×` in the corner. | ||
Apps register functions, called **handlers**, to be triggered when a submissions are received by the adapter using the | ||
`.viewSubmission(constraints, handler)` method or when closed interactions are received using the | ||
`.viewClosed(constraints, handler)` method. When registering a handler, you describe which submissions and closed | ||
interactions you'd like the handler to match using **constraints**. Constraints are [described in detail](#constraints) | ||
below. The adapter will call the handler whose constraints match the interaction best. | ||
These handlers receive a single `payload` argument. The `payload` describes the | ||
[view submission](https://api.slack.com/reference/interaction-payloads/views#view_submission) or | ||
[view closed](https://api.slack.com/reference/interaction-payloads/views#view_closed) | ||
interaction that occurred. | ||
For view submissions, handlers can return an object, or a `Promise` for a object which must resolve within the | ||
`syncResponseTimeout` (default: 2500ms). The contents of the object depend on what you'd like to happen to the view. | ||
Your app can update the view, push a new view into the stack, close the view, or display validation errors to the user. | ||
In the documentation, the shape of the objects for each of those possible outcomes, which the handler would return, are | ||
described as `response_action`s. If the handler returns no value, or a Promise that resolves to no value, the view will | ||
simply be dismissed on submission. | ||
View closed interactions only occur if the view was opened with the `notify_on_close` property set to `true`. For these | ||
interactions the handler should not return a value. | ||
```javascript | ||
const { createMessageAdapter } = require('@slack/interactive-messages'); | ||
const slackSigningSecret = process.env.SLACK_SIGNING_SECRET; | ||
const slackInteractions = createMessageAdapter(slackSigningSecret); | ||
const port = process.env.PORT || 3000; | ||
// Example of handling a simple view submission | ||
slackInteractions.viewSubmission('simple_modal_callback_id', (payload) => { | ||
// Log the input elements from the view submission. | ||
console.log(payload.view.state); | ||
// The previous value is an object keyed by block_id, which contains objects keyed by action_id, | ||
// which contains value properties that contain the input data. Let's log one specific value. | ||
console.log(payload.view.state.my_block_id.my_action_id.value); | ||
// Validate the inputs (errors is of the shape in https://api.slack.com/surfaces/modals/using#displaying_errors) | ||
const errors = validate(payload.view.state); | ||
// Return validation errors if there were errors in the inputs | ||
if (errors) { | ||
return errors; | ||
} | ||
// Process the submission | ||
doWork(); | ||
}); | ||
// Example of handling a view submission which pushes another view onto the stack | ||
slackInteractions.viewSubmission('first_step_callback_id', () => { | ||
const errors = validate(payload.view.state); | ||
if (errors) { | ||
return errors; | ||
} | ||
// Process the submission (needs to complete under 2.5 seconds) | ||
return doWork() | ||
.then(() => { | ||
return { | ||
response_action: 'push', | ||
view: { | ||
type: 'modal', | ||
callback_id: 'second_step_callback_id', | ||
title: { | ||
type: 'plain_text', | ||
text: 'Second step', | ||
}, | ||
blocks: [ | ||
{ | ||
type: 'input', | ||
block_id: 'last_thing', | ||
element: { | ||
type: 'plain_text_input', | ||
action_id: 'text', | ||
}, | ||
label: { | ||
type: 'plain_text', | ||
text: 'One last thing...', | ||
}, | ||
}, | ||
], | ||
}, | ||
}; | ||
}) | ||
.catch((error) => { | ||
// Log the error. In your app, inform the user of a failure using a DM or some other area in Slack. | ||
console.log(error); | ||
}); | ||
}); | ||
// Example of handling view closed | ||
slackInteractions.viewClosed('my_modal_callback_id', (payload) => { | ||
// If you accumulated partial state using block actions, now is a good time to clear it | ||
clearPartialState(); | ||
}); | ||
(async () => { | ||
const server = await slackInteractions.start(port); | ||
console.log(`Listening for events on ${server.address().port}`); | ||
})(); | ||
``` | ||
--- | ||
### Constraints | ||
@@ -369,10 +480,12 @@ | ||
| Property name | Type | Description | Used with `.actions()` | Used with `.options()` | | ||
|---------------|------|-------------|------------------------|------------------------| | ||
| `callbackId` | `string` or `RegExp` | Match the `callback_id` for attachment or dialog | ✅ | ✅ | | ||
| `blockId` | `string` or `RegExp` | Match the `block_id` for a block action | ✅ | ✅ | | ||
| `actionId` | `string` or `RegExp` | Match the `action_id` for a block action | ✅ | ✅ | | ||
| `type` | any block action element type or `message_actions` or `dialog_submission` or `button` or `select` | Match the kind of interaction | ✅ | 🚫 | | ||
| `within` | `block_actions` or `interactive_message` or `dialog` | Match the source of options request | 🚫 | ✅ | | ||
| `unfurl` | `boolean` | Whether or not the `button`, `select`, or `block_action` occurred in an App Unfurl | ✅ | 🚫 | | ||
| Property name | Type | Description | Used with `.action()` | Used with `.options()` | Used with `.viewSubmission()` and `.viewClosed()` | | ||
|---------------|------|-------------|-----------------------|------------------------|---------------------------------------------------| | ||
| `callbackId` | `string` or `RegExp` | Match the `callback_id` for attachment or dialog | ✅ | ✅ | ✅ | | ||
| `blockId` | `string` or `RegExp` | Match the `block_id` for a block action | ✅ | ✅ | 🚫 | | ||
| `actionId` | `string` or `RegExp` | Match the `action_id` for a block action | ✅ | ✅ | 🚫 | | ||
| `type` | any block action element type or `message_actions` or `dialog_submission` or `button` or `select` | Match the kind of interaction | ✅ | 🚫 | 🚫 | | ||
| `within` | `block_actions` or `interactive_message` or `dialog` | Match the source of options request | 🚫 | ✅ | 🚫 | | ||
| `unfurl` | `boolean` | Whether or not the `button`, `select`, or `block_action` occurred in an App Unfurl | ✅ | 🚫 | 🚫 | | ||
| `viewId` | `string` | Match the `view_id` for view submissions | 🚫 | 🚫 | ✅ | | ||
| `externalId` | `string` or `RegExp` | Match the `external_id` for view submissions | 🚫 | 🚫 | ✅ | | ||
@@ -379,0 +492,0 @@ All of the properties are optional, its just a matter of how specific you want to the handler's behavior to be. A |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
115404
1206
615