0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Googleカレンダーに予定を追加・削除・変更したタイミングでその内容をLINE・Slackに通知させる

Last updated at Posted at 2024-10-13

はじめに

最近、家族との共有用にGoogleカレンダーを使い始めました。
以前まで使用していたアプリでは、予定の追加や変更があるたびに通知が飛んでいたのですが、Googleカレンダーだとそれができませんでした。
そこで、GASをこねくり回して無理やり通知機能を作ってみました。

通知イメージは次のような感じです。
予定の変更時に、変更前と変更後を出せるのは便利かな~と思っています。

image.png

概要

  1. Googleカレンダーでの予定追加・削除・変更をトリガーとしてGASを起動
  2. 一定期間内の予定をすべて取得し、前回実行時にスプレッドシートに保存しておいたものと比較する
  3. 今回取得した予定の数が前回実行時より多い場合は追加されたと判断
  4. 今回取得した予定の数が前回実行時より少ない場合は削除されたと判断
  5. 今回取得した予定の数が前回実行時と等しい場合は変更されたと判断
  6. 追加・削除・変更された予定のタイトル、開始日時、終了日時、詳細情報を取得
  7. 取得した内容をLINEまたはSlackに通知

手順3~5がこねくり回しポイントです。

注意点

  • 通知先にLINEとSlackを使用しています
  • ※今回LINE通知のために使用したLineNotifyは2025年3月にサービス終了するみたいです
  • 同時に複数回予定の編集を行った場合に、通知がうまく飛ばなかったりします。十秒程度感覚が空いていれば動くため、あくまで私用の範囲であれば問題ないと思います
  • 現時点から過去2か月~1年先の予定を検知の対象としています

前準備

Lineの通知設定

下記を参考にしました。
スプレッドシートからLineへ通知を送る

Slackの通知設定

下記を参考にしました。
Incoming WebhookをつかってGASからSlackにメッセージを送信する方法

スプレッドシートの準備

下記のようなスプレッドシートを作成。2行目には適当なデータを入れておいてください。
シート名はカレンダーIDにする。カレンダーIDの取得方法はこちらを参照

image.png

作成したスプレッドシートから、「拡張機能」>「App Script」でGASを作成します。
最終的な構成は下図の通りです

image.png

スプレッドシートの初期化

カレンダーは複数設定できるようにしました。
カレンダーIDは先ほど取得したものを、カレンダー名は通知時に分かりやすいように名前をつけてください。

init.gs
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行目にデータが入っていないとエラーになるので注意。

image.png

実行するとシートに予定の情報が一覧で追加されます。(idとtitleは念のため隠しています)

image.png

通知処理の実装

コード.gsは名前を変えるのが面倒なためそのままです。

コード.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);
  }
}

通知部分のみ切り出しました。

notify.js
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カレンダーに予定を追加

下図の通り予定を作成

image.png

LINEへの通知

タイトルなし.png

Slackへの通知

image.png

Googleカレンダーの予定変更

先ほど追加した予定を下図の通り変更

image.png

LINEへの通知

タイトルなし2.png

Slackへの通知

image.png

Googleカレンダーの予定を削除

テスト用の予定を削除

image.png

LINEへの通知

タイトルなし3.png

Slackへの通知

image.png

所感

かなり強引な気もしますが、シンプルな作りになっている気もします。
注意点にも書いた通り、ほぼ同時に予定の追加等が行われた場合に通知がうまく飛ばなかったりしますが、私用の範囲であれば問題ないはずです。

0
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?