目次
概要
アンケートを作成する際など、選択式のメッセージを送信したい時などある。
そういうときはボタンテンプレートのメッセージを送るようにする。
ソースコード
全体像だが、もっとわかりやすく整理する余地あり。
定数定義部分
const HeaderString = {
messageId: "MessageID",
userId: "UserId",
messageType: "MessageType",
dateTime: "StartDateTime",
question1: "Question1",
question2: "Question2",
question3: "Question3"
}
const SheetNames = {
main: "Main",
shopList: "shop_list"
}
const postBack = "ButtonTemplate_Shop_List";
メイン部分
// POSTリクエストに対する処理
function doPost(e) {
// JSONをパース
if (e == null || e.postData == null || e.postData.contents == null) {
console.log("error");
return;
}
const ss = SpreadsheetApp.getActive()
const sheet = ss.getSheetByName(SheetNames.main);
var lastRow = sheet.getLastRow()
var lastColumn = sheet.getRange(lastRow, sheet.getMaxColumns()).getNextDataCell(SpreadsheetApp.Direction.PREVIOUS).getColumn();
const headers = sheet.getRange(1,1,1,sheet.getLastColumn()).getValues()[0];
if (lastColumn >= headers.length) {
lastRow += 1
lastColumn = 1
}
const requestJSON = e.postData.contents;
const requestObj = JSON.parse(requestJSON);
switch(headers[lastColumn]) {
case HeaderString.messageId:
case HeaderString.userId:
case HeaderString.messageType:
case HeaderString.dateTime:
var values = [];
for (i in headers){
var val = "";
switch(headers[i]) {
case HeaderString.messageId:
val = lastRow - 1;
break;
case HeaderString.userId:
val = "373737";
break;
case HeaderString.messageType:
val = requestObj.type;
break;
case HeaderString.dateTime:
val = new Date();
break;
default:
break;
}
values.push(val);
}
sheet.appendRow(values);
questionShopList("");
break;
case HeaderString.question1:
const postback = requestObj.content.postback;
if (postback == postBack) {
sheet.getRange(lastRow, lastColumn + 1).setValue(requestObj.content.text);
sendMessage("あなたの名前を教えてください。");
} else {
questionShopList(requestObj.content.text);
}
break;
case HeaderString.question2:
sheet.getRange(lastRow, lastColumn + 1).setValue(requestObj.content.text);
sendMessage("どのようなことでお困りでしょうか?");
break;
case HeaderString.question3:
sheet.getRange(lastRow, lastColumn + 1).setValue(requestObj.content.text);
sheet.getRange(lastRow, 8).setValue("質問完了");
sendMessage("どうもありがとうございました。");
break;
}
}
function sendMessage(message) {
const accessToken = getAccessToken().access_token;
const url = `https://www.worksapis.com/v1.0/bots/${configureData.botId}/channels/${configureData.channelId}/messages`
if (typeof message == 'string') {
const options = {
method: 'post',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
payload: JSON.stringify({
'content': {
'type': 'text',
"text": message
}
})
}
try {
const response = UrlFetchApp.fetch(url, options);
Logger.log(response)
} catch(e) {
// 例外エラー処理
Logger.log('Error(messages):')
Logger.log(e)
}
}
}
function questionShopList(inputText) {
const ss = SpreadsheetApp.getActive()
const sheet = ss.getSheetByName(SheetNames.shopList)
const shopListData = sheet.getRange(1,1,sheet.getLastRow(),3).getValues();
const shopListKana = shopListData.filter(value => value[2].match(inputText)).splice(0,10);
var questionText = "所属店舗はどちらですか?\n(頭文字を入力すると検索が部分一致検索ができます)";
var actions = [];
if (inputText == "") {
for (index in shopListData.splice(0,10)) {
const action = {
"type": "message",
"label": shopListData[index][1],
"postback": postBack
};
actions.push(action);
}
} else {
if (shopListKana.length > 0) {
shopListKana.forEach(function(shop){
const action = {
"type": "message",
"label": shop[1],
"postback": postBack
};
actions.push(action);
})
} else {
questionText = "一致する店名がありませんでした。もう一度入力してください。"
for (index in shopListData.splice(0,10)) {
const action = {
"type": "message",
"label": shopListData[index][1],
"postback": postBack
};
actions.push(action);
}
}
}
const accessToken = getAccessToken().access_token;
const url = `https://www.worksapis.com/v1.0/bots/${configureData.botId}/channels/${configureData.channelId}/messages`
const options = {
method: 'post',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
payload: JSON.stringify({
"content": {
"type": "button_template",
"contentText": questionText,
"actions": actions
}
})
}
try {
const response = UrlFetchApp.fetch(url, options);
Logger.log(response)
} catch(e) {
// 例外エラー処理
Logger.log('Error(messages):')
Logger.log(e)
}
}
トークンなど
const configureData = {
clientiD: [Client ID],
clientSecret: [Client Secret],
serviceAccount: "Service Account",
privateKey: "-----BEGIN PRIVATE KEY-----\n[Private Key]\n-----END PRIVATE KEY-----",
botId: "[Bot ID]",
channelId: "[チャンネルID]"
}
const getAccessToken = () => {
const time = Date.now();
const header = Utilities.base64Encode(JSON.stringify({ 'alg': 'RS256', 'typ': 'JWT' }));
const claimSet = Utilities.base64Encode(JSON.stringify({
'iss': configureData.clientiD,
'sub': configureData.serviceAccount,
'iat': Math.floor(time / 1000),
'exp': Math.floor(time / 1000 + 3600)
}));
const privateKey = configureData.privateKey;
const signature = Utilities.base64Encode(
Utilities.computeRsaSha256Signature(
`${header}.${claimSet}`,
privateKey
)
);
const jwt = `${header}.${claimSet}.${signature}`;
const endpoint = 'https://auth.worksmobile.com/oauth2/v2.0/token';
const options = {
method: 'post',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
payload: {
'assertion': jwt,
'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
'client_id': configureData.clientiD,
'client_secret': configureData.clientSecret,
'scope': 'bot,bot.read'
}
}
try {
const res = JSON.parse(UrlFetchApp.fetch(endpoint, options));
Logger.log(res)
return res;
} catch(e) {
// 例外エラー処理
Logger.log('Error(token):')
Logger.log(e)
return "";
}
}
デモ動画
詳細
選択式のフォームメッセージの実装部分
function questionShopList(inputText) {
const ss = SpreadsheetApp.getActive()
const sheet = ss.getSheetByName(SheetNames.shopList)
const shopListData = sheet.getRange(1,1,sheet.getLastRow(),3).getValues();
const shopListKana = shopListData.filter(value => value[2].match(inputText)).splice(0,10);
var questionText = "所属店舗はどちらですか?\n(頭文字を入力すると検索が部分一致検索ができます)";
var actions = [];
if (inputText == "") {
for (index in shopListData.splice(0,10)) {
const action = {
"type": "message",
"label": shopListData[index][1],
"postback": postBack
};
actions.push(action);
}
} else {
if (shopListKana.length > 0) {
shopListKana.forEach(function(shop){
const action = {
"type": "message",
"label": shop[1],
"postback": postBack
};
actions.push(action);
})
} else {
questionText = "一致する店名がありませんでした。もう一度入力してください。"
for (index in shopListData.splice(0,10)) {
const action = {
"type": "message",
"label": shopListData[index][1],
"postback": postBack
};
actions.push(action);
}
}
}
const accessToken = getAccessToken().access_token;
const url = `https://www.worksapis.com/v1.0/bots/${configureData.botId}/channels/${configureData.channelId}/messages`
const options = {
method: 'post',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
payload: JSON.stringify({
"content": {
"type": "button_template",
"contentText": questionText,
"actions": actions
}
})
}
try {
const response = UrlFetchApp.fetch(url, options);
Logger.log(response)
} catch(e) {
// 例外エラー処理
Logger.log('Error(messages):')
Logger.log(e)
}
}
}
ひとつひとつ見ていこう。
手順で言えば、以下の通りだ。
- すでに記載しているシートのリストからデータを取得する
- 初回のボタンテンプレートメッセージかどうか判別する
- 送られてきたメッセージが質問に対する回答か、単なるメッセージか判別する
- 単なるメッセージの場合、部分検索と判定して絞り込みを行う
- 質問に対する回答だった場合、シートにデータを記載する
- データを元に選択肢のデータを作成する
すでに記載しているシートのリストからデータを取得する
まずは、選択のリストを取得する。
以下のように記載したシートを配列にする。
それが以下の部分だ。
- データを入力しているシートを取得する
- シートに入力されている特定の範囲を配列にする
const ss = SpreadsheetApp.getActive()
const sheet = ss.getSheetByName(SheetNames.shopList)
const shopListData = sheet.getRange(1,1,sheet.getLastRow(),3).getValues();
const shopListKana = shopListData.filter(value => value[2].match(inputText)).splice(0,10);
- shop_listのシートを取得
- getRangeメソッドを使用して、1行目の1列目を始点として、行に関しては入力されている最後の行(LastRow)まで、列に関しては始点から3列の範囲を取得(getRange)する。
初回のボタンテンプレートメッセージかどうか判別する
該当箇所は以下。
var questionText = "所属店舗はどちらですか?\n(頭文字を入力すると検索が部分一致検索ができます)";
var actions = [];
if (inputText == "") {
for (index in shopListData.splice(0,10)) {
const action = {
"type": "message",
"label": shopListData[index][1],
"postback": postBack
};
actions.push(action);
}
} else {
if (shopListKana.length > 0) {
shopListKana.forEach(function(shop){
const action = {
"type": "message",
"label": shop[1],
"postback": postBack
};
actions.push(action);
})
} else {
questionText = "一致する店名がありませんでした。もう一度入力してください。"
for (index in shopListData.splice(0,10)) {
const action = {
"type": "message",
"label": shopListData[index][1],
"postback": postBack
};
actions.push(action);
}
}
}
// 初回で呼び出す時
questionShopList("");
// なんかの入力を返事に反映させる場合
questionShopList(requestObj.content.text);
上記のように、初回で呼び出す時は長さ0の文字列を引数に設定し、それ以外は送信したメッセージを引数に渡すように実装している。
for (index in shopListData.splice(0,10)) {
const action = {
"type": "message",
"label": shopListData[index][1],
"postback": postBack
};
actions.push(action);
}
上記のように初回の場合はshopListDataの10項目を取得する。
const shopListKana = shopListData.filter(value => value[2].match(inputText)).splice(0,10);
shopListKana.forEach(function(shop){
const action = {
"type": "message",
"label": shop[1],
"postback": postBack
};
actions.push(action);
})
何かのメッセージが送られてきた場合は、上記のようにshopListKanaのリストにinputText(メッセージの内容)と一致するもののみを取得するようにしている。
送られてきたメッセージが質問に対する回答か、単なるメッセージか判別する
case HeaderString.question1:
const postback = requestObj.content.postback;
if (postback == postBack) {
sheet.getRange(lastRow, lastColumn + 1).setValue(requestObj.content.text);
sendMessage("あなたの名前を教えてください。");
} else {
questionShopList(requestObj.content.text);
}
break;
「requestObj.content.postback」というオブジェクトを見て判断する。
こちらは後述するが、選択肢のデータを作成する際、「postback」というパラメータを設定している。
これは、選択肢のボタンが押された時にメッセージを送るのと併せてデータ別のを載せることができる。
ここのpostbackが設定されているかどうかで質問に回答したのか、それとも単にメッセージを送ったのかを判別する
参考ページ
- LINE WORKS Developers「Bot > ボタンテンプレート」
- LINE WORKS Developers「Bot > アクション」
- LINE WORKS Developers「ポストバックアクション 」
- SAMURAI ENGINEER Blog「【JavaScript入門】これでわかる!enumとは」
- 非IT企業に勤める中年サラリーマンのIT日記「Google Apps Scriptdeで2次元配列にフィルター(filter)をかけて抽出する方法」
- 平社員てつお「【GAS】配列から要素を削除する方法まとめ(pop / shift / splice / filter)」
- いつも隣にITのお仕事「Google Apps Scriptでスプレッドシートの列データを配列として取得する方法」
- いつも隣にITのお仕事「【初心者向け】Google Apps Scriptでクラスを理解するためのオブジェクトの基礎知識」
- Qiita「Google Apps ScriptにおけるLINE WORKS API 2.0の認証」
- Qiita「【GAS】LINEbotでテンプレートメッセージを活用する」
- Qiita「【GoogleAppsScript】ログ出力(デバッグ目的)」