Cloud Functionsで作る選択肢つき Slack BOT

次のGIF画像のような、メンションに反応して選択肢を表示、そのあと選択に応じた処理をしてくれる BOT を作ります。

いわゆる ChatOps 的な使い方ができそうなやつです。

![choices-bot.gif](https://qiita-image-store.s3.amazonaws.com/0/10713/b218946e-f813-6e68-c453-5d1258bb5b1d.gif)


# 全体の流れ
Slack の3つの機能を使っているため、ここをちゃんと分割して考えることができれば、理解がしやすくなります。

- ユーザーからのメンションを受け取る部分 (**Event Subscriptions**)
- チャンネルへメッセージを投稿する部分 (**Slack API**)
- 選んだ選択肢を受け取る部分 (**Interactive Components**)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/10713/f04072b3-f733-6ba8-7c39-984ddf6639de.png)

# Cloud Functions の準備
Slack App から来るイベントを受け取るための HTTP サーバーとして Google Cloud Platform の [Cloud Functions](https://cloud.google.com/functions/) を使います。

**Event Subscriptions** の受け取り先になるには、そのサーバーを所有していることを証明する必要があり、 url_verification と呼ばれてます。

https://api.slack.com/events/url_verification

簡単にまとめると、`challenge` という値がリクエストに含まれて来るので、その `challenge` をそのまま返せばOKです。

さっそく `app.js` というファイルを作って、満たす関数をつくってみましょう。

```js:app.js
const onRequest = (req, res) => {
    const payload = req.body;

    if (payload.type === 'url_verification') {
        return res.status(200).json({ 'challenge': payload.challenge });
    }

    res.status(200).send('OK');
}

exports.slackChoicesBot = onRequest;
```

書き終わったら、gcloud コマンドですぐにデプロイできます。 （最近東京リージョンや Node.js 8 に対応したので嬉しいですね！）

```sh
npm init
gcloud beta functions deploy slackChoicesBot --runtime nodejs8 --trigger-http --region asia-northeast1
```

デプロイ完了したら、`https://asia-northeast1...` という関数の URL が出るので、これをメモしておきましょう。

```
Deploying function (may take a while - up to 2 minutes)...done.
availableMemoryMb: 256
entryPoint: slackChoicesBot
httpsTrigger:
  url: https://asia-northeast1-xxxxxxx.cloudfunctions.net/slackChoicesBot
...
```

デプロイした関数は、[Google Cloud Platform のコンソール](https://console.cloud.google.com/functions) からも確認できます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/10713/6f57c2f1-969c-e5aa-7b91-52d65e83a140.png)




# Slack App の作成
Cloud Functions が準備できたので、次は Slack App を作って BOT ユーザーを作ります。

## (1) Slack App と BOT User をつくる

https://api.slack.com/apps にアクセスして、**Create New App** ボタンから Slack App をつくり、**Bot Users** というメニューから BOT 用のユーザーを作ります。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/10713/551c88b9-70b9-dc45-982e-381df70bbeaa.png)


## (2) メンションを受け取るサーバーを指定
App 設定の **Event Subscriptions** をONにして、**Request URL** に Cloud Functions にデプロイした関数の URL を入れます。

次に、**Subscribe to BOT Events** のところに **app_mention** を追加します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/10713/2994511b-c33f-dbbf-3454-6d980aa22017.png)



## (3) 選択結果を送るサーバーを指定
App の設定で次のように、Interactivity: ON にして、Request URL に Cloud Functions にデプロイした関数の URL を入れます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/10713/2b1e8e7f-23a2-8f51-f045-308396c33818.png)

## (4) Slack ワークスペースに App をインストール
App の設定の **OAuth & Permissions** の **Scopes** で、`bot` という名前の scope を追加します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/10713/e9fab1fa-e30d-0fbe-15c5-bae55e85fd7b.png)

そこまで完了したら、**OAuth & Permissions** ページにある **Install App to Workspace** で Slack ワークスペースにインストールしましょう。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/10713/4bc095ad-8eee-9187-c257-f33b0cf42834.png)

# メンションに反応して選択肢を返す
BOT に対してメンションを飛ばすと、Cloud Functions の関数に **[app_mention](https://api.slack.com/events/app_mention)** という種類のリクエストが来ます。

```json
{
    "type": "app_mention",
    "user": "U061F7AUR",
    "text": "<@U0LAN0Z89> is it everything a river should be?",
    "ts": "1515449522.000016",
    "channel": "C0LAN2Q65",
    "event_ts": "1515449522000016"
}
```

この内容に対して、選択肢つきの返信をチャンネルに投稿する処理を書きます。チャンネルへの投稿は [chat.postMessage API](https://api.slack.com/methods/chat.postMessage) を普通に使います。

選択肢は [Slack Attachments として記述できる](https://api.slack.com/docs/message-menus) ので、ドキュメントを読みながら組み立てていきました。

```js:app.js
const request = require('request-promise-native');

async function postMessage(payload) {
    await request.post('https://slack.com/api/chat.postMessage', {
        headers: { 'Authorization': `Bearer ${process.env.BOT_USER_TOKEN}` },
        json: payload,
    });
}

const onRequest = async (req, res) => {
    let payload = req.body;

    if (payload.type === 'url_verification') {
        return res.status(200).json({ 'challenge': payload.challenge });
    }

    if (payload.event && payload.event.type === 'app_mention') {
        if (payload.event.text.includes('hi')) {
            const slackRes = await postMessage({
                text: `<@${payload.event.user}> hi!`,
                channel: payload.event.channel,
                attachments: [createSlackAttachment('BOT response')],
            });
            return res.status(200).send('OK');
        }
    }

    ...
```

また、チャンネルに投稿するために BOT User OAuth Access Token が必要ですが、このトークンはソースコードには含めたくないものなので、[環境変数ファイルに分離](https://cloud.google.com/functions/docs/env-var) して関数をデプロイしました。

```yaml:.env.yml
BOT_USER_TOKEN: xoxp-xxxxx...
```

デプロイコマンドはこんな感じです。

```sh
gcloud beta functions deploy slackChoicesBot --runtime nodejs8 --trigger-http --env-vars-file .env.yml --region asia-northeast1

```

# 選択肢を選んだ結果を受け取る
BOT が出した選択肢を選んだとき、Cloud Functions の関数に **interactive_message** という種類のリクエストが来ます。

https://api.slack.com/docs/interactive-message-field-guide#action_payload

リクエストの中に「どの選択肢を選んだか」などの情報がはいっているので、それに応じて関数でいろいろなことができます！

※app_mention のときと payload の形式が異なっているので注意が必要です。 app_mention はそのまま構造化された JSON で来ましたが、interactive_message のときは payload という名前のフィールドに **文字列化されたJSONが入っている** 構造なので、payload を一度 JSON.parse する必要がありました。

```js:app.js
...
const onRequest = async (req, res) => {
    let payload = req.body;
    
    ...

    if (typeof payload.payload === 'string') {
        payload = JSON.parse(payload.payload)
    }

    if (payload.type === 'interactive_message') {
        const action = payload.actions[0];
        if (action.name === 'choices') {
            const selectedOption = action.selected_options[0];
            return res.status(200).send(`<@${payload.user.id}> Your select value: "${selectedOption.value}"`);
        }
    }
...

```

# まとめ
全体のソースコードは [GitHub にアップロードしておきました。](https://github.com/castaneai/slack-choices-bot)
今回は選んだ値を Slack に出すだけの単純な BOT でしたが、これをベースに様々な BOT が開発できそうです。

最近は Cloud Functions のような関数だけをさくっと公開できるサービスが充実してて、非常に開発がやりやすいですね！
その影響もあって、プログラミングの部分よりは、Slack API の仕様の理解のほうが苦労します :sweat_smile: