GASでslackゴミ出し抽選botを作成する(コード解説編)

#本記事について
[GASでslackゴミ出し抽選botを作成する(設定編)](https://qiita.com/marogoma/items/a92d87114eb609a82482)の続きになります。(先に設定編をお読み下さい)
[slackゴミ出し抽選botのgasコード](https://github.com/marozex/myGas/blob/master/gomibot/gomibot.gs)の処理の流れの解説になります。
コードの全文は[githubリンク](https://github.com/marozex/myGas/blob/master/gomibot/gomibot.gs)に載せています。

##処理の基本的な流れ
githubに記載しているコードの全文と併せてお読み下さい。
###処理1. 分単位のトリガー作成
午前8~9時の間に設定されたGASのトリガーが作動する
　→このトリガーが作動したことにより下記の「処理2-1.及び処理2-2.」を実行するGASトリガーが新たに作られる。
###処理2-1. 抽選実行
朝9:40に抽選が実行される
###処理2-2. リマインド実行
朝11:30にリマインド投稿が実行される
###処理3. 再抽選処理
再抽選実行時はダイアログを表示し、ダイアログ入力内容を投稿＆再度抽選する。
###処理4. 完了処理
slackの抽選投稿内の作業完了ボタン選択時に、2-1.2-2.で作成されたトリガーが削除される。

##処理１.のコード
午前8~9時の間に設定されたGASのトリガーで呼ばれるのは下記の`setGomiTrigger()` です。

```js
function setGomiTrigger() {
  var triggerDay = new Date();
  //日本の祝日を取得する
  var calendarId = "ja.japanese#holiday@group.v.calendar.google.com";
  var calendar = CalendarApp.getCalendarById(calendarId);
  var todayEvents = calendar.getEventsForDay(triggerDay);
  if(todayEvents.length === 0){
    triggerDay.setHours(9);
    triggerDay.setMinutes(40);
    ScriptApp.newTrigger("sendGomiMsgWithButton").timeBased().at(triggerDay).create();
    triggerDay.setHours(11);
    triggerDay.setMinutes(30);
    ScriptApp.newTrigger("sendGomiReminder").timeBased().at(triggerDay).create();
  }
}
```
前半で日本の祝日を取得し、 `todayEvents.length === 0` ならば祝日ではないと判定→ゴミ出しの抽選を実行する、としています。
後半では

```js
    triggerDay.setHours(9);
    triggerDay.setMinutes(40);
    ScriptApp.newTrigger("sendGomiMsgWithButton").timeBased().at(triggerDay).create();
```
の部分で、分単位で決められた時刻に抽選が実行されるようにしています。
GASのトリガー設定画面では「8時から9時の間」という精度でしかトリガーの作動時刻を決められないため、このようにsetHours(),setMinutes()を使用しています。
GASではよく使われるテクニックです。

##処理2-1.のコード
朝9:40の抽選時には `sendGomiMsgWithButton()` が実行されます。
この関数の中では、
`selectRandomUser()` で当選者決定→ `slackapp.postMessage()` でslackに抽選結果を投稿という流れです。
特に `selectRandomUser()` を解説します。

```js
function selectRandomUser() {
  var membersList = slackapp.channelsInfo(channelId).channel.members;
  
   // 最新の当選者を取り除く。
  var latest = scriptProperties.getProperty('selected_user_id');
   
  if (membersList.indexOf(latest) > -1){
    membersList = membersList.filter(function(list){
      return list !== latest;
    })
  }
  
  var selected = membersList[Math.floor(Math.random() * membersList.length)];
  scriptProperties.setProperty('selected_user_id', selected);
  return selected;
}
```
`scriptProperties.setProperty('selected_user_id', selected);`
としてGASのプロパティストアをデータの置き場所として使ってるところがポイントになります。
slackのapiは3秒以内にレスポンスが無いと、タイムアウトするという仕様があります。
当選結果をスプレッドシートのカラムに記載してもいいのですが、タイムアウト回避のためには上記のようにプロパティストアを使うほうが確実です。

##処理3.のコード
slackのメッセージ上のボタンが押されると、GASにPOSTリクエストが投げられます。
POSTのリクエストを受けると `doPost(e)` 関数が呼ばれます。
`doPost(e)` の前半ではトークンの値を見て、slack由来の通信である場合のみ後半の処理を実行します。
後半部分で、どのボタンが押されたのかによって処理を分岐させています。

```js
  if (p.actions && p.actions[0].value === "finish"){
    text =  p.user.name + "さん、ゴミ捨てありがとうございました (" + unix_ts + ")";
    update(text);
    deleteGomiTrigger();
  } else if (p.actions && p.actions[0].value === "open_reason_dialog") {
    var org_msg = p.original_message.text.replace(message, '');
    var alt_text = "再抽選実行 by " + p.user.name +"." + unix_ts + "(元の当選者：" + org_msg + ")";
    var createdDialog = createDialog(p, alt_text);
    var options = {
      "method" : "POST",
      "payload" : createdDialog
    };
    var slackUrl = "https://slack.com/api/dialog.open";
    var response = UrlFetchApp.fetch(slackUrl, options);
    return ContentService.createTextOutput();
  } else if (p.callback_id === 'redo_dialog') {
    text = p.state + "\n理由：" + p.submission.redo_dialog_input;
    update(text);
    sendGomiMsgWithButton();
  }
```

##処理4.のコード
完了ボタンが押されたときは特に前述の分岐の中で

```  js
if (p.actions && p.actions[0].value === "finish"){
    text =  p.user.name + "さん、ゴミ捨てありがとうございました (" + unix_ts + ")";
    update(text);
    deleteGomiTrigger();
```
の処理が実行され、 `update(text)` が呼ばれます。

```js
function update(text){
  var options = {
    "method" : "POST",
    "payload" : {
      "token": token,
      "channel": channelId,
      "text": text,
      "ts": scriptProperties.getProperty('message_ts'),
      "attachments": [] //空配列で上書きしないと元の投稿のattachmentsが残ってしまう
    }
  };
  var slackUrl = "https://slack.com/api/chat.update";
  var response = UrlFetchApp.fetch(slackUrl, options);
  return ContentService.createTextOutput();
}
```
update(text)内ではslackAPIの `chat.update` によって元の投稿の上書きが実行されます。
上書き実行後に、 `deleteGomiTrigger();` が呼ばれ、
不要となったGASのトリガーが削除されて、一連の処理が終了となります。

```js
// その日のトリガーを削除する関数(消さないと残る)
function deleteGomiTrigger() {
  var triggers = ScriptApp.getProjectTriggers();
  for(var i=0; i < triggers.length; i++) {
    if (triggers[i].getHandlerFunction() === "sendGomiMsgWithButton"
        || triggers[i].getHandlerFunction() === "sendGomiReminder") {
        try {
          ScriptApp.deleteTrigger(triggers[i]);
        } catch (e) {
          Logger.log(e);
        }
    }
  }
}
```

以上となります。