メールとLINEで緊急連絡システムを作った話【コード全体編】
ここまで【構想編】【設計編】【環境編】と3回を準備にあてて来ました。今回は、コードと全体のフローチャートを書いてしまいます。
コードの全体像
コードだけペッと貼り付けてもわかりづらいと思い、フローチャート状にした全体図を作成しましたが
余計わかんないかも・・・
ざっくりと説明すると、上の紫の□内がフォーム側のコードで、こちらは主にメッセージの送信と送信結果の通知機能、それとブラストメールの情報を取得する機能を持たせています。緑の方はスプレッドシート側のコードで、主にLINEユーザーの挙動に合わせた動きを設定しています。(超ざっくり)
コード(スプレッドシート側)
それではまずスプレッドシート側から
var channel_access_token = "トークン****************";
var headers = {
"Content-Type": "application/json; charset=UTF-8",
"Authorization": "Bearer " + channel_access_token
};
var spreadsheet = SpreadsheetApp.openById("ID***********************");
var userSheet = spreadsheet.getSheetByName("LINEユーザー");
var logSheet = spreadsheet.getSheetByName("ログ");
var listSheet = spreadsheet.getSheetByName("リスト");
//イベントへのアクション
function doPost(e) {
var webhookData = JSON.parse(e.postData.contents).events[0];
var eventType = webhookData.type;
switch(eventType){
case "follow": //友だち追加
follow(webhookData);
break;
case "message": //メッセージ受信
messageEvent(webhookData);
break;
case "unfollow": //ブロック
unfollow(webhookData)
break;
case "postback": //ポストバックイベント
postbackEvent(webhookData)
break;
}
}
function postbackEvent(webhookData){
var postbackData = webhookData.postback.data
var replyToken = webhookData.replyToken;
var userId = webhookData.source.userId;
var backNumber = getBacknumberMessage()
for(var i = 0; i<backNumber.length;i++){
var date = backNumber[i].date
var subject = backNumber[i].subject
var textPart = backNumber[i].textPart
if(date==postbackData){
var replyText = `【${subject}】\n${textPart}`
Logger.log(replyText)
sendLineMessageFromReplyToken(replyToken, replyText)
}
}
}
function follow(webhookData){
var replyToken = webhookData.replyToken;
var userId = webhookData.source.userId;
var nickname = getUserProfile(userId);
var last_row = userSheet.getLastRow();
var followMessage = "様、「アカウント名****************」にご登録いただきありがとうございます。\nこのアカウントは、主に災害時・緊急時等の非常連絡手段として、施設名********の利用者、ご家族、関係機関の方々向けに情報発信を行なっています。\n\nまずはじめに、お名前の登録を行ないます。\n画面下の入力欄からご自身のお名前を入力し、送信してください。また関係機関の方は、事業所または法人名を併せてご記入いただけると幸いです。\n例①〇〇〇〇(●●の親など)\n例②〇〇〇〇(●●事業所など)";
var number = userSheet.getRange(last_row,1).getRow();
userSheet.getRange(last_row+1,1).setValue(number);
userSheet.getRange(last_row+1,2).setValue(nickname);
userSheet.getRange(last_row+1,3).setValue(userId);
userSheet.getDataRange().removeDuplicates([3]);
var text = `【管理者通知】\n${nickname}さんが「アカウント名************」に登録しました\nリストを編集してください`;
var textToUser = nickname + followMessage;
sendLineMessageFromReplyToken(replyToken,textToUser);
sendLineMessageToAdmin(text);
}
function messageEvent(webhookData){
var userId = webhookData.source.userId;
var nickname = getUserProfile(userId);
var timestamp = new Date(webhookData.timestamp)
var message = webhookData.message.text;
var replyToken = webhookData.replyToken;
var lastRow = logSheet.getLastRow();
switch (message) {
case '確認':
checkTotalUsage(replyToken);
break;
case '配信履歴':
getBackNumber(replyToken)
logSheet.getRange(lastRow+1,1).setValue(timestamp);
logSheet.getRange(lastRow+1,2).setValue(nickname);
logSheet.getRange(lastRow+1,3).setValue(userId);
logSheet.getRange(lastRow+1,4).setValue(message);
break;
default:
//メッセージのログ登録
logSheet.getRange(lastRow+1,1).setValue(timestamp);
logSheet.getRange(lastRow+1,2).setValue(nickname);
logSheet.getRange(lastRow+1,3).setValue(userId);
logSheet.getRange(lastRow+1,4).setValue(message);
//userIdからユーザー名の検索
var userFinder = userSheet.createTextFinder(userId).findAll();
for ( var i = 0; i < userFinder.length; i++ ) {
var userName = userFinder[i].offset(0,1).getValue()
}
//ユーザー名未登録の場合の処理
if(userName == ""){
userName = "氏名未登録"
}
var text =`【管理者通知】\n${nickname}(${userName})さんからメッセージがあります。\n内容:\n${message}`
//ユーザーからの返信を管理者に通知する
sendLineMessageToAdmin(text);
break;
}
}
function unfollow(webhookData){
var data = userSheet.getDataRange();
var userId = webhookData.source.userId;
var nickname = getUserProfile(userId);
var userFinder = data.createTextFinder(userId).findAll();
for ( var i = 0; i < userFinder.length; i++ ) {
var userRow = userFinder[i].getRow();
userSheet.deleteRows(userRow);
}
var text = `【管理者通知】\n${nickname}さんが「○○○○○○○」を退会しました\n${nickname}さんのデータをリストから削除しました。`
sendLineMessageToAdmin(text);
var idFinder = listSheet.createTextFinder(userId).findAll();
for ( var i = 0; i < idFinder.length; i++ ) {
idFinder[i].clearContent
}
}
function getUserProfile(userId){
var Url = 'https://api.line.me/v2/bot/profile/' + userId;
var userProfile = UrlFetchApp.fetch(Url,{
'headers': {
'Authorization' : 'Bearer ' + channel_access_token,
},
})
return JSON.parse(userProfile).displayName;
}
function checkTotalUsage(replyToken){
var totalUsage = getTotalUsage();
var remainingCapacity = 1000 - totalUsage;
var people = userSheet.getLastRow()-1;
var messageCap = Math.round(remainingCapacity / people);
var text = `現時点での送信数は概算で${totalUsage}通です。\n今月中に送信できる回数の目安は${messageCap}回(${remainingCapacity}通)です`;
sendLineMessageFromReplyToken(replyToken, text)
}
function getTotalUsage(){
var url = "https://api.line.me/v2/bot/message/quota/consumption";
var options = {"headers": headers};
var response = UrlFetchApp.fetch(url, options);
var data = JSON.parse(response);
return data.totalUsage
}
//返信する
function sendLineMessageFromReplyToken(token, replyText) {
var url = "https://api.line.me/v2/bot/message/reply";
var postData = {
"replyToken": token,
"messages": [{
"type": "text",
"text": replyText
}]
};
var options = {
"method": "POST",
"headers": headers,
"payload": JSON.stringify(postData)
};
return UrlFetchApp.fetch(url, options);
}
//特定の人にメッセージを送る
function sendLineMessageFromUserId(text,userId) {
var url = "https://api.line.me/v2/bot/message/push";
var postData = {
"to": userId,
"messages": [{
"type": "text",
"text": text,
}]
};
var options = {
"method": "POST",
"headers": headers,
"payload": JSON.stringify(postData)
};
return UrlFetchApp.fetch(url, options);
}
//管理者にメッセージを送る
function sendLineMessageToAdmin(text){
var adminList = [];
var adminFinder = userSheet.createTextFinder('○').findAll();
for ( var i = 0; i < adminFinder.length; i++ ) {
var admin = adminFinder[i].offset( 0 , -3).getValue();
adminList.push(admin)
}
var url = "https://api.line.me/v2/bot/message/multicast";
var postData = {
"to": adminList,
"messages": [{
"type": "text",
"text": text,
}]
};
var options = {
"method": "POST",
"headers": headers,
"payload": JSON.stringify(postData)
};
return UrlFetchApp.fetch(url, options);
}
//登録者全員にメッセージを送る
function sendLineMessageUsingBroadcast(text) {
var Url = "https://api.line.me/v2/bot/message/broadcast";
var postData = {
"messages": [{
"type": "text",
"text": text,
}]
};
var options = {
"method": "POST",
"headers": headers,
"payload": JSON.stringify(postData)
};
return UrlFetchApp.fetch(Url, options);
}
function getBackNumber(token) {
var itemArray = [];
var length;
var message = getBacknumberMessage()
if(message.length>13){
length = 13
}else{
length = message.length
}
for(var i = 0; i < length; i++){
var date = message[i].date
var year = Number(date.slice(0,4))
var month = Number(date.slice(4,6))
var day = Number(date.slice(6,8))
var hour = Number(date.slice(9,11))
var minute = Number(date.slice(12,14))
var newDate = new Date(year,month-1,day,hour,minute)
var object = {"type":"action","action":{"type":"postback","label":`${month}/${day} ${hour}:${minute}`,"data":date,"displayText":`${month}/${day} ${hour}:${minute}のメッセージ`}}
itemArray.push(object)
}
UrlFetchApp.fetch('https://api.line.me/v2/bot/message/reply', {
'headers': {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + channel_access_token,
},
'method': 'POST',
'payload': JSON.stringify({
"replyToken": token,
"messages": [{
"type": "text", // ①
"text": "過去に送信したメッセージを再確認できます(最新13通分まで)\n左右にスクロールし、日時を選択してください",
"quickReply": { // ②
"items": itemArray
}
}],
}),
});
}
function blastmailLogin() {
var url = "https://api.bme.jp/rest/1.0/authenticate/login";
var postData = {
"f": "json",
"username": "********************",
"password": "********************",
"api_key": "********************"
};
var options = {
"method": "POST",
"payload": postData,
"muteHttpExceptions":true
};
var response = UrlFetchApp.fetch(url, options);
var data = JSON.parse(response);
return data.accessToken;
}
function getBacknumberMessage(){
var token = blastmailLogin()
var url = "https://api.bme.jp/rest/1.0/message/backnumber/detail/search";
var requestQuery = `?access_token=${token}&f=json&groupID=1`;
url = url + requestQuery;
var options = {"muteHttpExceptions":true};
var response = UrlFetchApp.fetch(url, options);
var json = JSON.parse(response)
return json.message
}
コード(フォーム側)
続いてフォーム側のコードです
var channel_access_token = "トークン*******************";
var headers = {
"Content-Type": "application/json; charset=UTF-8",
"Authorization": "Bearer " + channel_access_token
};
var spreadsheet = SpreadsheetApp.openById("ID******************");
var userSheet = spreadsheet.getSheetByName("LINEユーザー");
var logSheet = spreadsheet.getSheetByName("ログ");
var mailingSheet = spreadsheet.getSheetByName("一斉連絡");
var reservationSheet = spreadsheet.getSheetByName("送信予約");
var listSheet = spreadsheet.getSheetByName("リスト");
var blastUserSheet = spreadsheet.getSheetByName("blastmailユーザー");
var failedMailSheet = spreadsheet.getSheetByName("blastmail送信失敗");
var blastStatuslogSheet =spreadsheet.getSheetByName("blastmail登録者推移");
var ngwordSheet = spreadsheet.getSheetByName("NGワード");
function sendMessageToBlastmailAndLine(){
var subject = mailingSheet.getRange(mailingSheet.getLastRow(),2).getValue();
var text = mailingSheet.getRange(mailingSheet.getLastRow(),3).getValue();
var name = mailingSheet.getRange(mailingSheet.getLastRow(),4).getValue();
var reservation = mailingSheet.getRange(mailingSheet.getLastRow(),5);
var wordChecker = wordCheck(subject + text + name)
if(wordChecker[0].length>0){ //禁止ワード入ってる場合
var value = `【管理者通知】\nただいま作成した一斉連絡メッセージには「環境依存文字」が含まれているため、送信できませんでした。\n以下の文字を修正のうえ、改めて送信してください。\n使用できない文字:${wordChecker}\n送信者名:${name}`
sendLineMessageToAdmin(value)
Logger.log("NGワード")
}else if(reservation.isBlank()){ //即時配信
var whatToSend = `【${subject}】\n${text}\n\n会社名**********\n${name}`;
sendLineMessageUsingBroadcast(whatToSend);
sendLineMessageToSupportTool(whatToSend);
sendBlastmailNow(subject,text,name);
Logger.log("即時送信")
}else{ //予約配信
var value = mailingSheet.getRange(mailingSheet.getLastRow(),1,1,5).getValues();
reservationSheet.getRange(reservationSheet.getLastRow()+1,1,1,5).setValues(value);
Logger.log("予約送信")
}
};
function sendReservation(){
for(var i = 2 ; i<= reservationSheet.getLastRow() ; i++){
var subject = reservationSheet.getRange(i,2).getValue();
var text = reservationSheet.getRange(i,3).getValue();
var name = reservationSheet.getRange(i,4).getValue();
var reservation = reservationSheet.getRange(i,5);
var sentOrNot = reservationSheet.getRange(i,6);
if(reservation.getValue() < new Date() && sentOrNot.isBlank()){
var textForLine = `【${subject}】\n${text}\n\n会社名**********\n${name}`;
sendLineMessageUsingBroadcast(textForLine);
sendLineMessageToSupportTool(whatToSend);
sendBlastmailNow(subject,text,name);
sentOrNot.setValue("済");
}else{
Logger.log("送信するメッセージはありませんでした")
}
}
}
function unregistered(messageId,token){
var nameArr = [];
var telArr = [];
var newArr = [];
var result = [];
var errorArr= [];
var result2 = [];
for(var i = 2 ; i <= listSheet.getLastRow() ; i++){
var name = listSheet.getRange(i,2);
var mail = listSheet.getRange(i,4);
var line = listSheet.getRange(i,5);
var tel = listSheet.getRange(i,6);
if(mail.isBlank() && line.isBlank()){
nameArr.push(name.getValue());
telArr.push(tel.getValue());
}
}
nameArr.unshift("利用者名");
telArr.unshift("電話番号");
newArr.push(nameArr,telArr);
for(var j = 0 ; j < newArr[0].length ; j++){
result[j] = newArr[0][j] + " , " + newArr[1][j];
}
var text = `以下の方はメッセージが届いていません。\n直接電話で連絡してください。\n\n【未登録・送信解除中】\n${result.join("\n")}\n\n※なお今回のメールで送信エラーは発生しておりません`;
Utilities.sleep(180000);//getFailedの処理を待つ時間。ミリ秒
var failed = getFailed(messageId,token)
if(failed.name.length){
failed.name.unshift("利用者名");
failed.number.unshift("電話番号");
errorArr.push(failed.name,failed.number)
for(var i = 0 ; i < errorArr[0].length ; i++){
result2[i] = errorArr[0][i] + " , " + errorArr[1][i];
text = `以下の方はメッセージが届いていません。\n直接電話で連絡してください。\n\n【未登録・送信解除中】\n${result.join("\n")} \n\n【送信エラー】\n${result2.join("\n")}`;
}
}else{
Logger.log("送信失敗なし")
}
sendLineMessageToAdmin(text)
}
function sendLineMessageFromUserId(text,userId) {
var url = "https://api.line.me/v2/bot/message/push";
var postData = {
"to": userId,
"messages": [{
"type": "text",
"text": text,
}]
};
var options = {
"method": "POST",
"headers": headers,
"payload": JSON.stringify(postData)
};
return UrlFetchApp.fetch(url, options);
}
function sendLineMessageUsingBroadcast(text) {
var Url = "https://api.line.me/v2/bot/message/broadcast";
var postData = {
"messages": [{
"type": "text",
"text": text,
}
]};
var options = {
"method": "POST",
"headers": headers,
"payload": JSON.stringify(postData)
};
return UrlFetchApp.fetch(Url, options);
}
function sendLineMessageToAdmin(text){
var adminList = [];
var adminFinder = userSheet.createTextFinder('○').findAll();
for ( var i = 0; i < adminFinder.length; i++ ) {
var admin = adminFinder[i].offset( 0 , -3).getValue();
adminList.push(admin)
}
var url = "https://api.line.me/v2/bot/message/multicast";
var postData = {
"to": adminList,
"messages": [{
"type": "text",
"text": text,
}]
};
var options = {
"method": "POST",
"headers": headers,
"payload": JSON.stringify(postData)
};
return UrlFetchApp.fetch(url, options);
}
function blastmailLogin() {
var url = "https://api.bme.jp/rest/1.0/authenticate/login";
var postData = {
"f": "json",
"username": "*********",
"password": "*********",
"api_key": "**********"
};
var options = {
"method": "POST",
"payload": postData,
"muteHttpExceptions":true
};
var response = UrlFetchApp.fetch(url, options);
var data = JSON.parse(response);
return data.accessToken;
}
function blastmailLogout(token) {
var url = "https://api.bme.jp/rest/1.0/authenticate/logout";
var postData = {"access_token": token };
var requestQuery = `?access_token=${token}`;
url = url + requestQuery;
var options = {"muteHttpExceptions":true};
UrlFetchApp.fetch(url, options);
}
function sendBlastmailNow(subject,text,name) {
var text = text + "\n\n会社名****************\n" + name
var url = "https://api.bme.jp/rest/1.0/message/sendnow/create";
var token = blastmailLogin()
var postData = {
"format": "json",
"senderID": "1",
"groupID": "1",
"subject": subject,
"textPart": text,
"access_token": token,
"public":"true"
};
var options = {
"method": "POST",
"payload": postData,
"muteHttpExceptions":true
};
var response = UrlFetchApp.fetch(url, options);
var jsonData = JSON.parse(response)
var messageId = jsonData.messageID
unregistered(messageId,token)
}
function getBlastmailUser() {
var url = "https://api.bme.jp/rest/1.0/contact/list/export";
var token = blastmailLogin()
var requestQuery = `?access_token=${token}`;
url = url + requestQuery;
var options = {"muteHttpExceptions":true};
var response = UrlFetchApp.fetch(url, options);
var data = response.getContentText("shift-jis");
var csv = Utilities.parseCsv(data);
try{
blastUserSheet.getRange(1,1,blastUserSheet.getLastRow(),blastUserSheet.getLastColumn()).clearContent()
}catch(e){
var result = "エラーの内容="+e;
Logger.log(result)
}
blastUserSheet.getRange(2, 2, csv.length, csv[0].length).setValues(csv);
blastUserSheet.getRange(1,1).setValue("Blastmailユーザー一覧("+ Utilities.formatDate(new Date(), 'Asia/Tokyo', 'yyyy/MM/dd') + " 現在)")
blastUserSheet.getRange(2,1).setValue("No.")
for(var i = 3 ; i<=blastUserSheet.getLastRow() ; i++){
blastUserSheet.getRange(i,1).setValue(i-2);
}
blastmailLogout(token)
}
function getBlastmailStatusLog() {
var url = "https://api.bme.jp/rest/1.0/statuslog/list/export";
var token = blastmailLogin();
var date = new Date(2020,11,9);//利用開始日
var startDate = Utilities.formatDate(date, 'Asia/Tokyo', "yyyyMMdd'T'HH:mm:ss");
var today = Utilities.formatDate(new Date(), 'Asia/Tokyo', "yyyyMMdd'T'HH:mm:ss");
var requestQuery = `?access_token=${token}&beginDate=${startDate}&endDate=${today}`;
url = url + requestQuery;
var options = {"muteHttpExceptions":true};
var response = UrlFetchApp.fetch(url, options);
var data = response.getContentText("shift-jis");
var csv = Utilities.parseCsv(data);
try{
blastStatuslogSheet.getRange(1,1,blastStatuslogSheet.getLastRow(),blastStatuslogSheet.getLastColumn()).clearContent()
}catch(e){
var result = "エラーの内容="+e;
Logger.log(result)
}
blastStatuslogSheet.getRange(2, 1, csv.length, csv[0].length).setValues(csv);
blastStatuslogSheet.getRange(1,1).setValue("Blastmail登録者推移("+ Utilities.formatDate(new Date(), 'Asia/Tokyo', 'yyyy/MM/dd') + " 現在)");
blastmailLogout(token)
}
function getFailed(messageId,token) {
var nameArray = [];
var numberArray = [];
var url = "https://api.bme.jp/rest/1.0/history/list/export";
var requestQuery = `?access_token=${token}&messageID=${messageId}&status=1`;
url = url + requestQuery;
var options = {"muteHttpExceptions":true};
var response = UrlFetchApp.fetch(url, options);
var data = response.getContentText("shift-jis");
var csv = Utilities.parseCsv(data);
try{
failedMailSheet.getRange(1,1,failedMailSheet.getLastRow(),failedMailSheet.getLastColumn()).clearContent()
}catch(e){
var result = "エラーの内容="+e;
Logger.log(result)
}
failedMailSheet.getRange(2, 2, csv.length, csv[0].length).setValues(csv);
failedMailSheet.getRange(1,1).setValue("Blastmail送信失敗宛先一覧(messageID : "+ messageId + " 送信日 : " + Utilities.formatDate(new Date(), 'Asia/Tokyo', 'yyyy/MM/dd') + ")")
failedMailSheet.getRange(2,1).setValue("No.")
for(var i = 3 ; i<= failedMailSheet.getLastRow() ; i++){
failedMailSheet.getRange(i,1).setValue(i-2);
}
for(var i = 3; i <= failedMailSheet.getLastRow(); i++){
var address = failedMailSheet.getRange(i,2).getValue();
var addressFinder = listSheet.createTextFinder(address).findAll();
for(var j = 0; j < addressFinder.length; j++ ) {
var name = addressFinder[j].offset( 0 , -2).getValue();
var number = addressFinder[j].offset( 0 , 2).getValue();
nameArray.push(name)
numberArray.push(number)
}
}
var failedData = {"name": nameArray,"number":numberArray}
return failedData
}
function wordCheck(str) {
var ngWord = getWords();
var array = []
var arr = []
for(var i = 0; i < ngWord.length; i++){
var list = str.match(ngWord[i])
arr.push(list)
}
array.push(String(arr.filter(Boolean)));
return array
}
//機種依存文字リスト
function getWords(){
var array1 = ngwordSheet.getDataRange().getValues()
var array2 = Array.prototype.concat.apply([], array1).filter(Boolean);
return array2
}
function checkAndRegistration(){
var list = [];
var str = "" //文字チェックするとき使う
var ngwordSheet = spreadsheet.getSheetByName("NGワード");
var unregisteredArr = wordCheck(str)
for(var i = 0; i <= unregisteredArr[0].length; i++){
var str = str.replace(unregisteredArr[0][i],"");
}
list.push(str.split(""))
if(list[0].length == 0){
Logger.log("すべて登録済みの文字です");
}else{
ngwordSheet.getRange(ngwordSheet.getLastRow()+1,1,list.length,list[0].length).setValues(list)
Logger.log(list[0] + "をスプレッドシートに登録しました")
}
}
トリガー設定
トリガーはすべてフォーム側で設定しています。
①sendMessageToBlastmailAndLine(メッセージ送信)
②sendReservation(予約送信)
③ブラストメール登録者リストアップ
ブラストメール登録者推移リストアップ
以上が今回の緊急連絡システムの全容です。これだけで使えてしまいそうな方はぜひご活用いただければと思います。
この後数回かけて、細かい部分の解説を行なう予定です。