Slack
slackbot

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

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

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

Bot・自動連携の種類

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

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

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

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

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

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

Incomming webhooks

image.png

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

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

  1. 以下の Slash Appの 「Incomming webhooks」(着信webフック)のページを開く
  2. その中の「設定を追加」を開き、webhook設定を追加する
  3. 設定を追加するとwebhook用のURLが生成されるので、そのURLに指定の形式のJSONをPOSTする

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

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

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

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

Slash command

image.png

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

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

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

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

Slackからメッセージを受け取れるようにするため、インターネット上にwebサーバーを公開する必要があります。
URLを特定されないかぎりは、勝手に他者に利用されるようなことは基本的には無いはずですが、一般的なwebサービスとしてのセキュリティを考慮した方がよいと思います。
公式ドキュメントを見た限りでは、Slack側のIPを指定したIP制限をする方法は無いようでした。
代わりに、Verification Tokenを使ったアクセス元の制限をする方法が提供されています。
Slack上でSlach commandの設定を行うと、Verification Tokenというランダム文字列が生成されます。
そのTokenがSlackからのPOSTリクエストに含まれるので、期待する文字列でなければ不正アクセスと判断できます。

Bot users (簡易、RTM API利用)

image.png

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

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

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

  1. Slack Appの「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がシンプルで簡単そうです。

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

Bot users (Event API利用)

image.png

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

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

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

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

Bot users (フル)

image.png

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

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

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

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

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

ボタンなどを利用するには 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で指定する
    • このJSONをwebhookへのリクエストに対するレスポンスとして返すか、渡されたイベント内容中のresponse_urlに対してPOSTする

Slackからwebhookにアクセスがきたときは、この場合もSlash commandのときと同様にVerification Tokenでアクセスの正当性を確認します。

Unfurling links / リンク拡張

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

image.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 イベントを判別する
  8. リンクの詳細情報を取得するための別スレッドを起動し、webhookのレスポンスは素早く 200 OKを返答する
    • 他のwebhookと違ってレスポンスの本文でJSONを指定することはできない
    • webhookのHTTPリクエストはすぐに返答しないといけないので、リンクの詳細情報を調べる処理は別スレッドなどで実行する
  9. Web APIの 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作成にあたっては、1から自作するより既存のBot用Frameworkを使う方が便利だし、簡単だと思います。
どのFrameworkを使うかを検討するときは、ここで説明したどの方式でSlackと連携するのか、Slackのどの機能を使えるのか、Slackのセキュリティ機能(Verification Tokenチェック)が使えるのか、といったことを確認すると良いと思います。

補足

SlackのメッセージID

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

Slackが提供するIP制限

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

これは自分の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、ボタンなどのインタラクションの3種類あります。
今回の記事を書くにあたって試しにそれぞれを実装してみたのですが、なぜかそれぞれの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側から手元で開発中のwebookにアクセスをつなぐために ngrokというツールを使うことが紹介されています。

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

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