Slackに通知してくれる日直リマインダーを作る

# 背景
弊社では、エンジニアの進捗管理や情報共有なとを目的とした`日次定例MTG`があります。
日次定例MTGでは、日ごとに日直を決めて司会進行と気になる記事（ニュース）を共有することになっているのですが、

「あ、やべ、今日は日直だった！なにも準備してない！」

ということが度々起きたので、日直リマインダーを作ることにしました。

# 前提
### 日次定例MTGで使っているツール
- 日報やニュースの共有：Slack
- MTG時刻の管理：Googleカレンダー
- 日直の管理：スプレッドシート
- ビデオ通話：Zoom

### 日直の管理方法
日次定例MTGではスプレッドシート上に簡単なカレンダーが作られていて、日毎に日直の名前を入れる。という方法で管理されていました。
後述しますが、このシートから日直リマインダーを作る場合、今日の日付から日直の名前を参照する必要があったので、少し複雑な処理ができるツールを利用することになりました。
スプレッドシート上のカレンダーは下記画像のような形式です。
![スクリーンショット 2021-12-02 15.28.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2128777/54d14e92-8db6-86fa-6983-cab29617cf87.png)

# ツールの選定
Slackはとても便利で、手軽にリマインダーを作れそうなツールがいくつかあったので、選定からはじめました。

1. Slackのリマインド機能
特別なツールやインストールを必要とせず、簡単に設定できる。
しかし、スプレッドシートと連携できないため、いつ、誰にリマインドするかを毎度設定しなければならない。
→ **不採用**

2. Slackワークフロー
ワークフロービルダーにスプレッドシートアプリをインストールする必要があるが、ノーコードで設定が完結する。
スプレッドシートと連携できてCRUDの機能が一通り用意されているが、「日毎に参照するセルの値を変える」などの細かい操作はできなかった。
いまの日直管理シートの書き方では、うまく通知できなさそう。
→ **不採用**

3. GAS（Google Apps Script）
Googleによって開発されたスクリプト言語で、JavaScriptでプログラムを記述して複雑な処理を実行できる。
スプレッドシートやカレンダーとも連携でき、SlackAPI経由でSlack側にメッセージ送信等も可能。
→ **採用**

# リマインダの作成
### 1. SlackAPIトークンを取得する
大まかに下記の3ステップで取得できます。
取得方法について、[こちらの記事](https://qiita.com/ykhirao/items/3b19ee6a1458cfb4ba21)の説明が詳しくて分かりやすいです。
　1. Slack Botを作成する
　2. Slackのワークスペースに作ったSlack Botをインストールする
　3. メッセージ送信用トークンを取得する

### 2. Apps Scriptのエディタを開く
ブラウザでスプレッドシートを開き、拡張機能 > Apps Script を選択すると、GASの編集画面が起動します。
![スクリーンショット 2021-12-02 15.46.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2128777/c3027d99-702d-54e1-c8f6-a34242d20848.png)
![スクリーンショット 2021-12-02 15.49.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2128777/f9fb0ae1-02ea-efe8-1f6b-0066a0298817.png)

### 3. コードを書く
②で開いたApp ScriptのエディタにJavaScriptでリマインド機能を実装します。
詳細は省きますが、大まかに下記の用件を満たす機能を作成しました。
  ・ 土日、祝祭日に通知をしないように、Googleカレンダーを参照して日次定例MTGの予定のある日だけ実行する
  ・ 当日に通知されても準備が間に合わないケースがあるため、日直がある日の2営業日前から毎日通知する
  ・ 日直にはメンションで通知する

実装したコードも載せておきます。

~~~JavaScript
// 毎日の日直を通知する
function dailyFacilitatorReminder() {
  const date = new Date();

  // カレンダーを参照して定例がある日だけ実行
  if (!hasDailyMeeting(date)) {
     console.log('本日は定例がありません。');
    return;
  }

  // スプレッドシード上にある日直カレンダーから最大10日先までの日程を参照し、
  // 直近の最大3名に日直のリマインドメッセージを生成する。
  let messages = [];
  for (let i = 0; i < 10; i++) {
    const facilitator_name = findFacilitatorNameFromSpreadsheet(date);
    if (facilitator_name) {
      if (messages.length == 0) {
        messages.push('本日の日直は<@' + facilitator_name + '>さんです。');
      } else if (messages.length == 1) {
        messages.push('次の日直は<@' + facilitator_name + '>さんです。');
      } else if (messages.length <= 2) {
        messages.push('その次の日直は<@' + facilitator_name + '>さんです。');
        break;
      }
    }
    date.setDate(date.getDate() + 1);
  }
  
  // メッセージをSlackに送信する
  sendSlackMessage(messages.join('\n'));
}

// 技術部日次定例があるか
function hasDailyMeeting(date) {
  const calendar_id = 'foobar@gmail.com';
  const event_name = '技術部日次定例';
  const calendar = CalendarApp.getCalendarById(calendar_id);
  const calendar_events = calendar.getEventsForDay(date, {search: event_name, max: 1});
  return Boolean(calendar_events.length);
}

// スプレッドシート上に作成されたカレンダーから日直の名前を取得する
function findFacilitatorNameFromSpreadsheet(date)
{
  const year = date.getFullYear() % 100;
  const month = date.getMonth() + 1;
  const day = date.getDate();
  const sheet_name = year + '-' + month + '月';
  const targeet_date = month + '/' + day;
  const sheet_id = '/** シートID **/';
  const ss = SpreadsheetApp.openById(sheet_id);
  const sheet = ss.getSheetByName(sheet_name);
  const text_finder = sheet.createTextFinder(targeet_date);
  const cell = text_finder.findNext().getA1Notation();
  const target_cell = cell.slice(0, -1) + (Number(cell.slice(-1)) + 2);
  const facilitator_name = sheet.getRange(target_cell).getValue();
  return getSlackName(facilitator_name);
}

// カレンダーに記載されている名前からSlackでの名前を取得する
function getSlackName(name) {
  const user_name_map = {
    '〇〇': 'U01A0DFSQ2Y',
    '△△': 'ECGE22YGAHT',
    '◇◇': 'P3E1ALVMEWD',
  };
  return user_name_map[name] ?? '';
}

// Slackにメッセージを送信
async function sendSlackMessage(message) {
  const url     = 'https://slack.com/api/chat.postMessage';
  const method  = 'post'
  const payload = {
    'token'      : '/** SlackAPIのトークン **/',
    'channel'    : '/** チャンネル名 **/',
    'text'       : message,
  };
  const params = {
  'method' : method,
  'payload' : payload
  };
  UrlFetchApp.fetch(url, params);
}
~~~

### 4. スクリプトを自動で実行してくれるように、トリガーを設定する
- App Scriptのエディタ画面の左のタブメニューから[トリガー]を押下し、トリガー管理画面に遷移
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2128777/12114316-129b-7886-db7f-51b9b74289ca.png" width="50%">
- 画面右下の[+トリガーを追加]ボタンを押下して、トリガー登録に遷移
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2128777/71068bc6-9b54-cf02-c393-4624262af545.png" width="80%">
- 毎日定時に実行してくれるように入力後に[保存]ボタンを押下し、トリガーを登録

　今回作成したスクリプトの場合)
　実行する関数：dailyFacilitatorReminder
　イベントのソース：時間主導型
　トリガーのタイプ：日付ベースのタイマー
　時刻：午前9時〜10時
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2128777/918682f4-a5d3-dae1-4996-f75bd8377b46.png" width="80%">

### 5. 動作テスト
ここまで来たら、リマインダーは一通り完成です。
トリガーが発火するのを待つか、エディタから直接実行することで動作を確認することができます。
うまくいくと下記画像のようになります。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2128777/5635a90b-3213-c1a3-49b5-a80481c0d858.png" width="50%">

# 最後に
今回初めてGASに触れましたが、GASでもっといろいろなことができそうでワクワクしました。
こんなに便利な機能をタダで提供してくれるGoogleさまには頭が上がりません。