Slack Botの種類と大まかな作り方

社内にSlackを導入したのでさっそくbotでの自動化のためにbotの作り方を調べました。

botは便利である反面、Slack上のメッセージの取得、社内固有の情報を取得・操作できるものでもあるため、迂闊に作成するとセキュリティ面で怖い部分もあります。
そのため、最初に仕組みと注意点を把握しておきたいと思い、以下に整理しました。


Bot・自動連携の種類
-----------

Slackの公式ドキュメントは以下にあります。

* Slack API
  * https://api.slack.com/

Slackとの連携の仕組みはいくつかあり、種類と特徴を以下の表にまとめます。

| 名前                          | 概要                                                                                           | メッセージ送受信方向 | 常時稼働 | Webサーバー | 公式文書                                                                           |
| ----------------------------- | ---------------------------------------------------------------------------------------------- | -------------------- | -------- | ----------- | ---------------------------------------------------------------------------------- |
| Incoming webhooks / 着信Webフック | Slackにメッセージを送信する。受信はできない                                                    | ツール ⇒ Slack      | 不要     | 不要        | [LINK](https://api.slack.com/incoming-webhooks)                                     |
| Slash command                 | 決まった命令(コマンド)で機能を起動する                                                         | ツール ⇔ Slack      | 必要     | 必要        | [LINK](https://api.slack.com/slash-commands)                                        |
| Bot users (簡易、RTM API利用) | 擬似ユーザのBotとメッセージを送受信して自動処理をさせる。簡易に作る場合。                      | ツール ⇔ Slack      | 必要     | 不要        | [LINK](https://api.slack.com/bot-users), [RTM API](https://api.slack.com/rtm)       |
| Bot users (Event API利用)     | BotをEvent APIを使って作る場合。                                                               | ツール ⇔ Slack      | 必要     | 必要        | [LINK](https://api.slack.com/bot-users), [Event API](https://api.slack.com/events-api) |
| Bot users (フル)              | Botに自動処理をさせる。メッセージ中でボタンやプルダウンなどを使ったinteractionを活用する場合。 | ツール ⇔ Slack      | 必要     | 必要        | 同上                                                                               |
| Unfurling links               | リンク展開機能。メッセージ中のURLの詳細情報をそのメッセージに自動で付与する                                    | ツール ⇔ Slack      | 必要     | 必要        | [LINK](https://api.slack.com/docs/message-link-unfurling#handling_event_deliveries) |

「Bot users」の作り方がいくつかあるため、別々に記述しています。簡易なものならWebサーバーは不要ですが、凝ったことをやろうとするとwebhookのためにwebサーバーとして稼働させる必要があります。

最後の「Unfurling links」(リンク展開)はいわゆるbotの自動処理などとは違うのですが、他のものより少し特殊なので別枠として表記しています。


Slackでは上記のような機能をひとまとめのパッケージとしたものを 「Slack App」というようです。
上記の Incoming webhooksや簡易なBotなどはworkspaceに対して単体でも設定できますが、一部の設定は Slack Appでなければ使えないものがあります。
一度単体で作成したBotを、あとから App に変換するようなことはできないようです。
印象としては、自動連携機能はSlack Appに統一していこうとしているのかもしれません。


Incoming webhooks
-------

![image.png](https://qiita-image-store.s3.amazonaws.com/0/47562/a725a5f5-5942-f901-8908-81d5683a3c86.png)

Incoming webhooksは、Slackに対してメッセージ送信のみができる機能です。
なんらかの定期バッチの完了をSlackメッセージとして投稿させるような使い方ができます。

使い方は簡単には以下のとおりです。(Slash App方式ではない方法です）

  1. 以下の Slash Appの 「Incoming webhooks」(着信webフック)のページを開く
    * https://slack.com/apps/A0F7XDUAZ--web-
    * これは対象のworkspaceの App管理 から 「Incoming webhooks」を探すと見つけることができます
  2. その中の「設定を追加」を開き、webhook設定を追加する
  3. 設定を追加するとwebhook用のURLが生成されるので、そのURLに指定の形式のJSONをPOSTする


この機能は単にHTTPのPOSTをするだけなので curlコマンドなどからも利用できます。

POSTするJSONの形式は以下を参照してください。このJSON形式は他のbot連携方式でメッセージ送信する場合でも共通です。

  * Basic message formatting | Slack
    * https://api.slack.com/docs/message-formatting

webhook設定をするときに送信先のチャンネルやアイコン、ユーザ名などを設定しますが、これらはあくまでデフォルト値であって、POSTするメッセージで逐次変更することができます。
そのため、１つのwebhookのURLを複数の用途で流用してもさほど問題はなさそうです。

webhookのURLに対する認証や制限といったことはできないようなので、このURLは外部に漏れるようなことなく管理する必要があります。
あくまで送信しかできないので、外部からSlack上の情報を抜き取られるようなことはできないと思います。
ですが、フィッシングURLを投稿して誘導するようなことはできるので注意が必要です。
（後述する Slack App の IP制限設定も効果がありません)


Slash command
-------

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47562/485a405c-ab01-df56-7b70-7ffa1901a4f6.png)

Slash commandは、Slack上で `/` の文字に続けて指定のコマンドを記述したメッセージを投稿することで、そのコマンドに対応した機能を起動するものです。
公式での `/remind` とか `/feed` などがこれにあたります。
ターミナルでのCLI環境のような感じのことをチャット上でできます。

Slash commandの大まかな流れを簡単にいうと以下の通りです。

  1. Slackからアクセスされるwebサーバーを作成して、インターネット上からアクセスできるようにする
  2. Slack Appの「Slash Commands」のページを開く
      * https://slack.com/apps/A0F82E8CA-slash-commands
  3. その中の「設定を追加」を開き、コマンド名とSlackがアクセスするURLなどを設定する
      * このとき設定するURLは、Step.1で作成したwebサーバーのもの
  4. 指定のコマンドを実行するメッセージがSlackに投稿されると、設定しておいたURLに対して投稿されたメッセージの内容がPOSTで渡される
  5. Step.1で作成した自作サーバーで受け取ったメッセージに応じて自動処理などを行い、返答メッセージなどを投稿する。
      * 結果のメッセージを返す方法は２つ
          * (1) SlackからアクセスされたPOSTリクエストに対するレスポンスにメッセージ文字列を返答する
              * Content-Typeが無指定や"text/plain"のときは返答がそのままメッセージ本文になる
              * Content-Typeが"application/json"で指定のJSON形式で返答すれば、メッセージの体裁を指定できる
          * (2) POSTで渡されたパラメータに"response_url"というキー名でSlack側のURLが渡されるので、そのURLに対してメッセージをPOSTする
              * 非同期で処理を実行したあとの返答などで利用する想定

Slash commandは、Slack内部で`/hoge_fuga`といったコマンドのメッセージを判定し、そのメッセージのみをwebサーバーに通知してくれます。
つまりは、Slack上でのすべてのメッセージが渡されるわけではなく、あくまでコマンドを起動するときのメッセージのみが渡されてきます。

このbotを作成するには、Slackからのメッセージを受け取れるようにするため、インターネット上にwebサーバーを公開する必要があります。URLを特定されないかぎりは、勝手に他者に利用されるようなことは基本的には無いはずですが、勝手にbotの機能を使われることがないように対策する必要があります。

公式ドキュメントを見た限りでは、Slack側のIPが公開されていないようなのでIP制限で不正アクセスを防止する方法は無いようです。代わりにSlackからのHTTPリクエストが正当なものであることを検証するための機能があります。

* Verifying requests from Slack | Slack
  * https://api.slack.com/docs/verifying-requests-from-slack

上記の紹介の記事を https://qiita.com/namutaka/items/20951da3dddd1750af4a に記載したので詳細はこちらを参照してください。


なお、以前の`Verification Token`を使ったリクエストの検証方式は、非推奨になったようです。 ( https://api.slack.com/docs/token-types#verification )


Bot users (簡易、RTM API利用)
-------

![image.png](https://qiita-image-store.s3.amazonaws.com/0/47562/66fceaa3-3380-4f2a-55e0-e63196edf737.png)

Botを作成する方法はSlackからのイベント情報の受け取り方の違いで大きく２つあります。
１つは、Real Time Messageing APIを使う方法で、もう一つはEvent APIを使う方法です。

Real Time Messaging APIは、BotアプリからSlackへWebSocketを使って接続するAPIです。
WebSocketを通じてSlackからイベント情報が送られてくるので、Botアプリ側にWebhookのURLを作成する必要がありません。

作成方法の概要としては以下の通りです。

  1. Slack Appの「Bots」のページを開く
      * https://slack.com/apps/A0F7YS25R-bots
  2. その中の「設定を追加」を開き、Botの名称などを入力してbot設定を追加する
  3. RTM APIを使ったBotサービスを作成して、起動する
    * APIへの接続時に、bot設定で払い出された [APIトークン]を指定する
    * Botからのメッセージの送信は、Web APIでもRTM APIでも利用できる

インターネットにwebサーバーを晒す必要がなく、作成手順としてもかなり気軽にbotを作成することができます。
反面、Slackの詳細な機能を使うことができないなどの制限があります。それについては後の章で説明します。

なお、もう一方のEvent API（webhookでイベント情報を受け取る方法）との違いを整理すると以下の通りです。

  * 受け取れるEventの種類が違う。
      * 参考: Event種別の定義 https://api.slack.com/events
      * どちらも良し悪しありますが、Event APIの方が「Botへのメンションのメッセージ受信」や Unfurlingのための「URLリンクがシェアされた」などの有用なイベントが使えます
  * あるBotを複数のworkspaceにインストールした場合、それぞれごとにWebSocketのコネクションを接続する必要がある
      > https://api.slack.com/bot-users
      > 「Alternatively, some bot users interact on a given workspace by connecting to the Real Time Messaging API (RTM API for short) and opening up a websocket connection with Slack. This is not ideal for bot users that will be installed in multiple workspaces as a websocket connection must be maintained for each instance.」

      * BotをSlack Appとして一般公開するような場合はこれが問題になってくると思う
  * RTM APIはbotアプリ側にwebhookが不要
  * RTM APIは 「Slack App」形式じゃなくても作成できるが、Event APIは「Slack App」でのみ利用可能
      * Eent APIは、Slack App形式なので受け取るEventを事前に設定し、Appをインストール時に認可が必要
      * RTM APIは何もせずともすべてのイベントを受け取る

Pythonでこの方式のBotは、以下のFrameworkがシンプルで簡単そうです。

  * lins05/slackbot
      * https://github.com/lins05/slackbot/

また、Slack公式のPython SDKでも以下にごく基礎的な実装方法の説明があります。

  * Slack Developer Kit for Python : Connecting to the Real Time Messaging API
      * https://slackapi.github.io/python-slackclient/real_time_messaging.html#connecting-to-the-real-time-messaging-api

Bot users (Event API利用)
-------

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47562/9be71833-36b4-b0a1-345a-6f380e72369b.png)

別のBotの作成方法は、Event APIを使う方法です。
これは、Botアプリ側にSlackからイベント情報を受け取るwebhookのURLを実装し、それをつかってSlackとインタラクションする方式です。
Botアプリの実装方法はSlash commandと同じような感じです。
Event APIを使う場合は、Slack Appの形式でbotを作成する必要があります。

実装の流れは以下のような感じです。

  1. Slackからアクセスされるwebサーバーを作成して、インターネット上からアクセスできるようにする
  2. 以下のページからSlack Appを新規に作成する
    * https://api.slack.com/apps
  3. [Event Subscriptions]のページで、[Enable Events]を"ON"にする
  4. Step.1で作成したwebhoook URLを [Request URL]に設定する
  5. ここで設定したURLは、正当性の検証が行われるので、所定の返答をするようにアプリを実装しておく
     * 詳細: https://api.slack.com/events-api#request_url_configuration__amp__verification
     * 所定の形式のJSONがSlackからPOSTされるので、それに対して適した内容をレスポンスすることで検証が成功する
  6. [Subscribe to Workspace Events]のところで受け取りたいイベントの種類を設定する
  7. この時点でEventを受け取る準備ができているが、Botとしてメッセージを送信するためにこの[Bot Users]ページでSlack AppにBotユーザを追加する
  8. 最後に、このAppをBotを利用するworkspaceにインストールする
    * インストール時に、Step.6で設定したイベントに応じた認可確認が行われる

Slackからwebhookへのアクセスは、Slash commandのときと同様にリクエストの正当性を確認します。
Event APIはSlackからのイベントを受け取るのみの仕組みですので、Botからメッセージを送信するときは、WebAPIを使ってメッセージを送信します。


Bot users (フル)
-------

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47562/3295172e-2c4d-9d53-2b47-c57717e837b0.png)

Slack上ではBotで利用できるいろいろな機能があります。
機能を活用したBotの設計方針は、以下の記事が参考になります。

  * Lessons Learned from Beep Boop’s new Slack bot – Slack Platform Blog – Medium
      * https://medium.com/slack-developer-blog/lessons-learned-from-beep-boops-new-slack-bot-bd250e8d9887

端的にいうと、Botとの会話のやりとりが多くなるとそのチャンネルがbotのメッセージで埋もれてしまうので、それを防ぐように機能を活用するべし、といった内容です。
それを実践するために以下のような機能を使うのがよいとのことです。

  * Botからの返答は「個人向け送信」(「あなただけに表示されています」とついたメッセージ）を活用する
      * Slackでは`postEphemeral`という機能名
  * 会話のやりとりではなく、メッセージ中にボタンやプルダウンなどを使って、インタラクションを簡潔にする
  * ユーザからの返答に対するリアクションは、新規メッセージではなく既存メッセージの文面を更新して対応する
  * 長期処理などの進捗などは新規メッセージではなく、既存メッセージを更新する
  * Botに指示した操作をキャンセルされたときはBotからの返答メッセージを削除する

これらの動作は、Slack公式のSlash Commandの `/remind` の動作が参考になります。

この中の「メッセージ中のボタンやプルダウン」を使う方法は、以下のドキュメントで説明されています。

  * Interactive messages | Slack
      * https://api.slack.com/interactive-messages

ボタンなどを利用するには Slack AppとしてBotを作成する必要があります。
その上で、ボタンやメニューなどをクリック・選択したイベントを受け取るためのwebhookを作成する必要があります。
これは、Event APIとはまた別物として作成します。

ざっとした作成方法は以下のような感じです。
(公式文書の https://api.slack.com/interactive-messages#lifecycle_of_a_typical_interactive_message_flow の方が詳細です)

  1. 自作のBotアプリに、インタラクションイベントを受けとるwebhookを作成する
  2. 作成したSlack Appについての設定メニューの[Interactive Components]のページを開く
  3. Step.1でのURLを[Request URL]に設定する
  4. Botから送信するメッセージJSONに`actions`という指定でボタンなどを設置する

    ```
    {
      "callback_id": "tender_button", <= 任意文字列。操作の種類を識別する
      "actions": [
          {
              "type": "button",   <= 操作の種類を定義された文字列で指定
              "name": "this_button",  <= name,valueの組で操作を識別する
              "value": "clicked_this_button"
              "text": "Click This Button", <= ボタンに表示するテキスト
          }
      ]
    }
    ```
  5. 作成したWebhookにSlackからボタンのイベントが渡されてくる
    * イベントに対する返答は以下の文書に定義されたJSONで指定する
        * https://api.slack.com/docs/interactive-message-field-guide#message
        * 新規メッセージ送信、既存メッセージ更新、メッセージ削除などができる
    * このJSONをwebhookへのリクエストに対するレスポンスとして返すか、渡されたイベント内容中の`response_url`に対してPOSTする

Slackからwebhookへのアクセスは、この場合もSlash commandのときと同様にリクエストの正当性を確認します。


Unfurling links / リンク拡張
-------

SlackにメッセージとしてURLを投稿すると、自動でそのメッセージにURLの詳細情報が付与されることがあります。
この、URLの詳細を付与する機能が `Unfurling links / リンク拡張 `です。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/47562/73401f81-1ab2-7663-a479-23daaa52979f.png)

これは、実際には Event API の使い方の一種ですが、特殊な機能なので個別に取り上げています。

リンク拡張 の流れは以下のような感じになります。

  1. Eent APIと同様にwebhookとして動作するwebサービスを作成する
  2. Slack Appを新規に作成し、[Event Subscriptions]を"ON"にしてwebhookURLを設定する
  3. 同じ画面の[Subscribe to Workspace Events]の章で、"link_shared" イベントを追加する
      * 同時に、OAuth permission scopeに、"links:read"(リンクがあるメッセージを取得する権限)が追加される
  4. 同じ画面の[App Unfurl Domains]の章で、リンク拡張するURLのドメインを追加する
  5. 作成したSlack Appの[OAuth & Permissions]設定画面を開く
  6. [Scopes]の章で、"links:write"を追加する
      * "links:write"は既存メッセージにリンクの詳細を追記するための権限
  7. 自作のwebサービス側でwebhookを受け取ったときに、link_shared イベントを判別する
      * 受信するjsonの構造は https://api.slack.com/events/link_shared を参照
      * typeが２重構造になっているのに注意
  8. リンクの詳細情報を取得するための別スレッドを起動し、webhookのレスポンスは素早く 200 OKを返答する
      * 他のwebhookと違ってレスポンスの本文でJSONを指定することはできない
      * webhookのHTTPリクエストはすぐに返答しないといけないので、リンクの詳細情報を調べる処理は別スレッドなどで実行する
  9. Web APIの [`chat.unfurl` メソッド](https://api.slack.com/methods/chat.unfurl)を使ってリンクの拡張情報を送信する
      * このとき、APIにtokenを送信するが、これはBot User用の[Bot User OAuth Access Token]ではなく、[OAuth Access Token]を使うことに注意

リンク拡張は、Botユーザとしての動作ではないので前述までのBotとしてのTokenの扱いや権限の扱いとは違うのが注意点です。

まとめ
------------

SlackにはBot向けに機能が豊富にあります。
機能によって実装方法や設定などが色々とことなっているので少しややこしい感じがあります。
ネット上でBotの作り方などの説明を読む時も、上記のどのタイプについてのことなのかを意識すると理解しやすくなるかと思います。

Botを作成するときは、できることならSlackのフル機能を生かした形で作りたいところです。
ボタンやプルダウンなどのインタラクションも含めた形で作るとすると、簡易なRTM APIではなく、Event API主体での実装方式になりそうです。
個人的には、できるならWebサービスを公開しなくてよい、RTM API方式ですべてが簡潔できるとありがたいとは思いつつも、OAuthの権限制御などで難しいのかもしれません。

ただ、一方で、社内などの閉じた環境で自分たちのためだけに使うBotであれば、多少邪魔なメッセージが混ざっても我慢することにして、簡易な方法でやりたいことを作った方がいいのかもしれません。

Bot作成にあたっては、１から自作するより既存のBot用Frameworkを使う方が便利だし、簡単だと思います。
どのFrameworkを使うかを検討するときは、ここで説明したどの方式でSlackと連携するのか、Slackのどの機能を使えるのか、Slackのセキュリティ機能(Signing Secretでのチェック)が使えるのか、といったことを確認すると良いと思います。

補足
-------------

### SlackのメッセージID

Slackでは、個々のメッセージを識別するIDはメッセージの送信日時(timestamp ID: `ts`)が使われるようです。
Botから送信したメッセージを後で書き換えるには Web APIの [`chat.update`メソッド](https://api.slack.com/methods/chat.update)を使うことができますが、更新対象のメッセージをこの`ts`で指定するためため、最初にBotからメッセージを送信したときにその`ts`を取得しておくことが必要です。
Bot Frameworkによっては、メッセージ送信時にこの`ts`を受け取れないものがあります。


### Slackが提供するIP制限
SlackではSlack側のAPIにアクセスするIPを制限する機能があります。

  * Slack - Safely Storing Credentials - Restricting token use by IP address
      * https://api.slack.com/docs/oauth-safety#ip_whitelisting

これは自分のSlack AppのOAuth Tokenを利用できるIPアドレスを制限することができます。
もしOAuth Tokenが漏洩（もしくは社員が自宅から使おうとするとか)した場合に、それをSlack側のWeb APIに対して利用できるIPを制限できます。

設定は、作成したSlack Appの[OAuth & Permissions]のページで、[Restrict API Token Usage]の章でアクセス可能なIPアドレスを指定できます。

### Webhookでのデータの受け取り方の違い

前述のとおり、Slackでは webhookを利用する機能が Slack Command、Event API、ボタンなどのインタラクションの３種類あります。
今回の記事を書くにあたって試しにそれぞれを実装してみたのですが、なぜかそれぞれのwebhookでSlackからのリクエストの渡され方が違いました。

* Slack Command
    * Content-Type: application/x-www-form-urlencoded
    * 通常のFormの形式でデータが渡される
    * Python Flaskでの実装例: `data = request.form`
* Event API
    * Content-Type: application/json
    * リクエストのボディに直接JSON文字列が渡される
    * Python Flaskでの実装例: `data = request.json`
* ボタン等のインタラクション
    * Content-Type: application/x-www-form-urlencoded
    * 通常のFormの形式で "payload"というパラメータに対する値にJSON文字列がある
    * Python Flaskでの実装例: `data = json.loads(request.form["payload"])`

歴史的経緯なのかもしれませんが、実装するときにはちょっと戸惑います。


### ngrokについて

Slackの公式ドキュメントにて、webhookを開発するときにSlack側から手元で開発中のwebhookにアクセスをつなぐために `ngrok`というツールを使うことが紹介されています。

* Using ngrok to develop locally for Slack | Slack
    * https://api.slack.com/tutorials/tunneling-with-ngrok

これは一種のwebサービスで、インターネット上のngrok.ioドメインへのアクセスをローカルで起動しているサーバーへバイパスしてくれるツールです。
閉じたネットワーク上にある開発中のサーバーを手軽に一時的にインターネット上に公開できるのでかなり便利です。

ただこの動作は、極端にはSSHのportforwardのようなもので、インターネットからのアクセスを閉じたネットワーク内に引き込むことになるので、セキュリティ的には危険な行為になると思います。
会社内で利用するなら社内のルールなどに注意した方がいいと思います。
また、利用するときはngrokを長期的に起動しつづけることのないように注意した方が良さそうです。
（起動するたびに、ドメイン名がランダムに決定されるので、ドメインがわからなければ不正にアクセスはされないはず）

## 更新履歴

*追記: 2019-07-12*

* `Verification Token`の説明を、`Verifying requests`(`Signing Secretでの検証`)に修正