【チャットボット風】Interactiveな対話形式Slack Botを作ってみる

![Untitled Diagram(20).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/31a747b2-89c5-2b76-9182-a00635810658.png)

## 背景

業務効率化のため、何か知りたい事があった場合にSlackのスラッシュコマンドで呼び出せるチャットボット的な存在が欲しいと思いました。

たとえば、組織の仕組みや開発・運用ノウハウといった情報をそこにまとめておき、質問に対して適宜レスポンスをしてくれるようにしておけば、新入社員さんなどに「もし困ったら一旦このSlack Botに聞いてみてくださいね」といった感じである程度の部分は任せてしまう事ができるため、教育コストといった面でも相当楽になるんじゃないかなと。

世は大リモートワーク時代。業務のほとんどがオンラインで行われるようになった事で、「隣に座る同僚に対してラフに質問を投げかける」といったオフラインならではの行為が難しくなってしまいました。

どんな些細な疑問であってもわざわざかしこまった質問メッセージを作成しなければいけないとなると、精神的な負担も大きくなってしまいますよね。

そこで、「テンプレ的な回答が可能なものに関しては全てSlack Botにやらせてしまおう！」と考えたのがそもそもの始まりです。

## 完成イメージ

![interactive_bot.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/edf71c32-fcb6-08ff-e253-6d9336311b16.gif)

今回はサンプルとして「/animals」というスラッシュコマンドを作成してみました。

コマンドを実行すると「どの動物について知りたいですか？」とSlack Botから聞かれるので、いずれかのボタンを選択すると回答に合わせた結果（今回はWikipediaの記事）を返してくれます。

とてもシンプルですが、このロジックを上手く活用すれば簡易的なチャットボットのようなものが作れるはず。

## 仕様

- 言語: Ruby 2.7
- フレームワーク: Sinatra

今回はRubyとその軽量フレームワークであるSinatraを採用しました。

## 実装

前置きはほどほどに、早速実装していきましょう。

### Slackアプリの作成

まず、Slack Botを動かすための認証トークンなどを取得していきます。

![スクリーンショット 2021-08-08 7.15.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/42a6aa8f-5268-5e95-9e40-357a95929d6b.png)

[https://api.slack.com/apps](https://api.slack.com/apps)

↑のページからSlackアプリを作成しましょう。

![スクリーンショット 2021-08-08 7.18.51_censored.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/24863935-2700-47a1-de17-73f6a37b70a8.jpeg)

- App Name
    - 適当なアプリ名を入力
- Pick a workspace to develop your app in
    - 追加したいワークスペースを選択

![スクリーンショット 2021-08-08 7.24.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/7c07e453-c576-0492-eeb0-e8b6623975fe.png)

Slackアプリの作成が終わったら、左サイドバーの「OAuth & Permissions」をクリックし、Scopesの設定を行います。今回はスラッシュコマンドの実行とメッセージの送信ができれば良いので、

- commands
- chat:write

を追加してください。

![スクリーンショット 2021-08-08 7.26.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/cda0cb81-012c-48a3-f4ec-0073c7a7f4c8.png)

Scopesの設定が終わったら、「Install to WorkSpace」をクリック。

![スクリーンショット 2021-08-08 7.29.11_censored.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/4cc35ace-618a-3820-7b7b-d6cbfb1d2719.jpeg)

すると認証トークンが発行されるので、一旦メモなどに控えておきましょう。

![スクリーンショット 2021-08-08 7.35.16_censored.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/f88960c3-fd7a-48ca-adc8-bfba91c3d22a.jpeg)

あとはチャンネル設定の「インテグレーション」から「アプリを追加する」をクリックし、先ほど作ったSlackアプリを追加すればOKです。

![スクリーンショット 2021-08-08 7.37.45_censored.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/525770c0-051a-1d09-c85d-9fda4e3178b9.jpeg)

### サーバーの作成

次は、スラッシュコマンドが実行された後に結果を返すためのサーバーを作成していきましょう。

#### 作業ディレクトリ & 各種ファイルの作成

```
$ mkdir interactive-slack-bot && cd interactive-slack-bot
$ mkdir data
$ touch data/dog.json data/cat.json data/rabbit.json data/questions.json app.rb Gemfile Gemfile.lock .env
```

最終的に次のようなディレクトリ構成になっていればOKです。

```
interactive-slack-bot
└── data
    ├── cat.json
    ├── dog.json
    ├── questions.json
    └── rabbit.json
├── Gemfile
├── Gemfile.lock
├── app.rb
```

#### Rubyのバージョンを指定

```
$ rbenv local 2.7.1
```

※バージョン管理にrbenvを使っている事を想定。

お好みのバージョンで構いませんが、今回は2.7.1で動作確認済みなのでなるべく近いものにすると正常な動作が保証されると思います。

#### 各種gemのインストール

```rb:./Gemfile
# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem "sinatra"
gem "rack"
gem "rack-contrib"
gem "slack-ruby-bot"
gem "dotenv"
```

これから必要になるgemをインストールしましょう。

```
$ bundle install --path vendor/bundle
```

#### コード

```:./env
SLACK_BOT_TOKEN=xoxb-**************-**************-**************
```

```rb:./app.rb
require "sinatra"
require "slack-ruby-client"
require "json"
require "dotenv"

Dotenv.load

client = Slack::Web::Client.new(token: ENV["SLACK_BOT_TOKEN"])

# POSTメソッドで「/slack/command」にリクエストが来た場合の処理
post "/slack/command" do
  blocks = File.open("./data/questions.json"){ |j| JSON.load(j) }
  
  channel = params["channel_id"] # どのチャンネルから送信されたのかを取得
  user = params["user_id"] # どのユーザーから送信されたのかを取得

  # スラッシュコマンド実行者に対してのみ表示されるメッセージ（ephemeral）を作成
  client.chat_postEphemeral(
    channel: channel,
    user: user,
    blocks: blocks,
    as_user: true
  )

  return
end

# POSTメソッドで「/slack/command/answer」にリクエストが来た場合の処理
post "/slack/command/answer" do
  # Interactiveボタン押下の結果はpayloadという値で渡ってくる
  payload = JSON.parse(params[:payload])

  # case文でどのJSONを返すか分岐
  blocks = case payload["actions"][0]["value"]
    when "dog"
      File.open("./data/dog.json"){ |j| JSON.load(j) }
    when "cat"
      File.open("./data/cat.json"){ |j| JSON.load(j) }
    when "rabbit"
      File.open("./data/rabbit.json"){ |j| JSON.load(j) }
  end
  
  channel = payload["channel"]["id"] # どのチャンネルから送信されたのかを取得
  user = payload["user"]["id"] # どのユーザーから送信されたのかを取得

  # スラッシュコマンド実行者に対してのみ表示されるメッセージ（ephemeral）を作成
  client.chat_postEphemeral(
    channel: channel,
    user: user,
    blocks: blocks,
    as_user: true
  )

  return
end
```

```json:./data/questions.json
[
  {
    "type": "section",
    "text": {
      "type": "mrkdwn",
      "text": "どの動物について知りたいですか？"
    }
  },
  {
    "type": "actions",
    "elements": [
      {
        "type": "button",
        "text": {
          "type": "plain_text",
          "text": "犬について",
          "emoji": true
        },
        "value": "dog"
      },
      {
        "type": "button",
        "text": {
          "type": "plain_text",
          "text": "猫について",
          "emoji": true
        },
        "value": "cat"
      },
      {
        "type": "button",
        "text": {
          "type": "plain_text",
          "text": "兎について",
          "emoji": true
        },
        "value": "rabbit"
      }
    ]
  }
]
```

```json:./data/dog.json
[
  {
    "type": "image",
    "image_url": "https://www.pakutaso.com/shared/img/thumb/PPW_uturogenashibaken_TP_V.jpg",
    "alt_text": "image"
  },
  {
    "type": "section",
    "text": {
      "type": "mrkdwn",
      "text": "犬については次の記事を読んでください。"
    },
    "accessory": {
      "type": "button",
      "text": {
        "type": "plain_text",
        "text": "Click Me",
        "emoji": true
      },
      "url": "https://ja.wikipedia.org/wiki/%E3%82%A4%E3%83%8C"
    }
  }
]
```

```json:./data/cat.json
[
  {
    "type": "image",
    "image_url": "https://www.pakutaso.com/shared/img/thumb/AME19716064_TP_V.jpg",
    "alt_text": "image"
  },
  {
    "type": "section",
    "text": {
      "type": "mrkdwn",
      "text": "猫については次の記事を読んでください。"
    },
    "accessory": {
      "type": "button",
      "text": {
        "type": "plain_text",
        "text": "Click Me",
        "emoji": true
      },
      "url": "https://ja.wikipedia.org/wiki/%E3%83%8D%E3%82%B3"
    }
  }
]
```

```json:./data/rabbit.json
[
  {
    "type": "image",
    "image_url": "https://www.pakutaso.com/shared/img/thumb/USAGI0I9A6075_TP_V.jpg",
    "alt_text": "image"
  },
  {
    "type": "section",
    "text": {
      "type": "mrkdwn",
      "text": "兎については次の記事を読んでください。"
    },
    "accessory": {
      "type": "button",
      "text": {
        "type": "plain_text",
        "text": "Click Me",
        "emoji": true
      },
      "url": "https://ja.wikipedia.org/wiki/%E3%82%A6%E3%82%B5%E3%82%AE"
    }
  }
]
```

各JSONについては、 [Block Kit Builder](https://app.slack.com/block-kit-builder) を使って作成したものを使っています。簡単に言うと、Slackのメッセージをよりリッチな感じで作れるツールですね。

![スクリーンショット 2021-08-08 8.05.49_censored.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/4c9edad9-ba5f-4c51-ab06-cb505e3a8758.jpeg)

たとえば、「./data/questions.json」の内容だと↑こんな風なメッセージが送信できます。

ボタンだけでなくテキストフォームやチェックボックスなどたくさんのパーツがあるので、時間のある時にでも色々試してみてください。

### スラッシュコマンドの作成

![スクリーンショット 2021-08-08 8.13.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/15751eae-0e5e-21af-5697-a5dae9fe7ca1.png)

Slackアプリの設定画面を開き、左サイドバーの「Slash Commands」から「Create New Command」をクリック。

![スクリーンショット 2021-08-08 8.18.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/13e8cbd3-5889-b123-959e-6e7561df67e5.png)

- Command: スラッシュコマンド
    - /animals
- Request URL: スラッシュコマンドを実行した際に飛ぶリクエスト先URL
    - https://***********.ngrok.io/slack/command
    - localhostでは動かないため、ngrokを使って独自のドメインを割り当てる
    - 参照: [ngrokの利用方法](https://qiita.com/Marusoccer/items/7033c1bb9c85bf6789bd)
    - Sinatraのポート番号は4567なので「$ ngrok http 4567」
- Short Description: コマンドの説明
    - 動物のWikipedia記事を返すコマンド
- Usage Hint: 使い方のヒント
    - 空欄でOK。

各項目を入力し、保存してください。

これで今回の場合、「/animals」というスラッシュコマンドを実行すると「https://***********.ngrok.io/slack/command」にPOSTリクエストが飛ぶようになり、先ほど「./app.rb」ファイル内で定義した

```rb:./app.rb
# POSTメソッドで「/slack/command」にリクエストが来た場合の処理
post "/slack/command" do
  blocks = File.open("./data/questions.json"){ |j| JSON.load(j) }
  
  channel = params["channel_id"] # どのチャンネルから送信されたのかを取得
  user = params["user_id"] # どのユーザーから送信されたのかを取得

  # スラッシュコマンド実行者に対してのみ表示されるメッセージ（ephemeral）を作成
  client.chat_postEphemeral(
    channel: channel,
    user: user,
    blocks: blocks,
    as_user: true
  )

  return
end
```

この部分の処理が走るというわけですね。

### Interactivityの有効化

最後に、Interactivityの有効化を行います。

![スクリーンショット 2021-08-08 8.29.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/2d177f53-37f4-de4c-217e-97c58833dd8b.png)

今のままだと、送信されてきたメッセージ内のボタンを押した際にその内容をサーバーに伝える術がありません。つまり、ボタンを押しても何も起こらずエラーになってしまいます。

![スクリーンショット 2021-08-08 8.32.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/c9bc4534-4775-d5c1-2a7a-f6b2300ca53f.png)

これを解決するためには、Slackアプリ管理画面の左サイドバーの「Interactivity & Shortcuts」からInteractivityをONにする必要があります。

![スクリーンショット 2021-08-08 8.34.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/52864c71-cdb0-6e7f-801f-47e4512b2371.png)

- Request URL
    - https://***********.ngrok.io/slack/command/answer

ONに変更後、Request URLに上記のようなURLを入力しましょう。

すると、今後Interactiveなコンポーネント（先ほど [Block Kit Builder](https://app.slack.com/block-kit-builder) で作成したようなボタン、テキストフォーム、チェックボックスなど）に対してリアクションを起こした際には「https://***********.ngrok.io/slack/command/answer」にPOSTリクエストが飛ぶようになり、先ほど「./app.rb」内で定義した

```rb:./app.rb
# POSTメソッドで「/slack/command/answer」にリクエストが来た場合の処理
post "/slack/command/answer" do
  # ボタン押下の結果はpayloadという値で渡ってくる
  payload = JSON.parse(params[:payload])

  # case文でどのJSONを返すか分岐
  blocks = case payload["actions"][0]["value"]
    when "dog"
      File.open("./data/dog.json"){ |j| JSON.load(j) }
    when "cat"
      File.open("./data/cat.json"){ |j| JSON.load(j) }
    when "rabbit"
      File.open("./data/rabbit.json"){ |j| JSON.load(j) }
  end
  
  channel = payload["channel"]["id"] # どのチャンネルから送信されたのかを取得
  user = payload["user"]["id"] # どのユーザーから送信されたのかを取得

  # スラッシュコマンド実行者に対してのみ表示されるメッセージ（ephemeral）を作成
  client.chat_postEphemeral(
    channel: channel,
    user: user,
    blocks: blocks,
    as_user: true
  )

  return
end
```

この部分の処理が走るという仕組みです。

## 動作確認

全ての実装が完了したので、いよいよ動作確認です。

```
$ bundle exec ruby app.rb

[2021-08-08 08:44:35] INFO  WEBrick 1.6.0
[2021-08-08 08:44:35] INFO  ruby 2.7.1 (2020-03-31) [x86_64-darwin19]
== Sinatra (v2.1.0) has taken the stage on 4567 for development with backup from WEBrick
[2021-08-08 08:44:35] INFO  WEBrick::HTTPServer#start: pid=54712 port=4567
```

![スクリーンショット 2021-08-08 8.49.24_censored.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/625fafe7-89f9-0069-1650-aa0919b03f2f.jpeg)

Slack Botを追加したチャンネル内で「/animals」と入力し、

![スクリーンショット 2021-08-08 8.50.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/0f9cae1e-e055-4920-713e-fdfc540eb9e7.png)

こんな感じで対話できるようになっていれば成功です。

## あとがき

以上、簡易チャットボット風の対話形式Slack Botを作ってみました。リモートワーク中心の社会となった事で、Slackの活用は業務効率化のために欠かせないものになっていると感じます。

その気になればもっと便利な機能も実装できそうなので、この記事をベースにぜひとも色々と試してみてください。

https://github.com/kazama1209/interactive-slack-bot