Skip to content

Commit

Permalink
Update documentation for async middleware and App#processEvents
Browse files Browse the repository at this point in the history
  • Loading branch information
Michael Barlock committed Feb 3, 2020
1 parent 5881471 commit c495689
Show file tree
Hide file tree
Showing 11 changed files with 185 additions and 128 deletions.
117 changes: 72 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
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,62 @@ 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:
In order to process the event after the listener, the middleware passes a function to `await next()`. 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 do processing after listeners** - You can choose to do work going _before_ listener functions by putting code
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` and `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;

// 制御とリスナー関数を次のミドルウェアに引き渡し
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` を呼び出して以前に実行したミドルウェアチェーンにエラーを渡す必要があります。

たとえば、アプリが、対応する内部認証サービス (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` を呼び出して以前に実行されたミドルウェアチェーンにエラーを渡す必要があります。

たとえば、リスナーが人間からのメッセージのみを扱うのであれば、ボットメッセージを除外するリスナーミドルウェアを作成できます。
</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))` として呼び出されます。

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

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

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 関数があると仮定
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

0 comments on commit c495689

Please sign in to comment.