Bolt for JavaScript を使った Slack アプリ開発で知っておくと捗る 7 つのこと

## Bolt とは？

Slack でユーザとのインタラクション（メッセージ内の Block Kit のボタンのクリック、プルダウンでの選択、モーダルやダイアログからのデータ送信、Home タブの制御 etc）を処理したり、Events API を subscribe したりする Slack アプリを作る場合、Slack からリクエストを受け付ける Web サービスを立ててあげる必要があります。

この Web サービスを Node.js で簡単に、かつ迷わず作るためのフレームワークとして、2019 年に Slack から公開されたのが Bolt for JavaScript です。この辺の記事もみてみてください。

* [公式の日本語ドキュメント](https://slack.dev/bolt/ja-jp/tutorial/getting-started)
* [Hello World, Bolt! ⚡️ Bolt フレームワークを使って Slack Bot を作ろう](https://qiita.com/girlie_mac/items/93538f9a69eb4015f951) 

## 知っておくとちょっと便利な 7 つのこと

Bolt は比較的簡単に扱えるフレームワークだと思いますが、知っておくと便利かなということがいくつかあるので、それを共有したいと思います。

* 再起動なしで変更を反映
* .env の活用 & 強制上書き
* ログレベルの変更
* リクエスト情報を丸ごとログ出力
* FaaS で動かすなら `processBeforeResponse: true` を設定
* サンプル・テンプレート
* コミュニティ

なお、これから説明することの多くは以下のリポジトリに盛り込んであります。コードを見る方が早いという方はこちらを見てください。もしもこのテンプレートが何かの役に立ったなら :star: を増やしておいてください :smiley: 

https://github.com/seratch/bolt-starter

プロジェクトを始めるのに必要な手順を示した [日本語の README](https://github.com/seratch/bolt-starter/blob/master/README_ja.md) もありますので、そちらも参考にしてみてください。

### 再起動なしで変更を反映

まず、基本的なところから。コードを書きながら動作を確認するときにいちいち再起動していては効率が悪すぎます。

シンプルな Bolt アプリであれば、以下のように `npm run local` （名前は違うものでもよいでしょう）のような感じで nodemon を使うのが簡単でわかりやすいですね。実際に運用するときは `npm start` で起動させます。

```json
{
  "scripts": {
    "local": "node_modules/.bin/nodemon index.js",
    "start": "node index.js"
  },
  "dependencies": {
    "@slack/bolt": "^2.1.1",
    "dotenv": "^8.2.0"
  },
  "devDependencies": {
    "nodemon": "^2.0.1"
  }
}
```

TypeScript の場合、webpack を使われているケースも多いですが、シンプルにやるなら `nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/index.ts` のようにできるかと思います。

TypeScript で使う場合は、こちらの記事も参考にしてみてください。

* [Bolt + jsx-slack + TypeScript を使って Slack App をつくる](https://qiita.com/phigasui/items/63c066e06a1708628ba7)
* https://github.com/phigasui/slack_app_boilerplate
* https://github.com/seratch/bolt-starter-ts


### .env の活用 & 強制上書き

これは Bolt 固有ではないですが、Node.js のアプリで、環境変数の参照だけでなく `.env` というファイルからそれらの情報を読み込むために [dotenv という npm パッケージ](https://www.npmjs.com/package/dotenv)が広く使われています（使わないといけないわけではないです）。

ただ、この dotenv は同じキー名が環境変数にもある場合、`.env` での設定は無視されます。ローカルでの開発の場合だと上書きしたいユースケースも多いかと思います。その場合は以下のようなコードをそのまま貼り付けておくと `.env` を正として上書きしてくれるので便利かもしれません。

```js
const config = require("dotenv").config().parsed;
for (const k in config) {
  process.env[k] = config[k];
}
```

また `.gitignore` に `.env` も追加することを忘れずに。`.env.sample` とか `_env` のような名前で雛形を置いておくのもよいですね。

### ログレベルの変更

デフォルトだと Bolt は INFO レベルでログを出力します。デバッグしているときに Bolt の内部で出力されているログをもう少し見たいというときは、以下のような感じで、logLevel を渡します。

```js
const { App } = require("@slack/bolt");
const { LogLevel } = require("@slack/logger");

const app = new App({
  logLevel: LogLevel.DEBUG,
  token: process.env.SLACK_BOT_TOKEN,
  signingSecret: process.env.SLACK_SIGNING_SECRET
});
```

`app.command("/echo", fn)` の `fn` にあたるリスナーの関数内でも、同じ設定の `Logger` を使ってログを出したいというユースケースは多いでしょう。その場合はリスナー関数の引数で `Logger` を受け取ることができます。

```js
const { App } = require("@slack/bolt");
const { LogLevel } = require("@slack/logger");

const logLevel = process.env.SLACK_LOG_LEVEL || LogLevel.INFO;

const app = new App({
  logLevel: logLevel,
  token: process.env.SLACK_BOT_TOKEN,
  signingSecret: process.env.SLACK_SIGNING_SECRET
});

app.event("app_mention", async ({ event, logger }) => {
  logger.debug(`${event.user} mentioned me!`);
});
```

### リクエスト情報を丸ごとログ出力

Slack からペイロードを処理していて「あの項目はリクエストのどの階層にどんなキー名で存在しているのかな？ :thinking: 」と少し困ったことがあるかもしれません。そもそも API ドキュメントがもっと網羅的になるべき・・ということは前提として、利用側で今すぐに簡単にできる工夫を一つご紹介します。

ローカルでの開発中限定で、以下のような Middleware を設定してみましょう。

```js
if (process.env.SLACK_REQUEST_LOG_ENABLED === "1") {
  app.use(async (args) => {
    const copiedArgs = JSON.parse(JSON.stringify(args));
    copiedArgs.context.botToken = 'xoxb-***';
    if (copiedArgs.context.userToken) {
      copiedArgs.context.userToken = 'xoxp-***';
    }
    copiedArgs.client = {};
    copiedArgs.logger = {};
    args.logger.debug(
      "Dumping request data for debugging...\n\n" +
      JSON.stringify(copiedArgs, null, 2) +
      "\n"
    );
    const result = await args.next();
    args.logger.debug("next() call completed");
    return result;
  });
}
```

すると、以下のような出力を見ることができるようになります。これによって、リスナー関数で使える値の構造を簡単に把握することができますし、うまくルーティングされないときのデバッグも楽になるかと思います。

```json
{
  "context": {
    "botToken": "xoxb-***",
    "botUserId": "U1234567",
    "botId": "B1234567"
  },
  "body": {
    "type": "block_actions",
    "team": {
      "id": "T1234567",
      "domain": "your-subdomain"
    },
    "user": {
      "id": "U1234567",
      "username": "seratch",
      "name": "seratch",
      "team_id": "T1234567"
    },
    "api_app_id": "A1234567",
    "token": "random",
    "container": {
      "type": "message",
      "message_ts": "12345.12345",
      "channel_id": "C1234567",
      "is_ephemeral": true
    },
    "trigger_id": "12345.12345.random",
    "channel": {
      "id": "C1234567",
      "name": "general"
    },
    "response_url": "https://hooks.slack.com/actions/T1234567/12345/random",
    "actions": [
      {
        "action_id": "action-id",
        "block_id": "block-id",
        "text": {
          "type": "plain_text",
          "text": "Button",
          "emoji": true
        },
        "value": "click_me_123",
        "type": "button",
        "action_ts": "12345.12345"
      }
    ]
  },
  "payload": {
    "action_id": "action-id",
    "block_id": "block-id",
    "text": {
      "type": "plain_text",
      "text": "Button",
      "emoji": true
    },
    "value": "click_me_123",
    "type": "button",
    "action_ts": "12345.12345"
  },
  "action": {
    "action_id": "action-id",
    "block_id": "block-id",
    "text": {
      "type": "plain_text",
      "text": "Button",
      "emoji": true
    },
    "value": "click_me_123",
    "type": "button",
    "action_ts": "12345.12345"
  },
  "client": {},
  "logger": {}
}
```

あくまでローカルでのデバッグのときのみ有効となるように switch できるフラグを上記のように環境変数などで渡すとよいでしょう。

### FaaS で動かすなら `processBeforeResponse: true` を設定

AWS Lambda や Google Cloud Functions のような FaaS (Function as a Service) 環境で動かすなら v2.0 から導入された `processBeforeResponse` というフラグを true に設定してください。これを設定することでリスナー関数が 3 秒以内に処理を完了できる前提で FaaS 環境で正常に動作させることができるようになります。

より詳細な内容を知りたい方は、英語のみとなりますが、私が書いている GitHub issue とそこからリンクされている議論や pull request を参考にしてください。 https://github.com/slackapi/bolt/issues/361

```js
const { App } = require("@slack/bolt");

const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  processBeforeResponse: true,
});

app.command("/hello", async ({ ack, respond }) => {
  await ack();

  // processBeforeResponse: true の場合、これ以降の await つきの処理も
  // 全てランタイムが破棄される前に実行されることが保証される
  await respond("What's up?");
});
```

### サンプル・テンプレート

いくつか参考になりそうなサンプルやテンプレートを紹介しておきます。

* https://api.slack.com/tools/block-kit-builder - 「Select a Template」ボタンからテンプレートを探せます
* https://github.com/slackapi/app-interaction-patterns - よくある Block Kit の活用例集
* https://glitch.com/@slack - Slack スタッフが公開しているサンプルが豊富

以下はプロジェクトの雛形です。

* https://github.com/seratch/bolt-starter - この記事の内容を盛り込んだテンプレートプロジェクト
* https://github.com/seratch/bolt-on-heroku - TypeScript で使う際の最小構成 & Heroku デプロイのサンプル例
* https://github.com/phigasui/slack_app_boilerplate - Bolt + jsx-slack + TypeScript のプロジェクトテンプレート
* https://github.com/seratch/bolt-starter-ts

### コミュニティ

Slack Community という Slack のプラットフォーム機能を活用する人たちが集まるコミュニティがあります。slackcommunity.com に参加すると、新しいイベントが公開されたときに通知が来ます。

* ウェブサイト http://slackcommunity.com/

日本では meetup.com より connpass が広く使われているので、日本のチャプターでは connpass でもミラーのイベント情報が流れるようになっています。

* https://spc-tokyo.connpass.com/ - 東京でのイベント情報
* https://spc-osaka.connpass.com/ - 大阪でのイベント情報

そして、**普段のオンラインのコミュニケーションには Slack ワークスペースがあります**。[こちら](https://qiita.com/seratch/items/d67f36d0ff7dfaadcc3b#slack-platform-community-spc)から参加することができます。2019 年に始まったばかりですが、すでに活気のあるコミュニティになってきています！

## 年末年始は Bolt 入門で決まり（？）

以上、Bolt に関するいくつかの tips を紹介しました。

「年末年始に新しいことを始めたい」という人は、Bolt を使った Slack アプリ開発に入門してみてはどうでしょうか？

日々をちょっと楽しくしたり、作業の効率を良くするためのアプリをつくってみてください！