イントロダクション
この記事の概要
Googleカレンダーにで予定が登録/更新/削除されたことをLINEに通知する方法をまとめる。
背景
Googleカレンダーを家族で共有して、お互いの予定を確認できるようにしている。
ただ、Googleカレンダーは通知機能が非常にしょぼく、予定が新規登録されたとき、カレンダーのオーナー宛にメール通知するくらいしかできない(オーナーが登録した予定はメンバーには通知されない)。IFTTTと連携することでLINEに通知することはできるが、予定が「登録」されたときのみで更新や削除は通知されないので、物足りない。
Google Calendar APIを叩いてJavaでエンドポイントをつくろうと思ったが、たかだかカレンダーの変更通知のために重厚長大になりすぎる。。。と思ってたら、カレンダーの変更をトリガーにGASを起動できると知ったので挑戦する。
ざっくりアーキテクチャ
開発環境
TypeScriptでコーディングし、ClaspでGASへデプロイを実施している。
-
Windows 11
-
yarn (v1.22.19)
-
clasp (v2.3.0)
0. コード全文
いきなり完成形。細かい解説は後述する。
// Compiled using ts2gas 3.6.4 (TypeScript 4.2.2)
const PROPERTY_KEY_LINE_TOKEN = 'LINE_TOKEN';
const PROPERTY_KEY_SLACK_WEBHOOK_ENDPOINT = 'SLACK_WEBHOOK_ENDPOINT';
const ENDPOINT_LINE_NOTIFY_API = 'https://notify-api.line.me/api/notify';
const PROPERTIES = PropertiesService.getScriptProperties();
function onUpdatedEvent(event) {
console.time('onUpdatedEvent');
console.log(`Updated Calendar. calendarId: ${event.calendarId}`);
try {
getUpdatedEvents(event.calendarId).forEach((e) => {
let message;
if (e.status === "cancelled") {
message = `\nGoogleカレンダーの予定が削除されました。\n==========\nタイトル:${e.summary}`;
}
else {
let startDateTime;
let endDateTime;
let location;
startDateTime = new Date(e.start.dateTime).toLocaleString("ja-JP", { timeZone: e.start.timeZone });
endDateTime = new Date(e.end.dateTime).toLocaleString("ja-JP", { timeZone: e.end.timeZone });
location = e.location == undefined ? "" : e.location;
message = `\nGoogleカレンダーの予定が更新されました。\n==========\nタイトル:${e.summary}\n開始日時:${startDateTime}\n終了日時:${endDateTime}\n場所 :${location}`;
}
notifyLINE(message);
});
}
catch (e) {
console.error(e);
var slackOptions = {
method: 'post',
payload: JSON.stringify({ 'username': 'google-calendar-watchdog', 'text': 'カレンダー変更通知処理中にエラーが発生しました。<https://script.google.com/home/projects/1VE5tPlGhiNWUOsJje9HYVOjX4BjvK-VLx5_8-LsV7A2StRMUsu3qXWuM/executions|[ログ]>\nERROR=>' + e.message }),
muteHttpExceptions: true
};
callExternalAPI(PROPERTIES.getProperty(PROPERTY_KEY_SLACK_WEBHOOK_ENDPOINT), slackOptions);
throw e;
}
console.timeEnd('onUpdatedEvent');
}
class CalendarQueryOptions {
}
function getUpdatedEvents(calendarId) {
var _a;
console.time('getUpdatedEvents');
const key = `syncToken: ${calendarId}`;
const syncToken = PROPERTIES.getProperty(key);
let options = { maxResults: 100, showDeleted: true };
if (syncToken) {
options = { ...options, syncToken: syncToken };
}
else {
options = { ...options, timeMin: getRelativeDate(-10, 0).toISOString() };
}
const events = (_a = Calendar.Events) === null || _a === void 0 ? void 0 : _a.list(calendarId, options);
if (events === null || events === void 0 ? void 0 : events.nextSyncToken) {
PROPERTIES.setProperty(key, events === null || events === void 0 ? void 0 : events.nextSyncToken);
}
console.timeEnd('getUpdatedEvents');
return (events === null || events === void 0 ? void 0 : events.items) ? events.items : [];
}
function notifyLINE(message) {
console.time('notifyLINE');
var token = PROPERTIES.getProperty(PROPERTY_KEY_LINE_TOKEN);
var options = {
method: 'post',
payload: 'message=' + message,
headers: { 'Authorization': 'Bearer ' + token },
muteHttpExceptions: true
};
callExternalAPI(ENDPOINT_LINE_NOTIFY_API, options);
console.timeEnd('notifyLINE');
}
function getRelativeDate(daysOffset, hour) {
var date = new Date();
date.setDate(date.getDate() + daysOffset);
date.setHours(hour);
date.setMinutes(0);
date.setSeconds(0);
date.setMilliseconds(0);
return date;
}
function callExternalAPI(endpoint, options) {
var response = UrlFetchApp.fetch(endpoint, options);
return response;
}
1. トリガーを設定する
トリガーの設定はこのようにする。
「カレンダーのオーナーのメールアドレス」とあるが、要するに通知対象のカレンダーIDを入力する。カレンダーIDの確認方法は下記を参考にする。
これでカレンダーに予定が登録などされると、onUpdatedEvent
関数がキックされるようになる。
2. コードの解説
onUpdatedEvent
関数の引数
function onUpdatedEvent(event) {
...
}
引数event
にはカレンダー変更イベント情報が含まれているが、「どのカレンダーが変更されたか」しかわからない。どの予定が登録/更新/削除されたのかは、event
から取得したカレンダーIDを用いてGoogle Calendar APIから予定を取得することになる。
SyncToken
function getUpdatedEvents(calendarId) {
var _a;
...
const key = `syncToken: ${calendarId}`;
const syncToken = PROPERTIES.getProperty(key);
let options = { maxResults: 100, showDeleted: true };
if (syncToken) {
// syncTokenがプロパティから取得できたら差分の予定一覧を取得
options = { ...options, syncToken: syncToken };
} else {
// syncTokenがプロパティから取得できなかったら過去10日ぶんの予定一覧を取得
options = { ...options, timeMin: getRelativeDate(-10, 0).toISOString() };
}
const events = (_a = Calendar.Events) === null || _a === void 0 ? void 0 : _a.list(calendarId, options);
if (events === null || events === void 0 ? void 0 : events.nextSyncToken) {
// 取得したsyncTokenはプロパティへ保存しておく
PROPERTIES.setProperty(key, events === null || events === void 0 ? void 0 : events.nextSyncToken);
}
...
}
前述のとおり、カレンダー変更イベントから変更された予定を直接取得することはできないので、カレンダー変更イベントを受けたらGoogle Calendar APIを呼び出して、予定の一覧を取得することになる。
ただ、毎回全予定を取得して更新日時を見るのでは効率が悪いのでSyncTokenを使う。SyncTokenは予定一覧のしおりの役割をしており、前回取得してからの差分(つまり、更新された予定一覧)を取得できる。
最新のSyncTokenはプロパティから取得するが、初めての起動時はプロパティから取得できないため、全予定一覧を取得することで、そこからSyncTokenを取り出すことができる。ここで取得する全予定一覧はあくまでSyncTokenを取得するためのものなので、全量を取得する必要はなく、timeMin
を指定して件数を絞り込んで性能劣化を避ける。
取得したsyncTokenはGASのプロパティに保存しておくことで、二回目以降の起動時はプロパティから取得できるようになる。
予定の更新 or 削除の判別
function onUpdatedEvent(event) {
...
try {
getUpdatedEvents(event.calendarId).forEach((e) => {
let message;
if (e.status === "cancelled") {
// ステータスがcancelledなら「予定の削除」
message = `\nGoogleカレンダーの予定が削除されました。\n==========\nタイトル:${e.summary}`;
} else {
// ステータスがcancelled以外なら「予定の登録、更新」
...
message = `\nGoogleカレンダーの予定が更新されました。\n==========\nタイトル:${e.summary}\n開始日時:${startDateTime}\n終了日時:${endDateTime}\n場所 :${location}`;
}
...
イベントステータスを確認して、更新/削除のいずれなのか判別するロジックは上記のとおり。
予定の「削除」
更新後の予定のstatus
がcancelled
であれば、予定が「削除」されたとわかる。
デフォルトオプションでは削除された時点で予定のタイトルなどは取得できなくなってしまうため、イベント一覧を取得する際にshowDeleted
オプションにTRUEを指定することで削除済みイベントの詳細情報も含めて返却してもらえる。
function getUpdatedEvents(calendarId) {
...
let options = { maxResults: 100, showDeleted: true };
...
}
LINEに通知する
function notifyLINE(message) {
...
var token = PROPERTIES.getProperty(PROPERTY_KEY_LINE_TOKEN);
var options = {
method: 'post',
payload: 'message=' + message,
headers: { 'Authorization': 'Bearer ' + token },
muteHttpExceptions: true
};
callExternalAPI(ENDPOINT_LINE_NOTIFY_API, options);
...
}
function callExternalAPI(endpoint, options) {
var response = UrlFetchApp.fetch(endpoint, options);
return response;
}
通知メッセージを組み立てたら、LINE Notify APIを使ってLINEへ通知を行なう。LINE Notify APIの使い方は下記記事を参考にした。
Google Apps ScriptからLINE NotifyでLINEにメッセージを送る
まとめ
GASを使うことでサーバレス/省コードで実現できた。