Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v2] Add documentation for async middleware and App#processEvent #381

Merged
merged 5 commits into from
Mar 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 71 additions & 45 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ In fact, middleware can be _chained_ so that any number of middleware functions
and they each run in the order they were added to the chain.

Middleware are just functions - nearly identical to listener functions. They can choose to respond right away, to extend
the `context` argument and continue, or trigger an error. The only difference is that middleware use a special `next()`
the `context` argument and continue, or trigger an error. The only difference is that middleware use a special `next`
argument, a function that's called to let the app know it can continue to the next middleware (or listener) in the
chain.

Expand Down Expand Up @@ -230,7 +230,7 @@ const app = new App({
app.use(authWithAcme);

// The listener now has access to the user details
app.message('whoami', ({ say, context }) => { say(`User Details: ${JSON.stringify(context.user)}`) });
app.message('whoami', async ({ say, context }) => { await say(`User Details: ${JSON.stringify(context.user)}`) });

(async () => {
// Start the app
Expand All @@ -240,32 +240,33 @@ app.message('whoami', ({ say, context }) => { say(`User Details: ${JSON.stringif

// Authentication middleware - Calls Acme identity provider to associate the incoming event with the user who sent it
// It's a function just like listeners, but it also uses the next argument
function authWithAcme({ payload, context, say, next }) {
async function authWithAcme({ payload, context, say, next }) {
const slackUserId = payload.user;

// Assume we have a function that can take a Slack user ID as input to find user details from the provider
acme.lookupBySlackId(slackUserId)
.then((user) => {
// When the user lookup is successful, add the user details to the context
context.user = user;

// Pass control to the next middleware (if there are any) and the listener functions
next();
})
.catch(async (error) => {
// Uh oh, this user hasn't registered with Acme. Send them a registration link, and don't let the
// middleware/listeners continue
if (error.message === 'Not Found') {
try {
// Assume we have a function that can take a Slack user ID as input to find user details from the provider
const user = await acme.lookupBySlackId(slackUserId);

// When the user lookup is successful, add the user details to the context
context.user = user;
} catch (error) {
// middleware/listeners continue
stevengill marked this conversation as resolved.
Show resolved Hide resolved
if (error.message === 'Not Found') {
// In the real world, you would need to check if the say function was defined, falling back to the respond
// function if not, and then falling back to only logging the error as a last resort.
await say(`I'm sorry <@${slackUserId}, you aren't registered with Acme. Please use <https://acme.com/register> to use this app.`);
return;
}

// This middleware doesn't know how to handle any other errors. Pass control to the previous middleware (if there
// are any) or the global error handler.
next(error);
});
}

// This middleware doesn't know how to handle any other errors.
// Pass control to the previous middleware (if there are any) or the global error handler.
throw error;
}

// Pass control to the next middleware (if there are any) and the listener functions
// Note: You probably don't want to call this inside a `try` block, or any middleware
// after this one that throws will be caught by it.
await next();
}
```

Expand All @@ -282,9 +283,9 @@ before the listener attached to `message` events:

```js
// Listener middleware - filters out messages that have subtype 'bot_message'
function noBotMessages({ message, next }) {
async function noBotMessages({ message, next }) {
if (!message.subtype || message.subtype !== 'bot_message') {
next();
await next();
}
}

Expand Down Expand Up @@ -317,36 +318,61 @@ The examples above all illustrate how middleware can be used to process an event
middleware in the chain) run. However, middleware can be designed to process the event _after_ the listener finishes.
In general, a middleware can run both before and after the remaining middleware chain.

In order to process the event after the listener, the middleware passes a function to `next()`. The function receives
two arguments:
How you use `next` can
have four different effects:

* `error` - The value is falsy when the middleware chain finished handling the event normally. When a later
middleware calls `next(error)` (where `error` is an `Error`), then this value is set to the `error`.
* **To both preprocess and post-process events** - You can choose to do work going _before_ listener functions by putting code
stevengill marked this conversation as resolved.
Show resolved Hide resolved
before `await next()` and _after_ by putting code after `await next()`. `await next()` passes control down the middleware
stack in the order it was defined, then back up it in reverse order.

* `done` - A callback that **must** be called when processing is complete. When there is no error, or the incoming
error has been handled, `done()` should be called with no parameters. If instead the middleware is propagating an
error up the middleware chain, `done(error)` should be called with the error as its only parameter.
* **To throw an error** - If you don't want to handle an error in a listener, or want to let an upstream listener
handle it, you can simply **not** call `await next()` and `throw` an `Error`.

* **To handle mid-processing errors** - While not commonly used, as `App#error` is essentially a global version of this,
you can catch any error of any downstream middleware by surrounding `await next()` in a `try-catch` block.

* **To break the middleware chain** - You can stop middleware from progressing by simply not calling `await next()`.
By it's nature, throwing an error tends to be an example of this as code after a throw isn't executed.

The following example shows a global middleware that calculates the total processing time for the middleware chain by
calculating the time difference from before the listener and after the listener:

```js
function logProcessingTime({ next }) {
async function logProcessingTime({ next }) {
const startTimeMs = Date.now();
next((error, done) => {
// This middleware doesn't deal with any errors, so it propagates any truthy value to the previous middleware
if (error) {
done(error);
return;
}

const endTimeMs = Date.now();
console.log(`Total processing time: ${endTimeMs - startTimeMs}`);

// Continue normally
done();
});

await next();

const endTimeMs = Date.now();
console.log(`Total processing time: ${endTimeMs - startTimeMs}`);
}

app.use(logProcessingTime)
```

The next example shows a series of global middleware where one generates an error
and the other handles it.

```js
app.use(async ({ next, say }) => {
try {
await next();
} catch (error) {
if (error.message === 'channel_not_found') {
// Handle known errors
await say('It appears we can't access that channel')
} else {
// Rethrow for an upstream error handler
throw error;
}
}
})

app.use(async () => {
throw new Error('channel_not_found')
})

app.use(async () => {
// This never gets called as the middleware above never calls next
})
```
2 changes: 1 addition & 1 deletion docs/_advanced/context.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ async function addTimezoneContext({ payload, context, next }) {
context.tz_offset = user.tz_offset;

// Pass control to the next middleware function
next();
await next();
}

app.command('request', addTimezoneContext, async ({ command, ack, context }) => {
Expand Down
3 changes: 3 additions & 0 deletions docs/_advanced/ja_context.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ async function addTimezoneContext({ payload, context, next }) {

// ユーザのタイムゾーン情報を追加
context.tz_offset = user.tz_offset;

// 制御とリスナー関数を次のミドルウェアに引き渡し
stevengill marked this conversation as resolved.
Show resolved Hide resolved
await next();
}

app.command('request', addTimezoneContext, async ({ command, ack, context }) => {
Expand Down
32 changes: 16 additions & 16 deletions docs/_advanced/ja_middleware_global.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,29 @@ order: 4
---

<div class="section-content">
グローバルミドルウェアは、すべての着信イベントに対して、リスナーミドルウェアより前に実行されます。`app.use(fn(payload,...,next))` を使用すると、グローバルミドルウェアをいくつでもアプリに追加できます。
グローバルミドルウェアは、すべての着信イベントに対して、リスナーミドルウェアより前に実行されます。`app.use(fn({payload,...,next}))` を使用すると、グローバルミドルウェアをいくつでもアプリに追加できます。

グローバルミドルウェアとリスナーミドルウェアは、いずれも、`next()` を呼び出して実行チェーンの制御を次のミドルウェアに渡すか、`next(error)` を呼び出して以前に実行したミドルウェアチェーンにエラーを渡す必要があります。
グローバルミドルウェアとリスナーミドルウェアは、いずれも、`await next()` を呼び出して実行チェーンの制御を次のミドルウェアに渡すか、`throw` を呼び出して以前に実行したミドルウェアチェーンにエラーを渡す必要があります。
stevengill marked this conversation as resolved.
Show resolved Hide resolved

たとえば、アプリが、対応する内部認証サービス (SSO プロバイダ、LDAP など) で識別されたユーザーにのみ応答する必要があるとします。この場合、グローバルミドルウェアを使用して認証サービス内のユーザーレコードを検索し、ユーザーが見つからない場合はエラーとなるように定義するのがよいでしょう。
</div>

```javascript
// Acme ID情報管理プロバイダ上のユーザからの着信イベントと紐つけた認証ミドルウェア
function authWithAcme({ payload, context, say, next }) {
async function authWithAcme({ payload, context, next }) {
const slackUserId = payload.user;
const helpChannelId = 'C12345';

// Slack ユーザ ID を使って Acmeシステム上にあるユーザ情報を検索できる関数があるとと仮定
acme.lookupBySlackId(slackUserId)
.then((user) => {
// 検索できたらそのユーザ情報でコンテクストを生成
context.user = user;

// 制御とリスナー関数を次のミドルウェアに引き渡し
next();
})
.catch((error) => {
try {
const user = await acme.lookupBySlackId(slackUserId)

// 検索できたらそのユーザ情報でコンテクストを生成
context.user = user;
} catch (error) {
// Acme システム上にユーザが存在しないのでエラーをわたし、イベントプロセスを終了
if (error.message === 'Not Found') {
app.client.chat.postEphemeral({
await app.client.chat.postEphemeral({
token: context.botToken,
channel: payload.channel,
user: slackUserId,
Expand All @@ -41,7 +38,10 @@ function authWithAcme({ payload, context, say, next }) {
}

// 制御とリスナー関数を(もしあれば)前のミドルウェア渡す、もしくはグローバルエラーハンドラに引き渡し
next(error);
});
throw error;
}

// 制御とリスナー関数を次のミドルウェアに引き渡し
await next();
}
```
```
8 changes: 4 additions & 4 deletions docs/_advanced/ja_middleware_listener.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ order: 5

組み込みリスナーミドルウェアはいくつか用意されており、例えば、メッセージのサブタイプをフィルタリングする `subtype()` や、ボットに直接 @ メンションしないメッセージを除外する `directMention()` のように使用することができます。

ただしもちろん、よりカスタマイズされた機能を追加するために、独自のミドルウェアを作成することもできます。独自のミドルウェアを記述する際には、関数で `next()` を呼び出して制御を次のミドルウェアに渡すか、`next(error)` を呼び出して以前に実行されたミドルウェアチェーンにエラーを渡す必要があります。
ただしもちろん、よりカスタマイズされた機能を追加するために、独自のミドルウェアを作成することもできます。独自のミドルウェアを記述する際には、関数で `await next()` を呼び出して制御を次のミドルウェアに渡すか、`throw` を呼び出して以前に実行されたミドルウェアチェーンにエラーを渡す必要があります。
stevengill marked this conversation as resolved.
Show resolved Hide resolved

たとえば、リスナーが人間からのメッセージのみを扱うのであれば、ボットメッセージを除外するリスナーミドルウェアを作成できます。
</div>

```javascript
// 'bot_message' サブタイプを持つメッセージをフィルタリングするリスナーミドルウェア
function noBotMessages({ message, next }) {
async function noBotMessages({ message, next }) {
if (!message.subtype || message.subtype !== 'bot_message') {
next();
await next();
}
}

Expand All @@ -28,4 +28,4 @@ app.message(noBotMessages, ({ message }) => console.log(
`(MSG) User: ${message.user}
Message: ${message.text}`
));
```
```
31 changes: 22 additions & 9 deletions docs/_advanced/ja_receiver.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ order: 8
<div class="section-content">
レシーバーは、Slack から送信されたイベントを処理およびパースして発行するので、Bolt アプリがそのイベントにコンテキストを追加し、アプリのリスナーに渡すことができます。レシーバーは、レシーバーのインターフェイスに準拠している必要があります。

| メソッド | パラメーター | 戻り値の型 |
| メソッド | パラメーター | 戻り値の型 |
|--------------|----------------------------------|-------------|
| `on()` | `type: string`, `listener: fn()` | `unknown` |
| `init()` | `app: App` | `unknown` |
| `start()` | None | `Promise` |
| `stop()` | None | `Promise` |

Bolt アプリでは `on()` が 2 回呼び出されます。
* `Receiver.on('message', listener)` は、解析されたすべての着信リクエストを `onIncomingEvent()` にルーティングする必要があります。これは、Bolt アプリでは `this.receiver.on('message', message => this.onIncomingEvent(message))` として呼び出されます。
* `Receiver.on('error', listener)` は、エラーをグローバルエラーハンドラーにルーティングする必要があります。これは、Bolt アプリでは `this.receiver.on('error', error => this.onGlobalError(error))` として呼び出されます。
Bolt アプリでは `init()` が 2 回呼び出されます。
* `await app.processEvent(event)` は、解析されたすべての着信リクエストを `onIncomingEvent()` にルーティングする必要があります。これは、Bolt アプリでは `this.receiver.on('message', message => this.onIncomingEvent(message))` として呼び出されます。
* `await app.handleError` は、エラーをグローバルエラーハンドラーにルーティングする必要があります。これは、Bolt アプリでは `this.receiver.on('error', error => this.onGlobalError(error))` として呼び出されます。
stevengill marked this conversation as resolved.
Show resolved Hide resolved

Bolt アプリを初期化するときにカスタムレシーバーをコンストラクタに渡すことで、そのカスタムレシーバーを使用できます。ここで紹介するのは、基本的なカスタムレシーバーです。

Expand All @@ -40,6 +40,10 @@ class simpleReceiver extends EventEmitter {
this.app.post(endpoint, this.requestHandler.bind(this));
}
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line 31-33 still extends EventEmitter

init(app) {
this.bolt = app;
}

start(port) {
return new Promise((resolve, reject) => {
Expand All @@ -65,21 +69,30 @@ class simpleReceiver extends EventEmitter {
})
}

requestHandler(req, res) {
async requestHandler(req, res) {
let ackCalled = false;
// 着信リクエストをパースするparseBody 関数があると仮定
stevengill marked this conversation as resolved.
Show resolved Hide resolved
const parsedReq = parseBody(req);
const event = {
body: parsedReq.body,
// レシーバーが確認作業に重要
ack: (response) => {
if (!response) {
if (ackCalled) {
return;
}

if (response instanceof Error) {
res.status(500).send();
} else if (!response) {
res.send('')
} else {
res.send(response);
}

ackCalled = true;
}
};
this.emit('message', event);
await this.bolt.processEvent(event);
}
}
```
```
Loading