GAS+Slackでサンタからメッセージが届く[イベントアプリ]

# イントロダクション
　私が今勤めている会社では行事を大事にしています。お正月のお餅や、新人歓迎会などは勿論、ハロウィンの時にはオフィスに🎃やコウモリの飾りがシレッと置かれていたり・・・。そして、当然 X'masもその中の一つとして何かをするには絶好の機会となるのです。

　今回は、今年の X'masに向けて用意したサンタメッセージアプリをGAS＋Slackで作ったのでご紹介します。

　ちなみにネタアプリなので難しいことは **一切** していません。ただ、GASはこういったちょっとしたことを気軽に実装できるところが **ちょ～最高🌟🌟！！！** だと思ってます。GAS is God！

# 仕様
- 社員をランダムにペアリング
- クリスマス・イブに、〇〇さんにメッセージを贈ろう！とSlackにて社員に通知
- 通知されたメッセージからダイアログを出し、メッセージを入力してもらう。
- 一定期間未入力だった人にはリマインダを送付
- クリスマス当日に〇〇さんに「△△さんから X'mas メッセージが届いたよ！＆△△さんから入力されたメッセージ」をサンタBotからslackで送る

SecretSanta のメッセージ版みたいなものになりますね。

# 実装

という訳で早速実装です。

## Slackの設定

### まずはSlackのAPIの設定
[SlackAPI]-[Create New App]でapiを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40007/bf0f4115-faa4-751c-0319-fe01aec381ad.png)

名前は適当に。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40007/fb5c1ad9-bee8-d8d7-0272-82a2e3582450.png)

### [Bot Users]の設定
先にBotを作っておきましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40007/7633dcfb-065e-8389-ba0c-d17aa1a68404.png)

### [OAuth & Permissions]の設定
権限周りとScopeの設定をします。
[Scopes]-[Add an OAuth Scope]で

- chat:write:bot
- im:write
- users:read

を付加しましょう。（botは既に登録されている筈です）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40007/52b6403a-e750-4116-6d01-2bc3c3cf4f46.png)

そして、[OAuth Tokens & Redirect URLs]-[Install APp to Workspace]でインストールを行います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40007/59b3bf0d-55da-45b4-d7e5-402a29a0936a.png)

インストールが済むと、[Bot User OAuth Access Token]が表示されていると思いますのでそちらを確保しておきましょう。

### [Interactive Components]の設定
[Interactivity]をOnにします。こちらはdialogを使ってメッセージを受け取ったりするために必要になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40007/ece48ec4-6df8-c33f-264f-548f0f230cf1.png)

[Interactivity]-[Request URL]には、GASで用意するWebアプリケーションのURLを貼り付けます。後で貼り付けましょう。

## GoogleSpreadSheet の準備
GoogleSpreadSheetを用意します。
とりあえず、シート名は[users]等にして、GASで操作できるように、シートのハッシュ値を確保しておきましょう。
※シートのハッシュ値はURLのdocs.google.com/spreadsheets/d/XXXXXX/のXXXXX部分です。

## GASのコーディング

### GASを用意します。
[G Suite Developers Hub](script.google.com/home) から、[新しいプロジェクトを選択]
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40007/ba87434d-9b3d-2872-dd4b-a8de734bb71c.png)


### 最低限の設定を入れてWebアプリケーションとして公開します

設定周りのコードを入れて・・・

```js:santa.gs
// spreadsheet 関係
var spreadsheet = SpreadsheetApp.openById('上記で取得したスプレッドシートのハッシュ値')
var sheet = spreadsheet.getSheetByName('users')
// Slack API関係
var token = 'xoxb-XXXXX(slack apiで取得したOAuthトークン)'
```

[メニュー]-[公開]-[Webアプリケーションとして導入] からWebアプリケーション化を行います。実行権は[Anyone,even anonymous]を指定しておきます。
ここで表示される[Current web app URL]がSlackの[Interactivity]-[Request URL]に設定するURLになるので公開後、貼り付けておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40007/849520e5-d3d7-0ef3-5513-1f0aa8da0255.png)

これで、設定周りは完了。後はGASのコードを書いていくだけです。

### ユーザーリストを取得する
まず、space上にいる社員メンバーリストを取得する部分を書きましょう。
ユーザーリストは slack.com/api/users.list で取得します。

```js:getUserList.gs
function getUserList(){
  try{
    var geturl = 'https://slack.com/api/users.list?token=' + token
    var ret = UrlFetchApp.fetch(geturl);
    var userinfo = JSON.parse(ret.getContentText());
    if(!userinfo.ok){
      throw new Error("UserInfo Error!!")
    }
    // memberlist
    var row = 1
    userinfo.members.forEach(function(member){
      if(member.deleted == false && member.is_bot == false){
        sheet.getRange(row, 1).setValue(member.id)
        sheet.getRange(row, 2).setValue(member.profile.display_name)
        row = row + 1
      }
    });
  }catch(err){
    console.log(err.message);
  }
}
```

APIでは細かな条件指定でメンバーを抽出できないので、上記コードでは取得後に[deleted]及び[is_bot]の条件でリストを絞り込み、SpreadSheetに書き出す形にしています。

こちらを実行すると、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40007/90d1c375-387e-6791-c57a-7e29f6843447.png)

といった形で、１列目にSlackのUserID（実際に個人宛に送る際に必要になるキー）、２列目に表示名が取得できていると思います。

こちらの２列目をコピーし３列目に貼り付け、そのまま範囲指定して右クリック-[範囲をランダム化]すればペアリングが完成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40007/0f3efc6a-c2b3-82f6-ec51-04af5bae3268.png)

２列目迄が送付側情報、３列目が受取側情報としましょう。

### Xmasメッセージ入力をお願いするメッセージを送る
次に、送付側に受取側へのメッセージを入力してもらうメッセージを送付するメソッドを作ります。
ダイレクトメッセージを送るためにはまず im.open してから chat.postMessage する必要があります。
また、attachments部分を JSON.stringify　せねばなりません。（JSONで送ったり、全体をstringifyしたりしてうまく行かず、結構ハマりました）

```js:sendInitMessage.gs
function sendInitMessage(){
  try{
    for(var i = 1; i <= sheet.getLastRow(); i++){
      var imPayload = {
        "token": token,
        "user": sheet.getRange(i,1).getDisplayValue()
      };
  
      var imOptions ={
        "method" : "post",
        "contentType": "application/x-www-form-urlencoded",
        "payload" : imPayload
      };
    
      var messagePayload = {
        "token": token,
        "channel":JSON.parse(UrlFetchApp.fetch('https://slack.com/api/im.open', imOptions)).channel.id,
        "text": sheet.getRange(i,3).getDisplayValue() + " に X'masカードを送ってみませんか？",
        "icon_emoji" : ':santa:',
        "username": 'サンタ',
        "attachments": JSON.stringify([
          {
              "text": "早速メッセージを書こう",
              "fallback": "ダメです",
              "callback_id": "start_msg",
              "color": "#3AA3E3",
              "attachment_type": "default",
              "actions": [
                  {
                      "name": "write",
                      "text": "書く",
                      "type": "button",
                      "value": "1"
                  }
              ]
          }
        ])
      };
    
      var messageOptions = {
        "method" : "post",
        "contentType": "application/x-www-form-urlencoded",
        "payload" : messagePayload
      };
      var res = UrlFetchApp.fetch('https://slack.com/api/chat.postMessage', messageOptions);
    }
  }catch(err){
    console.log(err.message);
  }
}
```

こちら、実行してみるとslackに下記のようなメッセージが飛ぶのが確認できると思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40007/f34cf3e9-7947-622b-daff-77917cdfd734.png)

### ダイアログ部分の準備
次は、上記のボタンを押された時に表示させたいダイアログを用意します。
仕組みとして、「ボタンを押下された」、「ダイアログで確定された」などは、payload.typeに格納されてくるのでそれにより処理を振分けます。

大枠の構成

```js:doPost.gs
function doPost(e){
    const payload = JSON.parse(e.parameter.payload);
    if(payload.type === 'interactive_message') {
       // メッセージのボタンが押下された時のアクション
       // (A) ダイアログの表示をさせる
    } else if (payload.type === 'dialog_submission') {
       // ダイアログの確定を押された時の処理
       // (B) 入力されたメッセージを取得する
    }
}
```

###　(A) ダイアログの表示をさせる

ダイアログの部分です。ダイアログの表示はダイアログで表示する要素を作った上で dialog.open をコールします。
ダイアログに受取側の名前を表示させるために、このボタンを押下した送付側の名前を spreadsheet で検索した上で、その行に書かれた受取側の名前を取得しています。

```js:partsOfdoPostA.gs
      var target = sheet.createTextFinder(payload.user.id).findNext()
      var dialog = {
          "token": token,
          "trigger_id": payload.trigger_id,
          "dialog": JSON.stringify({
            "callback_id": "xmas",
            "title": 'メッセージを入力して下さい',
            "submit_label": "送信する",
            "elements": [
              {
                "type": "text",
                "label": sheet.getRange(target.getRow(), 3).getDisplayValue() + "さんへの X'masメッセージ",
                "name": "xmasmsg"
              },
            ]
          })
      };

      var options = {
        'method' : 'post',
        'payload' : dialog
      }; 

      // dialog出力
      var res = UrlFetchApp.fetch("https://slack.com/api/dialog.open", options);
```

こちらを上記の大枠の中に記載した上で、Webアプリケーションの更新をして公開すると、先程のメッセージのボタンが有効になっている筈です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40007/ce52c50d-2fe6-1e14-31f5-84cde96503fc.png)

### (B) 入力されたメッセージを取得する

続いて、上記のダイアログでメッセージを入力されたら受取る部分です。
こちらは、大した処理ではありませんが、ユーザーIDから保存する行を取得し、入力されたメッセージを４列目に保存しています。

```js:partsOfdoPostB.gs
      var target = sheet.createTextFinder(payload.user.id).findNext()
      sheet.getRange(target.getRow(), 4).setValue(payload.submission.xmasmsg)
```

### 大事な部分

ダイアログ入力を受付けた後、slackは応答を3秒以内に返されないとエラー判定をしてしまいます。処理後必ず下記の記述にて応答を返してあげます。

```js:finally.gs
return ContentService.createTextOutput();
```

### 一定期間未入力だった人にはリマインダを送付

リマインダ用のメソッド。なんの工夫もしておりません。

```js:reminderMsg.gs
    for(var i = 1; i <= sheet.getLastRow(); i++){
      if(sheet.getRange(i,4).isBlank() == true){
         var imPayload = {
           "token": token,
           "user": sheet.getRange(i,1).getDisplayValue()
         };
  
         var imOptions ={
           "method" : "post",
           "contentType": "application/x-www-form-urlencoded",
           "payload" : imPayload
         };
    
         var messagePayload = {
           "token": token,
           "channel":JSON.parse(UrlFetchApp.fetch('https://slack.com/api/im.open', imOptions)).channel.id,
           "text": "送るメッセージで悩んでいる？さぁ、気持ちを素直に届けてみよう！",
           "icon_emoji" : ':santa:',
           "username": 'サンタ'
         };
    
         var messageOptions = {
           "method" : "post",
           "contentType": "application/x-www-form-urlencoded",
           "payload" : messagePayload
         };
    
         var res = UrlFetchApp.fetch('https://slack.com/api/chat.postMessage', messageOptions);
      }
```

これを最初のメッセージから一定期間後に叩くだけです。

### X'masメッセージの送付

送付もそのまんまですが、送付先のIDをspreadsheetの5列目に用意しておくことで簡便に送れるようにしています。列２の送付元の名前でソートした上で、送付先の名前でvlookupをかけて用意してます。

```js:sendXmasMessage.gs
function sendXmasMessage(){
  try{
    for(var i = 1; i <= sheet.getLastRow(); i++){
      var imPayload = {
        "token": token,
        "user": sheet.getRange(i,5).getDisplayValue()
      };
  
      var imOptions ={
        "method" : "post",
        "contentType": "application/x-www-form-urlencoded",
        "payload" : imPayload
      };
    
      var msg = "<@" + sheet.getRange(i,2).getDisplayValue() + "> から君にクリスマスのメッセージが届いたよ！『" + sheet.getRange(i,4).getDisplayValue() + "』\n良いクリスマスを！\n"
      var messagePayload = {
        "token": token,
        "channel":JSON.parse(UrlFetchApp.fetch('https://slack.com/api/im.open', imOptions)).channel.id,
        "text": msg,
        "icon_emoji" : ':santa:',
        "username": 'サンタ'
      };
    
      var messageOptions = {
        "method" : "post",
        "contentType": "application/x-www-form-urlencoded",
        "payload" : messagePayload
      };
    
      var res = UrlFetchApp.fetch('https://slack.com/api/chat.postMessage', messageOptions);
    }
  }catch(err){
    console.log(err.message);
  }
}
```

## できたもの
結局色々デコレーションやらをして最終的には下記のような感じになりました。

メッセージ入力メッセージ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40007/f4f35e3f-f919-a94e-21fc-319a7fdc51f9.png)

ダイアログ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40007/ede221cd-9cb4-4bd0-2b1a-f682bf539403.png)

リマインダメッセージ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40007/50efd8c5-066d-c98b-0772-4d46d8e3a18d.png)

入力後のspreadSheet
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40007/547106ed-4f64-c233-18ed-e1b7d011f9df.png)

メッセージ送付
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40007/fe1213af-adcd-1ecb-594c-1814acabe491.png)

※ ちなみに今回は説明を省きましたが、GASの処理にはセキュリティを考慮し、セキュリティトークンのチェック等を行う処理をした方がベターです。

## 終りに
やっぱりGASは、こんな感じでペロっと作れるところが良いですねー。
今回のは純粋な遊びですが、MSOffice系で運用されているドキュメントを中心に置いた業務の改善なんかには絶大な力を発揮してくれると思います。
もっと色々やりたい！！