1. はじめに
以前、Google Apps Script (以下、GAS と記述します) のカレンダートリガーで、Notion タスクの登録する処理を作成しました。ただ、これは問題点があって、カレンダーデータを更新してもタスクが作成されてしまい、余計な Notion タスクを消す作業に追われました。Notion page の ID さえわかれば、作成ではなくプロパティなどの更新ができるので、カレンダーのメモに ID を記録することで、作成・更新を切り替えられるようにしたので、そのスクリプトを解説します。GAS を始めて使う人もいると思うので、過去に書いた記事を参照しながら、説明します。ただ、スクリプトは当時とだいぶ変わっているので、ここに全て記録することにします。
2. GAS の準備
まだ GAS を使ったことがない人は、以下の記事を参考にスクリプトエディタを開いてください。スクリプトはまだ記述しません。
3. Calendar API の準備
まだ、Calendar API を有効にしていない人は、以下のページを参考に有効化してください。「Google Calendar API を追加」の画像の部分まで進めてください。
4. API の接続テストと認証の取得
コードをたくさん記述する前に Calendar API がうまく動作するかを確認します。スクリプトエディタに以下のコードを記述します。3 で取得したカレンダーID を下から4行目の部分に記述してください。
// 前回保存したカレンダーのSyncTokenを取り出す、前回保存分が無い場合は今回のSyncTokenを利用する function getSyncToken(calendarId){ var token = PropertiesService.getScriptProperties().getProperty('SYNC_TOKEN') if (token) { return token } let events = Calendar.Events.list(calendarId, {'timeMin': (new Date()).toISOString()}) return events.nextSyncToken } // 初回認証のためだけのテスト呼び出し関数 function readCalendar() { let calendarId = "この部分にカレンダーIDを書きます。"; let token = getSyncToken(calendarId); Logger.log(token); }
初回だけ認証が必要なので、下にあるように readCalendar を選択した状態で、「実行」をしてください。許可を行って文字列がコンソールに出力されたら成功です。終わったら readCalender 関数は消してしまってよいです。
5. タスクデータベースID とNotion token の保存
次に Notion の Token とタスクデータベースの ID を環境変数に記録しておきます。見えるところに ID などを記録しておきたくないためです。storeTokenAndId の MY_NOTION_TOKEN と DATABASE_ID の説明の部分にそれぞれを記述します。先ほどと同様に storeTokenAndId を実行して、実行ログにそれぞれが表示されたら成功です。こちらも終わったら、storeTokenAndId 自体を消すか、記録した ID を消してしまってください。
// MY_NOTION_TOKEN と DATABASE_ID をプロパティに登録する(1回だけ使い、終わったらIDを消す) function storeTokenAndId() { const scriptProperties = PropertiesService.getScriptProperties() scriptProperties.setProperties({ "MY_NOTION_TOKEN": "ここに「Notion Token」を記述", "DATABASE_ID": "ここに「データベースID」を記述" }) // 登録できたことを確認 console.log("myNotionToken = " + myNotionToken()) Logger.log("databaseId = " + databaseId()) } // MY_NOTION_TOKEN を取得 function myNotionToken() { return PropertiesService.getScriptProperties().getProperty("MY_NOTION_TOKEN") } // DATABASE_ID を取得 function databaseId() { return PropertiesService.getScriptProperties().getProperty("DATABASE_ID") }
6. その他の関数の準備
メイン以外のユーティリティ関数を記述しておきます。これまでに説明したものもありますが、軽く解説しておきます。
6.1 sendNotion: Notion に payload を送信
いつもの Notion に payload を送る関数です。以前は url や method を直接書いていましたが、共通化のために外から渡すように変更しています。
// Notion に payload を send する function sendNotion(url, payload, method){ let options = { "method" : method, "headers": { "Content-type": "application/json", "Authorization": "Bearer " + myNotionToken(), "Notion-Version": "2021-05-13", }, "payload" : JSON.stringify(payload) }; // デバッグ時にはコメントを外す // Logger.log(options) return JSON.parse(UrlFetchApp.fetch(url, options)) }
6.2 createPage: ページを作成
ページを作成する関数です。先ほどの sendNotion にページ作成の url と post を渡しているだけです。
function createPage(payload) { return sendNotion("https://api.notion.com/v1/pages", payload, "POST") }
6.3 patchNotion: Notion に payload をパッチ
ページを更新する関数です。同じく page を指定した url と patch を渡しています。
function updatePage(pageId, payload) { let url = "https://api.notion.com/v1/pages/" + pageId // API URL sendNotion(url, payload, "PATCH") }
6.4 saveSyncToken: syncToken をプロパティに保存
新しく作成・変更されたイベントを取得するには、保存されている syncToken と比較して、それより新しいものを取得します。このため、作業が終わったらその時点の syncToken をプロパティに保存しておきます。そのための関数です。
// SyncTokenをプロパティに保存する function saveSyncToken(token){ PropertiesService.getScriptProperties().setProperty('SYNC_TOKEN', token) }
6.5 eventToHash: カレンダーイベントから hash を作成
取得したイベントから payload 用のハッシュに変換する関数です。日付が設定されていない場合には、null を返します。
// カレンダーイベントから日付の hash を作成 (削除時にも呼ばれるので、イベントに日付が設定されていなければ null を返却) function eventToHash(e) { let hash = null if ('dateTime' in e.start){ hash = { "start": e.start.dateTime.replace("T", " "), "end": e.end.dateTime.replace("T", " ") } } if ('date' in e.start) { hash = { "start": e.start.date } } return hash }
6.6 createPayload: 作成用の payload を作成
title と 6.5 で作成した日付 hash から作成用の payload を生成します。
// title と date_hash から create payload を作成 function createPayload(title, date_hash) { return { "parent": { "database_id": databaseId() }, "properties": { "タスク名": { "title": [ { "text": { "content": title } } ] }, "日付": { "type": "date", "date": date_hash }, } } }
6.7 updatePayload: 更新用の payload を作成
こちらは更新用の payload 作成関数です。
// title と date_hash から update payload を作成 function updatePayload(title, date_hash) { return { "properties": { "日付": { "date": date_hash }, "タスク名": { "title": [ { "text": { "content": title } } ] } } } }
7. メイン関数の作成
カレンダーイベントの description の最終行を取得し、「id:」で始まった場合には、その後ろの id を取得し、ページ更新するようにしています。存在しなかった場合にはタスクを生成した上で、カレンダーの description に id を記録します。
// カレンダーイベントが変更されたら呼ばれるメソッド function doCalendarPost(event) { let calendarId = event.calendarId // カレンダーIDのの取得 let token = getSyncToken(calendarId) // 前回実行時に取得したカレンダーTokenの取得 let events = Calendar.Events.list(calendarId, {'syncToken': token}) // Token から後のカレンダーを取得 let filteredItems = events.items.filter(e => {return e.status == "confirmed"}) // ステータスを見て登録、もしくは更新の予定のみにフィルタリング filteredItems.forEach(e => { let descriptions = (e.description || "").split("\n") console.log(descriptions) let dlen = descriptions.length let exist = false let id = '-' if (dlen > 0) { // 行が存在する場合 last_line = descriptions[dlen-1] // 最後の行を取得 let ids = last_line.match(/^id:(.*)$/) // id を取得 if (ids != null) { // 取得できた場合 id = ids[1]; // 取得した id exist = true } } let date_hash = eventToHash(e) // イベント日付を取得 if (date_hash) { // イベントに日付がある場合だけ実施 if (exist) { // 存在した場合は update let payload = updatePayload(e.summary, date_hash) updatePage(id, payload) Utilities.sleep(1000); } else { let payload = createPayload(e.summary, date_hash) let ans = createPage(payload) Utilities.sleep(1000); id = ans["id"] descriptions.push("id:" + id) e.description = descriptions.join("\n") Calendar.Events.patch(e, calendarId, e.id) } } }) saveSyncToken(events.nextSyncToken) // 今回のTokenを保存する(次回のScript実行時に利用) }
8. テスト
Google Calendar 上でテストしてみました。すでに Twitter で報告したようにこんな感じでタイトル変更・時間変更などうまく動いているようです。
Google Calendar で変更する分には問題ない感じ。 pic.twitter.com/AtrCrMoII1
— hkob (@hkob) 2021年7月16日
9. おわりに
Web 上の Google Calendar からはこれでうまく動作しました。ただ、Alfred から「ac タスク名」とした場合に、タスクが二重化する現象が発生してしまいました。作成時にタスクが作成され、description 保存時に再度イベントが発行され、タイミングにより description の id が読み込まれずに再度作成されてしまうのかもしれません。遅延を入れたりしたのですが、それでもたまに失敗するので、Alfred 側を修正することにしました。これは別記事にします。