GAS + slack で褒めあえる仕組みを作る

# TL;DR:
- 以前、[「褒めあえる仕組みをslack hubot で作る」](https://qiita.com/tmkt/items/12561e3ebe7e87f3b269)というものを書いたが、今度はGAS + slack で作ったよ、という話
- [ベイエリアで働くエンジニアがやりやすいと感じる会社の特徴５つ](https://qiita.com/hiroyamada/items/aa12af972075919eeba1) インスパイア
- GAS初めて触ったけど超便利。GoogleSpredSheet操作はExcelVBA、GASそのものはjavascriptなので学習障壁も低い。色々業務上の小ネタを作るのに使えそう

# 実装要件
- メンバー同士で定期的にお互いを褒め合える（トロフィーを送る）
- メンバーみんなが褒められた人となぜ褒められたかを認知できる

# 仕様
- slack で対象者を指定してトロフィー贈与（現トロフィー所有者からしか贈れない）

`send toPerson reason`

> fromPerson(メッセージ投稿者） から toPerson　に reason によりトロフィーが授与されました！

- slack でトロフィー履歴が確認できる

`history`

| 受賞日       |      誰から |    誰に    |    理由    |
|:-----------:|:----------:|:----------:|:----------:|
| YYYY/MM/DD |織田信長 | 豊臣秀吉 | 仇を討ってくれた |
| YYYY/MM/DD |豊臣秀吉 | 徳川家康 | 家臣になってくれた | 

- トロフィー履歴はAPIで管理
- 一定周期毎にトロフィー保持者に次のトロフィー贈与を促す

# 実装

## トロフィー履歴は GoogleSpeadSheet を用いる

まずは GoogleSperedSheet を用意しましょう

![GAS1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40007/d5b9e357-770a-cd99-fc82-06d05d45800f.jpeg)

ここに受賞履歴を格納していく形にします

## GASのプロジェクトを作成する

そして、GASのプロジェクトを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40007/0c5ee068-c9d6-c824-b73a-1caebec6efb8.png)

GAS上で指定のスプレッドシートを利用するには、スプレッドシートを開いた際のURLのキーを指定すれば、利用できます（https\://docs.google.com/spreadsheets/d/XXXXXXXXX/~）

```js:example.gs
var spreadsheet = SpreadsheetApp.openById('XXXXXXXXX')
var sheet_name = 'hoge'
var sheet = spreadsheet.getSheetByName(sheet_name);

sheet.getRange(1, 3).getDisplayValue()
```

## slackにメッセージを送りつけるWebhookを用意する

Slackにて用意してくれているAPI Incoming Webhook を用います。**GAS -(Incoming Webhook)-> slack** の部分です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40007/a5a28bc4-b1af-655a-9570-6ac2f651cbcf.png)


設定で投稿するチャネルなどを指定します。WebHook URL は後でGASで設定で使用します。
![GAS6.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40007/c685ea7f-dc85-d2f3-5399-0cc5f9318cfd.jpeg)


## slackとメッセージをやり取りするメソッドを作成する

メッセージを受取り、ごにょごにょした後にメッセージを返すメソッドをGASに用意します。
下記では、受け取ったメッセージのパラメータ（ユーザー名、入力文字列）をslackにメッセージとして返す部分だけをまず作っています。
最後、メッセージを送る際のURLに上記で用意した Incoming Webhook URL を指定しています。

```vb:example.gs
function doPost(e) {
  // 送信
  var options =
      {
        "method" : "post",
        "contentType" : "application/json",
        "payload" : JSON.stringify(
          {
            "text" : e.parameter.user_name + 'から [' + e.parameter.text + ']というメッセージを受け取ったよ'
          }
        )
      }
  UrlFetchApp.fetch('https://hooks.slack.com/services/XXXX(incoming webhook URL)', options);
}
    
```

## Webアプリケーションとして公開する

GASをWebアプリケーションとして公開しましょう。GASのメニューから公開処理を行います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40007/f8afcbc8-2dde-5be7-0bab-2ef25ba0fd4f.png)

![GAS7.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40007/3e5c0c6a-ebee-e2c0-85b2-44daa6c461de.jpeg)

表示されるURLが、受け取り側のURLとなります。次で使います。

## slackからメッセージを受取るWebhookを用意する

こちらもSlackにて用意してくれているAPI Outgoing Webhook を用います。**slack -(Outgoing Webhook)-> GAS** の部分です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40007/b6fd3b2b-8f42-1f14-5927-5b67f34d21f2.png)

設定で、引き金となる言葉や、対象のチャネルの他、キックするURLとして、GASで設定したURLを指定します。

![GAS4.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40007/59b1f04e-3377-4f9f-3ab5-bbe44b1418a7.jpeg)

ここでは、引き金となる言葉として、fumiを設定しました。上記の仕様と合わせると、送るメッセージは、
`fumi send toPerson reason`
という様なイメージになります

## 起きた課題

slackでトロフィーを授与する際に、UXを考慮してメンション形式で指定させることにしようとしましたが・・・

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40007/99b09fa6-a540-b473-3108-85b6629a58a4.png)


メンションだと、パラメータとしてGAS側で渡されるユーザー名はslackのuser_idになってしまいます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40007/79c0bdd4-40df-1991-88af-4be9f91ea2ce.png)

※ @trello で送って "trello" を期待していたら、trelloのuser_idの<@XXXXXX>で届いてしまう。

しょうがないのでuser.infoを取得し名前をひき直すことにしました。

### slack-apiを作成

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40007/ebbb648c-d04b-9b80-ea0d-42ad5faf9fda.png)

Create New App から AppName等を設定をして Permissions を選択


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40007/75e0c4b9-4b08-dd97-bd6d-499f75f897cf.png)

Scpoesでユーザー情報を取得する users:read を指定

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40007/5287232a-66f9-0a05-d4d7-30fc003ca75d.png)

許可します

![GAS2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40007/8f3b90f6-24e4-9b6b-b2b4-3fd5d499df98.jpeg)

OAuthのトークンが取得できるのでそちらをコピーしておきます

![GAS3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40007/9643e269-1f5f-9623-8107-69d258be5bce.jpeg)

### GAS側の処理

GAS側はこんな感じで引き渡されたslack_uidからusers.infoを呼び出してuserinfo.user.profileを取得し名前を取得します
cf) [users.info](https://api.slack.com/methods/users.info)

```js:example.gs
function getUserName(slack_uid){
  try{
    // slack_uidからidのみを取得する
    slack_uid = slack_uid.replace('<','').replace('>', '').replace('@', '')
    
    var geturl = 'https://slack.com/api/users.info?token=' + OAuthのトークン + '&user=' + slack_uid + '&pretty=1'
    var ret = UrlFetchApp.fetch(geturl);
    var userinfo = JSON.parse(ret.getContentText());
    if(!userinfo.ok){
      throw new Error("Invalid User!!")
    }
    return userinfo.user.profile.display_name
  } catch(e){
    throw e
  }
}
```

## 最終的にはこんな感じにGAS側でコードを用意しました。
```js:example.gs
// spreadsheet 関係
var spreadsheet = SpreadsheetApp.openById('XXXXXXX')
var sheet_name = 'trophies'
var sheet = spreadsheet.getSheetByName(sheet_name);
// slack - outgoing関係
var ogtoken = 'YYYYYYYY'
var ogurl = 'https://hooks.slack.com/services/ZZZZZZZZ'
// slack - userget関係
var ugtoken = 'xoxp-111111111111111111111'

function doPost(e) {
  try{
    var sendmessage = ''
    var title = ''
    
    var text = e.parameter.text.split(' ')
    if(text[1] == 'history'){
      title = '**** トロフィーヒストリー ****\n'
      sendmessage = title + getHistory()
      
    } else if(text[1] == 'send'){
      title = '**** トロフィー贈与！！ ****\n'
      
      if(text.length != 4) {
        sendmessage = 'パラメタがおかしいよ！\nfumi send [to] [reason]'
      } else {
        // 贈り手チェック
        if(e.parameter.user_name != getHolder()){
          sendmessage = '今のトロフィー所有者以外は贈れないよ\n現所有者 = ' + getHolder()
        } else {
          // 記録
          setHistory(e.parameter.user_name, text[2], text[3])
          sendmessage =  title + e.parameter.user_name + 'から <' + text[2] + '> に 『' + text[3] + '』という理由でトロフィーが送られました！おめでとう！！\n' 
        }
      }
    } else if(text[1] == 'usage' || text.length <= 1){
      title = '**** usage ****\n'
      
      sendmessage = title + 'fumi history            : トロフィーヒストリーを表示するよ\nfumi send [to] [reason] : トロフィーを[reason]の理由で[to]に送るよ'
    }
  } catch(err){
    sendmessage = sendmessage + '\n' + err.message
  } finally {    
    // 送信
    var options =
        {
          "method" : "post",
          "contentType" : "application/json",
          "payload" : JSON.stringify(
            {
              "text" : sendmessage
            }
          )
        };
    
    UrlFetchApp.fetch(ogurl, options);
  }
}

function getHistory(){
  try{
    var startrow = 2
    var rangerow = 5
    if (sheet.getLastRow() > rangerow){
      startrow = sheet.getLastRow() - rangerow
    }
    var bufarr = sheet.getRange(startrow, 1, rangerow, 4).getDisplayValues()
    var mem = new Array()
    
    for(var i = 0; i < bufarr.length; i++){
      mem.push('|' + bufarr[i][0] + '| ' + bufarr[i][1] + ' ==> ' + bufarr[i][2] + '  ♡' + bufarr[i][3])
    }
    return(mem.join('\n'))
  } catch(err){
    throw err
  }
}

function setHistory(fromn, ton, reason){
  try{
    var row = sheet.getLastRow()+1
    var uname = getUserName(ton)
    
    sheet.getRange(row, 1).setValue(Utilities.formatDate(new Date(), "JST", "yyyy/MM/dd"))
    sheet.getRange(row, 2).setValue(fromn)
    sheet.getRange(row, 3).setValue(uname)
    sheet.getRange(row, 4).setValue(reason)
  } catch(err){
    throw err
  }
}

function getHolder(){
  try{
    return sheet.getRange(sheet.getLastRow(), 3).getDisplayValue()
  } catch(err){
    throw err
  }
}

function getUserName(slack_uid){
  try{
    slack_uid = slack_uid.replace('<','').replace('>', '').replace('@', '')
    
    var geturl = 'https://slack.com/api/users.info?token=' + ugtoken + '&user=' + slack_uid + '&pretty=1'
    var ret = UrlFetchApp.fetch(geturl);
    var userinfo = JSON.parse(ret.getContentText());
    if(!userinfo.ok){
      throw new Error("指定ユーザーが不正です")
    }
    return userinfo.user.profile.display_name
  } catch(err){
    throw err
  }
}
```

動かしてみます。

`fumi history`
![GAS8.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40007/4bda49c3-db06-4d3f-be6c-12a87c56443f.jpeg)

`fumi send toPerson reason`
![GAS9.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40007/02a179c2-4ad5-b3c6-dab1-ac541d011983.jpeg)

トロフィー所有者じゃない人がトロフィーを送った場合
![GAS10.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40007/20759991-ca6d-8511-3512-413701f6ef3c.jpeg)

とりあえず、Slack <> GAS間でメッセージをやり取りし、データを GoogleSpreadSheet に登録するところは動くようになりました。

## 一定周期毎にトロフィー保持者に次のトロフィー贈与を促す

トロフィーは１週間位で定期的にメンバーを移動するようにしたいです。そのため、トロフィー所有者にslackから通知が行くような機能を用意したいです。この実装にはGASのトリガー機能を使えば簡単にできそうです。

先にトリガーでキックされるメソッドを用意しておきます。ここではトロフィーが贈られてから１週間経ったら通知がいくようにします。

```js:example.gs
function noticeHolderChange(){
var termdate = 7 
  try{
    var sendmessage = ""
    var presenddate = new Date(sheet.getRange(sheet.getLastRow(), 1).getDisplayValue())
    var nowdate = new Date()
    if((nowdate - presenddate)/86400000 > termdate){
      var presendmember = sheet.getRange(sheet.getLastRow(), 3).getDisplayValue()
      sendmessage = "<@" + presendmember + "> そろそろ次の人にトロフィーを送ろう！！\nfumi send [to] [reason]"
    }
  }catch(err){
    sendmessage = err.message
  }finally{
    if(sendmessage != ""){
      var options =
          {
            "method" : "post",
            "contentType" : "application/json",
            "payload" : JSON.stringify(
              {
                "text" : sendmessage
              }
            )
          };
      
      UrlFetchApp.fetch(ogurl, options);
    }
  }
}
```

次にGASのトリガーを登録します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40007/fa095416-0287-40e9-5a89-da38d0c0a5c2.png)

上記で用意したメソッドを指定し、１日１回起動されるようなトリガーの設定で用意しました。
![GAS11.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40007/c6275d7b-94f9-92da-7b1a-726049fd82a7.jpeg)

試してみます。
GoogleSpreadSheet の中身はこんな状態
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40007/2b681538-87b4-87ef-20ed-400784120764.png)

今日は2019/10/3です。トリガーを仕掛けておいて出力を確認しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40007/cfa20075-ebd7-81db-8cf9-540072fa9126.png)

# まとめ
- おおよそやりたかったことは簡単にできた。
- GAS凄く便利。Excelとかが未だに使われているけど、共有やAPI連携で柔軟なのでもっと使っていきたい。
- herokuとかAWSとかでアプリ用意するより気軽にできる。でもセキュリティ系はやや注意。仕事で使う場合はしっかり設定すべし。