google calendar上の予定をslack通知させて予定の守れるエンジニアになりたい！

## 背景
![スクリーンショット 2021-05-04 21.32.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178971/ad560431-5d68-841d-13bc-34aec313f1fa.png)

これは、チーム内のKPTでProblemの洗い出しをしているときに「自分最近作業に集中しちゃってmtg遅刻しちゃってるな」と感じたので、戒め & 自分の意識付けのために書いたチケットです。(チームのProblemじゃないじゃん、というところは目をつぶっていただいて:pray:)
ただその意識も永久的に続くわけではないだろうなと思ったのでどうせならシステムで解決したい！

ということで、 Google Calendarに上がっているmtg予定の1分前になったらslackにメンションを飛ばしてくれる仕組みを作りました。

## 実は... [5/5 14:20 追記]
知ってる方もいるかも知れませんが、slackにはgoogle calendarのインテグレーションが備わっており、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178971/97a67609-d7b3-1918-14fe-92032a619f33.png)
こちらを導入するだけで簡単に **1分前にslack通知してくれる** 機能は実現できるのですが、
![スクリーンショット 2021-05-05 14.05.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178971/c8dbe538-e6db-207e-7ce8-adcd14642b95.png)
今回自作するに至った理由は、このスクショのように

- メンションの設定ができない
- フォーマットのカスタマイズができない
  - このタイトルについてるリンクをカスタマイズできない(google calendarの予定に飛んでしまう)
  - mtgの参加者やアジェンダなどの表示ができない

といったことができず、mtgのリマインダとして利用するにはちょっと情報量不足・要件が満たせないと感じたためです。
もしこの情報量で十分だという方は黙ってこれを導入しちゃうのが一番だと思います。

special thx)
https://twitter.com/sakichi01_/status/1389734107848921088?s=20

## とりあえず成果物
ということで成果物はこちら。
インテグレーションで実現できなかったメンションやアジェンダ、参加者情報の記載が出来るようになりました。
![スクリーンショット 2021-05-04 22.16.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178971/6e8d7c22-2084-992e-ea42-b6bb79ebf6b0.png)

以下のソースコードをgasに入れて、トリガーを1分毎に回しています。

```js
const calendarId = 'calendarId@group.calendar.google.com'; 
const channel = '#channel_name';
const mention = '@sakas1231';
const username = '通知くん';
const icon_emoji = ':ghost:';
const incomingHookUrl = 'https://hooks.slack.com/services/webhook_url';
const tagName = 'notifired';

// slackリマインドさせる時刻
// e.g.) 1なら1分前に通知
const PRE_NOTI_MINUTES = 1;

const isNotifiableEvent = event => !event.isAllDayEvent && 
    !event.isNotifired;

const buildEventObject = event => {
  // よしなにeventをparseしてプロパティを埋める
  const url = 'xxx';
  const members = 'xxx';
  return {
    isAllDayEvent: event.isAllDayEvent(),
    startTime: moment(event.getStartTime().getTime()),
    endTime: moment(event.getEndTime().getTime()),
    title: event.getTitle().replace('\n', ''),
    description: event.getDescription(),
    url,
    members,
    isNotifired: !!event.getTag(tagName),
  }
};

const buildSlackBlock = eventObj => {
  const restTotalSeconds = eventObj.startTime.unix() - moment().unix();
  const rest = {
    seconds: restTotalSeconds % 60,
    minutes: parseInt(restTotalSeconds / 60, 10),
  }
  return {
    channel,
    mention,
    icon_emoji,
    username,
    link_names: 1,
	  blocks: [
		  {
	  		type: 'section',
  			text: {
			  	type: 'mrkdwn',
		  		text: `${mention} あと *${rest.minutes}分${rest.seconds}秒* でmtgだよ`
	  		}
  		},
      {
	  		type: 'section',
  			text: {
				  type: 'mrkdwn',
			  	text: `*<${eventObj.url}|${eventObj.title}>*`
		  	}
	  	},
      {
        type: 'section',
        text: {
  			  type: 'mrkdwn',
				  text: `*説明:*\n${eventObj.description}`
			  },
      },
	  	{
  			type: 'section',
			  fields: [
				  {
			  		type: 'mrkdwn',
		  			text: `*参加者:*\n${eventObj.members}`
	  			},
  				{
					  type: "mrkdwn",
				  	text: `*時間:*\n${eventObj.startTime.format("HH:mm")} - ${eventObj.endTime.format("HH:mm")}`
			  	},
		  	]
	  	}
  	]
  }
};

const notification = event => {
  const jsonData = buildSlackBlock(event);
  const payload = JSON.stringify(jsonData); 
  const options = {
    method : 'post',
    contentType : 'application/json',
    payload,
    muteHttpExceptions: true
  };  
  const res = UrlFetchApp.fetch(incomingHookUrl, options);
  return res.getResponseCode();
};

function main(){
  // カレンダーから予定を取得する
  const cal = CalendarApp.getCalendarById(calendarId);
  const events = cal.getEventsForDay(new Date());
  const now = moment().unix();

  events.forEach(event => {
    const eventData = buildEventObject(event);
    // 通知する必要ないイベントの場合はcontinue
    if (!isNotifiableEvent(eventData)) {
      return;
    }
    // 通知時刻
    const eventNotiTime = eventData.startTime.clone().add(-1 * PRE_NOTI_MINUTES, 'm').unix();
    if (eventNotiTime > now) {
      return;
    }
    // ステータスコードが400未満でsetTag
    if (notification(eventData) < 400) {
      event.setTag(tagName, 'true');
    }
  });
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178971/526bf002-5ea5-1cfd-63d5-e3e2df9aee26.png)


## 説明
```js
  const cal = CalendarApp.getCalendarById(calendarId);
  const events = cal.getEventsForDay(new Date());
```
Google App ScriptではGoogle CalendarのAPIを利用できるクラスが用意されており、それを利用しています。

https://developers.google.com/apps-script/reference/calendar/calendar-app

```js
  events.forEach(event => {
    const eventData = buildEventObject(event);
    // 通知する必要ないイベントの場合はcontinue
    if (!isNotifiableEvent(eventData)) {
      return;
    }
    // 通知時刻
    const eventNotiTime = eventData.startTime.clone().add(-1 * PRE_NOTI_MINUTES, 'm').unix();
    if (eventNotiTime > now) {
      return;
    }
    // ステータスコードが400未満でsetTag
    if (notification(eventData) < 400) {
      event.setTag(tagName, 'true');
    }
  });
```
それぞれの予定毎に通知対象となる(開始1分前)ような予定をピックアップしてslack通知させています。
通知が完了した予定には `event.setTag(key, value)` を用いてmetadataを付与することで

- 通知が完了したイベントの再送をさせない
- 通知ができなかったイベントは次のトリガーで通知を飛ばす

ようにしています。
もっと正確にやるのであれば、 `event.setTag(key, value)` の部分でリトライ処理などを入れても良いかもしれません。

```js
const notification = event => {
  const jsonData = buildSlackBlock(event);
  const payload = JSON.stringify(jsonData); 
  const options = {
    method : 'post',
    contentType : 'application/json',
    payload,
    muteHttpExceptions: true
  };  
  const res = UrlFetchApp.fetch(incomingHookUrl, options);
  return res.getResponseCode();
};
```

slackのincoming webhookURLを用いてslack通知をさせています。
中身の構造作成は別関数でやっています。
`UrlFetchApp.fetch` の optionに `muteHttpExceptions: true` をつけるとエラー系のステータスコードも取得できるので有効にしています。

https://qiita.com/kunihiros/items/255070ba950a7ba95ae4

```js
const buildSlackBlock = eventObj => {
  const restTotalSeconds = eventObj.startTime.unix() - moment().unix();
  const rest = {
    seconds: restTotalSeconds % 60,
    minutes: parseInt(restTotalSeconds / 60, 10),
  }
  return {
    channel,
    mention,
    icon_emoji,
    username,
    link_names: 1,
	  blocks: [
		  {
	  		type: 'section',
  			text: {
			  	type: 'mrkdwn',
		  		text: `${mention} あと *${rest.minutes}分${rest.seconds}秒* でmtgだよ`
	  		}
  		},
      {
	  		type: 'section',
  			text: {
				  type: 'mrkdwn',
			  	text: `*<${eventObj.url}|${eventObj.title}>*`
		  	}
	  	},
      {
        type: 'section',
        text: {
  			  type: 'mrkdwn',
				  text: `*説明:*\n${eventObj.description}`
			  },
      },
	  	{
  			type: 'section',
			  fields: [
				  {
			  		type: 'mrkdwn',
		  			text: `*参加者:*\n${eventObj.members}`
	  			},
  				{
					  type: "mrkdwn",
				  	text: `*時間:*\n${eventObj.startTime.format("HH:mm")} - ${eventObj.endTime.format("HH:mm")}`
			  	},
		  	]
	  	}
  	]
  }
};
```
slack通知の構造を作成しています。
Block Kit Builderを用いると簡単に雛形の作成ができるのでおすすめです。

https://app.slack.com/block-kit-builder/

ちなみにしれっと今回は諸事情でmoment.jsを使用しましたが、moment.jsはレガシープロジェクトとして[使用が非推奨](https://momentjs.com/docs/#/-project-status/)となっていますのでその他のライブラリを使用するほうが適切です。


## その後
まだ実運用で試してませんが、これでおそらく自分のmtg遅刻は解消されるはずです。
もし、mtg遅刻してしまうなんていう問題に同じく悩まされているのであればぜひお試しください！