(GASでSlack Events APIを利用してご飯のメニューを決めるbot作成 ① Slackとシートの設定の続きです)
前回ではシートとSlackの設定でBot作成の準備をしました。今度はGoogle Apps Scriptにコードを書いていきます。
スクリプト
Slackとシートの準備ができたら、スプレッドシートの「ツール」から「スクリプトエディタ」を選んでスクリプト画面を開きます。
プロジェクトのプロパティにWebhockURLなどを格納
これからコードを書いていきますが、その前に
「ファイル」>「プロジェクトのプロパティ」に、Slackで取得したOAuth Access TokenとWebhockURLを格納します。
プロパティ欄にWEBHOOK_URLなどの任意の名前と、値の欄にWebhockURLなどを書いていきます。
トークン情報などをコードに直接書いても良いですが、ソースを公開したりした際にうっかり自分の取得したトークンやWebhockURLを公開して悪用される…ということはなくなります。
他、チャンネルIDやシートIDも個人情報に当たるので、プロジェクトのプロパティに書いておいたほうが良いです。
「プロジェクトのプロパティ」に記録したトークンは以下のコードで取得できます。
PropertiesService.getScriptProperties.getProperty(プロパティ名)
今回のコードではこのように書いてます。
const INCOMING_WEBHOOK_URL = PropertiesService.getScriptProperties().getProperty("WEBHOOK_URL");
const token = PropertiesService.getScriptProperties().getProperty("TOKEN");
const channel = PropertiesService.getScriptProperties().getProperty("CHANNEL");
const sheet = SpreadsheetApp.openById(PropertiesService.getScriptProperties().getProperty("SHEET"))
メッセージの組み立て
いよいよメッセージを送るためのコードを書いていきます。
SlackのbotのWebhockURLにJSONを送信することでメッセージが表示されます。
コードの流れとしてはこんな感じです
・材料一覧シートから入力された材料とカテゴリを取得
↓
・材料を入れたJSONを作成
↓
・WebhockURL宛にPOST
プルダウンで材料一覧シートに登録した材料が表示されるようにします。
材料一覧シートから入力された材料とカテゴリを取得
以下のコードで材料一覧シートの入力値を取得します。
材料と材料のカテゴリは分けて取ります。
・コード
var zairyo, menu_name, category = new Array();
zairyo = sheet_mate.getRange(2, 2, mate_last_row - 1, menu_last_col).getValues(); //材料取得
category = sheet_mate.getRange(2, 1, mate_last_row - 1).getValues().flat(); //材料の種類取得
sheet.getRange
やgetLastRow
、getLastColumn
を使用してシートに入れた情報をすべて取得できます。
//シートの値取得
sheet.getRange({行番号},{列番号},{行数※任意},{列数※任意}) .getValues();
//最終行の取得
sheet.getLastRow();
//最終列取得
sheet.getLastColumn();
sheet.getRange
で取得された値は二次元配列になっています。(一行しかない場合は一次元配列)
//材料取得 カテゴリを除く2列目・2行目以降の値
zairyo = sheet_mate.getRange(2, 2, mate_last_row - 1, menu_last_col).getValues();
//zairyoの取得結果
[ [ '牛肉', '豚肉', '鶏胸肉', '鶏もも肉', '鶏むね肉', '', '', '' ],
[ '人参', '玉ねぎ', '蕪', 'チンゲン菜', 'じゃがいも', '舞茸', '太ねぎ', '細ねぎ' ],
[ '鯖', '鰤', '', '', '', '', '', '' ],
[ '白米', '素麺', 'パスタ', '', '', '', '', '' ],
[ '厚揚げ', '油揚げ', 'こんにゃく', '豆腐', 'めんつゆ', '卵', '', '' ] ]
JSONに入れていくには空白は不要なので、それらを削除します。
for (let i = 0; i < zairyo.length; i++) {
zairyo[i] = zairyo[i].filter(function delete_space(x) {
return !(x === null || x === undefined || x === "");
});
};
//delete_space(x)実行結果
[ [ '牛肉', '豚肉', '鶏胸肉', '鶏もも肉', '鶏むね肉'],
[ '人参', '玉ねぎ', '蕪', 'チンゲン菜', 'じゃがいも', '舞茸', '太ねぎ', '細ねぎ' ],
[ '鯖', '鰤'],
[ '白米', '素麺', 'パスタ'],
[ '厚揚げ', '油揚げ', 'こんにゃく', '豆腐', 'めんつゆ', '卵'] ]
カテゴリも材料と同様にsheet.getRange
で取得していますが、末尾にflat
を付けて一次元配列にしています。
//.flatをつけないで取得
[ [ '肉類' ], [ '野菜類' ], [ '魚' ], [ '炭水化物' ], [ 'その他' ] ]
//.flatをつけて取得
[ '肉類', '野菜類', '魚', '炭水化物', 'その他' ]
材料を入れたJSONを作成
送信するJSONを作成するためのコードを書いていきます。
メッセージの基礎的なJSONはBlock Kit Bilder(要Slackログイン)で組み立て、それをもとに材料を追加していくコードを書いて、取得した材料がすべて入るようにしました。
Block Kit Bilderについてはこちらが詳しいです。
Block Kit Builder を使ってインタラクティブな Slack アプリをプロトタイピングしよう
送信するJSONの基礎的な部分です。
{
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "今日は何があるのだ?",
},
"accessory": {
"type": "static_select",
"placeholder": {
"type": "plain_text",
"text": "材料を選んでください",
"emoji": true
},
"option_groups": [
{
"label": {
"type": "plain_text",
"text": "----------肉類----------",
"emoji": true
},
"options": [
{
"text": {
"type": "plain_text",
"text": "牛肉",
"emoji": true
},
"value": "牛肉"
},
{
"text": {
"type": "plain_text",
"text": "豚肉",
"emoji": true
},
"value": "豚肉"
},
・
・
・
{
"text": {
"type": "plain_text",
"text": "鶏むね肉",
"emoji": true
},
"value": "鶏むね肉"
}
]
},
{
"label": {
"type": "plain_text",
"text": "----------野菜類----------",
"emoji": true
},
・
・
・
これだけでは分かりにくかったかもしれませんが、基本的な構造はこんな感じになっています。
block # JSONのブロック
├─ {section} # メッセージテキスト
├─ accessory # メッセージの種類を指定 "type": "static_select" でプルダウンメニュー
| └─ placeholder # プルダウンメニューのテキスト設定
└─ option_groups # プルダウンメニューの選択肢
├─ {label} # カテゴリタイトル ここは選択不可
└─ {text} # プルダウンメニュー選択肢
accessory
のメッセージの種類はプルダウンメニューの他に複数ボタン・カレンダー・テキストフィールドなどが指定できます。詳しい解説はSlack APIリファレンスのInteractivity in Block Kit(英語のみ)をご確認ください。
シートに書いてあるカテゴリのタイトルと材料名をすべて入れていきたいので、取得した材料リストから繰り返しJSONを追加していくコードを書きます。
var option_groups = [];
//材料選択JSON作成
for (let i = 0; i < category.length; i++) {
var option_group = {}; //要素グループ初期化
option_group.label = {
type: "plain_text",
text: "----------" + category[i] + "----------"
};
var options = []; //要素設定(複数)
for (let i = 0; i < category.length; i++) {
var option_group = {}; //要素グループ初期化
option_group.label = {
type: "plain_text",
text: "----------" + category[i] + "----------"
};
var options = []; //要素設定(複数)
for (let s = 0; s < zairyo[i].length; s++) {
var option = {}; //要素設定
option.text = {
type: "plain_text",
text: zairyo[i][s]
};
option.value = zairyo[i][s];
options.push(option)
}
option_group.options = options;
option_groups.push(option_group);
};
WebhockURL宛にPOST
最後に作成したJSONを送信するコードです。
var data = {
"blocks": [{
"type": "section",
"text": {
"type": "mrkdwn",
"text": text1
},
"accessory": {
"type": "static_select",
"placeholder": {
"type": "plain_text",
"text": "材料を選んでください"
},
"option_groups": option_groups
}
}]
} ;
var params = {
'method': 'post',
'payload': JSON.stringify(data)
};
UrlFetchApp.fetch(INCOMING_WEBHOOK_URL, params);
}
UrlFetchApp.fetch
を使ってHTTPリクエストを送信し、データをPOSTします。
こちらを実行し、メッセージが表示されれば成功です。
今回のメッセージ送信用の全コードはこちらです。
食材選択
function bot() {
//共通の変数
const INCOMING_WEBHOOK_URL = PropertiesService.getScriptProperties().getProperty("WEBHOOK_URL");
const token = PropertiesService.getScriptProperties().getProperty("TOKEN");
const channel = PropertiesService.getScriptProperties().getProperty("CHANNEL");
const sheet = SpreadsheetApp.openById(PropertiesService.getScriptProperties().getProperty("SHEET"))
const sheet_setting = sheet.getSheets()[0];//メッセージ設定シート
const sheet_mate = sheet.getSheets()[1]; //材料シート
const sheet_menu = sheet.getSheets()[2]; //メニューシート
var text1 = sheet_setting.getRange('B2').getValue();//最初に出すメッセージ
const menu_last_row = sheet_menu.getLastRow();
const menu_last_col = sheet_menu.getLastColumn();
const mate_last_row = sheet_mate.getLastRow();
const mate_last_col = sheet_mate.getLastColumn();
var zairyo, menu_name, category = new Array();
zairyo = sheet_mate.getRange(2, 2, mate_last_row - 1, menu_last_col).getValues(); //材料取得
//配列編集、空白削除
for (let i = 0; i < zairyo.length; i++) {
zairyo[i] = zairyo[i].filter(function delete_space(x) {
return !(x === null || x === undefined || x === "");
});
};
category = sheet_mate.getRange(2, 1, mate_last_row - 1).getValues().flat(); //材料の種類取得
var option_groups = [];
//材料選択JSON作成
for (let i = 0; i < category.length; i++) {
var option_group = {}; //要素グループ初期化
option_group.label = {
type: "plain_text",
text: "----------" + category[i] + "----------"
};
var options = []; //要素設定(複数)
for (let s = 0; s < zairyo[i].length; s++) {
var option = {}; //要素設定
option.text = {
type: "plain_text",
text: zairyo[i][s]
};
option.value = zairyo[i][s];
options.push(option)
}
option_group.options = options;
option_groups.push(option_group);
};
var data = {
"blocks": [{
"type": "section",
"text": {
"type": "mrkdwn",
"text": text1
},
"accessory": {
"type": "static_select",
"placeholder": {
"type": "plain_text",
"text": "材料を選んでください"
},
"option_groups": option_groups
}
}]
} ;
var params = {
'method': 'post',
'payload': JSON.stringify(data)
};
UrlFetchApp.fetch(INCOMING_WEBHOOK_URL, params);
}
返答用のコード
続いて返答用のコードも書いていきます。
GASとSlack Events APIを利用してご飯のメニューを決めるbot作成③ スクリプト(返信メッセージ)へ続きます。
返答用コード
function doPost(e) {
//受け取りメッセージ種別
var INCOMING_WEBHOOK_URL = PropertiesService.getScriptProperties().getProperty("WEBHOOK_URL");
const sheet = SpreadsheetApp.openById(PropertiesService.getScriptProperties().getProperty("SHEET"))
const token = PropertiesService.getScriptProperties().getProperty("TOKEN");
var text = e.postData.getDataAsString();
var payload = JSON.parse(decodeURIComponent(text).replace("payload=", ""));
var type = payload['actions'][0]['type'];
var reply_url = payload['response_url'];
var sheetName ="sistem_log";
const sheet_setting = sheet.getSheets()[0];
function msdelete(payload,token) {//メッセージ削除
const channel = PropertiesService.getScriptProperties().getProperty("CHANNEL");
var ts = payload['message']['ts']; //タイムスタンプ
var url = "https://slack.com/api/chat.delete?token=" + token + "&channel=" + channel + "&ts=" + ts + "&pretty=1";
UrlFetchApp.fetch(url);
}
function select(value, sheet, sheet_setting, reply_url,sheetName) {
const sheet_mate = sheet.getSheets()[1]; //material 材料
const sheet_menu = sheet.getSheets()[2]; //menu
var text1 = sheet_setting.getRange('B3').getValue();
var menu_mate_o, menu_name_o, menu_mate, menu_name = new Array();
const menu_last_row = sheet_menu.getLastRow();
const menu_last_col = sheet_menu.getLastColumn();
//sheet.getRange({行番号},{列番号},{行数※任意},{列数※任意}) o=original
menu_name_o = sheet_menu.getRange(2, 1, menu_last_row - 1).getValues() //メニュー名取得
menu_mate_o = sheet_menu.getRange(2, 2, menu_last_row - 1, menu_last_col).getValues() //材料取得
menu_name = menu_name_o.flat(); //二次元配列を一次元配列に
var object = []; //配列設定
for (let i = 0; i <= menu_name_o.length - 1; i++) {//材料から空白削除
var menu_mate = menu_mate_o[i].filter(function delete_space(x) {
return !(x === null || x === undefined || x === "");
});
//メニュー検索
if (menu_mate.indexOf(value) < 0) {} else {
var json = {}; //要素設定
var manu_ac = {
"type": "button",
"text": {
"type": "plain_text",
"text": "これにする!"
},
"value": menu_name[i]
};
var manu = {
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*" + menu_name[i] + "*" //メニュー名は太字
},
"accessory": manu_ac
};
var material = {
"type": "context",
"elements": [{
"type": "mrkdwn",
"text": "*材料:* " + menu_mate
}]
};
var line = {
"type": "divider"
};
object.push(manu) //要素を配列に追加していく
object.push(material)
object.push(line)
var blocks = {
"blocks": object
};
}
}
var button = { //選びなおしとキャンセルボタン
"type": "actions",
"elements": [{
"type": "button",
"text": {
"type": "plain_text",
"text": "材料を選びなおす",
"emoji": true
},
"value": "re_select"
}, {
"type": "button",
"text": {
"type": "plain_text",
"text": "Cancel",
"emoji": true
},
"style": "danger",
"value": "cancel"
}]
}
object.push(button)
var reply = {
"replace_original": true,
"response_type": "in_channel",
"attachment_type": "default",
"options": text1,
"blocks": object
};
var params = {
'method': 'post',
'payload': JSON.stringify(reply)
};
UrlFetchApp.fetch(reply_url, params);
}
function cook(sheet,payload,token){
var triger = payload['trigger_id'];
var str = payload['actions'][0]['text']['text'];
var value = str.substr(0,str.indexOf("の作り方"))
const menu_cook = sheet.getSheets()[3];
var cook_last_row = menu_cook.getLastRow();
var cook_last_col = menu_cook.getLastColumn();
var cook_name_dat = menu_cook.getRange(2, 1, cook_last_row - 1).getValues().flat(); //料理名だけの配列
var dat = menu_cook.getRange(2, 1, cook_last_row - 1, cook_last_col - 1).getValues();
var cook_row = cook_name_dat.indexOf(value)
var blocks = [{
"type": "image",
"image_url": dat[cook_row][1],
"alt_text": value
}]
for (let i = 2; i < dat[cook_row].length - 1; i++) {
var section = {}
var n = i - 1
section.type = "section"
section.text = {
"type": "mrkdwn",
"text": "*" + n + "*\n" + dat[cook_row][i]
}
blocks.push(section)
}
var cook = {
"type": "modal",
"title": {
"type": "plain_text",
"text": value + "の作り方",
"emoji": true
},
"close": {
"type": "plain_text",
"text": "Cancel",
"emoji": true
},
"blocks": blocks
}
var cook_show = {
'method': 'post',
'headers': {
'contentType': 'application/json;charset=utf-8',
'Authorization': 'Bearer ' + token
},
'payload': {
'trigger_id': triger,
'view': JSON.stringify(cook)
}
};
UrlFetchApp.fetch("https://slack.com/api/views.open", cook_show);
}
function afterselect(INCOMING_WEBHOOK_URL,sheet_setting, reply_url){
var text2_setting = sheet_setting.getRange('B3').getValue();
var text2 = text2_setting.replace("*メニュー*", value);
var reply = {
"replace_original": true,
"response_type": "in_channel",
"title": text2,
"text": text2
};
var params = {
'method': 'post',
'Content-type': 'application/json',
'payload': JSON.stringify(reply)
};
//もう一品、作り方確認、終了
var button = [{
"type": "actions",
"elements": [{
"type": "button",
"text": {
"type": "plain_text",
"text": "もう一品選ぶ",
"emoji": true
},
"value": "re_select"
}, {
"type": "button",
"text": {
"type": "plain_text",
"text": value + "の作り方確認",
"emoji": true
},
"value": "cook"
}, {
"type": "button",
"text": {
"type": "plain_text",
"text": "終了",
"emoji": true
},
"style": "danger",
"value": "cancel"
}]
}]
var last_button = {
"attachment_type": "default",
"blocks": button
};
var params_last = {
'method': 'post',
'payload': JSON.stringify(last_button)
};
UrlFetchApp.fetch(reply_url, params);
Utilities.sleep(2000);
UrlFetchApp.fetch(INCOMING_WEBHOOK_URL, params_last);
}
if (type == "static_select") {
var value = payload["actions"][0]["selected_option"]["value"];
select(value, sheet, sheet_setting, reply_url,sheetName);
} else {
var value = payload['actions'][0]['value'];
if (value == "re_select") {
msdelete(payload,token);
bot();
} else if (value == "cook") {
cook(sheet,payload,token);
} else if (value == "cancel") {
msdelete(payload,token);
} else {
afterselect(INCOMING_WEBHOOK_URL,sheet_setting, reply_url);
}
}
}