LoginSignup
2
1

More than 1 year has passed since last update.

Google カレンダーの変更通知を Slack に飛ばす Google Apps Script

Posted at

家族で使っている共有 Google カレンダーの変更通知を家族 Slack に飛ばす Google Apps Script を書いた。

動いている様子はこんな感じ。
image.png

細かい実装はリポジトリを見てもらうとして、大まかな仕組みといくつかハマったポイントについて書いていく。

動作する仕組み

  1. カレンダー予定が変更される (予定の作成,更新,削除)
  2. Google Apps Script (以下 GAS) が起動する
    • GAS のトリガー設定で「イベントソース: カレンダー」を設定するだけでできる
  3. GAS 内で Slack Incoming Webhook を使って Slack にメッセージを飛ばす

簡単!

開発環境

GAS は Web 上のエディタで直接書くこともできるが、コードを Git で管理したいし TypeScript 使いたいので、Google 公式の GAS CLI である clasp を使って開発環境を作った。

clasp login して clasp create ... して適当に TypeScript 書いて clasp push したらもう動かせるようになるので本当に簡単で、「うひょーーー GAS 開発ぜんぜん楽勝じゃねえか」と思われた。しかし...。

カレンダーの変更内容を簡単に取得できない

前述の通り GAS ではカレンダーの変更時にトリガーすることができる。

しかしイベントとして取得できるのが「カレンダー ID のみ」である。

つまり「このカレンダーで何らかの変更があったよ」まではわかるが「どの予定が変更されたか」まではわからない。これは公式ドキュメントにも記載されており、「変更内容が知りたければ nextSyncToken を保持しておいて増分同期してね」的なことが書かれている。

何故こんな仕様に???

言っていることはわかるがとにかくめんどくさい。

仕方ないのでこのような実装を書いた。

  • User Properties に syncToken を保持するようにする
  • GAS 実行時に前回分の syncToken が存在しない場合、現在時刻からの予定のフルスキャンを行い、syncToken を入手して User Properties に保存する
  • GAS 実行時に前回分の syncToken が取得できる場合、それを使って予定の差分を入手する

(コードの詳細は冒頭に貼ったリポジトリを参照)

予定の新規作成と更新の区別がつかない

今回は「新規作成」か「更新」か「削除」かによって Slack メッセージの色を変えたかったので、それらを区別する必要があった。

しかし前述の増分更新で得られる Event オブジェクトはあくまで「(変更があった) 予定の詳細」を返してくれるのみで、「どのような変更があったか」は得られない。

「削除」だけは簡単に識別できて、予定の status というプロパティが cancelled になっていれば「予定が削除されている」=「キャンセルされた」と判断できる。

問題は「新規作成」と「更新」の区別で、これはもうどうやっても無理な気がしたので、 「予定の作成時刻が10秒以上前であったら更新」 という方法で識別することにした。10秒のラグを設定したのは、カレンダーの予定変更タイミングと GAS 発火タイミングで若干遅延がある可能性を考慮したため。(実際はかなり速く GAS が起動するので、ここは3秒とかでもいいかもしれない)

ES Modules (import/export) が使えない

コードを書いていたらボリュームが大きくなってきたので、普通の TypeScript のノリでファイルを分割したところ、うまく動かなくなってしまった。

GAS は ES Modules (import/export) をサポートしておらず、そもそもモジュールという概念がなく (?)、ファイルを分割しても名前空間が共有されるという仕様らしい。

例えば utils.gs 内で foo() という関数を定義した場合、別ファイルの main.gs 内からは特に import などを明示しなくても foo() という関数が使える。

楽といえば楽だが、今回は TypeScript で書いているので相性が悪い。

TypeScript で

main.ts
import { foo } from 'utils'

function main() {
    foo()
}

こんなコードを書いていると、clasp でトランスパイルしたときに

main.gs
// import { foo } from 'utils'

function main() {
    _module1.foo()
}

こんな感じになる。

import 文は勝手にコメントアウトされるくせに、 foo() の呼び出しはどこかにあるモジュールを参照している風になり、「_module1 など無い」とエラーが出る。めんどくさい。

clasp 公式では「いくつかの workaround があるよ」と紹介しているが、どれも一長一短ありケースバイケースな感じがする。

Webpack 使うのが一番良さそうではあるが、設定ファイルを書くのがめんどくさかったのでやめた。

parcel や ncc も使ってみたが、別の理由でうまくいかなかった。(詳細は割愛)

最終的にどうしたかというと、ビルド時に「import 文は消して全 ts ファイルを結合する」という原始的な方法で解決した。だって TypeScript のトランスパイルは clasp がやってくれるのだし...。

$ cat src/*.ts | grep -Ev '^import' > dist/bundle.ts

この方法だと npm モジュールを使うことなどができないので注意が必要だが、シンプルだし別の bundler を入れなくてもよいので、場合によってはかなりいいのではないかと思う。

2
1
0

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
2
1