営業日によるリマインド通知をSlackに送る


# まえおき

Slackには `/remind [誰(英語)] [何を(日本語)] [いつ(英語)]` で、リマインダーを設定することができます。


`[誰(英語)]` には、 `me` (自分宛て)や `@mention` (メンバー宛て)、または `#channel` (チャンネル宛て) を設定できます。

`[何を(日本語)]` には、リマインドしたいメッセージを設定できます。

`[いつ(英語)]` には、 そのメッセージをいつ通知するかを設定できます。


# 本題

今回は、`[いつ(英語)]` に着目します。

`[いつ(英語)]` は英語で設定するのですが、さまざまな書き方があります。
以下は、Slackヘルプセンターで記載されている例を参考に一覧化したものです。

| `[いつ(英語)]` の設定内容 | 実際に通知される時間 |
|:---|:--|
| in 15 minutes | 15分後に通知 |
| at 6pm tomorrow | 明日の午後6時に通知 |
| on March 9th at 8:55pm | 3月9日の午後8時55分に通知 |
| at 10am every weekday | 平日(月〜金曜)の午前10時に通知 |
| at noon on January 7 | 	1月7日の正午に通知 |
| on 8 Feb | 2月8日(の午前9時)に通知 |
| on 11/30/2018 | 2018年11月30日(の午前9時)に通知 |
| every Monday, Wednesday, and Friday| 毎週月曜日、水曜日、金曜日(の午前9時)に通知 |
| every other Tuesday | 隔週火曜日(の午前9時)に通知 |
| on Tuesdays | 毎週火曜日(の午前9時)に通知 |
| at 11:00 every Thursday | 毎週木曜11:00に通知 |
| every January 25 | 毎年1月25日(の午前9時)に通知 |
| on the 4th of every month | 毎月4日(の午前9時)に通知 |


## 問題:営業日による設定ができない

上記の一覧にあるように多様な設定が可能なのですが、 **営業日による通知設定だけはできません。** （第1営業日や第5営業日など）

これは営業日数を数えていくにあたって、週休の曜日、日本の祝日、記念日などの企業固有の休日などを考慮する必要があるためです。


## 対策:Google Apps Scriptと日本の祝日カレンダーで実現する

そこで、Googleのスプレッドシート、Google Apps Script、Googleカレンダーの[日本の祝日]を活用して、営業日によるリマインド通知を実現していきます。


### 1.SlackのAPI Tokenを取得する

Slack App Directoryの[「Bots」](https://myteam.slack.com/apps/A0F7YS25R-bots) にてAPI Tokenを取得します。


![スクリーンショット 2017-09-09 10.21.26.png](https://qiita-image-store.s3.amazonaws.com/0/148986/7e5c7a6b-25e3-c063-6307-8ce3c88eed70.png "スクリーンショット 2017-09-09 10.21.26.png")

「Add Configuration」をクリックして、新規作成する。

![スクリーンショット_2017-09-09_10_29_35.png](https://qiita-image-store.s3.amazonaws.com/0/148986/40b29af7-8897-9a34-7744-0371b1b1f3c4.png "スクリーンショット_2017-09-09_10_29_35.png")

上記のAPI Tokenをコピーして控えておきます。


### 2.Google スプレッドシートを作成する

Googleのスプレッドシートにて、下記のような表を作成します。

![スクリーンショット 2017-09-09 10.37.27.png](https://qiita-image-store.s3.amazonaws.com/0/148986/50c59318-5306-4d30-00f3-f5ab42211dd9.png "スクリーンショット 2017-09-09 10.37.27.png")

このとき、A列には、B~E列がすべて入力されていたら値を設定し、1つでも未入力があれば空文字を設定するように、関数を入れます。

セルA2の関数例：`=if(and(B2<>"",C2<>"",D2<>"",E2<>""),row()-1,"")`

### 3. Google Apps Scriptを記述する

スプレッドシートのメニュー[ツール]→[スクリプトエディタ]をクリックして、エディタ画面を表示して、下記のコードを書いて保存します。

![スクリーンショット 2017-09-09 10.58.29.png](https://qiita-image-store.s3.amazonaws.com/0/148986/3c8d24e6-42cf-e9ad-584e-e894bfd3e020.png "スクリーンショット 2017-09-09 10.58.29.png")

```
/*
  GASのトリガーでは、日毎に設定できるのは時間単位（例：0~1時）までで、分単位の設定はできません。
  そこで、setTriggerDay()を毎日23〜24時の間に実行するようにして、
  setTriggerDay()によって翌日の0時0分にトリガー設定されたsetTriggerTimer()が、
  0時0分を起点として何分後にsendSlack()のトリガーを設定するようにしています。*/ 

// 【日次】翌日の0時0分0秒に次に処理をする命令を出す。
function setTriggerDay() {
  var triggerDate = new Date();
  triggerDate.setDate(triggerDate.getDate() + 1);  // 次の日をセット

  ScriptApp.newTrigger("setTriggerTimer")
    .timeBased()
    .atDate(triggerDate.getFullYear(), triggerDate.getMonth()+1, triggerDate.getDate())
    .create();
}

// 【日次実行】スプレッドシートの内容に応じて、該当する営業日のトリガーをセットする
function setTriggerTimer() {

  // 既存のトリガーを削除
  deleteTrigger("setTriggerTimer");
  deleteTrigger("sendSlack");

  var todayBizDay = isBusinessDays();  // 今日の営業日数をセット

  if (todayBizDay == 0) {
   return;  // 今日は休業日のため、処理を終了
  }

  var sheet = SpreadsheetApp.getActiveSheet();
  var lastRow = sheet.getLastRow();
  var noCol, bizDate, sendTime, sendTiming, channelName, msgBody;
  var arrayTrigger = [];
  
  for (var i = 2; i <= lastRow; i++) {
    noCol = sheet.getRange(i, 1).getValue();
    if ( noCol == "") {
      // 1列目に空文字にヒットしたら処理を終了する。
      break;
    }

    bizDate = sheet.getRange(i, 2).getValue();
    sendTime = new Date(sheet.getRange(i, 3).getValue());
    sendTiming = (sendTime.getHours() * 60 * 60 * 1000) + (sendTime.getMinutes() * 60 * 1000);
    
    if (bizDate == todayBizDay) {

      if (isArrayExists(arrayTrigger, sendTiming)){
        // すでに登録済みのため、スキップ
      } else {
        arrayTrigger.push(sendTiming);

        ScriptApp.newTrigger("sendSlack")
         .timeBased()
         .after(sendTiming)
         .create();

      }
    }
  }
}

// 既存トリガーを削除する
function deleteTrigger(name) {
  var triggers = ScriptApp.getProjectTriggers();
  for(var i=0; i < triggers.length; i++) {
    if (triggers[i].getHandlerFunction() == name) {
      ScriptApp.deleteTrigger(triggers[i]);
    }
  }
}

// Slack　にメッセージを送信する
function sendSlack() {

  var nowTime = new Date();
  var todayBizDay = isBusinessDays();  // 今日の営業日数をセット
  
  var sheet = SpreadsheetApp.getActiveSheet();
  var lastRow = sheet.getLastRow();

  var noCol, bizDate, sendTime, sendTiming, channelName, msgBody;
  for (var i = 2; i <= lastRow; i++) {
    noCol = sheet.getRange(i, 1).getValue();
    if ( noCol == "") {
      // 1列目に空文字にヒットしたら処理を終了する。
      break;
    }

    bizDate = sheet.getRange(i, 2).getValue();
    sendTime = new Date(sheet.getRange(i, 3).getValue());

    channelName = sheet.getRange(i, 4).getValue();
    msgBody = sheet.getRange(i, 5).getValue();
    
    if (bizDate == todayBizDay  && sendTime.getHours() == nowTime.getHours() && sendTime.getMinutes() == nowTime.getMinutes()) {

      var options = 
        {
          "method" : "POST",
          "payload" : 
            {
              "token": "★★★ここにSlackで取得したTokenを設定します★★★",
              "channel": channelName,
              "text": msgBody,
              "icon_emoji" : ":spiral_calendar_pad:",
              "username" : "営業日カレンダー通知(GAS)"
            }
        }
      var url = "https://slack.com/api/chat.postMessage"
      UrlFetchApp.fetch(url, options);

    }
  }
}

// 今日が営業日の何日目かを求める
function isBusinessDays() {
  var today = new Date();
  var countDayOfMonth
  var day; // 0->日曜日
  var bizday = 0;

  day = today.getDay();  // Dateオブジェクトから曜日を求めるメソッド(0:日, 6:土曜日)

  // 今日が休業日の場合は、0を返す。
  if (day == 0 || day == 6 || isHoliday(today) || isCompanyHoliday(today)) {  // 土曜日または日曜日または日本の祝日または会社休業日かどうか
    return 0;
  }
  
  for (var i = 1; i <= today.getDate(); i++) {

    countDayOfMonth = new Date(today.getFullYear(), today.getMonth(), i);  //　次の日をセット
    day = countDayOfMonth.getDay();  // Dateオブジェクトから曜日を求めるメソッド(0:日, 6:土曜日)

    if (day == 0 || day == 6 || isHoliday(countDayOfMonth) || isCompanyHoliday(countDayOfMonth)) {  // 土曜日または日曜日または日本の祝日または会社休業日かどうか
      continue;
    } else {
      bizday = bizday + 1;  // 営業日をカウントアップ
    }
  }
  return bizday;  // 営業日を返す
}

// 日本の祝日チェック
function isHoliday(day) {
  var startDate = new Date(day.setHours(0, 0, 0, 0));
  var endDate = new Date(day.setHours(23, 59, 59));
  var cal = CalendarApp.getCalendarById("ja.japanese#holiday@group.v.calendar.google.com");  // [日本の祝日]を取得
  var holidays =  cal.getEvents(startDate, endDate);
  return holidays.length != 0; // 祝日ならtrue
}

// 会社休業日チェック
function isCompanyHoliday(day) {
  
  var isCompanyHoldayFlag = false;
  
  var dayStr = day.getFullYear() + ":" + (day.getMonth()+1) + ":" + day.getDate();
  
  var today = new Date();
  var companyHoliday0102 = today.getFullYear() + ":1:2";  // 年始休暇（1/2）
  var companyHoliday0103 = today.getFullYear() + ":1:3";  // 年始休暇（1/3）

  if (dayStr == companyHoliday0102 || dayStr == companyHoliday0103 ) {
    isCompanyHoldayFlag = true;
  }

  return isCompanyHoldayFlag; // 休業日ならtrue
}

function isArrayExists(array, value) {
  // 配列の最後までループ
  for (var i =0, len = array.length; i < len; i++) {
    if (value == array[i]) {
      // 存在したらtrueを返す
      return true;
    }
  }
  // 存在しない場合falseを返す
  return false;
}
```

上記のコードでは、非営業日の扱いは下記の通りです。必要に応じて、カスタマイズくださいませ。

* 土曜日と日曜日は非営業日の扱いとする
* [日本の休日]は非営業日の扱いとする
* 1/2と1/3は会社休業日として非営業日の扱いとする


### 4. トリガーを設定する

エディタ画面のメニュー[編集]→[現在のプロジェクトのトリガー]をクリックして、新しいトリガーを追加します。（下記画像参照）

![スクリーンショット 2017-09-09 12.22.22.png](https://qiita-image-store.s3.amazonaws.com/0/148986/3965f142-0841-2f24-4b51-ef2c799d0a0c.png "スクリーンショット 2017-09-09 12.22.22.png")


これにて準備は完了です。

### 5.リマインド内容を設定する

スプレッドシートにリマインドしたい営業日や時間、送信先のSlackチャンネルやメッセージ内容を入力します。

![スクリーンショット 2017-09-09 10.37.27.png](https://qiita-image-store.s3.amazonaws.com/0/148986/50c59318-5306-4d30-00f3-f5ab42211dd9.png "スクリーンショット 2017-09-09 10.37.27.png")

|設定項目名|設定内容|例|例の効果|
|:---|:---|:---|:---|
|①送信する営業日|メッセージを送信したい営業日を入力する|3|第3営業日に送信されます|
|②送信する時間|メッセージを送信したい時間を入力する|10:00|10時00分に送信されます|
|③送信先チャンネル|メッセージの送信先のチャンネル名を入力する|gas|#gasという名前のチャンネルに送信されます|
|④メッセージ本文|送信したいメッセージ本文を入力する。※メンションは<>で囲むこと|<!channel> 第一営業日です。|「@channel 第一営業日です。」のメッセージが送信されます|

#### メンションの書き方について
メンションを指定する際、`@channel`を使用する場合は`<!channel>`を、`@here`を指定する際は`<!here>`を、`@mention`（メンバー宛てメンション）を使用する場合は`<@mention>`と書くようにしてください。

### 6.実際のSlack通知

![スクリーンショット 2017-09-11 8.16.34.png](https://qiita-image-store.s3.amazonaws.com/0/148986/56ca4279-70ff-6e6e-ae51-eca79562bf40.png "スクリーンショット 2017-09-11 8.16.34.png")

![スクリーンショット_2017-09-11_8_10_06.png](https://qiita-image-store.s3.amazonaws.com/0/148986/7555240c-06d4-b911-1b30-459782cc0ed8.png "スクリーンショット_2017-09-11_8_10_06.png")


以上となります。


# あとがき

* リマインドされるメッセージの数が多いと、せっかくリマインドしても埋もれてしまい、見逃される恐れがあります。できる限り、必要なものだけに留めておきたいところです。
　

* そもそもリマインドしなくて済むような仕組みが一番だと思います。リマインドするということは、それを見た人が注視して何らかの次のアクションが必要だということです。その次のアクションを自動化したり、アクションそのものを廃止したりすることが可能ならば、そちらをオススメします。（リマインダーはそれまでの予防策として活用されればよいかと思います）

# 参考サイト

* Slack公式
    * [リマインダーを設定する](https://get.slack.help/hc/ja/articles/208423427-%E3%83%AA%E3%83%9E%E3%82%A4%E3%83%B3%E3%83%80%E3%83%BC%E3%82%92%E8%A8%AD%E5%AE%9A%E3%81%99%E3%82%8B) (Slackヘルプセンター)

* 今回の仕組みを考えるにあたって参考にさせていただいたサイト（ありがとうございます）
    * [月末営業日にタスクをリマインドする](http://qiita.com/ykhirao/items/782e20ab0465533c48f6)