GAS(Google App Script)を使ってSlackのbotを作成してみる

# はじめに
Google App Scriptとスプレッドシートを連携してSlackのbotを作成したので、
自分の備忘録も兼ねてどういうことをしたのかをまとめた。
## 前提/作りたかったbotの概要
以前は社内のチャットで毎朝以下の手順を実施していた。
1. 会社近くのお弁当屋さんをリストアップし、メッセージを投稿（「どこのお店にしましょう？」的なやつ）
1. お昼前にそのメッセージに押されたスタンプの数を集計（手動）
1. 多数決で決定したお店をチャット内で周知

→　毎朝同じ内容を投稿して、お昼前に人力で集計するのがとてもめんどくさい
　　リマインダーを使うにしても、スタンプの集計が出来ないので機能が不十分
→　botで出来たらとても素敵
## 実装した機能
上記を踏まえ、実装した機能としては以下の通り。

**① メッセージの投稿**
決まった時間に定型文を投稿する機能
集計結果を投稿する機能

**② メッセージへのスタンプ押下**
①で定型文を投稿すると同時に、メッセージに投票用のスタンプを押す機能
※投票したい人が選択肢のスタンプを探しに行くのがめんどくさいため

**③ メッセージに押されたスタンプ数の取得**
決まった時間に定型文メッセージに押されたスタンプの数を取得する機能
※今回は取得したスタンプをスプレッドシートで集計するために必要
## おおまかな仕組み/シーケンス図
```mermaid
sequenceDiagram
    autonumber
    actor ユーザー
    participant app as slack
    participant api as slack api
    participant service as 自作アプリ<br/>（今回はGAS）
    participant db as スプレッドシート<br/>（店舗情報のまとめ役）
    db->>service: あらかじめまとめていた<br/>店舗情報を取得
    service->>service: 店舗情報をもとに定型文作成
    service->>+api: 定型文の送信 & スタンプ押下 依頼
    api->>-app: 
    ユーザー->>app: スタンプで希望先を投票
    service->>api: 時刻トリガーでスタンプ情報の取得依頼
    api->>app: 
    app->>+api: slack api経由でスタンプ情報を取得
    api->>-service: 
    service->>db: 取得した結果の書き込み
    db->>db: 店舗情報と合わせて計算式で集計
    service->>+db: 時刻トリガーで集計結果を取得
    db->>-service: 
    service->>+api: 集計結果をいい感じに成型して<br/>メッセージとして送信依頼
    api->>-app: 
```
# 事前準備
## 必要なもの
・Googleアカウント（GAS、スプレッドシート利用のため）
・Slackのワークスペース権限（bot追加のため）
~~・やる気~~
## 先にしておくこと
#### slackのアプリ作成
   1. https://api.slack.com/apps の「Create New App」を選択
※2つ選択肢が出てくるので、From scratch を選択
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641495/34131738-5ac4-a7a8-d6f3-eccaae8ca4c9.png)
   1. 作りたいアプリの名前を入力し、追加したいワークスペースを選択
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641495/f7e7bb7e-21da-ee4e-2abd-f5985b8e670d.png)
   1. 「Create App」を選択してアプリ自体の作成は完了
#### アプリの権限周りの設定、ワークスペースへの追加
   1. 「OAuth & Permissions」タブを選択
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641495/3bde0efa-d336-cbef-dcf1-ddec2c005e47.png)
   1. 画面下部にある「Scopes」の「Add an OAuth Scope」を選択し、利用する権限を追加する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641495/04be5041-a234-52dc-af8c-2355353cc843.png)
今回は以下の権限を追加する。
「chat: write」...メッセージを投稿する権限
「reactions: read」...メッセージに追加されたスタンプを読み取る権限
「reactions: write」...メッセージにスタンプを追加する権限

   1. 「App Home」タブを選択し、「App Display name」の「Edit」を選択する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641495/6dc5aa72-fe52-533e-62b1-283b08f08846.png)
   1. チャンネルに表示される名前と、デフォルトのユーザー名を設定し「Add」する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641495/0d21ffdc-61a6-cf9b-a608-3e7363afe303.png)
   1. 名前を決めたら「Install App」タブを選択して「Install to Workspace」を選択
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641495/f863dd4b-0e1b-5c35-fecc-8e6e59d65bad.png)
   1. 以下のような確認画面が出てくるので許可する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641495/e5e473b8-714f-7527-4386-948802fc5336.png)
   1. 以上で権限を設定したアプリの、ワークスペースへの追加は完了
#### slackのbot用トークンの払い出し
ワークスペースへの追加が完了した状態で再度「Install App」タブを選択すると、
以下のようにbot利用のためのトークンが払い出されているのでコピーしておく
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641495/6824c3e0-9861-8c46-b502-6028c1395a9a.png)
#### チャンネルIDの取得
メッセージ送信のときに使うのでコピーしておく（slackアプリから確認可能）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641495/e1a67f4e-0554-fd6d-1e5c-bfa91cee83bf.png)
#### スプレッドシートのURL
こちらは必要な場合のみ。今回はスプレッドシートと連携したアプリなので必須。
# 自作アプリ作成
今回はGASでアプリを作成したので、GASでの手順をまとめる。
## GASとは
Google App Scriptの略。
Java ScriptをベースにGoogleが提供している開発言語。
スプレッドシートやGoogle Map、Gmailなどのツールと連携が可能。
## アプリ作成
1. https://script.google.com/home のページから新規でプロジェクトを追加する
1. javascriptでコードを書く
　a. 例）メッセージの送信関数
```javascript:postMessage
function postMessage(message) {
  // slack api のメッセージ送信機能
  const url        = 'https://slack.com/api/chat.postMessage';
  const method     = 'post';

  const payload = {
    // 払い出しておいたbotのトークン
    'token'      : BOT_USER_OAUTH_TOKEN,
    // slackチャンネルのID
    'channel'    : CHANNEL_ID,
    'text'       : message,
  };
  const params = {
    'method' : method,
    'payload' : payload
  };

  // メッセージ送信APIの呼び出し
  const responseJson = UrlFetchApp.fetch(url, params);
  // メッセージのタイムスタンプ取得のために、レスポンスを返却しておく
  return JSON.parse(responseJson.getContentText());
}
```
　b. 例）リアクションの追加関数
```javascript:addReaction
// timeStampはリアクションを追加したいメッセージの送信時タイムスタンプ
// reactionListはリアクションの文字列リスト　例）「:smile:」みたいなやつ
function addReaction(timeStamp, reactionList){
  // slack api のリアクション追加機能
  const url = 'https://slack.com/api/reactions.add?' + '&name=';
  const method = 'post';

  const payload = {
    // 払い出しておいたbotのトークン
    'token'      : BOT_USER_OAUTH_TOKEN,
    // slackチャンネルのID
    'channel' : CHANNEL_ID,
    'timestamp' : timeStamp,
  };
  const params = {
    'method': method,
    'payload': payload
  };

  // リストからリアクション文字列を取り出してループ
  for (const reaction of reactionList) {
    // urlにリアクション文字列を追加して、slack api 呼び出し
    UrlFetchApp.fetch(url + reaction, params);
    // 感覚をおかずにスタンプを送信させると、順番がバグることがあるため1秒待つ
    Utilities.sleep(1000);
  }
}
```
　c. 例）リアクションの取得関数
```javascript:getReactions
// timeStampはリアクションを追加したいメッセージの送信時タイムスタンプ
function getReactions(timestamp){
  // slack api のリアクション取得機能
  const url = "https://slack.com/api/reactions.get";
  const method = 'get';
  const payload = {
    // 払い出しておいたbotのトークン
    'token'      : BOT_USER_OAUTH_TOKEN,
    // slackチャンネルのID
    'channel' : CHANNEL_ID,
    'timestamp' : timestamp,
  };
  const params = {
    'method': method,
    'payload': payload
  };

  // リアクション取得apiを呼び出してレスポンス取得
  const response = UrlFetchApp.fetch(url, params);
  // レスポンスをJSON形式にパース
  const responseJson = JSON.parse(response.getContentText());

  // パースしたレスポンスからリアクション郡を返却
  return responseJson.message.reactions;
}
```
　d. 例）スプレッドシートとの連携
```javascript:
// SPREADSHEET_URLはスプレッドシートのURL
// sheetNameはスプレッドシートのシート名
const sheet = SpreadsheetApp.openByUrl(SPREADSHEET_URL).getSheetByName(sheetName);

// 書き込み
// この場合はシートのE2に値を設定する
const sheetValue = sheet.getRange("E2").setValue()

// 読み込み
// この場合はシートのE2の値を取得する
const sheetValue = sheet.getRange("E2").getValue()
```
３. 上記もろもろを組み合わせて、やりたい機能を実装する
~~あまりにもコードが乱雑だったのでコードは割愛~~
実際にできたものがこちら。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641495/c2b08e18-47ea-d561-11fa-b5d6bf481247.png)
リアクションもちゃんとbotが押してくれてる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641495/6fe71854-adb3-3f37-2d52-e006b1b24172.png)
## アプリにトリガーを追加
GASではトリガーを設定して、特定条件で命令を実行させることが可能。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641495/05a13ed1-5d7b-5922-f71d-cc8702bc2c7e.png)
「トリガーを追加」を選択すると以下のようなダイアログが出てくる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641495/82c64e40-9bbc-c2d6-2bcc-68ab64b8c5f4.png)
今回は朝の時間帯に定型文を送信したいため、日付ベースのタイマーを使って
朝の8~9時のどこかで定型文送信イベントを発火させる。
同様にリアクションの集計結果を取得するイベントをお昼前に発火させて、結果を送信させれば完了。
# まとめ
やってみると技術的な敷居も高くなかったし、
無料で作成出来るので（一番大事）「試しになんかやってみよう」の入門として最適だった。
他にもbotに権限を追加すればもっとやれることもあるので、
暇なときに機能追加とかしても面白いかもしれない。

**後日談**
スタンプでの集計ではなく、指名された誰かが日替わりで店舗を決める制度になったことと、
作成完了時点で作成者のやる気とデプロイする気力がなかったことから、このbotが実用化されることはなかった。