見出し画像

LINEで予定を登録→通知してくれるリマインダーアプリを作ろう

今回作ったものは、予定とリマンドして欲しい時間を言うと、自動的に通知してくれるアプリです。

このチュートリアルでは、利用者が42万人を超えるリマインダーLINEアプリ「リマインくん」を参考に作ります。(作者の@hoshi_gakiさんには快諾を頂きました。ありがとうございます。)

リマインくんのQRコードはこちらです。

チュートリアルでは、あなただけの「Myリマインくん」を作ります。

リマインくんはグループに対応していたりと機能が多いですが、Myリマインくんの機能は次のような基本的なものに絞ります。

1.予定を言う

2.時間を指定する

3.時間になるとメッセージが届く

実装方法

Google Apps Scriptという、Google製のJavaScriptをベースに、サーバレスな環境で作っているので、めっちゃくちゃ簡単です。

GoogleがGoogle Apps Scriptを起動時に、Web アプリとして公開ボタンをポチっとするだけで、無料でサーバーを提供してくれるので、環境構築も一切必要ありません。

さらに、Google Apps Scriptには、指定した時間に処理を行うトリガーという機能があります。これを利用して通知を送信します。

このチュートリアルでこのアプリを作るために必要な前提知識

JavaScriptの知識は必要になるので、Progateで勉強してきてください。if文、for文、while文、functionと言われてイメージが沸けば大丈夫です。ちなみにプログラミング未経験の人でも、Progateならわかりやすく学べるのでお勧めです!

必要な知識はこれだけです!

目次

・処理の全体を眺めて、だいたい何をしているのか理解しよう(LINE API・Google Apps Script・Google スプレッドシート )
・Google Apps Scriptで実装してみよう
・必要なファイルを作ろう
・スプレッドシートに記録する内容
・sheet.gsを完成させよう
・line.gsを完成させよう
・Moment.jsライブラリを導入しよう
・コード.gsを完成させよう
・trigger.gsを完成させよう
・完成品を動かしてみよう!
・完成品をシェアしよう
・終わりに

処理の全体を眺めて、だいたい何をしているのか理解しよう(LINE API・Google Apps Script・Google スプレッドシート )

予定を登録するとき

予定を通知するとき

このようになります。

LINEからメッセージを送り、実際にコードを書く部分が、Google Apps Scriptとなります。予定を保存しておくデータベースとして、Googleスプレッドシートを使います。

Google Apps Scriptで実装してみよう

それでは実装を始めましょう!

まずはナベさんプログラミング初心者でも無料で簡単にLINE BOTが作れるチュートリアルの無料部分を実践して下さい。とても分かりやすいです。

スプレッドシートへの書き込みと、オウム返しBotが完成すればOKです。

このオウム返しBotに機能を付け足してMyリマインくんを作ります。(アプリ名は自由に決めて下さい。)

オウム返しBotは作れましたか?それでは次のステップに進みましょう。

必要なファイルを作ろう

1つのファイルに全てのコードを書くとプログラムの管理がしにくくなります。プログラムの機能ごとにファイルを分けることで管理がしやすくなります。

ファイル>新規作成>スクリプトファイルから、次のファイルを新規作成して下さい。

・line
・sheet
・trigger

下のようになればOKです。(application.jsonは自動で生成されます。)

各ファイルの役割ですが、

コード.gs :  doPost関数など、アプリの中心的なファイル
line.gs : LINEとやりとりするための関数を集めたファイル
sheet.gs : スプレッドシートとやりとりするための関数を集めたファイル
trigger.gs : トリガーに関する関数を集めたファイル

となります。

スプレッドシートに記録する内容

スプレッドシートに記録するデータについて説明します。

1列目にはLINEのUserID、2列目には予定、3列目には日時、4列目にはトリガーのUniqueIdを記録します。トリガーのUniqueIdは、トリガーを識別するためのIdです。詳しくは後述します。

sheet.gsを完成させよう

スプレッドシートとのやりとりに関わる関数をsheet.gsにまとめて定義しましょう。以下をsheet.gsにコピペして下さい。

var spreadsheet = SpreadsheetApp.openById("スプレッドシートのID");
var sheet = spreadsheet.getSheetByName('webhook');
function appendToSheet(text) {
 sheet.appendRow([text]);
}
function searchRowNum(searchVal, col) {
 //受け取ったシートのデータを二次元配列に取得
 var dat = sheet.getDataRange().getValues();
 for (var i = 0; i < dat.length; i++) {
   if (dat[i][col] === searchVal) {
     return i;
   }
 }
 return false;
}
function getFromRowCol(sheetName, row, col) {
 var dat = sheet.getDataRange().getValues();
 return dat[row][col];
}
function setFromRowCol(val, row, col) {
 sheet.getRange(row + 1, col + 1).setValue(val);
}
function getUserIdCell(row) {
 return sheet.getRange(row + 1, 1);
}
function getTodoCell(row) {
 return sheet.getRange(row + 1, 2);
}
function getDateCell(row) {
 return sheet.getRange(row + 1, 3);
}
function getTriggerCell(row) {
 return sheet.getRange(row + 1, 4);
}


1行目の "スプレッドシートのID" は適切なものに置き換えて下さい。

以下、関数の解説をします。

appendToSheetは、スプレッドシートに新たな行を書き込みます。
searchRowNumは、検索する値とcolを指定して、見つけた行の番号を返します。なければfalseを返します。
getFromRowCol・setFromRowColは、rowとcolを指定して、値を読み込み・書き込みする関数です。
get〇〇Cell関数は、rowを指定して、その行の〇〇のセルを取り出します。

試しに関数を実行しても良いです。しかし、引数が渡せないので、下のように引数を関数内で上書きしてしまいましょう。(終わったら元に戻して下さい。)

function setFromRowCol(val, row, col) {
 val = 'テスト';
 row = 1;
 col = 1; 
 sheet.getRange(row + 1, col + 1).setValue(val);
}

line.gsを完成させよう

次はLINEに関わる関数を完成させましょう。以下をline.gsにコピペして下さい。

var channel_access_token = "LINEのアクセストークン";
var headers = {
   "Content-Type": "application/json; charset=UTF-8",
   "Authorization": "Bearer " + channel_access_token
};
function sendLineMessageFromReplyToken(token, replyText) {
 var url = "https://api.line.me/v2/bot/message/reply";
 var headers = {
   "Content-Type": "application/json; charset=UTF-8",
   "Authorization": "Bearer " + channel_access_token
 };
 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(userId, text) {
 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);
}


1行目の "LINEのアクセストークン" は適切なものに置き換えて下さい。

関数の説明をします。

sendLineMessageFromReplyTokenは、オウム返しBotと同じで、ユーザーからのメッセージに反応して返答をする関数です。

sendLineMessageFromUserIdは、指定されたUserIdに、アプリ側から自発的にメッセージを送る関数です。リマインダーには必須の関数となります。

Moment.jsライブラリを導入しよう

Moment.jsという、JavaScriptで日付や時刻を簡単に扱えるライブラリを導入します。私のブログで解説記事を書いたので、こちらを参照して下さい。

コード.gsを完成させよう

オウム返しBotのコードは消して下さい。

では、doPost関数など、アプリの中心となる関数を定義しましょう。以下をコード.gsにコピペして下さい。

var moment = Moment.load();
function doPost(e) {
 var webhookData = JSON.parse(e.postData.contents).events[0];
 var message, replyToken, replyText, userId;
 message = webhookData.message.text;
 replyToken = webhookData.replyToken;
 userId = webhookData.source.userId;
 var userDataRow = searchUserDataRow(userId);
 var todo = getTodoCell(userDataRow).getValue();
 var todoDate = getDateCell(userDataRow).getValue();
 switch (message) {
   case '使い方':
      replyText = 'はい!あとで思い出したいことをラインしてくれれば、いつお知らせしてほしいか聞くので、「10分後」「11月23日17時00分」など、教えてください♫その日時にお知らせします。';
      break;
   case 'キャンセル':
      replyText = cancel(userDataRow);
      break;  
   case '確認':
     if (todoDate) {
       replyText = '「' + todo + '」を' + todoDate + 'にお知らせする予定だよ!';
     } else {
       replyText = '登録されているリマインダーはありません!';
     }
     break;
   default:
     if (todoDate) {
       replyText = 'ごめん💦リマインダーは1つしか登録できないんだ💦「キャンセル」って言ってくれれば今のリマインダーをやめるよ〜';
     } else if (todo) {
       replyText = setDate(userDataRow, message);
     }
     else {
       replyText = setTodo(userDataRow, message);
     }
 }
 return sendLineMessageFromReplyToken(replyToken, replyText);
}
function searchUserDataRow(userId) {
 userDataRow = searchRowNum(userId, 0);
 if (userDataRow === false) {
   appendToSheet(userId);
 }
 return userDataRow;
}
function setTodo(row, message) {
 setFromRowCol(message, row, 1);
 return '「' + message + ' 」だね!覚えたよ!\nいつ教えてほしい?例:「10分後」「11月23日17時00分」など。「キャンセル」って言ってくれればやめるよ〜';
}
function setDate(row, message) {
 // 全角英数を半角に変換
 message = message.replace(/[A-Za-z0-9]/g, function (s) {
   return String.fromCharCode(s.charCodeAt(0) - 0xFEE0);
 });
 var date = Moment.moment(message, 'M月D日H時m分', true).format('YYYY年MM月DD日H時m分');
 if (date === 'Invalid date') {
   var match = message.match(/\d+/g);
   if (match !== null) {
     date = Moment.moment().add(+match[0], 'minutes').format('YYYY年MM月DD日H時m分');
   }
 }
 if (date === 'Invalid date') {
   return 'わかんない!いつ?「キャンセル」って言ってくれればやめるよ〜'
 } else if (date < Moment.moment()) {
   return 'タイムマシンが完成するまで待って!未来の日時で教えてね💦'
 }
 setTrigger(row, date);
 setFromRowCol(date, row, 2);
 return date + 'にお知らせするね!';
}
function cancel(row) {
 getTodoCell(row).clear();
 getDateCell(row).clear();
 triggerCell = getTriggerCell(row)
 var triggerId = triggerCell.getValue();
 if (triggerId) {
   deleteTrigger(triggerId);
 }
 triggerCell.clear();
 return 'またなんかあったら言ってね〜'
}
function remind(e) {
 var userDataRow = searchRowNum(e.triggerUid, 3);
 var userId = getUserIdCell(userDataRow).getValue();
 var todo = getTodoCell(userDataRow).getValue();
 var remindText = '「' + todo + '」の時間だよ! 気をつけて!';
 cancel(userDataRow);
 return sendLineMessageFromUserId(userId, remindText);
}

かなり長いですね^^;

でも安心して下さい。doPost関数のswitch文がアプリの本体です。それ以外はおまけにすぎません。switch文の先頭は次のようになっています。

 switch (message) {
   case '使い方':
      replyText = 'はい!あとで思い出したいことをラインしてくれれば、いつお知らせしてほしいか聞くので、「10分後」「11月23日17時00分」など、教えてください♫その日時にお知らせします。';
      break;
----------- 省略 -----------
 }
 return sendLineMessageFromReplyToken(replyToken, replyText);
}

messageが'使い方'のときは、使い方を返信するようになっていますね。

試しに「使い方」とメッセージを送ってみましょう。(各ファイルでctrl+sで変更を保存→Webアプリケーションとして導入→新規作成で保存を忘れずに!)

動きましたね!

処理の流れは、「使い方」「キャンセル」「確認」と「それ以外」に分かれます。

ここでは、重要な「それ以外」について解説します。これはswitch文のdefault部分に相当します。

 default:
     if (todoDate) {
       replyText = 'ごめん💦リマインダーは1つしか登録できないんだ💦「キャンセル」って言ってくれれば今のリマインダーをやめるよ〜';
     } else if (todo) {
       replyText = setDate(userDataRow, message);
     }
     else {
       replyText = setTodo(userDataRow, message);
     }

todoDateが空文字でない時(trueに評価される時)は、すでにリマインダーが登録されているということなので、「リマインダーは1つしか登録できません」というメッセージを返します。

さらにtodoが空文字でない時(trueに評価される時)は、2列目の予定は入っているけど、3列目の日時は入っていないことになります。この時には、messageは日時の指定が含まれていると予測して、setDate関数を呼び出して、日付を登録します。

それで以外の場合は、まだ予定が入っていないということなので、setTodo関数を呼び出して、予定を登録します。

trigger.gsを完成させよう

トリガーに関する関数をtrigger.gsに定義しましょう。以下をtrigger.gsにコピペして下さい。

function setTrigger(row, date) {
 var triggerDay = moment(date,'YYYY年MM月DD日H時m分').toDate(); 
 var trigger =  ScriptApp.newTrigger("remind").timeBased().at(triggerDay).create();
 setFromRowCol(trigger.getUniqueId(), row, 3);
}
function deleteTrigger(uniqueId) {
 var triggers = ScriptApp.getProjectTriggers();
 for(var i=0; i < triggers.length; i++) {
   if (triggers[i].getUniqueId() === uniqueId) {
     ScriptApp.deleteTrigger(triggers[i]);
   }
 }
}

では、解説します。

setTriggerは、トリガーの作成と、トリガーのUniqueIdの書き込みを行う関数です。コード.gsのsetDate関数から呼ばれます。
トリガーは、日時と、実行する関数を指定して作成します。こうして作成されたトリガーは、固有のIDを持っています。それがUniqueIdです。

実行する関数にはremindを指定しています。 remindはコード.gsに定義されている関数です。

function remind(e) {
 var userDataRow = searchRowNum(e.triggerUid, 3);
 var userId = getUserIdCell(userDataRow).getValue();
 var todo = getTodoCell(userDataRow).getValue();
 var remindText = '「' + todo + '」の時間だよ! 気をつけて!';
 cancel(userDataRow);
 return sendLineMessageFromUserId(userId, remindText);
}

remind関数は、呼び出し元のトリガーのUniqueIdを調べて、スプレッドシートの4列目からそのUniqueIdが記録されている行を探します。

その行からUserIdとtodoを特定して、LINEでメッセージを送ります。

deleteTriggerは、不要になったトリガーを削除する関数です。

なお、編集>現在のプロジェクトのトリガーから、トリガーを確認・編集することができます。

完成品を動かして見よう!

「カップラーメン」を「3分後」に通知するように言ってみます。

スプレッドシートには次のように記録されています。

3分後、通知が来ました!

完成品をシェアしよう

今回作成したアプリは、50人まで友達登録することができます。URLやQRコードをSNS等で公開して、ぜひシェアしてみて下さい。

まとめ

いかがでしたでしょうか?

「動かない」、「このコードが何をやっているのか分からない、解説が欲しい」という時には遠慮なくコメントして下さいね。

この記事が気に入ったら、サポートをしてみませんか?
気軽にクリエイターの支援と、記事のオススメができます!
44
5秒で始められるゲームプログラミング学習サービス プロアカ https://proacainc.com/ を運営しています。