これは TOWN Advent Calendar 2019 2日目のエントリーです。
オフィスや事務所の電話を代行して、チャットやメールでお知らせしてくれる fondesk というサービスがあります。
fondeskのSlack通知に対してメンションを追加するBOT
fondeskは受電したらSlackのチャンネルへ通知をしてくれるのですが、メンションをつけることができないため、メンションをつけてくれるBOTを作成しました。詳しくは「fondeskのSlack通知に対してメンションを追加するBOTを作った」に記載をしています。
BOTに機能を追加
今回さらに進化させて以下のような機能をつけました。
- 営業電話時にはメンションを飛ばさない対応
- OKボタン対応
- 営業電話ボタン対応
- 受電履歴の保存
コードはこちらのGistに載っています。
function doPost(e) {
var channel = 'XXXXXXXX'; // fondeskの通知先チャンネル
var url = 'https://hooks.slack.com/services/XXXXXXXX'; // Incoming Webhook URL
var TOKEN = 'XXXXXXXX'; // Verification Token
var botId = 'XXXXXXXX'; //fondeskのアプリのBOT ID
var historySave = true; //データをスプレッドシートに保存する
var sheetName = 'list'; //名簿リストのスプレッドシート名
var historySheetName = 'history'; //入電履歴のスプレッドシート名
var suppressedSheetName = 'suppressed'; //営業電話のスプレッドシート名
var postData = {};
var jsonData = {};
var isPost = false;
// subscribe posted messages
try {
var decodeData = unescapeUnicode(e.postData.getDataAsString());
postData = JSON.parse(decodeData);
isPost = true;
} catch (ex) {
// debug(ex);
}
// Interactive messages
try {
if (e.parameter) {
var parameter = e.parameter;
var payload = parameter.payload;
jsonData = JSON.parse(decodeURIComponent(payload));
}
} catch (ex) {
// debug(ex);
}
// 認証
if (isPost && postData.type == 'url_verification' && postData.token == TOKEN) {
var res = {
'challenge': postData.challenge
};
return ContentService.createTextOutput(JSON.stringify(res)).setMimeType(ContentService.MimeType.JSON);
}
// fondeskのチャンネルを監視
if (isPost && postData.token == TOKEN && postData.type == "event_callback" && postData.event.type == "message" && postData.event.channel == channel && postData.event.bot_id == botId) {
var getContents = "";
var getSender = "";
var historyData = [];
var message = '';
var nobody = true;
var array = [];
var options = {};
//投稿内容の取得
var thread = postData.event.event_ts;
var time = thread.split(".");
var date = new Date(time[0] * 1000);
historyData.push(formatDate(date));
var attachments = postData.event.attachments;
for (var i = 0; i < attachments.length; i++) {
var fields = attachments[i].fields;
for (var j = 0; j < fields.length; j++) {
historyData.push(fields[j].value);
if (fields[j].title == "発信者") {
getSender = fields[j].value;
}
if (fields[j].title == "内容") {
getContents = fields[j].value;
}
}
}
// 受電履歴を保存
if (historySave) {
var historySheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(historySheetName); //シートを取得
historySheet.appendRow(historyData);
}
//営業電話リストのチェック
var suppressedSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(suppressedSheetName);
var textFinder = suppressedSheet.createTextFinder(getSender);
var ranges = textFinder.findAll();
var isSuppressed = ranges.length > 0 ? true : false;
if (isSuppressed) {
//営業電話
message = "営業電話がありました。";
options = {
"method": "post",
"contentType": "application/json",
"payload": JSON.stringify({
"text": message,
"thread_ts": thread,
"reply_broadcast": true,
})
};
} else {
//該当するユーザーの検索
var recordsheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
var sheetValues = recordsheet.getRange('A:A').getValues(); //A列の値を全て取得
var lastRow = sheetValues.filter(String).length; //空白の要素を除いた長さを取得
var sheetRange = 'A2:C' + lastRow; //名簿リストのスプレッドシートデータ取得範囲
var range = recordsheet.getRange(sheetRange);
var data = range.getValues();
for (var k = 0; k < data.length; k++) {
if (data[k][0] != null && data[k][0] != "") {
var result1 = getContents.indexOf(data[k][0]);
var result2 = getContents.indexOf(data[k][1]);
if (result1 !== -1 || result2 !== -1) {
if (array.indexOf(data[k][2]) == -1) {
// メンションを追加
message = "<@" + data[k][2] + "> " + message;
nobody = false;
array.push(data[k][2]);
}
}
}
}
if (nobody) {
//該当者がいない場合
message = "<!here> *" + getSender + "* 様から 宛先不明のお電話です。 対応したら「OK」をお願いします。";
} else {
message = message + " *" + getSender + "* 様から お電話がありました。対応したら「OK」をお願いします。";
}
//返信投稿
options = {
"method": "post",
"contentType": "application/json",
"payload": JSON.stringify({
"text": message,
"thread_ts": thread,
"reply_broadcast": true,
"attachments": [{
"text": "",
"callback_id": "callback_button",
"attachment_type": "default",
"actions": [{
"name": "ok",
"text": "OK",
"type": "button",
"style": "primary",
"value": "ok"
},
{
"name": "suppressed",
"text": "営業電話",
"type": "button",
"style": "danger",
"value": getSender,
"confirm": {
"title": "営業電話",
"text": "*" + getSender + "* 様からの電話を今後誰にもメンションしないようにしてもよろしいですか?",
"ok_text": "はい",
"dismiss_text": "いいえ"
}
}
]
}]
})
};
}
UrlFetchApp.fetch(url, options);
} else if (!isPost && jsonData.token == TOKEN && jsonData.type == "interactive_message" && jsonData.channel.id == channel) {
// ボタンクリック時のアクション
var buttonName = '';
var buttonValue = '';
var text = '';
if (jsonData.actions[0].type == "button") {
buttonName = jsonData.actions[0].name;
buttonValue = jsonData.actions[0].value;
}
var actionUserName = jsonData.user.name;
var originalText = jsonData.original_message.text;
if (buttonName == 'ok') {
//電話応対済
text = actionUserName + " :telephone_receiver:が対応しました。";
} else if (buttonName == 'suppressed') {
//営業電話リスト追加
text = actionUserName + " が営業電話リスト:no_bell:に追加しました。";
var suppressedData = [];
suppressedData.push(formatDate(new Date()));
suppressedData.push(buttonValue);
suppressedData.push(actionUserName);
var suppressedSheet2 = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(suppressedSheetName); //シートを取得
suppressedSheet2.appendRow(suppressedData);
}
var replyMessage = {
"replace_original": true,
"response_type": "in_channel",
"text": originalText,
"attachments": [{
"text": text,
}],
};
return ContentService.createTextOutput(JSON.stringify(replyMessage)).setMimeType(ContentService.MimeType.JSON);
}
}
/**
* デコード
* @param {[String]} string
* @return {[String]}
*/
function unescapeUnicode(string) {
return string.replace(/\\u([a-fA-F0-9]{4})/g, function(matchedString, group1) {
return String.fromCharCode(parseInt(group1, 16));
});
}
/**
* 日付のフォーマッター
*
* @param {[Date]} date
* @param {[String]} format
* @return {[String]}
*/
function formatDate(date, format) {
if (!format) format = 'YYYY-MM-DD hh:mm:ss';
format = format.replace(/YYYY/g, date.getFullYear());
format = format.replace(/MM/g, ('0' + (date.getMonth() + 1)).slice(-2));
format = format.replace(/DD/g, ('0' + date.getDate()).slice(-2));
format = format.replace(/hh/g, ('0' + date.getHours()).slice(-2));
format = format.replace(/mm/g, ('0' + date.getMinutes()).slice(-2));
format = format.replace(/ss/g, ('0' + date.getSeconds()).slice(-2));
if (format.match(/S/g)) {
var milliSeconds = ('00' + date.getMilliseconds()).slice(-3);
var length = format.match(/S/g).length;
for (var i = 0; i < length; i++) format = format.replace(/S/, milliSeconds.substring(i, i + 1));
}
return format;
}
/**
* スプレッドシートに出力
*
* @param {[Object]} postData
*/
function debug(postData) {
var debugSheetName = 'debug'; //デバッグ出力先のスプレッドシート名
var data = [];
data.push(formatDate(new Date()));
data.push(postData);
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(debugSheetName);
sheet.appendRow(data);
}
Google Spreadsheetはこちらで公開しています。
今回OKボタン、営業電話ボタンを押したときのInteractive messagesを実装するにあたってこのへんを参考にさせていただきました。
https://api.slack.com/docs/message-buttons
https://qiita.com/tomoharr24/items/0b4c0f2d9097ab7fc7da
https://qiita.com/tfuruya/items/bff9db72a4a6665115a9
営業電話への対応
電話があったときには担当者にメンションがつくようになっています。「人事」や「採用」といったワードがあった場合にはその担当者にメンションが行くようにしていますが、営業電話が大半でメンションしなくてもよいケースが多く発生していました。
また、担当者が不明な場合には @here にメンションが行くようになっていました。このパターンになるとチャンネルにいる人全員の注意をひくことになってしまいあまりよいとは言えません。
「人事」や「採用」といったワードであれば担当者を割り当てやすいのですが、「営業」というワードだった場合に
「営業電話でした。」→誰にもメンションをしなくてよいパターン
「営業部の〇〇さん宛」→〇〇さんにメンションをしてほしいパターン
の2つのケースがあり、担当者を設定しづらい状況になっていました。
これらの理由から発信者を元に営業電話のリストを作成し、そこに載っている発信者の場合には電話があった旨だけをメンション無しで通知するようなBOTに改良しました。営業電話の場合、折り返しが不要で折り返し先電話番号がないケースが多いため、発信者を元にリスト化をしています。
手作業で営業電話のリストを作成するのも大変ですので、電話があった際に営業電話かどうかをSlackのボタンをクリックすることで登録できるようにしました。
また、以前のBOTでは「対応したらスタンプを押してください。」というコメントにしていましたが、アクションをしたことがより明確になるように「OK」ボタンを配置するようにしました。
投稿時とボタン押下時のPOSTデータの違い
fondeskのBOTからのPOSTデータを解析するのがこのBOTの第1ステップになるのですが、今回は同じエンドポイントでInteractive messagesを受け取るようにしたため、POSTされてくるデータの違いを以下のようにして吸収しています。
var postData = {};
var jsonData = {};
var isPost = false;
// subscribe posted messages
try {
var decodeData = unescapeUnicode(e.postData.getDataAsString());
postData = JSON.parse(decodeData);
isPost = true;
} catch (ex) {
// debug(ex);
}
// Interactive messages
try {
if (e.parameter) {
var parameter = e.parameter;
var payload = parameter.payload;
jsonData = JSON.parse(decodeURIComponent(payload));
}
} catch (ex) {
// debug(ex);
}
まとめ
今回の対応でメンション通知、営業電話であればメンションさせないようにするBOTが完成し、一通りの対応は完了したかなと思います。