Open4

Bolt を使って Slack app を実装する

@1000ch@1000ch

Bolt 入門ガイドに詳しく書かれているが、「一体 Slack アプリとは何なのか」を掴むのに時間がかかったので、自分の理解をまとめていく。

https://slack.dev/bolt-js/ja-jp/tutorial/getting-started

Slack アプリは、Slack 有料版に提供される Slack の機能を拡張する概念で、Slack App Directory で配布されているアプリワークスペース内で作成するアプリを含む。今回は Bolt for JavaScript という公式のフレームワークを使って Slack アプリを作っていくが、それを Slack App Directory に公開して使ってもらうことも可能なようだ。

@1000ch@1000ch

空の Slack アプリを作成する

Your Apps ページから新しい Slack アプリを作成すると、インストール先として選択した Slack workspace に空の Slack アプリが作成される。Slack アプリの管理のため必要となるトークンがいくつかある。

Basic Information ページ > App Credentials の Signing Secret

Slack が Slack アプリに対して送信するリクエストは、この secret を使って署名される。Slack アプリは送信されるリクエストが Slack からのものであることを検証するために使う。

Install App ページ > OAuth Tokens for Your Workspace の Bot User OAuth Token

Slack アプリを Slack workspace にインストールすると自動で生成されるトークンで、Slack アプリを認証するために使う。OAuth & Permissions ページで同じトークンの表示および管理が可能である。

Basic Information ページ > App-Level Tokens のトークン

先の2つのトークンだけでも Slack アプリは動作するが、Slack アプリを Socket Mode で動作させる方が Request URL などの設定が不要になるため、今回は Socket Mode ページから Socket Mode を有効化する。Slack アプリを Socket Mode で動作させるためには、scope を connections:write にした App Level Token を作成する必要がある。

@1000ch@1000ch

作成した Slack アプリを動作させるプログラムを実装する

Slack アプリを実際に動作させるためのプログラムは Slack API の仕様に沿っていれば、理論上はプログラム言語を問わないが、Slack アプリを開発するために Bolt というフレームワークが公式で用意されているのでこれを使う。提供されている言語は JavaScript (Node.js)、Python、Java があるが、今回は Bolt for JavaScript を使って Node.js で動作するプログラムを実装していく。

https://api.slack.com/lang/ja-jp

Slack アプリの動作に必要なトークンを環境変数にセットする

export SLACK_SIGNING_SECRET=<your-signing-secret>
export SLACK_BOT_TOKEN=xoxb-<your-bot-token>
export SLACK_APP_TOKEN=<your-app-token>

必要最小限の package.jsonsrc/index.ts を用意する

package.json
{
  "name": "slack-app",
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "node out/index.js",
    "dev": "tsc --watch"
  },
  "dependencies": {
    "@slack/bolt": "^3.13.1"
  },
  "devDependencies": {
    "typescript": "^5.0.4"
  }
}

src/index.ts
import bolt from '@slack/bolt';

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

await app.start(process.env.PORT || 3000);

npm run build でコンパイルし、 npm start を実行すると、作成した Slack アプリと接続する何もしないプログラムが起動する。

@1000ch@1000ch

Slack アプリで特定のコマンドでモーダルを開きその入力を受けつける

Slack workspace 内で /command_name を実行したときに、Slack 内でモーダルを表示して、その入力内容を Slack アプリで受け取るという処理を実装する。

Slash Commands ページで /command_name コマンドを追加する

作成した空の Slack アプリの Slash Commands ページでコマンドを、ここでは /command_name を追加する。そうすると、Slack アプリがインストールされた Slack workspace で /command_name コマンドが使えるようになる。

/command_name コマンドが実行された時にモーダルを表示する

前のステップで作成した Node.js で動作する Slack アプリの、bolt.App クラスのインスタンスに .command() メソッドがあり、これを使ってコマンドが実行された場合の処理を実装する。

src/index.ts
app.command('/command_name', async ({ack, body, client}) => {
  await ack();

  try {
    await client.views.open({
      trigger_id: body.trigger_id,
      view: {...}
    });
  } catch (error) {
    console.error(error);
  }
});

ここではモーダルを作成しているが、Slack 内で用意されている UI を作るためのツールとして Block Kit というツールが用意されており、モーダルについても Web ページ上でインタラクティブに作成できる。すると、作成したモーダルの表示に必要な JSON が出力されるので、これを上記の client.views.open() メソッドの引数の view プロパティに渡す。

https://api.slack.com/surfaces/modals

Block Kit で作成したモーダル UI の payload
{
  "type": "modal",
  "title": {
    "type": "plain_text",
    "text": "Modal Title"
  },
  "submit": {
    "type": "plain_text",
    "text": "Submit"
  },
  "blocks": [
    {
      "type": "input",
      "element": {
        "type": "plain_text_input",
        "action_id": "sl_input",
        "placeholder": {
          "type": "plain_text",
          "text": "Placeholder text for single-line input"
        }
      },
      "label": {
        "type": "plain_text",
        "text": "Label"
      },
      "hint": {
        "type": "plain_text",
        "text": "Hint text"
      }
    }
  ]
}

モーダルのフォームが送信された時の処理を実装する

bolt.App クラスのインスタンスの .view() メソッドを使って、モーダルのフォームの送信やモーダルが閉じられたタイミングを検知する。引数にはコールバック ID と呼ばれる任意の文字列で構成される識別子を指定し、どのモーダルが対象なのかを指定する必要がある。

src/index.ts
app.view('unique_callback_id', async ({ack, view, client}) => {
  await ack();

  try {
    const values = Object.values(view.state.values);
    const inputValue = values.find(v => v['sl_input'])?.['sl_input'].value;

    if (!inputValue) {
      throw new Error(`Input value is invalid`);
    }

    await client.chat.postMessage({
      channel: '...',
      text: `Input value is ${inputValue}`,
    });
  } catch (error) {
    console.error(error);
  }
});

同様にモーダルを表示する client.views.open() メソッドの引数の view プロパティにも、callback_id プロパティを指定する。

src/index.ts
app.command('/command_name', async ({ack, body, client}) => {
  await ack();

  try {
    await client.views.open({
      trigger_id: body.trigger_id,
      view: {
        callback_id: 'unique_callback_id',
        ...
      }
    });
  } catch (error) {
    console.error(error);
  }
});