April 30, 2023

Slack boltを使ってモーダルに入力した結果をスプレッドシートに登録する

はじめに

最近は家庭の運用でスプレッドシートに出費などをまとめたりしているのですが、 毎回スプレッドシートを開いて値を入力するのは意外と面倒で、スマホアプリだとスプレッドシートのインタフェースが難しくセルのフォーカスがうまくいかなかったりするので、 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

intractivity shortcuts settings

Modalの起動方法

Slack APIでモーダルを起動するには views.openAPI を経由する必要があります。
理由はモーダルの起動に trigger_id を渡す必要があるため、DMやリアクション、BOTへのメンションなどのEvents API を経由して起動することができないので注意してください。

views.open のイベントは主に以下の3つ で起動できます

今回はスラッシュコマンドでの起動にしました。

スラッシュコマンドの登録

Slack Appの設定にあるSlash Commands (https://api.slack.com/apps/[App_ID]/slash-commands ) に任意のコマンドを登録します。
今回は /test を登録しました。

slash commands settings

実装

WEB側でBoltを使うための設定ができたので、コードを書いていきます。

Block kit builder

その前にモーダルのデザインを作ります。

モーダルのデザインはBlock kit builder を使って生成します。
Block kit builderはSlackのAPIのペイロードにわたすJSON部分をWEB上で簡単に生成できます。
またモーダルだけでなくメッセージでのレイアウトも生成でき、URLでの共有も可能なためとても便利です。

今回はテキスト入力欄と日付のブロックを追加しました。

block kit builder

生成された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();
  });

これでスラッシュコマンドで入力した値をモーダルで取得できました。

エラーハンドリング

モーダルに入力された値のエラーハンドリング もできます。

エラーを返すには ackresponse_actionerrorsを指定して、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;
}
modal error

モーダルのエラー表示に関して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イベントをハンドリングしたところで呼び出しましょう。(初期化、読み込み処理は毎回行う必要はないので考慮が必要です。)

writing sprad sheet

最終的にはこんな感じでSlackのモーダルで入力した値をスプレッドシートに書き込めました!

他にもSlackのモーダルには様々な部品が用意されているので、いろいろな用途に応用できると思います!

© AAkira 2023