動機
私が所属しているとあるチームのコミュニケーションツールをSlackからDiscordに移行した
チームのSlackにはGoogleCalendarのイベントを通知するBotを導入していた
機能は専用のcalendarチャンネルに
- 毎朝今日のイベントを一括で通知
- イベント30分前に個別で通知
- カレンダーを編集した際に通知
でこれを継続的に使用可能な技術で
(つまりdiscord.jsや.pyを使わずに)
Discordで再現したかった
コード全文
const
t=new Date(),//実行時刻
prop=PropertiesService.getScriptProperties(),//ストレージアクセス
cals=CalendarApp.getAllOwnedCalendars(),//カレンダー
cfg={
locale:'ja-JP',//時刻表示形式
main:'07:00',//一括通知の時刻
trg:30,//N分前の個別通知
webhooks:[//メッセージを送信するwebhook url 複数可
'https://discord.com/api/webhooks/00000000/xxxxxxxx'
],
opt:{maxResults:65536,showDeleted:true},//変更非推奨 カレンダー参照設定
col:[//変更非推奨 イベント色パレット
null,'#a4bdfc','#7AE7BF','#BDADFF','#FF887C','#FBD75B',
'#FFB878','#46D6DB','#E1E1E1','#5484ED','#51B749','#DC2127'
]
},
fmt=(y,x=CalendarApp.getCalendarById(y.getOriginalCalendarId()))=>({
//使いやすいようフォーマット
name:x.getName(),
title:y.getTitle(),
color:cfg.col[y.getColor()]||x.getColor(),
desc:y.getDescription(),
isAD:y.isAllDayEvent(),
time:y.isAllDayEvent()?[y.getAllDayStartDate(),new Date(y.getAllDayEndDate().getTime()-1)]:[y.getStartTime(),y.getEndTime()],
id:y.getId()
}),
today=()=>cals.flatMap(x=>x.getEventsForDay(t).map(y=>fmt(y,x))),//カレンダーからイベント取得
eid=id=>cals.flatMap((x,y)=>(y=x.getEventById(id),y?[fmt(y,x)]:[]))[0],//idからイベント取得
widget=x=>({//discord embed形式
color:parseInt(x.color.slice(1),16),//10進変換
title:x.title,
description:(x.desc?x.desc+'\n':'')+
[...new Set(x.isAD? //Setで重複解除
x.time.map(x=>x.toLocaleDateString(cfg.locale)):
x.time.map(x=>x.toLocaleString(cfg.locale)))
].join(' ~ '),
footer:{text:x.name}
}),
send=(x={username:'McbeEringi',avatar_url:'https://mcbeeringi.github.io/img/icon.png',content:'ニャン'})=> //webhook送信
cfg.webhooks.forEach(y=>UrlFetchApp.fetch(y,{contentType:'application/json',method:'post',payload:JSON.stringify(x),muteHttpExceptions:false})),
main=()=>send({//一括通知
username:'Google Calendar',
avatar_url:'https://mcbeeringi.github.io/img/icon.png',
content:'今日のイベント',
embeds:today().map(widget)
}),
trg=()=>{//N分前個別通知
const arr=prop.getProperty('ids').split(','),
e=eid(arr[0]);//キューに入れたイベントの先頭一つを取得
send({//usernameとcontentは通知に表示される
username:`${Math.round((e.time[0].getTime()-t.getTime())/6e4)}分間待ってやる`,//イベントまでの分数を算出
avatar_url:'https://mcbeeringi.github.io/img/icon.png',
content:`${e.title} @${e.time[0].toLocaleString(cfg.locale)}`,
embeds:[widget(e)]
});
prop.setProperty('ids',arr.slice(1).join(','));//キューの先頭を削除
},
daily=()=>{//mainとtrgの更新 毎日起動
ScriptApp.getProjectTriggers().forEach(x=>'main,trg'.includes(x.getHandlerFunction())&&ScriptApp.deleteTrigger(x));//過去のトリガーを削除
const set=(x,y)=>ScriptApp.newTrigger(y).timeBased().at(x).create();
{//main
const t1=new Date(t.toDateString()+' '+cfg.main);
t1.getTime()>t.getTime()&&set(t1,'main');
}
prop.setProperty(//trg
'ids',
today().flatMap(x=>{
const t1=new Date(x.time[0]-cfg.trg*6e4);
if(x.isAD||!(t1.getTime()>t.getTime()))return[];//終日イベントと過去イベントは除外
set(t1,'trg');return[[t1.getTime(),x.id]];
}).sort((a,b)=>Math.sign(a[0]-b[0])).map(x=>x[1]).join(',')//イベントの開始時刻順に並べてキューに入れる
);
},
sync_init=()=>{//APIキー再取得 必要時に手動で起動
cals.forEach(x=>{
const id=x.getId();
prop.setProperty(`nst_${id}`,Calendar.Events.list(id,cfg.opt).nextSyncToken);
});
},
sync=(e={calendarID:'example.calendar@gmail.com'})=>{//カレンダーdiff取得 カレンダー変更時呼び出し
const w=Calendar.Events.list(e.calendarId,{...cfg.opt,syncToken:prop.getProperty(`nst_${e.calendarId}`)});
prop.setProperty(`nst_${e.calendarId}`,w.nextSyncToken);
w.items.length&&send({
username:'Google Calendar',
avatar_url:'https://mcbeeringi.github.io/img/icon.png',
content:`変更されたイベント`,
embeds:w.items.map(x=>({...widget(eid(x.id)),fields:[
{name:'操作',value:{confirmed:Date.parse(x.updated)-Date.parse(x.created)<5e3?'追加':'変更',tentative:'暫定',cancelled:'削除'}[x.status]}
]}))
});
daily();//trgの更新
};
GASの設定
- サービス+ > GoogleCalenderAPI の追加
- トリガーの設定
- 日付ベースのタイマー 0~1時 daily()
- カレンダーから カレンダー更新済み sync()
動作
main()
が一括通知
trg()
が個別通知
daily()
が走ると当日分のmain()
とtrg()
が所定の時間に走るようにトリガーが設定される
当日分以外のmain()
とtrg()
のトリガーは削除される
カレンダーを変更するとsync()
が走り前回の情報取得からの差分をAPIに取りに行きそれを整形して送信する
はまりポイント
-
Calendar.Events.list(iCalId)
でnextSyncToken
が返ってこない
代わりにnextPageToken
が返ってきていたらイベントが多いことが原因
オプションのmaxResults
を大きくすると解決する
Calendar.Events.list(iCalId,{maxResults:65536})
などで解決 -
CalendarApp.getEventById(iCalId)
でメインのカレンダーのイベントしか取得できない
一度自分が所有しているカレンダーをすべて取得して
それらすべてに対して.getEventById(iCalId)
する必要がある
つまりCalendarApp.getAllOwnedCalendars().flatMap(x=>(x=x.getEventById(iCalId),x?[x]:[]))[0]
などで解決 -
CaledarEvent.getAllDayEndDate().toLocaleDateString('ja-JP')
で表示した日付が一日遅れている
取得したDate
が終了日の24:00であることが原因
new Date(CaledarEvent.getAllDayEndDate().getTime()-1).toLocaleDateString('ja-JP')
などで解決 -
スクリプトプロパティに
[xxx@java-lang...
みたいなわけのわからん文字列が入る
配列を直接入れようとするとそうなる
横着せずに.join()
で解決 -
CalendarEvent.getColor()
でStringな数字が返ってくる hexカラーコードはどこ?
返ってくる数字はこれ
https://developers.google.com/apps-script/reference/calendar/event-color
これの逆引きが欲しいわけだが無理そうだったので上記ページのCSSを見て配列に起こした
上記コードcfg.col
を参照 -
Calendar.Events.list().items[]
のstatus
で追加か変更かの区別ができない
updated
とcreated
の差をとって数秒以内なら追加判定にする(雑)
Gmailも