目次
作りたいものプログラムの仕様
Google Calendarが更新されたとき、予定が重複していないか検知し、重複していたら、slackに通知する。
言語
Google Apps Script (GAS)
必要な処理と関数をまとめる
slackに通知するには、slackAppのライブラリを使うか、もしくはslackに対してペイロードを直接APIで投げればいいので、ここでは割愛します。
主題は、以下です。
- カレンダーが変更された時、どうやって取得する?
- 時間の重複をどうやって取得する?
カレンダーが変更された時、どうやって取得する?
これは普通に、トリガーで、「カレンダーが変更された時」で取得できます。カレンダーのID( a@bb.co.jp など )を指定できます。トリガーで設定された関数に、例えば e で受け取ると、e.calendarId の値でcalendarIdを取得できます。
注意点
a@bb.co.jp で実行されるGASにおいて、a@bb.co.jp のカレンダーを取得する場合は、難しいことを考える必要はありません。
しかし a@bb.co.jp で実行されるGASから、 b@bb.co.jp のカレンダーを検知したい場合は、以下の設定が必要です。
- b@bb.co.jp のカレンダーが、a@bb.co.jp に対して、共有されていること
- a@bb.co.jp のマイカレンダーに、b@bb.co.jp が設定されていること
二つ目は以外と忘れがちですので、注意します。
また、削除までおこなう場合、カレンダー変更の権限をGAS実行のIDに渡しておく必要があります。
時間の重複をどうやって取得する?
時間の重複は、以下のコードで取得できます。
この関数で重複を確認し、もし重複していた場合、最終更新日付が遅いものを削除します。
1 2 3 4 5 6 7 8 9 |
// tとlはstartAtとendAtと、同一性確認のためのtheTagを持つオブジェクトです。 function conflictWith(t,l){ if( t.theTag == l.theTag ){ return false } if( t.startAt >= l.startAt && t.startAt < l.endAt ){ return true } if( t.endAt > l.startAt && t.endAt <= l.endAt ){ return true } if( l.startAt >= t.startAt && l.startAt < t.endAt ){ return true } if( l.endAt > t.startAt && l.endAt <= t.endAt ){ return true } return false } |
コード全文
では、コード全文を紹介します。
まず、Google Driveで新規作成し、「Google Apps Script」を作成します。タイトルはなんでもOKです。
なお、実際には Underscore.js や、moment.js をライブラリとして読み込んでいましたが、今回はコード.js を一枚コピペして設定すれば動くように書き直しました。
なお、slackに投稿するコードは割愛します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 |
//------ // Google Calendar 重複通知プログラム // Written by Sugerfield // コピペ自由、改変自由 //------ // トリガー関数。この関数をカレンダー更新時に呼び出します。 function trigger(e){ let calendarId = e.calendarId let rev = getTimeLine(calendarId) let dayTagList = devideByDay( rev.timeLine ) let conflictList = checkConflictByDay(dayTagList) deletedEvents = deleteConflict(conflictList, rev.indexed) console.log(deletedEvents[0].label+"を削除しました") // これをslack等でpostします。 } // Utilities Moment.js や Underscore.js などを使うとなお良し。コピペ対応のため、機能を書き出しています。 // 指定日後のDateオブジェクトを返す function todayFrom(after) { let today = new Date() let millisecondsInDay = 1000 * 60 * 60 * 24 let targetDate = new Date(today.getTime() + after * millisecondsInDay); return targetDate; } // for loop を簡単に書くための関数です。 function _each(collection, func) { // 配列の場合 if (Array.isArray(collection)) { for (let i = 0; i < collection.length; i++) { func(collection[i], i, collection); } } else { // オブジェクトの場合 for (let key in collection) { if (Object.prototype.hasOwnProperty.call(collection, key)) { func(collection[key], key, collection); } } } } // 日付フォーマット: ラベル用 function formatDateTime0(date) { let month = ('0' + (date.getMonth() + 1)).slice(-2) let day = ('0' + date.getDate()).slice(-2) let hours = ('0' + date.getHours()).slice(-2) let minutes = ('0' + date.getMinutes()).slice(-2) let seconds = ('0' + date.getSeconds()).slice(-2) return `${month}月${day}日${hours}時${minutes}分${seconds}秒`; } // 日付フォーマット: 比較用1 function formatDateTime1(date) { let year = date.getFullYear() let month = ('0' + (date.getMonth() + 1)).slice(-2) let day = ('0' + date.getDate()).slice(-2) let hours = ('0' + date.getHours()).slice(-2) let minutes = ('0' + date.getMinutes()).slice(-2) let seconds = ('0' + date.getSeconds()).slice(-2) return `${year}${month}${day}${hours}${minutes}${seconds}`; } // 日付フォーマット: 日付タグ用 function formatDateTime2(date) { let year = date.getFullYear() let month = ('0' + (date.getMonth() + 1)).slice(-2) let day = ('0' + date.getDate()).slice(-2) return `${year}${month}${day}`; } // 日付フォーマット: 比較用2 function formatDateTime3(date) { let hours = ('0' + date.getHours()).slice(-2) let minutes = ('0' + date.getMinutes()).slice(-2) let seconds = ('0' + date.getSeconds()).slice(-2) return `${hours}${minutes}${seconds}`; } // 全体のイベントを含む、タイムラインを返す。 function getTimeLine(calendarId){ let indexed = {} let cal = CalendarApp.getCalendarById(calendarId) // 現時点から、一週間後までを計算 let events = cal.getEvents(todayFrom(0), todayFrom(7)) let timeLine = [] // 全てのイベントにおいて、必要な値を取得してオブジェクトリストにする _each( events, function(event){ let creators = event.getCreators() let title = event.getTitle() let LabelStartAt = formatDateTime0(event.getStartTime()) let LabelEndAt = formatDateTime0(event.getEndTime()) let dayTag = formatDateTime2(event.getStartTime()) let startAtTime = formatDateTime3(event.getStartTime()) let endAtTime = formatDateTime3(event.getEndTime()) let updateAtTime = formatDateTime3(event.getLastUpdated()) let updateTag = formatDateTime1(event.getLastUpdated()) let eventId = event.getId() // 繰り返しイベントには全て同じIDになってしまうため、日付を絡めてTagを生成する。 let theTag = `${eventId}--${dayTag}` timeLine.push({ dayTag: dayTag, startAt: startAtTime, endAt: endAtTime, updateAt: updateAtTime, updateTag: updateTag, title:title, eventId: eventId, label: `${title} @${LabelStartAt}~${LabelEndAt}`, deleteFlag : false, creators:creators, theTag: theTag }) indexed[theTag] = event }) return {timeLine: timeLine, indexed: indexed } } // タイムラインを日付のインデックスで分割する。 function devideByDay(timeLine){ let ret = {} _each( timeLine, function(v,k){ if( !ret[v.dayTag] ){ ret[v.dayTag] = [] } ret[v.dayTag].push(v) }) return ret } // 日付ごとに、重複を確認する。重複イベントを返す。 function checkConflictByDay(list){ let conflicts = {} _each( list, function(dayLine,day){ _each( dayLine, function(event,k2){ _each( dayLine, function(target, k3){ if( conflictWith(event,target) ){ let tag = [event.theTag,target.theTag].join('-') let tagR = [target.theTag,event.theTag].join('-') if( !conflicts[tag] && !conflicts[tagR] ){ if( event.updateTag < target.updateTag && event.deleteFlag == false ){ // そもそも残る方のdeleteFlagがtrueだと、無効 target.deleteFlag = true }else if( target.deleteFlag == false){ event.deleteFlag = true } conflicts[tag] = [event, target] } } }) }) }) return conflicts } // イベントの重複を確認する。重複していれば、trueを返す。 function conflictWith(t,l){ // tから見て、lが重複しているか if( t.theTag == l.theTag ){ return false } if( t.startAt >= l.startAt && t.startAt < l.endAt ){ return true } if( t.endAt > l.startAt && t.endAt <= l.endAt ){ return true } if( l.startAt >= t.startAt && l.startAt < t.endAt ){ return true } if( l.endAt > t.startAt && l.endAt <= t.endAt ){ return true } return false } // 渡されたイベントについて、イベントの削除を実行する。自分のカレンダーからは消え、相手のカレンダーからは招待を拒否したように見える。 function deleteConflict(conflictList, indexed){ let deletedEvents = [] let deletedIndexed = {} _each( conflictList, (v,k) => { let target = false if( v[0].deleteFlag == true ){ target = v[0] } else if( v[1].deleteFlag == true ){ target = v[1] } if( target ){ if( indexed[target.theTag] && !deletedIndexed[target.theTag]){ deletedEvents.push(target) deletedIndexed[target.theTag] = 1 indexed[target.theTag].deleteEvent() } } }) return deletedEvents } |
トリガーへの設定
トリガーへの設定ですが、時計のロゴをクリックし、新規追加します。以下のように設定することで、指定したカレンダーに対して、設定ができます。