勉強会に参加した記録(Googleカレンダー)をGoogleスプレッドシートに記録する(Google Apps Script GAS使用)

![gas_shigoto_makaseru.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15264/81d0c867-26d6-b936-01bd-1617fd3544d9.png)



# TL;DR(まとめ)

* Connpassなど、参加登録した勉強会を表形式で一覧にする必要があった
* Google Apps Script(通称GAS)を使い、Googleカレンダーに追加されたイベントをもとにGoogleスプレッドシートに登録した
* 無料でできるのは嬉しいけど、もうちょっとカレンダーイベントの取得方法がなんとかならないかなと（Googleの仕様
 

# はじめに

※平易な言葉で記述することを心掛けていますので、エンジニアの方には長いと感じるかもしれません。予め承知おきを。

私は、Connpassを始め、各種勉強会サイトを通して勉強会に参加する際、Googleカレンダーに自身の予定を登録しています。
Googleカレンダーに書いておけば簡単に誰かと共有できますし、ダブルブッキングも起きにくいですよね。

それだけでも十分便利だったのですが、ふとしたことから過去N年間でどこの勉強会に何回行ったのか集計する必要がありました。
じゃあ集計する際にGoogleカレンダーAPIにリクエストを投げて整理したらいいよね。まぁ、そうです。
[GoogleカレンダーAPIのドキュメントのこのあたり](https://developers.google.com/calendar/v3/reference/events/list)にやり方は書いています。
バッチ的に調べるだけならそれでもいいかもしれません。

ですが、どうせなら都度調べるのではなく、カレンダーに勉強会などのイベントを追加したタイミングを拾って解決したらいいのでは？と考えました。

そして個人の予定管理ですし、無料でやりたいですよね。
なのでGCPも使わずに、GoogleスプレッドシートとGoogle Apps Script(GAS)を用いて解決します。（それがなにかという説明は割愛）

「Googleカレンダーに追加された勉強会イベント情報を取得して、Googleスプレッドシートに追記するというGASスクリプト」を作成します。
なので、実質スプレッドシートを新規作成して、イベント情報のデータを追記するプログラムを書くということになります。

手順は以下の通りです。

1. イベント情報登録用のGoogleカレンダーIDを取得する
1. イベント情報記録用のGoogleスプレッドシートを新規作成する
1. Googleスプレッドシートに紐付けたGoogle Apps Scriptを作成する
1. Google Apps ScriptでinitSync関数を初期実行する(一度のみ)
1. カレンダーの変更イベントをきっかけにGoogle Apps Scriptを動かす設定を追加する（トリガー設定）

# 作成手順

## イベント情報登録用のGoogleカレンダーIDを取得する
後にコードに記述するため、GoogleカレンダーIDを取得します。
既存のカレンダーでも、今回用に新規にカレンダーを作成しても。どちらでもOKです。

Googleカレンダー -> 対象のカレンダーの設定 -> カレンダーの統合 -> カレンダーID
この値をコピーしておいてください。

![calendar01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15264/541c1731-bbba-a479-91a9-fe35ef67d988.png)


## イベント情報記録用のGoogleスプレッドシートを新規作成する
イベント情報を蓄積していくGoogleスプレッドシートを作成します。GASを紐付けるので新規作成すると良いでしょう。

Googleドライブ -> 新規 -> Googleスプレッドシート -> 空白のテンプレート

![drive01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15264/5104fde3-d94f-b399-1a66-687d3d8a7af0.png)



## Googleスプレッドシートに紐付けたGoogle Apps Scriptを作成する
続いてスプレッドシートに紐付けたGASプロジェクトを作ります。

ツール -> スクリプトエディタ

![spreadsheet01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15264/1db12ed2-1565-bdc8-b5dc-722d28a60bbd.png)


スプレッドシートのメニューを選ぶとGASの画面が表示されます。
プロジェクト名を任意のものに変えておきましょう。

functionの中にロジックを記述していきますが、その前にGoogleカレンダーAPIサービスを紐付けます。
※GASのカレンダー操作APIではなく、GoogleカレンダーAPIを用います。GASのカレンダー操作APIではカレンダー内の最新のイベントを取得できず、GoogleカレンダーAPIを頼る必要があります。この場合でも課金はされないのでご安心を。


サービスの横の＋

![appscript01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15264/e1913cc9-3466-fa1f-ded9-4498aeb4285e.png)


Google Calendar APIを選択して、追加

![appscript02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15264/5490e743-1488-c41d-e880-8d268e4c4606.png)



サービスの下に「Calendar」があることを確認します。
functionの内容を上書きする形で以下内容を記述します。
1行目の内容（カレンダーID）は先程取得した値に差し替えてください。
保存します。

![appscript03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15264/164b94ee-3e57-fef0-0a49-e2e2f13894b3.png)


```javascript
const CALENDAR_ID = "XXXXXXXXXXXXXXXXXXXXXXXX@group.calendar.google.com"; //同期させたいカレンダーIDを入れる

//トリガーとしてカレンダーIDは特定している。Learningのカレンダーに追加されたときのみ起動する
function modCalendarEvent(calEvent) {
  
  // トリガーから得られるcalEventには直接追加されたイベントの情報が含まれておらず、カレンダー自体の情報しか含まれていない。(仕様)
  // ガイドに沿って変更されたイベントの情報を取得する
  // https://developers.google.com/apps-script/guides/triggers/events#google_calendar_events
  var properties = PropertiesService.getScriptProperties();
  var nextSyncToken = properties.getProperty("syncToken");
  var optionalArgs = {
    syncToken: nextSyncToken
  };
  var events = Calendar.Events.list(calEvent.calendarId, optionalArgs);
  // 次に変更されるイベントのトークンを更新しておく
  var nextSyncToken = events["nextSyncToken"];
  properties.setProperty("syncToken", nextSyncToken);

  // eventが終日イベントか否かでstartの内容が異なる。必要なのは日付だけなので、日付を取得している
  // 終日イベントの場合 : date : 2021-04-21
  // 時間指定ありイベントの場合 : dateTime : 2021-04-18T18:15:00+09:00
  var startDate = events.items[0].start;
  var eventDate = 'date' in startDate? String(startDate.date).replace(/-/g,"/"): String(startDate.dateTime).split('T')[0].replace(/-/g,"/");

  // スプレッドシートに追加する情報
  var event = {
    "CheckFlag" : 0,
    "EventId" : events.items[0].id,
    "EventName" : events.items[0].summary,
    "EventDate" : eventDate,
    "CreatedDate" : new Date(events.items[0].created),
    "UpdatedDate" : new Date(events.items[0].updated),
    "Memo" : "modCalendarEventから追加"
  }

  // イベント作成から1秒以上経過しているなら、なにもしない（作成時のみ起動させるため）
  // 更新や削除時に必要なことがあればこの箇所も要変更
  if((event.UpdatedDate - event.CreatedDate) > 1000){
    return;
  }
  // スプレッドシートに追記する
  addEventDataToSpreadSheet(event);
}

// スプレッドシートに追記する
function addEventDataToSpreadSheet(event) {
  // console.log(event);
  // Activeなスプレッドシートに追加する
  var spread = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = spread.getSheets()[0];

  // 重複の確認
  var textFinder = sheet.createTextFinder(event.EventId);
  if(textFinder.findAll().length == 0){
    // 新規作成の場合は行追加
    sheet.appendRow([event.CheckFlag,event.EventId,event.EventName,event.EventDate, event.Memo]);
  }
}

// トークンの初期化に使用する。普段使うことはなく呼び出されることもない。
function initialSync() {
  var items = Calendar.Events.list(CALENDAR_ID);
  var nextSyncToken = items.nextSyncToken;
  var properties = PropertiesService.getScriptProperties();
  properties.setProperty("syncToken", nextSyncToken);
}

```

## Google Apps ScriptでinitSync関数を初期実行する(一度のみ)
プロパティ```syncToken```を設定するため、```initSync```関数を一度のみ実行します。
カレンダーのイベントが新規作成,更新または削除があった際に、この```syncToken```の値を取得するため、
初回のみ設定が必要です。繰り返し実行したとしても問題はありませんが、必ず一回は実行してください。

関数名```initSync```を選択して、実行

![appscript06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15264/47ee0eab-c2d0-f990-20aa-566cd38c382c.png)


初回のみ、GoogleやGASプロジェクトがスプレッドシートを触っても良いか？という確認をしてきます。（権限の付与）
以下画像を参考に適宜OKで進めてください。

![appscript07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15264/bc26eb73-ea25-e58f-65ab-fba0e161ce10.png)

![appscript08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15264/d0848757-0ee0-8e17-d9f3-0aea44460f75.png)

![appscript09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15264/67bb30ca-ef87-6a44-ca7f-90b098452f99.png)

![appscript10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15264/289846de-36a4-5488-a0b6-c934dfff63fc.png)


初回```initSync```関数を実行できたら、画面に以下メッセージが表示されます。次に進んでください。

![appscript11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15264/b4401718-1f0d-a1d8-124c-0edbab03e73a.png)


## カレンダーの変更イベントをきっかけにGoogle Apps Scriptを動かす設定を追加する（トリガー設定）
Googleカレンダーで変更(イベントの新規作成,更新,削除)があったら```modCalendarEvent```を動作させるという形です。
カレンダーの変更をきっかけ、いわゆるトリガーにするわけです。

画面左のトリガーを選択、トリガーを作成

![appscript12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15264/b78d9482-a9f3-b143-3d5e-83cabd7424fe.png)


トリガーの設定を以下のようにする
関数：modCalendarEvent
イベントのソース：カレンダーから
カレンダーの詳細：カレンダー更新済み
カレンダーのオーナーのメールアドレス：上で取得したカレンダーID。＠以降も含む

![appscript13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15264/06d8c3f9-0e02-13d6-71a9-5ea840f5c986.png)


トリガーの設定ができたら、設定は完了です。

Googleカレンダーで任意の日にイベントを追加してください。
うまくいけばGoogleスプレッドシートのシートに1行追加されているはずです（イベント名をaaaaaとした行が追加されました）

![spreadsheet02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15264/1bd5bb6c-5ed2-df5d-5bb2-97cc6351011a.png)

なお、GASトリガーの動作の確認は、GASプロジェクトの左、実行数のところを見ると確認できます。ログの確認もここからできます。

![appscript14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15264/e702cc36-5ee0-1906-3b16-292fbbec8221.png)


# ソースコード補足

* Googleカレンダーでイベントを登録する際に、終日イベントか時間に限りのあるイベントかで連想配列の中身が異なります。
* Googleスプレッドシートに残す情報としては開始時間を含めない形にしています。必要あれば形式を変えてください。
* Googleスプレッドシートにではなく、別のサービスに連携することもできます。AWS API GatewayやSlack APIなどのWEB APIエンドポイントにリクエストを投げるならばUrlFetchAppを使うと良いでしょう。[ドキュメント](https://developers.google.com/apps-script/reference/url-fetch/url-fetch-app)
* 別のサービスに連携するなどが増えた場合、GASの認証の項目が増えます。承認が必要となります。
* イベントドリブンでなく、バッチで十分であれば、cron的に動かすことが可能です。トリガーを時間主導型にすると実現可能です。

# 考察

## GoogleAppsScriptに送られてくるeventにはイベントの内容が含まれず、calendarIdだけが連携されることについて

これはただの憶測ですが・・・。
calendarIdしか連携されなければ、情報漏洩が起きないという意味で安全だからかもしれません。
calendarIdからさらにカレンダーの参照権限を持っていないと、イベントの詳細を知りえないようにするためという意味です。
calendarIdを知っていてもされにそこからカレンダーのイベント情報を取得できる権限を持っていないとイベントの詳細はわからないわけなので、
その方が安全と判断し、仕様をそのようにしているのかもしれません。
まぁGASを扱う側として、手間は手間ですけどね。

## トリガーはカレンダーイベント？時間主導型？

ここまでイベントドリブンな記述をしておいて何ですが、トリガーを時間主導型にする、例えば月イチで動作させるという方が楽な可能性があります。
月イチでGASスクリプトを動かして、Googleカレンダーを確認＆そこで得た内容をGoogleスプレッドシートに残すというやり方ですね。
[GoogleAppsScriptのドキュメント](https://developers.google.com/apps-script/reference/calendar/calendar-app)によれば、GoogleカレンダーにおいてはgetEvents(startTime,endTime)という項目があり、指定期間のイベントが取得できます。
今回はカレンダーが追加されたタイミングですぐ動作させたかったため、上記の方法を取得しました。
しかし、月イチで取れたらいいんだよ、という場合であったらバッチ処理の方が適切かもしれません。
やるとしたら月初にGASスクリプトを起動して、前月の予定１月分を取得するイメージでしょうか。過去のイベントを削除するケースはあまりないと思うので。

## 外部連携したい場合はUrlFetchApp

Googleカレンダーに追加があったらSlackやオレオレAPIに投稿したいんだ、わかります。
UrlFetchAppを使えば簡単に外部連携が可能です。POSTしましょう。


以上です。よいGASライフを！