はじめに
最近は家庭の運用でスプレッドシートに出費などをまとめたりしているのですが、 毎回スプレッドシートを開いて値を入力するのは意外と面倒で、スマホアプリだとスプレッドシートのインタフェースが難しくセルのフォーカスがうまくいかなかったりするので、 Slackから入力できるようにしました。
準備
Slack API
bolt-js を使って実装します。
コード自体はTypeScriptでやっています。Spread Sheet API
Boltをtsで実装したので、node-google-spreadsheet を使いました。ngrok
ローカルでBoltを動かしてSlackのAPIと繋げる必要があるのでngrok を使います。
ポート開放とかをしているなら不要です。
普段Boltはserverless を使ってAWS上で動作させているのですが、そのやり方は今回解説しません。
Slack APIとの接続
ngrokを起動してIPアドレスを取得します。
Slack Appの設定にあるInteractivity & Shortcuts (https://api.slack.com/apps/[APP_ID]/interactive-messages
) にngrokで取得したURLに /slack/events
のパスを追加して登録します。
e.g. https://1111-2222-3333-4444-5555.ngrok-free.app/slack/events
Modalの起動方法
Slack APIでモーダルを起動するには views.open
のAPI
を経由する必要があります。
理由はモーダルの起動に trigger_id
を渡す必要があるため、DMやリアクション、BOTへのメンションなどのEvents API
を経由して起動することができないので注意してください。
views.open
のイベントは主に以下の3つ
で起動できます
今回はスラッシュコマンドでの起動にしました。
スラッシュコマンドの登録
Slack Appの設定にあるSlash Commands (https://api.slack.com/apps/[App_ID]/slash-commands
) に任意のコマンドを登録します。
今回は /test
を登録しました。
実装
WEB側でBoltを使うための設定ができたので、コードを書いていきます。
Block kit builder
その前にモーダルのデザインを作ります。
モーダルのデザインはBlock kit builder
を使って生成します。
Block kit builderはSlackのAPIのペイロードにわたすJSON部分をWEB上で簡単に生成できます。
またモーダルだけでなくメッセージでのレイアウトも生成でき、URLでの共有も可能なためとても便利です。
今回はテキスト入力欄と日付のブロックを追加しました。
生成されたJSONはこちら です。
Boltの実装
モーダルを表示するための準備ができたので、Bolt側で実装してきます。
モーダルの起動
const app = new App({
token: process.env.SLACK_BOT_TOKEN,
signingSecret: process.env.SLACK_SIGNING_SECRET,
});
app.command("/test", async ({ ack, context, body, client }) => { // ①
await ack(); // ②
await client.views.open({
token: context.botToken,
trigger_id: body.trigger_id, // ③
view: {
type: "modal",
callback_id: "call_back_id", // ④
title: {
type: "plain_text",
text: "スプレッドシートに登録",
emoji: true,
},
submit: {
type: "plain_text",
text: "登録",
emoji: true,
},
close: {
type: "plain_text",
text: "Cancel",
emoji: true,
},
blocks: [
{
block_id: "block_id_text", // ⑤
type: "input",
element: {
type: "plain_text_input",
action_id: "plain_text_input-action", // ⑥
},
label: {
type: "plain_text",
text: "登録するもの",
emoji: true,
},
},
{
block_id: "block_id_date", // ⑤
type: "input",
element: {
type: "datepicker",
initial_date: "2000-01-01", // ⑦
placeholder: {
type: "plain_text",
text: "Select a date",
emoji: true,
},
action_id: "datepicker-action", // ⑥
},
label: {
type: "plain_text",
text: "日付",
emoji: true,
},
},
],
},
});
});
①
app.command
でスラッシュコマンドが発動した場合の処理を書きます。②
Boltではコマンドなどのイベントをハンドリングした場合3秒以内にack
を返す必要があります。③
このtrigger_id
がとても重要です。body.trigger_id
の値を設定しましょう。④
call_back_id
はモーダルのsubmitボタンを押したときのハンドリングに使います。⑤
Block kit builderで生成した場合には設定されていませんが、入力後の値を取得するためにblock_id
は必ず設定します。⑥
Block kit builderで生成した場合にはデフォルトの値が設定されています。そのままでも構いませんが入力後の値を取得するためにaction_id
は必ず設定します。⑦
初期の日付は固定になってしまうため、当日の日付を設定したい場合はこのようにしてDateを作って設定すると良いでしょう。
const today = new Date();
{
...
initial_date: today.toISOString().split("T")[0],
...
}
モーダルのハンドリング
モーダルを閉じた時のイベントなどもハンドリングできるのですが、今回は割愛します。
Submitを押したときは app.view
でハンドリングできます。
第一引数のidには前項で設定した call_back_id
を設定しましよう。
値は view["state"]["values"]["[block_id]"]["[action_id]"]
で取得できます。
最後の.valueなどはblockの種類によって変わります。
最後にack
で応答します。
app.view("call_back_id", async ({ ack, view }) => {
const values = view["state"]["values"];
const text = values["block_id_text"]["plain_text_input-action"].value;
const date = values["block_id_date"]["datepicker-action"].selected_date;
await ack();
});
これでスラッシュコマンドで入力した値をモーダルで取得できました。
エラーハンドリング
モーダルに入力された値のエラーハンドリング もできます。
エラーを返すには ack
の response_action
にerrors
を指定して、block idに表示するstringを渡します。
この場合は今日の日付以降が選択された場合にエラーを表示します。
const date = values["block_id_date"]["action_id_date"].selected_date;
if (!date) {
await ack({
response_action: "errors",
errors: {
["block_id_date"]: "日付を入力してください",
},
});
return;
}
const today = new Date();
if (today < new Date(date)) {
await ack({
response_action: "errors",
errors: {
["block_id_date"]: "今日より前の日付を登録してください",
},
});
return;
}
モーダルのエラー表示に関して1点注意があります。
block builderで生成したモーダルのblocksのtypeは input
のみエラー表示に対応しています。
typeが section
のアイテムだとエラー表示されないので気をつけてください。
blocks: [
{
block_id: "block_id_text",
type: "input", // <- inputだと問題ない
element: {
type: "plain_text_input",
action_id: "plain_text_input-action",
},
label: {
type: "plain_text",
text: "登録するもの",
emoji: true,
},
},
{
block_id: "block_id_date",
type: "input", // <- inputだと問題ない
element: {
type: "datepicker",
initial_date: "2000-01-01",
placeholder: {
type: "plain_text",
text: "Select a date",
emoji: true,
},
action_id: "datepicker-action",
},
label: {
type: "plain_text",
text: "日付",
emoji: true,
},
},
],
スプレッドシートに登録
前述のとおりnode-google-spreadsheet を使います。
使い方はとても簡単で、環境変数からスプレッドシートのIDとアカウント利用するためのメールアドレス、Keyを指定して初期化したらあとは書き込み処理を書くだけです。
// 初期化
const doc = new GoogleSpreadsheet(process.env.GOOGLE_SPREAD_SHEET_ID);
await doc.useServiceAccountAuth({
client_email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
private_key: process.env.GOOGLE_PRIVATE_KEY,
});
// 読み込み
await doc.loadInfo();
// sheet指定
const sheet = doc.sheetsByIndex[0];
// 書き込み
await sheet.setHeaderRow(["text", "date"]);
await sheet.addRow({
text: text,
date: new Date(date).toLocaleDateString(),
});
これを app.view
でモーダルのSubmitイベントをハンドリングしたところで呼び出しましょう。(初期化、読み込み処理は毎回行う必要はないので考慮が必要です。)
最終的にはこんな感じでSlackのモーダルで入力した値をスプレッドシートに書き込めました!
他にもSlackのモーダルには様々な部品が用意されているので、いろいろな用途に応用できると思います!