SOMPO Digital Lab 開発チームブログ

安心・安全・健康に資する開発情報を発信します

GASでGoogleカレンダーの予定を同期する

SOMPO Digital Lab の小菅です。私はデジタル事業を行う SOMPO Light Vortex 株式会社にも兼務しており、グループ会社のメールアドレスを2つ以上持っている状態にあります(合計3つあります)。各アカウントはそれぞれ独自のスケジュールが設定されており、全ての予定を確認するためには3つのカレンダーを見る必要があります。さらに、私と同じ兼務状況にない同僚たちは、私の全ての予定を確認することができないといった問題が生じていました。

カレンダーアプリを活用すれば、自分自身の予定はまとめて確認することもできるでしょう。しかし、同僚目線ではそうはいきません。この問題を解決するために、Google Apps Script を用いて複数の Google アカウントの予定を同期するスクリプトを作成しました。これにより、同じ予定を複数アカウントで共有すれば、どれか1つのアカウントを見るだけで全ての予定を確認できるようになるはずです。今回は、この個人的な取り組みについてご紹介したいと思います。

要件

この記事で紹介するスクリプトは、以下の要件を満たすものとして作成しています。

  • 自身が持っている複数のGoogleアカウントでカレンダーの予定を同期する
  • 自動同期するGoogleアカウントは1つ以上設定できる
  • カレンダーの変更イベントをトリガーに即時実行する
  • 初回実行時、既存の予定には自動招待しない
    • 過去の予定まで書き換えると、予定の数によっては大変なことになるため

設定方法

★とにかく使いたい人向けの手順

とにかく使いたい人は下記の手順で利用できます。

Google Apps Script の画面を開き、新しいプロジェクトを作成する
https://script.google.com/home

② エディタで以下のコードを保存する
https://github.com/kkosuge/gas-gcal-synchronizer/blob/main/out/main.gs

Google Calendar API サービスを追加する カレンダーの権限を渡していいか聞かれますので、承認します。

④ プロジェクト設定からスクリプトプロパティを追加する

  • プロパティの名前に TARGET_EMAILS、値に同期対象のメールアドレスをカンマ区切りで設定する(GASを動かすアカウントのメールアドレスは記載しない)

⑤ エディタに戻り、main 関数を選択し、実行する

  • ログに「nextSyncToken is set」が出力されたことを確認する
  • この初回実行時より後に変更された予定に対して、同期が実行されるようになります

⑥ トリガー画面から、トリガーを追加する

  • 実行する関数: main
  • 実行するデプロイ:Head
  • イベントのソース:カレンダーから
  • カレンダーのオーナーのメールアドレス:GASアカウントのメールアドレス

Google Workspace の組織設定で「カレンダーから」のイベントソースが使えない場合があります。 設定できてもトリガーが発火しません。その場合は「時間ベースのタイマー」をイベントソースに指定して、10分おきごとに main 関数を実行してもOKです。

⑦ 予定を登録する可能性のあるGoogleアカウントの数だけ、同様の手順でGASを登録する

以上の手順で利用できるようになります。GASの「実行数」の画面から実行された時間とログを確認することができます。使用をやめる際はトリガーを削除するか、GASのプロジェクトごと削除してください。

注意点

カレンダーの予定に別組織のアカウントを招待する行為に同意を得ている組織でのみ行ってください。同一人物だとしても、別組織のアカウントが勝手に招待されるのは気持ち悪いと思います。単純に予定の公開範囲を広げてしまう行為ですし。

仕組み

フロー図
やりたい事は予定の同期ですが、同期の実現方法は予定に招待するだけです。あるアカウントでイベントを受け取った際に、自身の他のアカウントが参加者に含まれていなければ、それらのアカウントをそのイベントに招待します。予定が削除されたり、予定の説明が変わったとしても、すべてのアカウントは同じ予定を見ているだけですので、その場合は何か操作をする必要はありません。スルーします。

Calendar Event で飛んでくるデータ

前述のフロー図を見ると単純な仕組みに感じますが、いざ実装してみると面倒事が多かったです。まず Calendar Event トリガーで飛んでくるデータには中身がありません。予定の追加イベントならその予定のデータ、せめて予定のIDを送ってきてくれるものかと思いきや、単純に「なんらかのイベントがあった」ことしか通知してくれません。そのため、前回のスクリプト実行時から予定の差分を抽出する必要があります。Google Calendar の予定取得API(Events: list)には nextSyncToken パラメータに「この結果が返された後に変更されたエントリのみを取得するために後で使用されるトークン」が入ってくるため、これを GAS のプロパティサービスに保存して、次回のスクリプト実行時に利用します。

function getNextSyncToken(nextPageToken?: string): string {
  const options = nextPageToken ? { pageToken: nextPageToken } : {}
  const events = Calendar.Events.list(calendarId, options)

  if (events.nextSyncToken) {
    return events.nextSyncToken
  } else if (events.nextPageToken) {
    return getNextSyncToken(events.nextPageToken)
  } else {
    throw new Error('nextSyncToken or nextPageToken not found')
  }
}

function main() {
  // プロパティサービスの nextSyncToken を利用して新しい予定を取得する
  const nextSyncToken = properties.getProperty('nextSyncToken')
  const events = Calendar.Events.list(calendarId, { syncToken: nextSyncToken })  
  ...

  // 最後に nextSyncToken を保存する
  const newSyncToken = getNextSyncToken()
  properties.setProperty('nextSyncToken', newSyncToken)
}

また、予定が短時間に頻繁に更新された場合、前回からの更新のみを取得したつもりが、同じ予定に対する変更が複数入ってきてしまう可能性があります。その中から古い予定を新しい予定として上書きしてしまった場合、「古い予定 → 取得してきた新しい予定 → 古い予定に対しての誤った更新」といった順番で書き換えてしまう可能性があり、イベントデータが先祖返りしてしまうかもしれません。そのため、予定を更新する(Events: update)際には、必ず更新対象のイベントの etag を If-Match にセットし、アトミック性を確保するようにします。

const events = Calendar.Events.list(calendarId, { syncToken: nextSyncToken })

for (event of events.items) {
  const updatedEvent = Calendar.Events.update(
    event,
    calendarId,
    event.id,
    {
      sendUpdates: 'none', // 予定変更通知メールを送らない
    },
    { 'If-Match': event.etag }
  )

また、実装してみてわかったことですが、予定には「自分が organizer の場合は attendees が空かもしれない」「ゲストに招待権限がない(けど自分が organizer なら当然OK)」などいろいろな状態があり、ややこしかったです。当初は「出席状況または任意参加かをを変更した場合はサブアカにも同じ状態を反映させる」機能を実装していたのですが、サブアカに登録された予定をメインアカに自動招待するためには同じスクリプトを複数のアカウントで動かす必要があり、そうするとどのアカウントの出席状況が最新のものか判断する方法がなくなってしまうため正しく動作せず、この機能を削りました(全ての予定のスナップショットを保存しておくなどやりようはありますが、そこまでやるならGAS使うのやめたい)

前回のエントリーでご紹介した通り、Google Apps Script の開発環境は clasp を利用しています(今だったらgoogle/asideで開発環境を構築するといいかも)。GitHubソースコードを置いていますので、コードで管理したい方、改変したい方はぜひご利用ください。 https://github.com/kkosuge/gas-gcal-synchronizer

課題

以上の仕組みで何ヶ月間かGASを動かしており、予定を見落とすような問題を起こさずに生活ができています。しかし対症療法的なやり方ですので、以下のような課題を感じています。

  • ゲストに招待権限が付いていない予定は同期ができない
  • これを使うとカレンダーの予定の参加者に同一人物が複数入ることになる。表示される人数が多くなってしまったり、参加者の一覧性が悪くなる。
    • 招待ではなく同じ時間枠に予定をコピーすれば良いかもしれない...
  • こういう事が必要な組織であれば、全員が同じ仕組みを動かさないとあまり意味がない。他の人を招待する際には、結局、招待者 x アカウントの数を参加者に追加しないといけないので
    • しかし各自GASを作ってください形式をお願いするのは無理。バグの修正やアップデートを行う際、大変なオペレーションになってしまう
  • そもそもこういう工夫が必要な組織形態がおかしいのでは?

そういう訳で、私個人的にはこういう工夫をしていましたというお話でした。他の人にも使ってもらうためには「Google Workspace の Admin SDK を使う」あるいは「システム用のメールアドレスに予定の編集権限を与えることによって自動で同期するシステムをGCPで構築する」などのやり方もあると思います。他にもいいやり方があったらぜひ教えてください。

SOMPO Digital Labでは一緒に働くソフトウェアエンジニアを募集しています

いろいろと動きがあり、こういった工夫をせずともカレンダーを利用することができることになりそうです。 以下のリンクからカジュアル面談の応募ができますので、組織としてはどういう解決を行うことにしたか、興味を持っていただけた方は是非話を聞きに来て下さい。兼務先のデジタル新事業に興味のある方もぜひこちらから

www.wantedly.com