日報を書くとき、カレンダーの今日の予定一覧を見ながら書くことが多いと思います。しかし、カレンダーの予定を毎回日報にコピペするのは面倒。日報に最初から予定の一覧が流し込まれていれば、もっと楽をできる! ということでやってみました。私の場合、esa.ioに日報を書いていますが、APIが使えるサービスなら大体同じことができると思います。
当日の0:00頃に以下のような形で、今日の予定一覧が日報に流し込まれてきます。
## 本日の予定 - 健康診断 - 時間: 9:00 ~ 12:00 - 説明: 病院のURLがここに - XXX勉強会 - 時間: 19:00 ~ 20:00 - メンバー: syou6162, xxx, yyy - ...
やり方: Google App Scriptを使う
認証回りや実行環境で悩みたくなかったので、Google App Script(GAS)を使います。GASは5年前くらいに少し使ったことがあったけど、Web上のエディタでよく分からないSDKを触るのはあまり体験がよくなかったんですが、最近はとてもよくなっていました。手元のエディタ(VSCode)でTypeScriptの型の恩恵を受けながら、スムーズに書くことができました。人権が存在していた。
GASには定期実行するトリガーがありますが、dailyだと「0~1時の間に実行」という形でしか指定できません。これだと微妙に困るので、こんな感じで回避しています。マジかよ...という感じですが、ググったところよくある回避方法のようです。
- 前日の23時~0時に「トリガーを登録するdailyのトリガー」を実行
- 当日の0:00に実行されるトリガーを登録する。一度きりのトリガーだと、実行時間で分まで指定でできる
- 当日の0:00にトリガーが実行され、予定一覧がesa.ioに流し込まれる
- 一度きりのトリガーがゴミとして残ってしまうので、実行前に不要なトリガーの掃除をする
外部サービスに依存する場合、失敗の通知などの考慮も必要になってきますが、GASではディフォルトで通知してくれるのでこれまた便利でした。
コード
コードはこんな感じ。100行くらいで書けるのでお手軽だし、claspのお陰でgithubでの管理もできる。
const getEvents = (): GoogleAppsScript.Calendar.CalendarEvent[] => { const calendar = CalendarApp.getDefaultCalendar() const now = new Date() const today = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0) const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59) return calendar.getEvents(today, tomorrow) } const event2HourAndMinute = (d: GoogleAppsScript.Base.Date): string => { return Utilities.formatDate(d, "Asia/Tokyo", "HH:mm") } const event2TimeString = (e: GoogleAppsScript.Calendar.CalendarEvent): string => { return `${event2HourAndMinute(e.getStartTime())} ~ ${event2HourAndMinute(e.getEndTime())}` } const event2MembersString = (e: GoogleAppsScript.Calendar.CalendarEvent, maxMembers: number): string => { const members = e.getGuestList() if (members.length == 0) { return "" } return members.slice(0, Math.min(members.length - 1, maxMembers)).map((g: GoogleAppsScript.Calendar.EventGuest) => { if (g.getName() != "") { return g.getName() } else { return g.getEmail() } }).join(", ") } const todayEvents2String = (): string => { const events = getEvents() if (events.length == 0) { return "" } let str = "" str += "## 本日の予定\n" events.forEach((calendarEvent: GoogleAppsScript.Calendar.CalendarEvent) => { str += `- ${calendarEvent.getTitle()}\n` str += ` - 時間: ${event2TimeString(calendarEvent)}\n` // 一行に抑えたいので、改行を置換する const description = calendarEvent.getDescription().replace(/\r?\n/g, " ") if (description != "") { str += ` - 説明: ${description}\n` } const membersStr = event2MembersString(calendarEvent, 3) if (membersStr != "") { str += ` - メンバー: ${membersStr}\n` } }) return str } function formatDate(dt: Date): string { const y = dt.getFullYear(); const m = ('00' + (dt.getMonth() + 1)).slice(-2); const d = ('00' + dt.getDate()).slice(-2); return (y + '/' + m + '/' + d); } const teamName = PropertiesService.getScriptProperties().getProperty("esaTeamName") const token = PropertiesService.getScriptProperties().getProperty("esaAPIToken") const apiBaseUrl = 'https://api.esa.io/v1/teams/' + teamName const postEsa = (content: string) => { const payload = { "post": { "name": "日報", "category": `日報/${formatDate(new Date())}`, "body_md": content, "wip": false, "message": "Update by GAS" } } const url = apiBaseUrl + "/posts" const options: GoogleAppsScript.URL_Fetch.URLFetchRequestOptions = { method: "post", contentType: 'application/json; charset=utf-8', headers: { 'Authorization': 'Bearer ' + token }, payload: JSON.stringify(payload) } const resp = UrlFetchApp.fetch(url, options) console.log(`${resp.getResponseCode()}`) console.log(resp.getContentText()) } // 一回だけ作るトリガー function setOnceTrigger() { ScriptApp.newTrigger("setTrigger").timeBased().atHour(23).everyDays(1).create(); } function setTrigger() { const today = new Date() const tomorrow = new Date(today) tomorrow.setDate(tomorrow.getDate() + 1) tomorrow.setHours(0) tomorrow.setMinutes(0) ScriptApp.newTrigger('main').timeBased().at(tomorrow).create(); } const cleanUnusedTriggers = () => { const triggers = ScriptApp.getProjectTriggers() triggers.forEach((t => { if (t.getHandlerFunction() == "main") { console.log(t.getEventType.toString()) console.log(t.getHandlerFunction()) console.log(t.getTriggerSourceId()) ScriptApp.deleteTrigger(t) } })) } const main = () => { cleanUnusedTriggers() postEsa(todayEvents2String()) }
APIのTokenをコードに直接書きたくなかったので、プロパティを使いたかったんだけど、新UIでは設定する箇所がまだなかった。ここだけ困ったので、早く追加されて欲しい~。
ですよね、ありがとうございます。legacy editorからポチポチやりました。
— Yasuhisa Yoshida (@syou6162) March 11, 2021
コード上に残したくないsecretなどの値をを入れていくのにプロパティを使いたいので、setからできるのはあまりうれしくはないんですよね。。