【GAS】Slackで毎朝お知らせを自動投稿する機能を作った（前編）

# はじめに
弊社では「リモートワーク」「休暇取得者」「メンバーの誕生日」「清掃タイム」などを、Slackで自動通知しています。
「**毎朝お知らせしている情報は一つにまとめたい**:penguin:」という気持ちに駆られたので、
朝のお知らせbot君（仮）を作りました。

# やりたいこと一覧
### 毎朝のお知らせ
- 平日の朝、指定時刻頃にチャンネルへ自動投稿する
- その日のリモートワーク申請者を表示する
- その日の休暇取得者を表示する
- その日の誕生日メンバーを表示する
- その日の株価を表示する
- その週の清掃当番をGoogleカレンダーから取得して表示する
- 第2、第4金曜日のみ追加メッセージを表示する
- 締切の近い全体連絡を表示する
- 毎日異なるメッセージを表示する

### 清掃タイムのお知らせ
- 平日16時にチャンネルへ自動投稿する
- Slackのリマインダーと連動する


# 今回実装したこと
上記の一覧の中から、今回は下記の実装をします。
- 平日の朝9時頃にチャンネルへ自動投稿する
- その日のリモートワーク申請者を表示する
- その日の休暇取得者を表示する
- その日の誕生日メンバーを表示する
- その週の清掃当番をGoogleカレンダーから取得して表示する
- 毎日異なるメッセージを表示する
![Shoya_Ueno（DM）_-_HAB_Co__-_Slack.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3577266/39b0a5b2-9a14-f2a7-ca30-46b05537bb64.png)
（今回のアイコンは[ダ鳥獣戯画](https://chojugiga.com/)からお借りしました。LINEスタンプも愛用中）

:::note
記事を読んで、コードレビューやアドバイス等あれば、ぜひお願いします！
:::


# GASでシートやカレンダーから情報を取得する
社内でリモートワーク申請と誕生日はデータの取得と表示をしてくれていたので、
[前回作成した休暇連絡の通知](https://qiita.com/shoya_u/items/be000e2285c2ba7a91d6)と統合させて、整理します。

### 今日の日付を取得する
スプレッドシートのデータの保存形式がバラバラなので、日付を色々な形で取得します。
元のSlackのワークフローなどを見直して、データを統一した形で取得&蓄積できるように改善したいです。
```朝のお知らせ.gs
function getToday(){
  const date  = new Date();
  const month = Utilities.formatDate(date, 'JST', 'MM');
  const day   = Utilities.formatDate(date, 'JST', 'dd');
  const today = [
    Utilities.formatDate(date, 'JST', 'yyyy/MM/dd'),
    Utilities.formatDate(date, 'JST', 'M月d日') ,
    month + "/" + day,
    date.getDay(),
    date
  ];

  return today;
}
```

### 今日の「休暇取得者」「リモートワーク」「誕生日の人」を取得する
この3つは列順の違いによる多少の差異はありますが、基本は同じ方法で取得します。
スプレッドシートでは、画像のように情報が蓄積されています。
![Googleカレンダー連携テスト_-_Google_スプレッドシート.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3577266/05bd5a5d-dbdf-2ccc-0d76-80ae37e1fc12.png)
- 今日の日付と一致するセルを探す
- そのセルの行と列を取得する
- 「誰が」「時間」「何回目」の3つに該当するセルの値を取得
    - 日付（予定日）のセルを基準にして、columnの数値を増減させて取得する
```朝のお知らせ.gs
function getRemoteMatchCell(sheet, date){
  const cells = sheet.createTextFinder(date)
    .matchEntireCell(true)
    .findAll();
  const array = [];
  
  //一致するセルを取得
  for(let i=0; i<cells.length; i++){
    const cell   = cells[i].getA1Notation();
    const column = sheet.getRange(cell).getColumn();
    const row    = sheet.getRange(cell).getRow();

    const person = sheet.getRange(row, column -1).getValue();
    const hour   = sheet.getRange(row, column +1).getValue();
    const times  = sheet.getRange(row, column +2).getValue();
    array.push([person, hour, times]);
  }

  return array;
}
```


### Googleカレンダーから予定を取得する
社内で今週の清掃当番を、Googleカレンダーの予定に入れて表示しています。
その予定を取得し、該当のSlackユーザーIDを返して、メンション形式でお知らせします。
Googleカレンダーの予定は「今週の清掃当番:山田 & 田中」という表示形式です。

まずは、取得したい予定のGoogleカレンダーのIDを確認します。
Googleカレンダーのマイカレンダーから「設定と共有」を選択します。
![Gather_Mini_Mode.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3577266/79ccf29a-9132-b4d0-8289-917cc374f70a.png)

下の方にある「カレンダーの統合」に記載されている「カレンダーID」を確認します。
![Gather_Mini_Mode.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3577266/2c28d546-3b9e-e4ee-1f41-8428b8bf8ec6.png)

清掃当番シートに、タイトル、SlackユーザーID1、SlackユーザーID2のテーブルを用意します。
![Googleカレンダー連携テスト_-_Google_スプレッドシート.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3577266/17123c7f-8360-8158-09cf-f21ffdc65c43.png)

「予定のタイトルの一部」と一致する文字列を検索して、SlackのユーザーIDを返します。
（もう少しスマートなやり方がありそう）
- 取得したいカレンダーの設定
- その日の予定を取得
- イベントのタイトルに「清掃当番」を含むイベントを取得
- タイトル全体と、一致した部分を配列で取得
- 一致した行のユーザーIDをそれぞれ取得して返す

```朝のお知らせ.gs
function getCleaner(sheet, date){
  const myCalendar = CalendarApp.getCalendarById('カレンダーID');
  const events = myCalendar.getEventsForDay(date);
  const array = [];
  events.forEach(event => {
    if (event.getTitle().includes("清掃当番")) {
      const eventTitle = event.getTitle();
      console.log("eventTitle:" + eventTitle);

      const match = eventTitle.match(/清掃当番[:：](.*)/);
        if (match && match[1]) {
          const searchText = match[1].trim();
          const cell = sheet.createTextFinder(searchText).findNext();
          console.log("cell:" + cell);
          if (cell) {
            const row = cell.getRow();
            const col = cell.getColumn();
            const cleaner1 = sheet.getRange(row, col + 1).getValue();
            const cleaner2 = sheet.getRange(row, col + 2).getValue();
            array.push(cleaner1, cleaner2);
          }
        }
      }
  });
  return array;
}
```


### 毎日ランダムにメッセージを取得する
名言用シートに名言の内容と発言者のテーブルを作成します。
![休暇連絡_-_Google_スプレッドシート.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3577266/4222da3e-e047-0f8b-0ec2-2964233f9709.png)


名言用シートからランダムで内容と発言者を取得します。

```朝のお知らせ.gs
function getWord(sheet) {
  const firstRow = 2;
  const lastRow = sheet.getLastRow();
  const colIndices = [2, 3];
  const rowIndex = Math.floor(Math.random() * (lastRow - firstRow + 1)) + firstRow;
  
  const values = colIndices.map(colIndex => sheet.getRange(rowIndex, colIndex).getValue());
  return values;
}
```


# GASでSlackにメッセージを送信する

### 取得した情報でメッセージを作成する
それぞれのシートから取得した情報を使ってメッセージを作成します。
```朝のお知らせ.gs
function createMessage(birthday, remote, vacation, cleaner, word, today){
  let message = "*おはようございます！" + today + " 朝のお知らせテストです！*\n";
  message = message + "今週の清掃当番は <@" + cleaner[0] + ">と<@" + cleaner[1] + ">です！\n";
  if(today[3] = 0){

  }

  if(birthday.length > 0){
    message = message + "\n\n:birthday:*Happy Birthday!!*\n";
    for(let i=0; i<birthday.length; i++){
      message = message + birthday[i] + "さん！:tada:\n";
    }
  }

  message = message + "\n\n:computer:*リモートワークの方*\n";
  if(remote.length > 0){
    for(let i=0; i<remote.length; i++){
      message = message + remote[i][0] + ' ' + remote[i][1] + ',' + remote[i][2] + "\n";
    }
  }else{
    message = message + "本日のリモート申請者はいませんでした。\n"
  }

  message = message + "\n\n:desert_island:*休暇予定の方*\n";
  if(vacation.length > 0){
    for(let i=0; i<vacation.length; i++){
      message = message + vacation[i][0] + ' ' + vacation[i][1] + ',' + vacation[i][2] + "\n";
    }
  }else{
    message = message + "本日の休暇取得者はいませんでした。\n"
  }

  message = message + "\n\n現在改良中！〇〇もお知らせ予定です:qiitan-lgtm:\n";
  message = message + "\n\n:books:*今日の名言*\n「" + word[0] + "」\n" + "ー " + word[1] + "ー\n"

  return message;
}
```

### Slackにメッセージを送信する
まずは、SlackでIncoming WebHooksを連携させます。
場所はSlackの その他 -> 自動化 -> App -> Incoming WebHooks
![Gather_Mini_Mode.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3577266/3799e542-0cc9-432d-93ee-9fb5e072f470.png)

設定から、投稿するチャンネル、名前、アイコン画像を設定します。
WebHooks URLをコピーしておきます。

![スクリーンショット_2025_02_09_16_46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3577266/b082c2f7-7f55-f026-f1e6-4c2cafe2c9d5.png)

コピーしたWebHooksのURLを、コードに貼り付けます。

Incoming Webhooksを使って、Slackにメッセージを送信します。
- それぞれのシートを取得する
- 今日の日付を取得する
- 平日のみ通知する
- それぞれのシートからデータを取得する
- メッセージを作成する
- WebHookでSlackにメッセージを送信する

```朝のお知らせ.gs
function morningNotesBot(){
  const activeSpreadSheet = SpreadsheetApp.getActiveSpreadsheet();
  const birthdaySheet     = activeSpreadSheet.getSheetByName("誕生日通知"); 
  const remoteWorkSheet   = activeSpreadSheet.getSheetByName("リモート承認");
  const vacationSheet     = activeSpreadSheet.getSheetByName("通知用");
  const cleanSheet        = activeSpreadSheet.getSheetByName("美化通知");
  const wordSheet         = activeSpreadSheet.getSheetByName("名言");

  const today = getToday();
  console.log("today:" + today);

  if(today[3] > 0 && today[3] < 7){
    const birthdayPerson = getBirthdayMatchCell(birthdaySheet, today[2]);
    console.log("birthdayPerson:" + birthdayPerson);
    console.log("birthdayPerson count:" + birthdayPerson.length);

    const remoteWorker = getRemoteMatchCell(remoteWorkSheet, today[0]);
    console.log("remoteWorker:" + remoteWorker);
    console.log("remoteWorker count:" + remoteWorker.length);

    const vacationer = getVacationMatchCell(vacationSheet, today[0]);
    console.log("vacationer:" + vacationer);
    console.log("vacationer count:" + vacationer.length);
    
    const cleaner = getCleaner(cleanSheet, today[4]);
    console.log("cleaner:" + cleaner);

    const word = getWord(wordSheet);
    console.log("word:" + word);

    const message = createMessage(birthdayPerson, remoteWorker, vacationer, cleaner, word, today[1]);
    console.log("message:" + message);

    const webhookUrl = 'https://hoge.com/services/abcxxx'; // あなたのWebhook URLをここに
    const payload = JSON.stringify({text: message});
    const options = { method: 'POST', contentType: 'application/json', payload: payload };

    UrlFetchApp.fetch(webhookUrl, options);

  }else{
    console.log('休日のため実行されませんでした。');
  }
}
```

### トリガーを設定する
AppScript -> トリガー -> トリガーを追加から、
トリガーを追加します。
時間指定は1時間ごとのみで「9:00ピッタリ」などの設定はできません。
（任意の時間に送信するコードは、今後実装する予定です）
- 実行する関数を選択：Slackにメッセージを送信するメソッド
- イベントのソース：時間主導型
- 時間ベースのトリガーのタイプ：日付ベースのタイマー
- 時間を選択：任意の時間
![Gather_Mini_Mode.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3577266/c3bc09b0-270b-ddc6-e322-96b6b34b266b.png)


# まとめ
今回は、朝にお知らせをまとめて通知する機能を実装しました。
次の記事では、まだ実装できていない機能を追加して書きたいと思います。