LoginSignup
7
3

More than 1 year has passed since last update.

DiscordにGoogleCalendarの通知を流すbotをGASで作った

Last updated at Posted at 2022-09-07

動機

私が所属しているとあるチームのコミュニケーションツールを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で追加か変更かの区別ができない
    updatedcreatedの差をとって数秒以内なら追加判定にする(雑)

Gmailも

7
3
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
7
3