はじめに
- Slash Commands作成
- Block Kitレスポンス
- アクションAPI作成
- 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のより良い使い方であったり、別の面白そうな機能があったら、またブログを書いていきたいと思います。
拙い文章でしたが、最後までお付き合いいただきありがとうございました。