Slackで特定のスタンプ(リアクション)に反応するサーバレスボットをGASで作成してみた

この記事は[全部俺 Advent Calendar 2018](https://adventar.org/calendars/3702)の21日目の記事です。

# 作ったもの
2通りのことができるSlackサーバレスボットをGASを使って作成してみました。

1. 特定のスタンプ/リアクションに反応して一瞬だけ文字を表示する
<blockquote class="twitter-tweet" data-lang="ja"><p lang="en" dir="ltr">Slack bot on google spread sheet <a href="https://t.co/uunwTZmBWB">pic.twitter.com/uunwTZmBWB</a></p>&mdash; tez (@te22eret) <a href="https://twitter.com/te22eret/status/1076165821104193536?ref_src=twsrc%5Etfw">2018年12月21日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

※ みなさまお忘れかもしれませんが、シュタゲネタです:)

2. 特定のスタンプ/リアクションに反応してスタンプを追加/削除する
<blockquote class="twitter-tweet" data-conversation="none" data-lang="ja"><p lang="en" dir="ltr">Slack bot on google spread sheet <a href="https://t.co/e0Q6A1u07x">pic.twitter.com/e0Q6A1u07x</a></p>&mdash; tez (@te22eret) <a href="https://twitter.com/te22eret/status/1076166206908841984?ref_src=twsrc%5Etfw">2018年12月21日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>



## 作成手順
### GASの準備:その1
Googleスプレッドシートから、ツール → スクリプトエディタを選んでGASの編集画面に移ります。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/190807/077cdd60-f23a-1bf5-a40a-1c3054a2442e.png)

GAS編集画面にて、以下のソースコードを貼り付けます。

```javascript
function doPost(e){
  // Slackバリデーション用
  var params = JSON.parse(e.postData.getDataAsString());
  return ContentService.createTextOutput(params.challenge);
}
```

GAS側の設定が終わったら、これをSlack側からアクセスできるようにします。
GAS側の画面で、公開 → ウェブアプリケーションとして導入... を選択します。
`アプリケーションにアクセスできるユーザー`で`全員（匿名ユーザーを含む）`を選び、プロジェクトバージョンで`New`を選んで公開した後、`現在のウェブ アプリケーションの URL`で出てくるURLをコピーしておきます。
GAS側の設定は、一旦ここで終了です。

### Slack側の準備
[Slack API](https://api.slack.com/)にアクセスし、`start building`を押してSlack APIを新規作成します。

`Create an App`から新規アプリを作成します。ここでは`reactor`という名前で作成します。
`Event Subscriptions`をOnにして、`Request URL`に先程のURLを貼り付けます。
すると、以下の画面のようにVerifiedと表示されるはずです。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/190807/d25fe6f6-293d-03bf-a38a-7834508f3dfc.png)

`Subscribe to Workspace Events`にて、`reaction_added`と`reaction_removed`を追加した後、`Save Changes`を押します。

`OAuth & Permissions`にて、`Scopes`で以下の画面のようになるようにPermission Scopeを追加して`Save Changes`を押します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/190807/ee8e86cf-eaed-e4e8-22ca-0e82c4fe8483.png)

`Bot User`から、`Add a Bot User`を押して、ボットを追加します。名前はここでは`reactor`としました。

ここまで来たら、`Install App to Your Team`を押してSlackに作成したAppをインストールします。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/190807/e3b6845f-6fb8-5f41-b6a6-8da8f42df594.png)

ここの下部の`Bot User OAuth Access Token`を後ほど使うのでコピーしておいてください。

これから出てくるGASソースコードと全く同じ動作をさせたい方は、以下の画像のようにCustom Emojiを追加してください。
文字列についてはみんな大好き[絵文字ジェネレーター](https://emoji-gen.ninja/#!/)を利用しました。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/190807/9b0781fd-d389-b4c9-f8a8-065aeaa4e419.png)


### GAS側の準備:その2
先程のGAS画面に戻り、ソースコードを削除した後、今度は以下のソースコードを貼り付けます。

```javascript
function doPost(e){
  // postMessage emoji
  const message_emojis = ["eye", "eyes"]
  // 追加/削除のトリガーとなるemoji
  const reaction_torigger_emoji = "so";
  // 追加/削除対象のemoji
  const reaction_emojis = ["re", "na"];
  
  // SlackApp インスタンスの取得
  var token = PropertiesService.getScriptProperties().getProperty('SLACK_ACCESS_TOKEN');
  var slackApp = SlackApp.create(token);
  
  // event parametersの取得
  var params = JSON.parse(e.postData.getDataAsString()).event;

  if (params.type === "reaction_added") {
    if (message_emojis.indexOf(params.reaction) >= 0) {
      watchYou(slackApp);
    } else if (reaction_torigger_emoji.indexOf(params.reaction) >= 0) {
      addReaction(slackApp, params.item, reaction_emojis);
    }
  } else if (reaction_torigger_emoji.indexOf(params.reaction) >= 0) {
    if (params.reaction === "so") {
      removeReaction(slackApp, params.item, reaction_emojis);
    }
  }

  // Slackバリデーション用
  var params = JSON.parse(e.postData.getDataAsString());
  return ContentService.createTextOutput(params.challenge);
}

function watchYou(slackApp){
  var options = {
    channelId: "test",              //チャンネル名
    userName: "patghqwskm@nine...", //投稿するbotの名前
    emoji: ":red_jelly:",           //iconのemoji
    message: "お前を見ているぞ",        //投稿するメッセージ
  };

  res = slackApp.postMessage(options.channelId, options.message, {username: options.userName, icon_emoji: options.emoji});
  Utilities.sleep(1000);
  slackApp.chatDelete(res.channel, res.ts);
}
  
function addReaction(slackApp, item, emojis){
  const url = "https://slack.com/api/reactions.add";
  const token = PropertiesService.getScriptProperties().getProperty('SLACK_ACCESS_TOKEN');
  const method = 'post';

  var payload = {
    'token'      : token,
    'channel'    : item.channel,
    'timestamp'  : item.ts,
  };
  var params = {
    'method' : method,
    'payload' : payload
  };

  for (var i = 0; i < emojis.length; i++) {
    Utilities.sleep(1000);
    payload.name = emojis[i];
    UrlFetchApp.fetch(url, params);
  }
}
  
function removeReaction(slackApp, item, emojis){
  const url = "https://slack.com/api/reactions.remove";
  const token = PropertiesService.getScriptProperties().getProperty('SLACK_ACCESS_TOKEN');
  const method = 'post';

  var payload = {
    'token'      : token,
    'channel'    : item.channel,
    'timestamp'  : item.ts,
  };
  var params = {
    'method' : method,
    'payload' : payload
  };

  for (var i = 0; i < emojis.length; i++) {
    Utilities.sleep(1000);
    payload.name = emojis[i];
    UrlFetchApp.fetch(url, params);
  }
}
```
一部処理(post.massage)は[いい感じのGASライブラリ](https://qiita.com/soundTricker/items/43267609a870fc9c7453)をお借りしました！
スタンプ/リアクションの追加/削除を行う処理はライブラリに含まれていなかったためPOSTリクエストを自分で投げています。

ファイル → プロジェクトのプロパティから、先程コピーしたSlackの`Bot User OAuth Access Token`を`SLACK_ACCESS_TOKEN`として貼り付けます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/190807/143edaea-8882-a0bb-8752-7c0c411dfc8d.png)

先ほどと同様、GAS側の画面で、公開 → ウェブアプリケーションとして導入... を選択します。
`アプリケーションにアクセスできるユーザー`で`全員（匿名ユーザーを含む）`を選び、プロジェクトバージョンで`New`を選んで公開すると設定は完了です。
Slack画面を開いて、`#test`チャンネルを作成すると、一番最初の動画のような挙動をするアプリが作成できているはずです。

## まとめ
GASを利用して、簡単なbotを作成してみました。
GASは手軽にサーバレス環境を試せていい感じなのでちょくちょく使用しています。
（サービスレベル的に本番環境では運用できないので、失敗しても大丈夫な趣味プロジェクトでの使用に留めたほうがいいと思います。）
今後も思いついたら手軽に試していきたいと思います。