Skip to content

Commit

Permalink
updated based on feedback
Browse files Browse the repository at this point in the history
  • Loading branch information
stevengill committed Mar 24, 2020
1 parent 779b979 commit 62975af
Show file tree
Hide file tree
Showing 12 changed files with 133 additions and 83 deletions.
14 changes: 6 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,21 @@ app.event(eventType, fn);

// Listen for an action from a block element (buttons, menus, etc)
app.action(actionId, fn);
// Listen for dialog submission, message shortcut, or legacy action

// Listen for dialog submission, or legacy action
app.action({ callback_id: callbackId }, fn);

// Listen for a global shortcut
// Listen for a global shortcut, or message shortcut
app.shortcut(callbackId, fn);

// Listen for modal view requests
app.view(callbackId, fn);

// Listen for a slash command
app.command(commandName, fn);

// Listen for options requests (from menus with an external data source)
app.options(actionId, fn);

// Listen for modal view requests
app.view(callbackId, fn);
```

There's a special method that's provided as a convenience to handle Events API events with the type `message`. Also, you
Expand Down Expand Up @@ -158,9 +159,6 @@ Depending on the type of incoming event a listener is meant for, `ack()` should
to to update the message with a simple message, or an `object` to replace it with a complex message. Replacing the
message to remove the interactive elements is a best practice for any action that should only be performed once.

If an app does not call `ack()` within the time limit, Bolt for JavaScript will generate an error. See [handling
errors](#handling-errors) for more details.

The following is an example of acknowledging a dialog submission:

```js
Expand Down
3 changes: 1 addition & 2 deletions docs/_advanced/receiver.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ order: 8
---

<div class="section-content">
A receiver is responsible for handling and parsing any incoming events from Slack, then sending the event to the Bolt for JavaScript app so it can add context and pass it to your app’s listeners. Receivers must conform to the Receiver interface:
A receiver is responsible for handling and parsing any incoming events from Slack then sending it to the app, so that the app can add context and pass the event to your listeners. Receivers must conform to the Receiver interface:

| Method | Parameters | Return type |
|--------------|----------------------------------|-------------|
Expand All @@ -16,7 +16,6 @@ A receiver is responsible for handling and parsing any incoming events from Slac

`init()` is called after Bolt for JavaScript app is created. This method gives the receiver a reference to an `App` to store so that it can call:
* `await app.processEvent(event)` whenever your app receives an event from Slack. It will reject if there is an unhandled error.
* `await app.handleError` whenever you need to route errors to the global error handler. This gives the developer a chance to handle it.

To use a custom receiver, you can pass it into the constructor when initializing your Bolt for JavaScript app. Here is what a basic custom receiver might look like.

Expand Down
2 changes: 1 addition & 1 deletion docs/_basic/acknowledging_requests.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
title: Acknowledging incoming events
title: Acknowledging events
lang: en
slug: acknowledge
order: 7
Expand Down
111 changes: 87 additions & 24 deletions docs/_basic/listening_responding_shortcuts.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,48 +13,49 @@ Shortcuts are invokable UI elements within Slack clients. For global shortcuts,

Shortcuts must be acknowledged with `ack()` to inform Slack that your app has received the event.

Shortcut payloads include a `trigger_id` which an app can use to [open a modal](#creating-modals) that confirms the action the user is taking.
Shortcuts include a `trigger_id` which an app can use to [open a modal](#creating-modals) that confirms the action the user is taking.

⚠️ Note that global shortcut payloads do **not** include a channel ID. If your app needs access to a channel ID, you may use a [`conversations_select`](https://api.slack.com/reference/block-kit/block-elements#conversation_select) element within a modal. Message shortcuts do include channel ID.
⚠️ Note that global shortcuts do **not** include a channel ID. If your app needs access to a channel ID, you may use a [`conversations_select`](https://api.slack.com/reference/block-kit/block-elements#conversation_select) element within a modal. Message shortcuts do include channel ID.

</div>

```javascript
// The open_modal shortcut opens a plain old modal
app.shortcut('open_modal', async ({ payload, ack, context }) => {
// Acknowledge global shortcut request
ack();
app.shortcut('open_modal', async ({ shortcut, ack, context, client }) => {

try {
// Call the views.open method using the built-in WebClient
const result = await app.client.views.open({
// Acknowledge shortcut request
await ack();

// Call the views.open method using one of the built-in WebClients
const result = await client.views.open({
// The token you used to initialize your app is stored in the `context` object
token: context.botToken,
trigger_id: payload.trigger_id,
trigger_id: shortcut.trigger_id,
view: {
"type": "modal",
"title": {
"type": "plain_text",
"text": "My App"
type: "modal",
title: {
type: "plain_text",
text: "My App"
},
"close": {
"type": "plain_text",
"text": "Close"
close: {
type: "plain_text",
text: "Close"
},
"blocks": [
blocks: [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "About the simplest modal you could conceive of :smile:\n\nMaybe <https://api.slack.com/reference/block-kit/interactive-components|*make the modal interactive*> or <https://api.slack.com/surfaces/modals/using#modifying|*learn more advanced modal use cases*>."
type: "section",
text: {
type: "mrkdwn",
text: "About the simplest modal you could conceive of :smile:\n\nMaybe <https://api.slack.com/reference/block-kit/interactive-components|*make the modal interactive*> or <https://api.slack.com/surfaces/modals/using#modifying|*learn more advanced modal use cases*>."
}
},
{
"type": "context",
"elements": [
type: "context",
elements: [
{
"type": "mrkdwn",
"text": "Psssst this modal was designed using <https://api.slack.com/tools/block-kit-builder|*Block Kit Builder*>"
type: "mrkdwn",
text: "Psssst this modal was designed using <https://api.slack.com/tools/block-kit-builder|*Block Kit Builder*>"
}
]
}
Expand All @@ -69,3 +70,65 @@ app.shortcut('open_modal', async ({ payload, ack, context }) => {
}
});
```

<details class="secondary-wrapper">
<summary class="section-head" markdown="0">
<h4 class="section-head">Listening to shortcuts using a constraint object</h4>
</summary>

<div class="secondary-content" markdown="0">
You can use a constraints object to listen to `callback_id`s, and `type`s. Constraints in the object can be of type string or RegExp object.
</div>

```javascript
// Your middleware will only be called when the callback_id matches 'open_modal' AND the type matches 'message_action'
app.shortcut({ callback_id: 'open_modal', type: 'message_action' }, async ({ action, ack, context, client }) => {
try {
// Acknowledge shortcut request
await ack();

// Call the views.open method using one of the built-in WebClients
const result = await client.views.open({
// The token you used to initialize your app is stored in the `context` object
token: context.botToken,
trigger_id: shortcut.trigger_id,
view: {
type: "modal",
title: {
type: "plain_text",
text: "My App"
},
close: {
type: "plain_text",
text: "Close"
},
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: "About the simplest modal you could conceive of :smile:\n\nMaybe <https://api.slack.com/reference/block-kit/interactive-components|*make the modal interactive*> or <https://api.slack.com/surfaces/modals/using#modifying|*learn more advanced modal use cases*>."
}
},
{
type: "context",
elements: [
{
type: "mrkdwn",
text: "Psssst this modal was designed using <https://api.slack.com/tools/block-kit-builder|*Block Kit Builder*>"
}
]
}
]
}
});

console.log(result);
}
catch (error) {
console.error(error);
}
});
```

</details>
2 changes: 1 addition & 1 deletion docs/_basic/opening_modals.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
title: Opening modals using views.open
title: Opening modals
lang: en
slug: creating-modals
order: 10
Expand Down
2 changes: 1 addition & 1 deletion docs/_tutorials/getting_started.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ This is a basic example, but it gives you a place to start customizing your app

### Sending and responding to actions

To use features like buttons, select menus, datepickers, and dialogs, you’ll need to enable interactivity. Similar to events, you'll need to specify a URL for Slack to send the action (such as *user clicked a button*).
To use features like buttons, select menus, datepickers, dialogs, and shortcuts, you’ll need to enable interactivity. Similar to events, you'll need to specify a URL for Slack to send the action (such as *user clicked a button*).

Back on your app configuration page, click on **Interactive Components** on the left side. You'll see that there's another **Request URL** box.

Expand Down
10 changes: 5 additions & 5 deletions docs/_tutorials/hubot_migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,14 @@ Bolt for JavaScript uses a method called `event()` that allows you to listen to
[Read more about listening to events](https://slack.dev/bolt/concepts#event-listening).

### Using Web API methods with Bolt for JavaScript
In Hubot, you needed to import the `WebClient` package from `@slack/client`. Bolt for JavaScript imports a `WebClient` instance for you by default, accessible from `app.client`.
In Hubot, you needed to import the `WebClient` package from `@slack/client`. Bolt for JavaScript imports a `WebClient` instance for you by default, and exposes it as the `client` argument available on all listeners.

To use the built-in `WebClient`, you’ll need to pass the token used to instantiate your app or the token associated with the team your request is coming from. This is found on the `context` object passed in to your listener functions. For example, to add a reaction to a message, you’d use:

```javascript
app.message('react', async ({ message, context }) => {
app.message('react', async ({ message, context, client }) => {
try {
const result = await app.client.reactions.add({
const result = await client.reactions.add({
token: context.botToken,
name: 'star',
channel: message.channel,
Expand All @@ -130,7 +130,7 @@ app.message('react', async ({ message, context }) => {
});
```

> 👨‍💻👩‍💻Change your Web API calls to use the built-in client at `app.client`.
> 👨‍💻👩‍💻Change your Web API calls to use one the `client` argument.
[Read more about using the Web API with Bolt](https://slack.dev/bolt/concepts#web-api).

Expand All @@ -143,7 +143,7 @@ Bolt for JavaScript only has two kinds of middleware — global and listener:

In Bolt for JavaScript, both kinds of middleware must call `await next()` to pass control of execution from one middleware to the next. If your middleware encounters an error during execution, you can `throw` it and the error will be bubbled up through the previously-executed middleware chain.

To migrate your existing middleware functions, it’s evident that Hubot’s receive middleware aligns with the use case for global middleware in Bolt for JavaScript. And Hubot and Bolt’s listener middleware are nearly the same. To migrate Hubot’s response middleware, you can use a Bolt for JavaScript concept called a post-process function.
To migrate your existing middleware functions, it’s evident that Hubot’s receive middleware aligns with the use case for global middleware in Bolt for JavaScript. And Hubot and Bolt’s listener middleware are nearly the same. To migrate Hubot’s response middleware, wrap Bolt for JavaScript's `say()` or `respond()` in your own function, and then call it.

If your middleware needs to perform post-processing of an event, you can call `await next()` and any code after will be processed after the downstream middleware has been called.

Expand Down
17 changes: 0 additions & 17 deletions integration-tests/types/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,6 @@ app.action({ type: 'Something wrong' }, ({ action }) => {
return action;
});

/* Not working
// Should error because message_action doesn't have type action_id
// $ Expect Error
app.action({ type: 'message_action', action_id: 'dafasf' }, ({ action }) => {
return action;
});
*/

// $ExpectType void
app.action({ type: 'block_actions' }, async ({
action, // $ExpectType BlockElementAction
Expand All @@ -37,12 +29,3 @@ app.action({ type: 'dialog_submission' }, async ({
}) => {
return action;
});

/* Not working
// Should error because MessageAction doesn't have an action_id
// $ Expect Error
app.actiong<MessageAction>({ action_id: 'dafasf' }, ({ action }) => {
// NOT WORKING
return action;
});
*/
15 changes: 3 additions & 12 deletions integration-tests/types/shortcut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,18 @@ app.shortcut({ type: 'Something wrong' }, ({ shortcut }) => {
return shortcut;
});

// Action in listener should be - MessageAction
// Shortcut in listener should be - MessageShortcut
// $ExpectType void
app.shortcut({ type: 'message_action' }, async ({
shortcut, // $ExpectType SlackShortcut
shortcut, // $ExpectType MessageShortcut
}) => {
return shortcut;
});

// If action is parameterized with MessageAction, action argument in callback should be type MessageAction
// If shortcut is parameterized with MessageShortcut, shortcut argument in callback should be type MessageShortcut
// $ExpectType void
app.shortcut<MessageShortcut>({}, async ({
shortcut, // $ExpectType MessageShortcut
}) => {
return shortcut;
});

/* Not Working
// Should error because MessageAction doesn't have an action_id
// $ Expect Error
app.shortcut<MessageShortcut>({ action_id: 'dafasf' }, ({ shortcut }) => {
// NOT WORKING
return shortcut;
});
*/
24 changes: 18 additions & 6 deletions src/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,18 +326,30 @@ export default class App {
callbackId: string | RegExp,
...listeners: Middleware<SlackShortcutMiddlewareArgs<Shortcut>>[]
): void;
public shortcut<Shortcut extends SlackShortcut = SlackShortcut>(
constraints: ShortcutConstraints,
...listeners: Middleware<SlackShortcutMiddlewareArgs<Shortcut>>[]
public shortcut<Shortcut extends SlackShortcut = SlackShortcut,
Constraints extends ShortcutConstraints<Shortcut> = ShortcutConstraints<Shortcut>>(
constraints: Constraints,
...listeners: Middleware<SlackShortcutMiddlewareArgs<Extract<Shortcut, { type: Constraints['type'] }>>>[]
): void;
public shortcut<Shortcut extends SlackShortcut = SlackShortcut>(
callbackIdOrConstraints: string | RegExp | ShortcutConstraints,
...listeners: Middleware<SlackShortcutMiddlewareArgs<Shortcut>>[]
public shortcut<Shortcut extends SlackShortcut = SlackShortcut,
Constraints extends ShortcutConstraints<Shortcut> = ShortcutConstraints<Shortcut>>(
callbackIdOrConstraints: string | RegExp | Constraints,
...listeners: Middleware<SlackShortcutMiddlewareArgs<Extract<Shortcut, { type: Constraints['type'] }>>>[]
): void {
const constraints: ShortcutConstraints =
(typeof callbackIdOrConstraints === 'string' || util.types.isRegExp(callbackIdOrConstraints)) ?
{ callback_id: callbackIdOrConstraints } : callbackIdOrConstraints;

// Fail early if the constraints contain invalid keys
const unknownConstraintKeys = Object.keys(constraints)
.filter(k => (k !== 'callback_id' && k !== 'type'));
if (unknownConstraintKeys.length > 0) {
this.logger.error(
`Slack listener cannot be attached using unknown constraint keys: ${unknownConstraintKeys.join(', ')}`,
);
return;
}

this.listeners.push(
[onlyShortcuts, matchConstraints(constraints), ...listeners] as Middleware<AnyMiddlewareArgs>[],
);
Expand Down
5 changes: 0 additions & 5 deletions src/helpers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,6 @@ describe('getTypeAndConversation()', () => {

function createFakeActions(conversationId: string): any[] {
return [
// Body for a message action
// {
// type: 'message_action',
// channel: { id: conversationId },
// },
// Body for a dialog submission
{
type: 'dialog_submission',
Expand Down
11 changes: 10 additions & 1 deletion src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import {
SlackCommandMiddlewareArgs,
SlackOptionsMiddlewareArgs,
SlackActionMiddlewareArgs,
SlackShortcutMiddlewareArgs,
SlackAction,
OptionsSource,
MessageShortcut,
} from './types';

/**
Expand Down Expand Up @@ -55,11 +57,18 @@ export function getTypeAndConversation(body: any): { type?: IncomingEventType, c
conversationId: actionBody.channel !== undefined ? actionBody.channel.id : undefined,
};
}
if (body.type === 'shortcut' || body.type === 'message_action') {
if (body.type === 'shortcut') {
return {
type: IncomingEventType.Shortcut,
};
}
if (body.type === 'message_action') {
const shortcutBody = (body as SlackShortcutMiddlewareArgs<MessageShortcut>['body']);
return {
type: IncomingEventType.Shortcut,
conversationId: shortcutBody.channel !== undefined ? shortcutBody.channel.id : undefined,
};
}
if (body.type === 'view_submission' || body.type === 'view_closed') {
return {
type: IncomingEventType.ViewAction,
Expand Down

0 comments on commit 62975af

Please sign in to comment.