【slack】slackのBlock Kitを使ってみる(STEP②)

はじめに

本記事は前回の記事の続きとなります。

  1. Slash Commands作成
  2. Block Kitレスポンス
  3. アクションAPI作成
  4. Incoming Webhookでチャンネルメッセージ送信

Slackアプリの全体像

当記事は③と④の説明です。

アクションAPI作成

Slack APPの設定

早速、アクションAPIを作成していきます。
slack apiページにアクセスし、作成したアプリを選択します。
アプリ選択後、「Interactive」を有効にしてください。


↓Onに変更し、アクション発生時のエンドポイントのリクエストURLを設定して、保存してください。
設定するURLは
https://[glitchのプロジェクト名].glitch.me/mail
としてください。
※mailの実装は次に説明します。

Glitchの設定

先ほどアクションに設定したエンドポイントのサーバを作成していきます。
前回の記事で修正したindex.jsに以下のソースを追加します。

var selectedDateMap = {};
app.post("/mail", async (req, res, c) => {
  var payload = JSON.parse(req.body.payload);

  if (payload.actions[0].action_id === "datepicker") {
    selectedDateMap[payload.user.username] = payload.actions[0].selected_date;
    res.json({});
  } else {
    var blocks;
    if (!selectedDateMap[payload.user.username]) {
      blocks = [
        {
          type: "section",
          text: {
            type: "mrkdwn",
            text:
              "休暇予定日を選択し、メール送信ボタンをクリックしてください :ghost:\n"
          }
        },
        {
          type: "divider"
        },
        {
          type: "section",
          text: {
            type: "mrkdwn",
            text: "休暇予定日"
          },
          accessory: {
            action_id: "datepicker",
            type: "datepicker",
            placeholder: {
              type: "plain_text",
              text: "Select a date",
              emoji: true
            }
          }
        },
        {
          type: "section",
          text: {
            type: "mrkdwn",
            text: "休暇予定日を選択してください。"
          }
        },
        {
          type: "actions",
          elements: [
            {
              type: "button",
              action_id: "sendMail",
              text: {
                type: "plain_text",
                text: "メール送信"
              },
              style: "primary",
              value: "click"
            }
          ]
        },
        {
          type: "divider"
        }
      ];
      let options = {
        method: "post",
        baseURL: payload.response_url,
        headers: {
          "Content-Type": "application/json"
        },
        data: {blocks: blocks}
      };
      response(options);
    } else {
      let options = {
        method: "post",
        baseURL: payload.response_url,
        headers: {
          "Content-Type": "application/json"
        },
        data: {text: selectedDateMap[payload.user.username] + "で休暇連絡します :ghost:"}
      };
      response(options);
    }
  }
});

async function response(options) {
  try {
    const res = await axios.request(options);
  } catch (error) {
    console.log(error);
  }
}

動作確認

まずは「/kintai」でSlash Commandを呼び出します。

次に休暇予定日を選択。

最後にメール送信をクリックします。
※まだIncoming Webhooksの設定が完了していないため、他チャンネルへの投稿はされません。

休暇予定日を選択せずにメール送信した場合は以下のようになります。

Incoming Webhookでチャンネルメッセージ送信

これで最後です。
ここまで来ればあとは簡単ですね。

Slack APPの設定

作成したSlack APPのIncoming WebhooksをOnにします。

↓Onに変更し、画面下部の「Add New Webhook to Workspace」をクリックします。

↓投稿先にしたいチャンネルを選択し、「許可」をクリックしてください。
 今回はテスト用なので、generalに投稿するようにしています。

許可後、Incoming Webhooks画面に作成したwebhookが追加されたことを確認します。
下記のWebhook URLは後程使用しますので、コピーしておいてください。

Glitchの設定

作成したWebhook URLにメッセージを送信します。
index.jsに以下のソースを追記します。
※「process.env.WEBHOOK_URL」には自身が作成したWebhook URLを設定してください。
※下記サンプルのWebhook URLはenvファイルに設定しております

async function postMessage(payload, selectedDate) {
  let sendMessage =
    "お疲れ様です。" +
    payload.user.username +
    "です。\n" +
    selectedDate +
    "はお休みをいただきます。\nよろしくお願いいたします。";

  let options = {
    method: "post",
    baseURL:
      process.env.WEBHOOK_URL,
    headers: {
      "Content-Type": "application/json"
    },
    data: {
      text: sendMessage
    }
  };
  response(options);
}

上記ソースの追加後、作成したメソッドを呼び出すように修正します。
以下は完成時のソースコードです。

/* ********************************************************
 * Slack Node+Express Slash Commands Example with BlockKit
 *
 * Tomomi Imura (@girlie_mac)
 * ********************************************************/

const qs = require("qs");
const express = require("express");
const bodyParser = require("body-parser");
const axios = require("axios");
const signature = require("./verifySignature");

const app = express();

const rawBodyBuffer = (req, res, buf, encoding) => {
  if (buf && buf.length) {
    req.rawBody = buf.toString(encoding || "utf8");
  }
};

app.use(bodyParser.urlencoded({ verify: rawBodyBuffer, extended: true }));
app.use(bodyParser.json({ verify: rawBodyBuffer }));

const server = app.listen(process.env.PORT || 5000, () => {
  console.log(
    "Express server listening on port %d in %s mode",
    server.address().port,
    app.settings.env
  );
});

/*
 * Slash Command Endpoint to receive a payload
 */
app.post("/kintai", async (req, res) => {
  if (!signature.isVerified(req)) {
    res.sendStatus(404); // You may throw 401 or 403, but why not just giving 404 to malicious attackers ;-)
    return;
  } else {
    const blocks = [
      {
        type: "section",
        text: {
          type: "mrkdwn",
          text:
            "休暇予定日を選択し、メール送信ボタンをクリックしてください :ghost:"
        }
      },
      {
        type: "section",
        text: {
          type: "mrkdwn",
          text: "休暇予定日"
        },
        accessory: {
          action_id: "datepicker",
          type: "datepicker",
          placeholder: {
            type: "plain_text",
            text: "Select a date",
            emoji: true
          }
        }
      },
      {
        type: "actions",
        elements: [
          {
            type: "button",
            action_id: "sendMail",
            text: {
              type: "plain_text",
              text: "メール送信"
            },
            style: "primary",
            value: "click"
          }
        ]
      }
    ];

    // and send back an HTTP response with data
    const message = {
      response_type: "in_channel",
      blocks: blocks
    };
    res.json(message);
  }
});

var selectedDateMap = {};
app.post("/mail", async (req, res, c) => {
  var payload = JSON.parse(req.body.payload);

  if (payload.actions[0].action_id === "datepicker") {
    selectedDateMap[payload.user.username] = payload.actions[0].selected_date;
    res.json({});
  } else {
    var blocks;
    if (!selectedDateMap[payload.user.username]) {
      blocks = [
        {
          type: "section",
          text: {
            type: "mrkdwn",
            text:
              "休暇予定日を選択し、メール送信ボタンをクリックしてください :ghost:\n"
          }
        },
        {
          type: "divider"
        },
        {
          type: "section",
          text: {
            type: "mrkdwn",
            text: "休暇予定日"
          },
          accessory: {
            action_id: "datepicker",
            type: "datepicker",
            placeholder: {
              type: "plain_text",
              text: "Select a date",
              emoji: true
            }
          }
        },
        {
          type: "section",
          text: {
            type: "mrkdwn",
            text: "休暇予定日を選択してください。"
          }
        },
        {
          type: "actions",
          elements: [
            {
              type: "button",
              action_id: "sendMail",
              text: {
                type: "plain_text",
                text: "メール送信"
              },
              style: "primary",
              value: "click"
            }
          ]
        },
        {
          type: "divider"
        }
      ];
      let options = {
        method: "post",
        baseURL: payload.response_url,
        headers: {
          "Content-Type": "application/json"
        },
        data: { blocks: blocks }
      };
      response(options);
    } else {
      let options = {
        method: "post",
        baseURL: payload.response_url,
        headers: {
          "Content-Type": "application/json"
        },
        data: {
          text:
            selectedDateMap[payload.user.username] + "で休暇連絡します :ghost:"
        }
      };
      response(options);
      postMessage(payload, selectedDateMap[payload.user.username]);
    }
  }
});

async function response(options) {
  try {
    const res = await axios.request(options);
  } catch (error) {
    console.log(error);
  }
}

async function postMessage(payload, selectedDate) {
  let sendMessage =
    "お疲れ様です。" +
    payload.user.username +
    "です。\n" +
    selectedDate +
    "はお休みをいただきます。\nよろしくお願いいたします。";

  let options = {
    method: "post",
    baseURL:
      process.env.WEBHOOK_URL,
    headers: {
      "Content-Type": "application/json"
    },
    data: {
      text: sendMessage
    }
  };
  response(options);
}

通知確認

ハマったポイント

①Block Kitのボタン押下などのアクションをサーバに通知する方法がわからない。
 ⇒アクションAPIを使用します。

②ブロックの操作ごとにリクエストが発生!ボタン押下時に全ブロックのデータ送信はできないの?
 ⇒私が調べた限りでは、ブロックのアクションごとにアクションAPIへリクエストが送信されてしまいました。。そのため、action_idでどのブロックからのリクエストか判断する必要がありました。
今回のアプリで言うと、日付選択がまさにそれで、
・日付選択アクションを受信したら、ユーザ名をキーに選択した日付データをマップに格納
・メール送信アクションを受信したら、ユーザ名をキーにマップから日付データを取得し、WebHock実行。マップにデータが存在していないようなら、日付選択をしていないということで、警告付きのBlock Kitレスポンス
という仕組みにしました。
本来であればmemcachedなどで保持するのが良いかと思いますが、今回はネタ用のアプリなので、この仕組みで妥協しています。

こちらの仕組みの改善方法がわかりましたら、別でブログを書いてみようと思います。

まとめ

今回のBlock Kitを触ってみての感想ですが、
一問一答のようなやり取りのほうがフィットする気がしました。
例えていうと、カスタマーサービスの音声電話で数字を入力してステージ遷移するような仕組みですね。
slackアプリのことは知らないことがまだまだありますので、Block Kitのより良い使い方であったり、別の面白そうな機能があったら、またブログを書いていきたいと思います。

拙い文章でしたが、最後までお付き合いいただきありがとうございました。