Bolt を使って Slack app を実装する
Bolt 入門ガイドに詳しく書かれているが、「一体 Slack アプリとは何なのか」を掴むのに時間がかかったので、自分の理解をまとめていく。
Slack アプリは、Slack 有料版に提供される Slack の機能を拡張する概念で、Slack App Directory で配布されているアプリやワークスペース内で作成するアプリを含む。今回は Bolt for JavaScript という公式のフレームワークを使って Slack アプリを作っていくが、それを Slack App Directory に公開して使ってもらうことも可能なようだ。
空の 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 を作成する必要がある。
作成した Slack アプリを動作させるプログラムを実装する
Slack アプリを実際に動作させるためのプログラムは Slack API の仕様に沿っていれば、理論上はプログラム言語を問わないが、Slack アプリを開発するために Bolt というフレームワークが公式で用意されているのでこれを使う。提供されている言語は JavaScript (Node.js)、Python、Java があるが、今回は Bolt for JavaScript を使って Node.js で動作するプログラムを実装していく。
Slack アプリの動作に必要なトークンを環境変数にセットする
export SLACK_SIGNING_SECRET=<your-signing-secret>
export SLACK_BOT_TOKEN=xoxb-<your-bot-token>
export SLACK_APP_TOKEN=<your-app-token>
package.json
と src/index.ts
を用意する
必要最小限の {
"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"
}
}
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 アプリと接続する何もしないプログラムが起動する。
Slack アプリで特定のコマンドでモーダルを開きその入力を受けつける
Slack workspace 内で /command_name
を実行したときに、Slack 内でモーダルを表示して、その入力内容を Slack アプリで受け取るという処理を実装する。
/command_name
コマンドを追加する
Slash Commands ページで 作成した空の Slack アプリの Slash Commands ページでコマンドを、ここでは /command_name
を追加する。そうすると、Slack アプリがインストールされた Slack workspace で /command_name
コマンドが使えるようになる。
/command_name
コマンドが実行された時にモーダルを表示する
前のステップで作成した Node.js で動作する Slack アプリの、bolt.App
クラスのインスタンスに .command()
メソッドがあり、これを使ってコマンドが実行された場合の処理を実装する。
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
プロパティに渡す。
{
"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 と呼ばれる任意の文字列で構成される識別子を指定し、どのモーダルが対象なのかを指定する必要がある。
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
プロパティを指定する。
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);
}
});