LINEでやりとりをすることが多いのですが、会話の中で決まった予定を、LINEのbotに話しかけてGoogleカレンダーに打ち込めたら楽だろうと思って、GoogleAppsScript(以下、GAS)で作ることにしました。
無料でサーバレスなので、簡単なwebアプリケーションならGASも検討して良いと思います。
#完成品
うちの新田美波(自作のlinebot)は予定を教えてくれるし追加もできる#imas_cg #linebot pic.twitter.com/VSQncIhVZ1
— がわわ (@gawawa124) 2017年6月20日
#コード
http://inside.pixiv.net/entry/2016/11/14/150000
https://tonari-it.com/gas-calendar/
上記の記事をとても参考にして実装しました。
var CHANNEL_ACCESS_TOKEN = "Your CHANNEL_ACCESS_TOKEN";
var ERROR_SHEET_ID = "Your Spreadsheet ID for Error";
var dateExp = /(\d{2})\/(\d{2})\s(\d{2}):(\d{2})/;
var dayExp = /(\d+)[\/月](\d+)/;
var hourMinExp = /(\d+)[:時](\d+)*/;
function doPost(e) {
try {
handleMessage(e);
} catch(error) {
logging("ToCalendarFromLineBot");
logging(JSON.stringify(e));
logging(JSON.stringify(error));
var replyToken = JSON.parse(e.postData.contents).events[0].replyToken;
reply(replyToken, error.message);
}
}
function logging(str) {
var sheet = SpreadsheetApp.openById(ERROR_SHEET_ID).getActiveSheet();
var ts = new Date().toLocaleString("japanese", {timeZone: "Asia/Osaka"});
sheet.appendRow([ts, str]);
}
function handleMessage(e) {
var replyToken = JSON.parse(e.postData.contents).events[0].replyToken;
var lineType = JSON.parse(e.postData.contents).events[0].type
if (typeof replyToken === "undefined" || lineType === "follow") {
return;
}
var userMessage = JSON.parse(e.postData.contents).events[0].message.text;
var cache = CacheService.getScriptCache();
var type = cache.get("type");
if (type === null) {
if (userMessage === "予定") {
cache.put("type", 1);
reply(replyToken, "予定日を教えてください!\n「06/17, 6月17日」などの形式なら大丈夫です!");
} else if (userMessage === "予定教えて") {
reply(replyToken, getEvents());
} else {
reply(replyToken, "「予定」で予定追加を、「予定教えて」で予定参照ができるので気軽に話しかけてくださいね!");
}
} else {
if (userMessage === "キャンセル") {
cache.remove("type");
reply(replyToken, "キャンセルしました!");
return;
}
switch(type) {
case "1":
// 予定日
var [matched, month, day] = userMessage.match(dayExp);
cache.put("type", 2);
cache.put("month", month);
cache.put("day", day);
reply(replyToken, month + "/" + day + "ですね! 次に開始時刻を教えてください。「13:00, 13時, 13:20, 13時20分」などの形式なら大丈夫です!");
break;
case "2":
// 開始時刻
var [matched, startHour, startMin] = userMessage.match(hourMinExp);
cache.put("type", 3);
cache.put("start_hour", startHour);
if (startMin == null) startMin = "00";
cache.put("start_min", startMin);
reply(replyToken, startHour + ":" + startMin + "ですね! 次に終了時刻を教えてください。");
break;
case "3":
// 終了時刻
var [matched, endHour, endMin] = userMessage.match(hourMinExp);
cache.put("type", 4);
cache.put("end_hour", endHour);
if (endMin == null) endMin = "00";
cache.put("end_min", endMin);
reply(replyToken, endHour + ":" + endMin + "ですね! 最後に予定名を教えてください!");
break;
case "4":
// 予定名
cache.put("type", 5);
cache.put("title", userMessage);
var [title, startDate, endDate] = createEventData(cache);
reply(replyToken, toEventFormat(title, startDate, endDate) + "\nで間違いないでしょうか? よろしければ「はい」をやり直す場合は「いいえ」をお願いいたします!");
break;
case "5":
// 確認の回答がはい or いいえ
cache.remove("type");
if (userMessage === "はい") {
var [title, startDate, endDate] = createEventData(cache);
CalendarApp.getDefaultCalendar().createEvent(title, startDate, endDate);
reply(replyToken, "追加しました!\nお疲れ様でした!");
} else {
reply(replyToken, "お手数ですがもう一度お願いいたします!");
}
break;
default:
reply(replyToken, "申し訳ありません。\n形式に誤りがないか確認してみて、なければ「キャンセル」で予定入力をキャンセルすることができるので、そちらを試していただけますか?");
break;
}
}
}
function createEventData(cache) {
var year = new Date().getFullYear();
var title = cache.get("title");
var startDate = new Date(year, cache.get("month") - 1, cache.get("day"), cache.get("start_hour"), cache.get("start_min"));
var endDate = new Date(year, cache.get("month") - 1, cache.get("day"), cache.get("end_hour"), cache.get("end_min"));
return [title, startDate, endDate];
}
function toEventFormat(title, startDate, endDate) {
var start = Utilities.formatDate(startDate, "JST", "MM/dd HH:mm");
var end = Utilities.formatDate(endDate, "JST", "MM/dd HH:mm");
var str = title + ": " + start + " ~ " + end;
return str;
}
function reply(replyToken, message) {
var url = "https://api.line.me/v2/bot/message/reply";
UrlFetchApp.fetch(url, {
"headers": {
"Content-Type": "application/json; charset=UTF-8",
"Authorization": "Bearer " + CHANNEL_ACCESS_TOKEN,
},
"method": "post",
"payload": JSON.stringify({
"replyToken": replyToken,
"messages": [{
"type": "text",
"text": message,
}],
}),
});
return ContentService.createTextOutput(JSON.stringify({"content": "post ok"})).setMimeType(ContentService.MimeType.JSON);
}
function getEvents() {
var events = CalendarApp.getDefaultCalendar().getEventsForDay(new Date());
var body = "今日の予定は";
if (events.length === 0) {
body += "ありません!";
return body;
}
body += "\n";
events.forEach(function(event) {
var title = event.getTitle();
var start = toHHmm(event.getStartTime());
var end = toHHmm(event.getEndTime());
body += "*" + title + ": " + start + " ~ " + end + "\n";
});
body += "です! 張り切っていきましょ!";
return body;
}
function toHHmm(date){
return Utilities.formatDate(date, "JST", "HH:mm");
}
#つまずいたところなどの解説
webアプリケーションを公開する際の注意点
「ウェブアプリケーションとして導入」する際に「プロジェクトバージョン」を毎回新規作成してください。
こうしないと更新されません。
GASではプロジェクトバージョンを毎回新規作成することでバージョンごとのスナップショットが作られているようです。
###function logging
LINE botとやりとりしていてエラーが発生した場合、botからの応答がなくなります。
Logger.logでログが吐かれないので、エラーが発生した際にスプレッドシートに書くことでデバッグを行いやすくしています。
http://yoshiyuki-hirano.hatenablog.jp/entry/2015/10/09/144237
###Cache
ユーザーが予定を入力する時に、予定名や日時を一気に打つのは使いづらいbotと言えます。
なので、日時や予定名で区切ってやりとりするわけですが、その際にデータを一時保存したいわけです。
データベースを使うかスプレッドシートを使うか色々考えた上でGASで用意されているCacheを使うことにしました。
key-valueのvalueは全てStringになるところが注意点です。
https://developers.google.com/apps-script/reference/cache
###改良点
- 今のままだと来年の予定が入力できない
- 終了時刻は「〜時間後」の形式も受け付けた方が親切かも
余力があれば上記も実装したかったです。
###このbotを外部に公開できるのか?
結論から言うと僕にはできませんでした。わかる方がいたら教えていただきたいです。
理由は、LINE botからGASにアクセスする際に、アクセスしたユーザーのGoogleカレンダーの利用権限についてbotが覚えておけないためです。
今は僕をユーザーとしてwebアプリケーションを動かしているので、すでに僕のGoogleカレンダーの利用権限は認証された状態です。しかし、このまま外部に公開してしまえば、いろんなユーザーが僕のカレンダーに予定を追加したり参照してしまいます。
なので、LINE botを使うユーザーごとに動かす必要があって、ユーザーごとにGoogleカレンダーの利用権限が必要になります。
しかし、botからGASにアクセスする際に、ユーザーが利用権限を許可したかどうかがわからないためGoogleカレンダーの取得・追加ができないという結論に至りました。
ということで、この記事のbotが欲しい! という場合はGASやLINE botの勉強も兼ねて実装してみてください。
今は僕がこのbotを独り占めしておきます。
#参考記事
http://inside.pixiv.net/entry/2016/11/14/150000
https://tonari-it.com/gas-calendar/
http://yoshiyuki-hirano.hatenablog.jp/entry/2015/10/09/144237
https://developers.google.com/apps-script/reference/cache