【SlackBot】明日は休みだよBotを作ってみた

## きっかけ

- 昨今の事情もあり在宅勤務を行っている方は多いと思います
- そんな中でチームのSlackでこんなやりとりがありました

<img width="601" alt="スクリーンショット 2020-04-29 0.18.53.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175213/e0139cd0-92ef-1636-8c95-0d8f988aa770.png">

- 在宅でリモートだと曜日感覚なくなってきますよね
- 明日が休みだと思うことで生まれる**生産性爆上げ日**を逃してしまうのはもったいない！ということでStayHomeの暇つぶしも兼ねて次の日が休みの時にSlackに通知してくれるBotを作ってみました

## 登場人物

- AWS Lambda(Node)
- [Holidays JP API](https://holidays-jp.github.io/)
- [Slack Web API](https://slack.dev/node-slack-sdk/web-api)
- [ServerlessFramework](https://serverless.com/)

## できたもの

- 当日が平日で、翌日が土日祝日の場合、昼の12時にSlackに以下の投稿がされるようにしてみました

<img width="312" alt="スクリーンショット 2020-04-29 0.36.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175213/985d2e0c-3a80-e7b2-53fe-497454e41970.png">

- コードはこちらです
    - https://github.com/ozaki25/slack-push-day-before-holiday

## 実装内容

- [Lambdaの実装](#lambdaの実装)
- [土日祝日判定](#土日祝日判定)
- [Slackへの通知](#slackへの通知)
- [ServerlessFrameworkの設定](#serverlessframeworkの設定)
- [Local/Dev/Prodの使い分け](#localdevprodの使い分け)

### Lambdaの実装

- Lambdaにデプロイするベースとなる`handler.ts`はこんな感じです
    - 今日と明日が平日かどうか判定し、今日が平日で明日が土日祝日の場合はSlackにメッセージを送ります

```ts:handler.ts
import * as dayjs from 'dayjs';
import { APIGatewayProxyHandler } from 'aws-lambda';

import { isHoliday } from './holidayUtil';
import { pushMessage } from './slackApi';

export const noticeDayBeforeHoliday: APIGatewayProxyHandler = async () => {
  const today = dayjs();
  const tomorrow = today.add(1, 'day');

  if (await isHoliday(today)) {
    return { statusCode: 200, body: 'Today is Holiday!!!' };
  }
  if (!(await isHoliday(tomorrow))) {
    return { statusCode: 200, body: "Let's work tomorrow" };
  }

  await pushMessage({ text: '明日は休みだよ！残りの仕事も頑張ろう！' });
  return { statusCode: 200, body: 'Tomorrow is Holiday!!!' };
};
```

### 土日祝日判定

- 日本の祝日情報を取得するAPIがないかと検索したら[Holidays JP API](https://holidays-jp.github.io/)を見つけたので使ってみました
    - 以下のような形式で祝日一覧を返してくれるAPIです

```json
{
  "2017-01-01": "元日",
  "2017-01-02": "元日 振替休日",
  "2017-01-09": "成人の日"
}
```

- 祝日判定をしている`holidayUtil.ts`のコードはこんな感じです
    - 引数でもらった日付を土曜/日曜か判定し、どちらでもなければ Holidays JP API から取得した祝日に該当するかチェックしています

```ts:holidayUtil.ts
import fetch from 'node-fetch';
import * as dayjs from 'dayjs';

const getHolidays = async () => {
  const res = await fetch('https://holidays-jp.github.io/api/v1/date.json');
  return res.json();
};

export const isHoliday = async (date: dayjs.Dayjs) => {
  const formattedDate = date.format('YYYY-MM-DD');

  if (date.day() === 0) {
    console.log(`${formattedDate} is Sunday`);
    return true;
  }

  if (date.day() === 6) {
    console.log(`${formattedDate} is Saturday`);
    return true;
  }

  const holidays = await getHolidays();

  if (Object.keys(holidays).includes(formattedDate)) {
    console.log(`${formattedDate} is Holiday`);
    return true;
  }

  console.log(`${formattedDate} is Weekday`);
  return false;
};
```

### Slackへの通知

- Slackへの通知は公式が提供する[@slack/web-api](https://www.npmjs.com/package/@slack/web-api)を使いました
- Slackへ通知を送る`slackApi.ts`はこんな感じです
    - Tokenはリポジトリに含めたくない、投稿先チャンネルはdevとprodで差し替えたいといった事情から、それらの情報は環境変数から取得しています
        - 詳しいやり方は[こちらの記事](https://qiita.com/ozaki25/items/c855c3c7bcb45515539f)で

```ts:slackApi.ts
  
import { WebClient } from '@slack/web-api';

const { SLACK_API_TOKEN, SLACK_CHANNEL, SLACK_USERNAME } = process.env;

const slackClient = new WebClient(SLACK_API_TOKEN);

export const pushMessage = async ({ text }) => {
  const result = await slackClient.chat.postMessage({
    text,
    channel: SLACK_CHANNEL,
    username: SLACK_USERNAME,
    unfurl_links: true,
  });
  console.log({ result });
};
```

### ServerlessFrameworkの設定

- ServerlessFrameworkの設定ファイルである`serverless.yml`はこんな感じです
    - 上の説明でも出てきた環境変数周りの設定やLambdaの定期実行のためのschedule設定などをしています

```yml:serverless.yml
service:
  name: day-before-holiday
custom:
  webpack:
    webpackConfig: ./webpack.config.js
    includeModules: true
  defaultStage: dev
  slack:
    dev:
      token: ${env:SLACK_API_TOKEN_DEV}
      channel: ${env:SLACK_CHANNEL_DEV}
      username: ${env:SLACK_USERNAME_DEV}
    production:
      token: ${env:SLACK_API_TOKEN_PROD}
      channel: ${env:SLACK_CHANNEL_PROD}
      username: ${env:SLACK_USERNAME_PROD}
  schedule:
    noticeDayBeforeHoliday:
      dev: # スケジュールなし
      production: cron(0 3 * * ? *) # 日本時間で毎日昼12時
plugins:
  - serverless-webpack
  - serverless-dotenv-plugin
provider:
  name: aws
  runtime: nodejs12.x
  stage: ${opt:stage, self:custom.defaultStage}
  region: ap-northeast-1
  environment:
    AWS_NODEJS_CONNECTION_REUSE_ENABLED: 1
    TZ: Asia/Tokyo
    SLACK_API_TOKEN: ${self:custom.slack.${self:provider.stage}.token}
    SLACK_CHANNEL: ${self:custom.slack.${self:provider.stage}.channel}
    SLACK_USERNAME: ${self:custom.slack.${self:provider.stage}.username}
functions:
  noticeDayBeforeHoliday:
    handler: src/handler.noticeDayBeforeHoliday
    events:
      - schedule: ${self:custom.schedule.noticeDayBeforeHoliday.${self:provider.stage}}
```

### Local/Dev/Prodの使い分け

- ちょっとしたLambdaの開発ですが本番環境Onlyで突っ走るのは抵抗があります
- 今回はローカルとAWS(Dev環境)とAWS(Prod環境)に分けて動作確認をしていきました
    - 基本はローカルで開発を進める
    - 完成したらDevでテスト
    - OKならProdへデプロイ
- `package.json`に定義した`srcipts`はこんな感じです

```json:package.json
  "scripts": {
    "invoke": "sls invoke local -f noticeDayBeforeHoliday",
    "invoke:aws": "sls invoke -f noticeDayBeforeHoliday",
    "deploy": "sls deploy",
    "deploy:prod": "sls deploy --stage production"
  }
```

#### Local

- ローカルでは以下のコマンドで実行することができます

```sh
yarn invoke
```

#### Dev

- AWSにDevモードでデプロイする時は以下のコマンドです

```sh

yarn deploy
```

- AWSにデプロイしたLambdaを実行する時は以下のコマンドです

```sh
yarn invoke:aws
```

#### Prod

- AWSにProdモードでデプロイする時は以下のコマンドです

```sh

yarn deploy:prod
```

- Prodを暴発させるのは良くないのでProdのLambdaを実行するコマンドは入れませんでした


## まとめ

- ちょっとしたLambda関数の開発でしたがServerlessFrameworkが快適なので作ってて楽しかったです
- これで在宅勤務でも祝前日の生産性爆上げ日を確保できそうですね
- StayHomeが続きますが工夫して乗り切りましょう！