Help us understand the problem. What is going on with this article?

LINE BOTからGoogleカレンダーの予定の取得・追加を行う

More than 1 year has passed since last update.

LINEでやりとりをすることが多いのですが、会話の中で決まった予定を、LINEのbotに話しかけてGoogleカレンダーに打ち込めたら楽だろうと思って、GoogleAppsScript(以下、GAS)で作ることにしました。
無料でサーバレスなので、簡単なwebアプリケーションならGASも検討して良いと思います。

完成品

コード

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away