はじめに
最近、家族との共有用にGoogleカレンダーを使い始めました。
以前まで使用していたアプリでは、予定の追加や変更があるたびに通知が飛んでいたのですが、Googleカレンダーだとそれができませんでした。
そこで、GASをこねくり回して無理やり通知機能を作ってみました。
通知イメージは次のような感じです。
予定の変更時に、変更前と変更後を出せるのは便利かな~と思っています。
概要
- Googleカレンダーでの予定追加・削除・変更をトリガーとしてGASを起動
- 一定期間内の予定をすべて取得し、前回実行時にスプレッドシートに保存しておいたものと比較する
- 今回取得した予定の数が前回実行時より多い場合は追加されたと判断
- 今回取得した予定の数が前回実行時より少ない場合は削除されたと判断
- 今回取得した予定の数が前回実行時と等しい場合は変更されたと判断
- 追加・削除・変更された予定のタイトル、開始日時、終了日時、詳細情報を取得
- 取得した内容をLINEまたはSlackに通知
手順3~5がこねくり回しポイントです。
注意点
- 通知先にLINEとSlackを使用しています
- ※今回LINE通知のために使用したLineNotifyは2025年3月にサービス終了するみたいです
- 同時に複数回予定の編集を行った場合に、通知がうまく飛ばなかったりします。十秒程度感覚が空いていれば動くため、あくまで私用の範囲であれば問題ないと思います
- 現時点から過去2か月~1年先の予定を検知の対象としています
前準備
Lineの通知設定
下記を参考にしました。
スプレッドシートからLineへ通知を送る
Slackの通知設定
下記を参考にしました。
Incoming WebhookをつかってGASからSlackにメッセージを送信する方法
スプレッドシートの準備
下記のようなスプレッドシートを作成。2行目には適当なデータを入れておいてください。
シート名はカレンダーIDにする。カレンダーIDの取得方法はこちらを参照
作成したスプレッドシートから、「拡張機能」>「App Script」でGASを作成します。
最終的な構成は下図の通りです
スプレッドシートの初期化
カレンダーは複数設定できるようにしました。
カレンダーIDは先ほど取得したものを、カレンダー名は通知時に分かりやすいように名前をつけてください。
const calendarIds = ["カレンダーID1",
"カレンダーID2",
"カレンダーID3"]
const calendarNames = {"カレンダーID1":"カレンダー名1",
"カレンダーID2":"カレンダー名2",
"カレンダーID3":"カレンダー名3"};
function init() {
for (let i = 0; i < calendars.length; i++) {
let myCalendar = CalendarApp.getCalendarById(calendarIds[i]);
// 現在日時を取得
let today = new Date();
// 現在から2か月前の日時を取得
let startDate = new Date(today);
startDate.setMonth(today.getMonth() - 2);
// 現在から1年後の日時を取得
let endDate = new Date(today);
endDate.setFullYear(today.getFullYear() + 1);
// 2か月前から1年後までの期間の予定を取得
events = myCalendar.getEvents(startDate, endDate);
// スプレッドシートを初期化
clearValues(calendarIds[i]);
// 予定をスプレッドシートに転記
setEvents(events, calendarIds[i]);
}
}
初期化処理を実行
init()メソッドを指定して実行。シートの2行目にデータが入っていないとエラーになるので注意。
実行するとシートに予定の情報が一覧で追加されます。(idとtitleは念のため隠しています)
通知処理の実装
コード.gsは名前を変えるのが面倒なためそのままです。
const dateFormat = 'yyyy/MM/dd HH:mm';
function myFunction(e) {
const lock = LockService.getScriptLock()
if(!lock.tryLock(60000)) {
return
}
const myCalendar = CalendarApp.getCalendarById(e.calendarId);
// 現在日時を取得
let today = new Date();
// 現在から2か月前の日時を取得
let startDate = new Date(today);
startDate.setMonth(today.getMonth() - 2);
// 現在から1年後の日時を取得
let endDate = new Date(today);
endDate.setFullYear(today.getFullYear() + 1);
// 2か月前から1年後までの期間の予定を取得
events = myCalendar.getEvents(startDate, endDate);
// スプレッドシートから前回実行時の予定を取得
var arry = getEvents(e.calendarId);
if (arry.length > events.length) {
// 予定削除
forDeleteEvent(arry, events, e.calendarId);
} else if (arry.length < events.length) {
// 予定追加
forAddEvent(arry, events, e.calendarId);
} else {
// 予定変更
forUpdateEvent(arry, events, e.calendarId);
}
// スプレッドシートを初期化
clearValues(e.calendarId);
// 予定をスプレッドシートに転記
setEvents(events, e.calendarId);
}
// 前回実行時の予定を取得
function getEvents(calendarId) {
const sheet = SpreadsheetApp.getActive().getSheetByName(calendarId);
const lastRow = sheet.getLastRow();
return sheet.getRange(2,1, lastRow-1,5).getValues();
}
// 予定をスプレッドシートに転記
function setEvents(events, calendarId) {
// 予定の個数を取得
eventNum = events.length;
// シートを取得
const sheet = SpreadsheetApp.getActive().getSheetByName(calendarId);
// 各予定をスプレッドシートに転記していく
for (let i = 0; i < eventNum; i++) {
var arry = [events[i].getId(), events[i].getTitle(), events[i].getStartTime(), events[i].getEndTime(), events[i].getDescription()];
sheet.appendRow(arry);
}
}
// データ削除
function clearValues(calendarId) {
const sheet = SpreadsheetApp.getActive().getSheetByName(calendarId);
const lastRow = sheet.getLastRow();
sheet.getRange(2,1, lastRow-1,5).clearContent();
}
// 予定追加の場合
function forAddEvent(arry, events, calendarId) {
let message = "";
for (let i = 0; i < arry.length; i++) {
if (arry[i][0] != events[i].getId()) {
message += "予定追加";
message += "\n" + calendarNames[calendarId];
message += "\nタイトル:" + events[i].getTitle();
message += "\n開始日時:" + Utilities.formatDate(events[i].getStartTime(), 'JST', dateFormat);
message += "\n終了日時:" + Utilities.formatDate(events[i].getEndTime(), 'JST', dateFormat);
message += "\n詳細:" + events[i].getDescription();
break;
}
}
notifyEvents(message);
sendSlack(message);
}
// 予定削除の場合
function forDeleteEvent(arry, events, calendarId) {
let message = "";
for (let i = 0; i < events.length; i++) {
if (arry[i][0] != events[i].getId()) {
message += "予定削除";
message += "\n" + calendarNames[calendarId]
message += "\nタイトル:" + arry[i][1];
message += "\n開始日時:" + Utilities.formatDate(arry[i][2], 'JST', dateFormat);
message += "\n終了日時:" + Utilities.formatDate(arry[i][3], 'JST', dateFormat);
message += "\n詳細:" + arry[i][4];
break;
}
}
notifyEvents(message);
sendSlack(message);
}
// 予定変更の場合
function forUpdateEvent(arry, events, calendarId) {
var updatedNum = -1;
for (let i = 0; i < events.length; i++) {
// タイトル
if (arry[i][1] != events[i].getTitle()) {
updatedNum = i;
break;
}
// 開始日時
if (Utilities.formatDate(arry[i][2], 'JST', dateFormat) != Utilities.formatDate(events[i].getStartTime(), 'JST', dateFormat)) {
updatedNum = i;
break;
}
// 終了日時
if (Utilities.formatDate(arry[i][3], 'JST', dateFormat) != Utilities.formatDate(events[i].getEndTime(), 'JST', dateFormat)) {
updatedNum = i;
break;
}
// 詳細
if (arry[i][4] != events[i].getDescription()) {
updatedNum = i;
break;
}
}
if (updatedNum >= 0) {
let message = "";
message += "予定変更";
message += "\n" + calendarNames[calendarId];
message += "\n変更前";
message += "\nタイトル:" + arry[updatedNum][1];
message += "\n開始日時:" + Utilities.formatDate(arry[updatedNum][2], 'JST', dateFormat);
message += "\n終了日時:" + Utilities.formatDate(arry[updatedNum][3], 'JST', dateFormat);
message += "\n詳細:" + arry[updatedNum][4];
message += "\n\n変更後";
message += "\nタイトル:" + events[updatedNum].getTitle();
message += "\n開始日時:" + Utilities.formatDate(events[updatedNum].getStartTime(), 'JST', dateFormat);
message += "\n終了日時:" + Utilities.formatDate(events[updatedNum].getEndTime(), 'JST', dateFormat);
message += "\n詳細:" + events[updatedNum].getDescription();
notifyEvents(message);
sendSlack(message);
}
}
通知部分のみ切り出しました。
const lineToken = PropertiesService.getScriptProperties().getProperty('lineToken');
const lineNotifyApi = 'https://notify-api.line.me/api/notify';
const slackToken = PropertiesService.getScriptProperties().getProperty('slackToken');
// LINE通知
function notifyEvents(message) {
const options =
{
"method" : "post",
"payload" : {"message": message},
"headers" : {"Authorization":"Bearer " + lineToken}
};
UrlFetchApp.fetch(lineNotifyApi, options);
}
// Slack通知
function sendSlack(message) {
const payload = {
"text": "<!channel> " + message,
};
const options = {
"method" : "post",
"contentType" : "application/json",
"payload" : JSON.stringify(payload)
};
UrlFetchApp.fetch(slackToken, options);
}
トリガーの追加
GoogleカレンダーをGASのトリガーにする方法については下記を参照
【GAS】Googleカレンダー編集時に発動するトリガーを使いこなす
動かしてみる
Googleカレンダーに予定を追加
下図の通り予定を作成
LINEへの通知
Slackへの通知
Googleカレンダーの予定変更
先ほど追加した予定を下図の通り変更
LINEへの通知
Slackへの通知
Googleカレンダーの予定を削除
テスト用の予定を削除
LINEへの通知
Slackへの通知
所感
かなり強引な気もしますが、シンプルな作りになっている気もします。
注意点にも書いた通り、ほぼ同時に予定の追加等が行われた場合に通知がうまく飛ばなかったりしますが、私用の範囲であれば問題ないはずです。