Slack ペイロードに含まれる response_url を完全に理解する

## response_url を理解しよう

この記事では、Slack アプリを作ったことがある人でもあまり馴染みがないかもしれない [`response_url`](https://api.slack.com/interactivity/handling#message_responses) という仕組みについて網羅的に説明してみたいと思います [^1_01]。

[^1_01]: 日本語ではおそらく初めて

`response_url` は、スラッシュコマンドやショートカットのようなユーザーと Slack アプリの間で直接的なインタラクションが発生する機能のペイロードに含まれるものです。[Incoming Webhooks](https://api.slack.com/messaging/webhooks) や [chat.postMessage](https://api.slack.com/methods/chat.postMessage)で Slack に通知を送るだけの連携からもう一歩進んで、よりインタラクティブな Slack アプリを作るとき、この機能をうまく使うと、より良いユーザー体験を実現できるでしょう。

なお、この記事では日本語でできる限り丁寧に説明していきますが、こちらの英語のドキュメントにも多くの内容は書かれていますので、合わせて参考にしてみてください。
https://api.slack.com/interactivity/handling#message_responses

それでは、はじめましょう。読んでくれた方全員が「**response_url 完全に理解した！** [^1_02]」と思ってもらえることが、この記事のゴールです。

[^1_02]: ミームではなく、文字通りの意味で :smile:

## response_url とは？
response_url は、一言で言えば「**Incoming Webhooks と同じ要領でメッセージ送信にだけ使用できる URL**」なのですが、できるだけ正確かつ端的に定義するならば・・

**Slack から送信されるペイロードに含まれうる
特定のユーザーとチャンネルに紐づいていて
有効期限・利用回数上限がある
「あなただけに表示されています」なメッセージも送信可能な URL**

です！

長いし・・わかりやすくは・・ないですよね。ここから、一つ一つ紐解いていきましょう。

### Slack から送信されるペイロードに含まれうる

インタラクティブな Slack アプリをつくるとき、公開された HTTP エンドポイントを用意して、あらかじめその URL を Slack アプリの設定内の **Request URL** という項目に指定しておきます。そうすることで Slack の API サーバーが対象のイベントが発生したときにその情報を送信してきます。

ここでの「ペイロード」とは、その Request URL に送られてくる JSON データのことです。[^3_01]

[^3_01]: 2021 年初旬に「ソケットモード」という WebSocket でこのペイロードを受け取り、応答も WebSocket 経由で返せる新しい方式がリリースされますが、その場合にも同じ構造の JSON データが送られてきます

そして、この記事のテーマである `response_url` は、この JSON データの一部として送信されてくる URL です。例えば、以下の[メッセージショートカット](https://api.slack.com/interactivity/shortcuts/using#getting_started)のペイロード例 [^3_02] であれば `https://hooks.slack.com/app/T111/123/xyz` が `response_url` となります。

[^3_02]: かなり簡略化しています

```json
{
  "type": "message_action",
  "action_ts": "1607081569.852971",
  "team": {
    "id": "T111",
  },
  "user": {
    "id": "W111",
  },
  "channel": {
    "id": "C1111",
  },
  "callback_id": "unique-id-for-message-shortcut",
  "trigger_id": "111.222.xyz",
  "response_url": "https://hooks.slack.com/app/T111/123/xyz",
  "message_ts": "1607081410.000900",
  "message": {
    "text": "<@W111> さん！このメッセージは say() を使って送信しました。",
    "ts": "1607081410.000900"
  }
}
```

では、どのような機能を使ったときに `response_url` を受け取れるのでしょうか？以下にインタラクティブな機能と `response_url` の有無を整理してみました。

|機能名|response_url 有無|詳細|
|---|---|---|
|[スラッシュコマンド（Slash Commands）](https://api.slack.com/interactivity/slash-commands)|:o: 常に有|スラッシュコマンドは必ずチャンネル内で実行されるので、常にチャンネルに紐づいた `response_url` がペイロードに含まれます|
|[メッセージショートカット（Message Shortcuts）](https://api.slack.com/interactivity/shortcuts/using#getting_started)|:o: 常に有|メッセージショートカットは必ずチャンネル内のメッセージを指定して起動されるので、そのチャンネルに紐づく `response_url` がペイロードに含まれます|
|[ボタンやセレクトメニューのインタラクション (Block Kit)](https://api.slack.com/block-kit)|:o: メッセージは有<br/><br/>:x: モーダル・ホームタブは無|そのブロックがどこに配置されているかによります。|
|[グローバルショートカット（Global Shortcuts）](https://api.slack.com/interactivity/shortcuts/using#global_shortcuts)|:x: 常に無|グローバルショートカットはチャンネル以外でも実行されるので、特定のチャンネルに紐づく `response_url` はペイロードに含まれません|
|[モーダルでのデータ送信（View Submissions）](https://api.slack.com/surfaces/modals/using#modal_response_url)|:x: 基本は無<br/><br/>:o: 特定条件下で `response_urls` として有|`response_url_enabled: true` な input block を持つ場合のみ `response_urls` が含まれます|
|[イベント API（Events API）](https://api.slack.com/events-api)|:x: 常に無|ユーザーとの**直接的な**インタラクションではない Events API では `response_url` が含まれることがありません|

端的には、Slack ワークスペースのエンドユーザーが、**チャンネル（DM も含む）内で**何かコマンド的な操作を行ったとき、そのペイロードに含まれます。

そのため、[Block Kit](https://api.slack.com/block-kit) の UI コンポーネントの場合、**メッセージ内にある**ボタンのクリックやセレクトメニューからアイテムを選択したときのみ含まれます。モーダルやホームタブの場合は発行されません。

一方、グローバルショートカットの場合は、検索ウィンドウから実行したときだけでなく、チャンネル内のメッセージ投稿エリアから起動した場合にも `response_url` は発行されません。グローバルショートカットでの対処法については後述します。

### 特定のユーザーとチャンネルに紐づいている

次に、ユーザー・チャンネル（DM も含む）との紐づけについてです。

既に上で説明した通りですが `response_url` は、チャンネルに紐づくユーザーアクションが発生したときのみ発行されるものです。上記の JSON ペイロード例で言えばチャンネルは `C111` で、ユーザーは `W111` ということになります。

`response_url` を使うと、その対象のチャンネルでのみメッセージを投稿できるようになりますが、それ以外の権限は一切付与されません[^4_01]。

[^4_01]: 投稿したメッセージのメタデータなども `response_url` だけでは取得できません

そして `response_url` は、チャンネルだけでなく、そのアクションを発生させたユーザーにも紐づいています。このユーザーのコンテキスト情報を持っているため、**Slack の画面上で「あなただけに表示されています」と表示されているメッセージ**も送信できます。

### 有効期限・利用回数上限がある

`response_url` には使用回数の上限と有効期限があります。

一つ一つの `response_url` は、それぞれ **30 分以内に 5 回まで**使用することができます。この時間内であれば、非同期に利用することが可能です。しかし、データベースに保存しておいて翌日の日次バッチ処理で使用する、といったことはできません。

また 5 回まで利用できるのは、一つの URL で 5 件までメッセージが送信できるようにするためというよりは（それも可能ではありますが）、後述する更新処理を想定した利用許可となっています。

### 「あなただけに表示されています」なメッセージも送信可能な URL

「あなただけに表示されています」と表示されているメッセージは API ドキュメントでは **[エフェメラルメッセージ（ephemeral message）](https://api.slack.com/messaging/managing#ephemeral)** と呼ばれます。

「ephemeral」は日本語で言えば「一時的な」とか「つかの間の」といった意味である通り、一定時間が経過すれば、そのメッセージは表示されなくはなります。時限式で消えるというよりは Slack クライアントをリフレッシュしても消えるなど揮発性のある一時的なメッセージですね。

すでに軽く説明しましたが、`response_url` は、このエフェメラルメッセージを送信するために使えます。もちろん、エフェメラルメッセージだけでなく、普通のチャンネル上でのメッセージも投稿できます。これを切り替えるためには `response_type` という項目を設定します。通常のメッセージとして送信する場合は `"in_channel"` を指定します。後ほどのコード例でも紹介します。

## 扱い方をコード例で理解する

ということで、まずは `response_url` とは何なのかを説明しました。ここからは、コード例とともにその使い方ともう少し詳細な内容を解説していきます。

コード例には Slack の公式フレームワークである [Bolt for Python](https://slack.dev/bolt-python/tutorial/getting-started) を使います。Python に馴染みがない方には申し訳ないですが・・どれもシンプルなコード例なので、きっと理解しやすいはずです。

### プロジェクトのセットアップ

Python 3.6 以上を使ってください。最近はあらかじめ入っている Python も `python3` は 3.6 以上であることがほとんどだと思います。`python3 --version` で確認しておきましょう。

#### requirements.txt

依存ライブラリは [`slack_bolt`](https://pypi.org/project/slack-bolt/) だけで試せます。[^5_01]　GitHub のソースコードをダウンロードして使用したい場合は https://github.com/slackapi/bolt-python/releases にアクセスしてください。

[^5_01]: 細かい話ですが `slack-bolt` と書いても大丈夫です。`slack_bolt` が本来の名前なのですが PyPI はどちらも解釈します。

```
slack_bolt>=1.4,<2
```

`requirements.txt` を配置したら以下を実行してみてください。Windows OS をお使いの方は、こちらの[Python 公式ガイド](https://docs.python.org/3.8/library/venv.html)を参考にしてみてください。


```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -U pip
pip install -r requirements.txt
```

#### main.py

こちらのコードが雛形になりますので、そのままコピペして試せます。

```python
import logging
from logging import Logger
from slack_bolt import App, Ack, BoltResponse, BoltContext, Respond, Say
from slack_sdk import WebClient
from typing import Callable, Dict, List

logging.basicConfig(level=logging.DEBUG)

# export SLACK_BOT_TOKEN=xoxb-***
# export SLACK_SIGNING_SECRET=***
app = App()

# ngrok を使っている場合 http://localhost:4040/inspect/http でも確認できますが
# リスナーのログと同時に見るためにペイロードを標準出力に表示するだけの middleware です
@app.use  # または @app.middleware でも可
def log_request(logger: Logger, body: dict, next: Callable[[], BoltResponse]):
    logger.info(body)
    next()

#
# ここに今から解説するコードをそのまま持ってくれば動作します
#

if __name__ == "__main__":
    app.start(3000)  # POST http://localhost:3000/slack/events
```

このコードだと変更する度に反映には再起動が必要ですが、自動反映させたい場合は Flask の開発モードを使いましょう（FastAPI などの他のフレームワークでも構いません）。 `requirements.txt` に `Flask>=1.1` を追加して `app.start(3000)` の代わりに以下のようにするとよいでしょう。

```python
if __name__ == "__main__":
    # app.start(3000)
    from flask import Flask, request
    from slack_bolt.adapter.flask import SlackRequestHandler
    flask_app = Flask(__name__)
    handler = SlackRequestHandler(app)

    @flask_app.route("/slack/events", methods=["POST"])
    def slack_events():
        return handler.handle(request)

    flask_app.run(port=3000, debug=True)
```

これで基本的なアプリの雛形の準備ができました。次は Slack アプリとしての設定をしていきましょう。

#### Slack アプリの権限を設定して、インストール

このアプリを動作させるために必要な Slack アプリ設定をしていきます。

まず https://api.slack.com/apps?new_app=1 にアクセスして、そこから Slack アプリを作成します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/7ea303f1-ef44-12c4-2119-350f478dce8b.png" width=300/>
アプリ設定画面が表示されたら **OAuth & Permissions** では、**Bot Token Scopes** にアクセスして、以下のように権限を設定してください。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/666e5609-54c2-4965-cd8b-f27b9c4ecb41.png" width=400 />
とりあえず、これだけ終わったら **Settings** > **Install App** にあるインストールボタンからワークスペースにインストールします。正常に完了すると **Bot User OAuth Access Token** が発行されます。インストール完了時の画面に表示されている `xoxb-` という形式で始まる長めの文字列です。

#### 環境変数を設定してアプリを起動

アプリの設定画面にある

* **Settings** > **Install App** にある **Bot User OAuth Access Token** 
* **Settings** > **Basic Information** にある **Signing Secret**

を、以下の環境変数で設定します（Windows OS の場合は `setx` などに読み換えてください）。

```bash
export SLACK_BOT_TOKEN=xoxb-***
export SLACK_SIGNING_SECRET=***
```

そして、いよいよアプリを起動してみましょう。エラーが特に表示されなければ大丈夫です。

```bash
source .venv/bin/activate
python3 main.py
```

別のターミナルで [ngrok](https://ngrok.com/) を立ち上げます。

```bash
ngrok http 3000
```

なお、このコマンドで立ち上げた場合、毎回サブドメインは変わります（一定時間が経過しても変わります）。ngrok の有料プランを契約すると、固定することもできます。

```bash
ngrok http 3000 --subdomain your-own-fixed-domain
```

これでここからのコード例を試す準備が整いました。最初のテーブルで説明した、各機能と `response_url` の関係性、扱い方の詳細を実際に試していきましょう。

### スラッシュコマンド

スラッシュコマンドは、必ずチャンネル内で実行されるため、チャンネルに紐づいた `response_url` が常にペイロードに含まれます。

試すために、まずは Slack アプリの設定画面でスラッシュコマンドの設定をつくりましょう。**Features** > **Slash Commands** のセクションで `/test-response-url` という名前のものを、有効な Request URL とともに設定します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/40cc4e65-f122-fe34-efec-eec55bbfa01b.png" width=500 />
準備ができたので、コード例をみていきます。

```python
# スラッシュコマンドのリスナー
@app.command("/test-response-url")
def tell_response_url(ack: Ack, body: dict, respond: Respond):
    # 必ず存在します
    assert body.get("response_url") is not None

    # タイムアウトにならないように空の ack() を呼び出して 200 OK を即応答
    # 引数を渡せば、これを使って返信を投稿することもできます
    ack()

    # respond 関数にはペイロード内の response_url が自動的に設定されています
    respond("このメッセージ送信は response_url を使って送信されました！")
```

Bolt を使っている場合は `respond()` という関数に `response_url` があらかじめ紐づけられます。`text` や `blocks` を渡せばメッセージの内容を指定できます。

もしエフェメラルではないメッセージを送信したいなら `response_type="in_channel"` も合わせて指定することができます。

```python
    respond(
        response_type="in_channel",
        text="このメッセージ送信は response_url を使って送信されました！"
    )
```

このようにスラッシュコマンドでは、常に `response_url` を使うことができます。

なお、コメントにも書いてある通り、HTTP レスポンスで応答を返す `ack()` でも同様に返信することができます。

「・・・ん、この `ack()` と `respond()` で返信するときの違いって何？」と思われた方、視点が鋭いですね。その違いについては、[こちらの説明](https://qiita.com/seratch/items/ed29acd565af36e65072#%E9%9D%9E%E5%90%8C%E6%9C%9F%E3%81%A7%E8%BF%94%E4%BA%8B%E3%82%92%E3%81%A7%E3%81%8D%E3%82%8B)を参照してください。

### メッセージショートカット

次はメッセージショートカットです。メッセージショートカットは、チャンネル内のメッセージを指定して起動されるので、そのチャンネルに紐づく `response_url` が必ずペイロードに含まれます。スラッシュコマンドと同様 Bolt では `respond()` 関数を使うだけで `response_url` での送信ができます。

Slack アプリの設定は **Features** > **Interactivity & Shortcuts** の画面で **Request URL** を設定した上で、グローバル・メッセージのショートカットをそれぞれ一つずつ設定します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/4c1c62ee-5360-e284-1b21-d42d631cfe07.png" width=500 />
**Name** は何でもよいのですが、以下のコード例をそのまま動かしたい場合は **Callback ID** がそれぞれ **"global-shortcut"** と **"message-shortcut"** でなければ、動作しないので注意して設定してください。

では、コード例をみていきましょう。

```python
# メッセージショートカットのリスナー
# Callback ID が "message-shortcut" の Message Shortcut を設定しておきます
@app.shortcut("message-shortcut")
def message_shortcut(ack: Ack, body: dict, respond: Respond):
    # 必ず存在します
    assert body.get("response_url") is not None

    # タイムアウトにならないように空の ack() を呼び出して 200 OK を即応答
    # 引数を渡せば、これを使って返信を投稿することもできます
    ack()

    # respond 関数にはペイロード内の response_url が自動的に設定されています
    respond("このメッセージ送信は response_url を使って送信されました！")
```

スラッシュコマンドとほぼ同様ですね。特に難しいことはないかとは思います。

### ボタンやセレクトメニューのインタラクション

Slack アプリの設定は前のメッセージショートカットが使える状態になっていれば、それ以外に特にありません。**Interactivity & Shortcuts** が有効になっていて **Request URL** が有効になっていれば OK です。

以下は、メッセージ内のボタン、セレクトメニューの例です。この場合は `response_url` が必ず存在することを期待できます。

```python
@app.command("/test-response-url")
def test_response_url(ack: Ack):
    ack(
        text="ボタンやセレクトメニューのテストです",
        blocks=[
            {
                "type": "actions",
                "elements": [
                    # これらのボタン・セレクトメニューを操作すると
                    # block_actions というイベントが発生します
                    # この場合は必ず response_url が発行されます
                    {
                        "type": "button",
                        "action_id": "button",
                        "text": {
                            "type": "plain_text",
                            "text": "クリック！",
                        },
                        "value": "start",
                    },
                    {
                        "type": "users_select",
                        "action_id": "users-select",
                        "placeholder": {
                            "type": "plain_text",
                            "text": "ユーザーを選ぶ",
                        },
                    },
                ]
            }
        ],
    )

@app.action("button")
def start(ack: Ack, body: dict, action: dict, respond: Respond):
    assert body.get("response_url") is not None
    ack()
    # メッセージ内のボタンから来たので常に response_url が存在します
    respond(f"受け取った内容: {action}", replace_original=False)

@app.action("users-select")
def start(ack: Ack, body: dict, action: dict, respond: Respond):
    assert body.get("response_url") is not None
    ack()
    # メッセージ内のセレクトメニューから来たので常に response_url が存在します
    respond(f"受け取った内容: {action}", replace_original=False)
```

`replace_original=False` を指定しているのは、ボタン・セレクトメニューを表示している元のメッセージを上書きしないためのオプションです。デフォルトは `True` で上書きするようになっているので `False` を指定しています。

このように「あなただけに表示されています」メッセージも含め、元のメッセージを上書きできるのが `response_url` の一つのメリットですが、この辺の詳細は最後のパートで説明しています。

一方で、ボタンやセレクトメニューを含む `actions` ブロックがモーダル内にある場合、チャンネルと紐づいていないため、`response_url` は発行されません。

```python
@app.command("/test-response-url")
def test_response_url(ack: Ack, body: dict, client: WebClient):
    ack()
    client.views_open(
        trigger_id=body["trigger_id"],
        view={
            "type": "modal",
            "callback_id": "modal-id",
            "title": {"type": "plain_text", "text": "チャンネルを指定するモーダル"},
            "submit": {"type": "plain_text", "text": "送信"},
            "close": {"type": "plain_text", "text": "キャンセル"},
            "blocks": [
                {
                    "type": "actions",
                    "elements": [
                        {
                            "type": "button",
                            "action_id": "button",
                            "text": {
                                "type": "plain_text",
                                "text": "クリック！",
                            },
                            "value": "start",
                        },
                        {
                            "type": "users_select",
                            "action_id": "users-select",
                            "placeholder": {
                                "type": "plain_text",
                                "text": "ユーザーを選ぶ",
                            },
                        },
                    ]
                }
            ],
        },
    )

@app.action("button")
def start(ack: Ack, body: dict):
    # response_url は存在しません
    assert body.get("response_url") is None
    ack()

@app.action("users-select")
def start(ack: Ack, body: dict):
    # response_url は存在しません
    assert body.get("response_url") is None
    ack()
```

つまり Bolt アプリで `respond()` を使用する `block_actions` イベントを発行するブロックをメッセージとモーダルで共有すると、モーダルの場合だけエラーになりますので、注意が必要です。

### グローバルショートカット・モーダル送信

次は、グローバルショートカットの例をみていきましょう。Slack アプリの設定方法は先のメッセージショートカットのところを参考にしてください。

グローバルショートカットは、チャンネル内のメッセージ投稿フォームから起動できるので、直感的には `response_url` がペイロードに含まれていそうにも思えますが、実際はそうではありません。グローバルショートカットは、検索ウィンドウからも起動可能で、実行したユーザーが今いる場所がチャンネルであるとは限らないためです。先ほどの Block Kit の例とは異なり、こちらは起動地点に依らず、一貫して `response_url` が発行されません。

とはいえ、ショートカットからモーダルを開いて、処理が完了したらチャンネルに通知したいというのはよくあるユースケースでしょう。

このニーズに対応するために Slack は `"response_url_enabled": true` というオプションを提供しています。これのオプションを設定した `conversations_select` タイプのブロックをモーダル内に一つ置いておくと、選択されたチャンネルと送信したユーザーに紐づく `response_url` が発行されるという仕組みです。さらに `"default_to_current_conversation": true` というオプションを併用すると「今いる場所がチャンネルだった場合、初期値をそのチャンネルにしておく」ということもできます[^6_01]。

[^6_01]: チャンネルにいないときは未設定になります

以下のコード例が、実際にそのようなモーダルを開く例です。

```python
# グローバルショートカットのリスナー
# Callback ID が "global-shortcut" の Global Shortcut を設定しておきます
@app.shortcut("global-shortcut")
def global_shortcut(ack: Ack, body: dict, client: WebClient):
    # グローバルショートカットはチャンネル以外でも実行されるので
    # 特定のチャンネルに紐づく response_url はペイロードに含まれません
    assert body.get("response_url") is None

    # タイムアウトにならないように空の ack() を呼び出して 200 OK を即応答
    # スラッシュコマンドとは違って、チャンネルに紐づいていないので
    # この ack() に text / blocks を渡しても返信として投稿はされません
    ack()

    # データ送信後に指定されたチャンネルにメッセージを投稿して通知することができるモーダルを開きます
    client.views_open(
        trigger_id=body["trigger_id"],
        view={
            "type": "modal",
            "callback_id": "modal-id",
            "title": {"type": "plain_text", "text": "チャンネルを指定するモーダル"},
            "submit": {"type": "plain_text", "text": "送信"},
            "close": {"type": "plain_text", "text": "キャンセル"},
            "blocks": [
                {
                    "type": "input",
                    "block_id": "current_channel",
                    "element": {
                        "type": "conversations_select",
                        "action_id": "input",
                        # response_urls を発行するためには
                        # このオプションを設定しておく必要があります
                        "response_url_enabled": True,
                        # 現在のチャンネルを初期値に設定するためのオプション
                        "default_to_current_conversation": True,
                    },
                    "label": {
                        "type": "plain_text",
                        "text": "起動したチャンネル",
                    },
                }
            ],
        },
    )
```

次に、このモーダルの送信ボタンが押された時に呼び出されるリスナー関数の定義です。

受け取ったペイロード全体を示す `body` には `response_urls` というキーで配列のデータが含まれます。ただ、将来の拡張を想定して配列になってはいますが、2022 年 4 月時点では、要素は必ず一つだけになります。[^6_02]

[^6_02]: response_url_enabled: true を二つのブロックで指定すると `{'ok': False, 'error': 'invalid_arguments', 'response_metadata': {'messages': ['[ERROR] response_url_enabled field can be used on a single block element in a view [json-pointer:view/blocks/1/element]']}}` というエラーが返されます

```python
# モーダルからのデータ送信を受け付けるリスナー
# 上記のリスナーのように response_url_enabled: True を指定されている
# conversations_select な element を持つ input block が存在していることが前提です
@app.view("modal-id")
def view_submission(ack: Ack, body: dict):
    # response_urls は 2022 年 4 月時点では基本的に一つだけ要素を持ちます
    # モーダル内で指定されたチャンネルに紐づいた response_url が入ります
    assert body.get("response_url") is None
    assert body.get("response_urls") is not None
    ack()

    response_urls: List[Dict[str, str]] = body.get("response_urls")

    # response_urls は配列ですが、2022 年 4 月時点で要素一つを含むパターンしかありません
    # そのため、一つ目の要素を取得するコードで問題ありません
    first_channel = response_urls[0]
    # もちろん、以下のようにキーで参照しても構いません
    current_channel = [
        ru for ru in response_urls
        if ru["block_id"] == "current_channel"
    ][0]
    assert first_channel == current_channel

    # 配列の要素は dict になっていて response_url のキーで URL を取得できます
    response_url = current_channel["response_url"]

    # ここでは仕組みを説明するために手動で Respond 関数を初期化していますが、次の例のように簡潔に書くことができます
    respond = Respond(response_url=response_url)
    respond("このメッセージ送信は response_url を使って送信されました！")
```

この記事が書かれた 2020 年 12 月時点では Bolt はこの URL を自動的に `respond()` に設定しなかったため、`Respond` オブジェクトの初期化を利用側のコードで行う必要がありました。

2021 年 4 月にリリースされた [v1.5.0](https://github.com/slackapi/bolt-python/releases/tag/v1.5.0) で Bolt for Python は `@app.view` リスナー内の `respond` ユーティリティに対応しました。

```python
# モーダルからのデータ送信を受け付けるリスナー
# 上記のリスナーのように response_url_enabled: True を指定されている
# conversations_select な element を持つ input block が存在していることが前提です
@app.view("modal-id")
def view_submission(ack: Ack, body: dict, respond: Respond):
    ack()
    respond("このメッセージ送信は response_url を使って送信されました！")
```

ちなみに、実行したユーザーへの DM を送れば十分というユースケースなら、送信ユーザーの ID は元々データ送信リクエストのペイロードに含まれていますので、以下のようなコードで十分かもしれません。

```python
@app.view("modal-id")
def handle(ack: Ack, body: dict, client: WebClient):
    ack()
    client.chat_postMessage(
        # chat.postMessage だけは channel に user_id を指定できます
        channel=body["user"]["id"],
        text="送信ありがとうございます！処理が完了したらお知らせします。"
    )
```

### イベント API

最後に、イベント API の場合をみてみましょう。ユーザーとの**直接的なインタラクションではない** Events API では、ペイロードに `response_url` が含まれることは**ありません**。`respond()` 関数も実行しようとすると実行時例外になります。

Slack アプリの設定は **Features** > **Event Subscriptions** セクションで設定します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/17e5460e-155e-4709-f3dd-fdec86f932f2.png" width=400 />
このセクションだけは **Request URL** 設定時に URL の死活確認が来ます。あらかじめ `SLACK_BOT_TOKEN` と `SLACK_SIGNING_SECRET` を設定してアプリと ngrok を起動し、その URL がリクエストを受けられる状態にしておいてください。

では、コード例をみてみましょう。

```python
# アプリが参加しているチャンネルのメッセージイベントのリスナー
# channels:history scope を追加して bot user をチャンネルに invite しておく必要があります
@app.event("message")
def events(body: dict, say: Say, context: BoltContext):
    # 常に存在しません
    assert body.get("response_url") is None

    # また Events API の場合は ack() を Bolt 側が自動で行う & もし手動で呼べたとしても
    # 常にチャンネルに紐づいたイベントとも限らないので ack() に text を渡して返信するといったことは
    # Bolt の仕様・実装の制約ではなく、仕組み上できません

    if context.channel_id:
        # もしチャンネルに紐づいているイベントであれば context.channel_id が存在しているはずで
        # その場合は say 関数（chat.postMessage のラッパーです）にそのチャンネルが自動で設定されています
        # （message event の場合は必ず say が使えますが、他の event の場合は常にそうとは限りません）
        say(f"<@{context.user_id}> さん！このメッセージは `say()` を使って送信しました。")
```

あるイベントがチャンネルで発生していて、イベントを受信したときに、そのチャンネルにメッセージを投稿したい場合、そのチャンネル ID を指定して [chat.postMessage](https://api.slack.com/methods/chat.postMessage) API を使用します。Bolt では、あらかじめ `context.channel_id` を紐づけておいてくれる `say()` という関数を利用すると楽でしょう。「あなただけに表示されています」なメッセージにしたい場合は [chat.postEphemeral](https://api.slack.com/methods/chat.postMessage) API メソッドを使うことができます。

## 結局 response_url を使うと何が嬉しいのか

ここまで、さまざまな具体例を見てきました。`response_url` の挙動はだいぶわかってきたのではないかと思います。「なるほど、仕組みは分かったけれど、どういうときに使うとよいのか？どんなメリットがあるのか？」と疑問に思われている方もいるでしょう。

ということで、最後に `response_url` を使うと何が嬉しいのか、3 点ポイントを紹介します。

* 非同期で返事をできる
* メンバーではない会話で返信ができる
* 「あなたにだけ表示されています」なメッセージの更新ができる

ここまで理解しておけば、もう本当に完璧です！

### 非同期で返事をできる

コード例で度々「`ack()` でも返信できる」と書いてあるのを見かけたかと思います。以下のスラッシュコマンドの例を見てみてください。この二つの処理はユーザー目線だと体験は何も変わりません。どちらも「あなただけに表示されています」なメッセージを応答として返してきます。

```python
@app.command("/request")
def test_response_url(ack: Ack):
    ack("受け付けました！")

@app.command("/request")
def test_response_url(respond: Respond):
    ack()
    respond("受け付けました！")
```

この `ack()` と `respond()` の違いは「**3 秒後以降に返信できるかどうか**」です。

Slack アプリには Slack からのリクエストに 3 秒以内に応答しなければならないという仕様があります。公式ドキュメントでは、以下で説明されている内容です。

>Always send an acknowledgment response.
>As soon as your app receives the interaction payload, a countdown begins, because this message will self-destruct in 3 seconds. If your app doesn't respond with an HTTP status 200 OK within 3000ms of receiving the payload, the person who used the shortcut will see a generic error message letting them know that something went wrong.

https://api.slack.com/interactivity/shortcuts/using#shortcut_response

`ack()` は Slack からのリクエストに対して HTTP レスポンスとして、同期的に応答を返します。

一方、`respond()` は先ほど出てきたように 30 分以内であれば、後からでも利用できます。このように非同期で応答できるのが `response_url` の大きなメリットです。

`ack()` は何もメッセージを投稿せずに、処理が終わってから `respond()` で通知してもよいですし `ack("受けつけました")` のような一時返答だけ `ack()` でやって、その後 `respond()` を併用しても OK です。

この挙動を確かめるなら以下のようなコードを実行してみてください。スラッシュコマンド自体の実行はタイムアウトのエラー表示となり `ack()` で返したメッセージも反映されません。一方で `respond()` のメッセージは 5 秒後に正常に投稿されるはずです。

```python
import time

@app.command("/test-response-url")
def test_response_url(ack: Ack, respond: Respond):
    time.sleep(5)
    ack("このメッセージを君が読むことはないだろう...")
    respond(text="5 秒かかってしまいましたが、お知らせです！")
```

このように HTTP レスポンスでのメッセージ投稿の制約を補完する形で `response_url` を利用することができます。

### メンバーではない会話で返信ができる

例えば、自分一人だけの DM にアクセスして、そこでスラッシュコマンドを実行したとしましょう。

この DM にはあなた以外いないので、当然ながら、アプリのボットユーザーもそこにはいません。このような場合、`chat.postMessage` や `chat.postEphemeral` でメッセージを投稿することはできません。ボットユーザーとして `chat.*` を実行するためには、そのボットユーザーが、その会話（チャンネル・DM）のメンバーである必要があるからです。そうでない場合は Web API コールに対して `channel_not_found` というエラーコードが返されます。[^7_01]

[^7_01]: 正確には `chat:write.public` 権限を持っていて、対象のチャンネルがパブリックチャンネルであれば、メンバーにならなくても chat.postMessage でメッセージを投稿できますが、プライベートチャンネルや DM ではそのような手段もありません。

しかし、スラッシュコマンドなどのユーザーインタラクションで `response_url` が発行された場合、その場合に限ってのみ、ボットユーザーがメンバーではないチャンネルや DM に対しても、応答としてのメッセージを残すことが可能になります。

実際に試してみましょう。以下のようなスラッシュコマンドを用意して[^7_02]、自分自身との DM で実行してみてください。

[^7_02]: `/test-response-url` を使いまわしてもかまいません

結果は、最後の `say()` のメッセージだけが届かず、エラーになります。

```python
@app.command("/run-anywhere")
def run_anywhere(ack: Ack, respond: Respond, say: Say):
    # 成功
    ack("HTTP レスポンスで送信しました！")
    # 成功
    respond("このメッセージは response_url を使って送信しました！")
    # channel_not_found エラーが返されます
    say("chat.postMessage を使っているので、このメッセージは届きません...")
```

見ての通り、このメリットについては `response_url` に限ったものではなく、HTTP レスポンスで返信するとき（Bolt でいう `ack()` がこれをします）にも当てはまります。

### 「あなたにだけ表示されています」なメッセージの更新ができる

先程少し紹介しましたが、[chat.postEphemeral](https://api.slack.com/methods/chat.postEphemeral) API を使って「あなたにだけ表示されています」なメッセージ（エフェメラルメッセージ）を送信するコードを書いたことがあるかもしれません。

この API には一つの制約があります。それは、一度送信したエフェメラルメッセージは、アプリ側の API 操作だけで書き換えたり削除したりすることができないという点です。

しかし、`response_url` であれば、条件によっては、エフェメラルメッセージの更新も可能です。以下の例を試してみましょう。

```python
import time

@app.command("/test-response-url")
def test_response_url(ack: Ack, respond: Respond):
    # ここでは respond で最初のエフェメラルメッセージを投稿していますが
    # ack() で投稿してもかまいません
    ack()
    respond(
        text="テストです",
        blocks=[
            {
                "type": "section",
                "text": {"type": "mrkdwn", "text": "テストです"},
                "accessory": {
                    # このボタンを押されたイベントの response_url をテストします
                    "type": "button",
                    "action_id": "start-button",
                    "text": {
                        "type": "plain_text",
                        "text": "処理を依頼",
                    },
                    "value": "start",
                },
            }
        ],
    )
```

このボタンが押されたときのハンドラーです。

```python
# ボタンが押されるとこのリスナーが呼び出されます
@app.action("start-button")
def start(ack: Ack, respond: Respond):
    ack()
    # デフォルトで replace_original=True になっています
    # 新しいメッセージとして投稿したい場合は
    # respond(text="処理中...", replace_original=False)
    respond("処理中...")

    # 時間のかかる処理
    time.sleep(3)

    # 再びメッセージを更新します
    respond("完了しました！")
```

このアプリを動かしてみると以下のような挙動になります。新しいエフェメラルメッセージを投稿せずに一つのメッセージが動的に切り替わっていますね。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/ead78e14-feda-14de-25f7-b82a92376c70.gif" width=500 />
なお、削除したい場合は `delete_original=True` を指定します。

一点、よくあるハマりポイントですが、これらのオプションは `repsonse_url` を発行するユーザーイベントが**すでに存在するメッセージ内**で発生している場合のみ利用可能であることに注意してください。ですので、以下のような場合に利用できるオプションではありません[^7_03]。

[^7_03]: 紛らわしいのですが、エラーにはならずにただ無視され、新しいメッセージが投稿されます

```python
@app.command("/test-response-url")
def test_response_url(ack: Ack, respond: Respond):
    ack()

    # このメッセージを差し替える手段はありません
    # もしこのメッセージが blocks を使っていてボタンなどを持っている場合
    # このリスナー内ではなく、別の @app.action なリスナー内で書き換えることができます
    respond("最初のメッセージです！")

    respond(
        text="これは最初のメッセージに書き換えにならず、新規メッセージとなります...",
        replace_original=True # このオプションは無視されます
    )
```

`replace_original`/`delete_original` は「**メッセージ内の**セレクトメニューやボタンのインタラクションが発生したときに使えるオプション」と覚えておくとよいでしょう。

あと、`"in_channel"` な普通のメッセージについては `chat.update` API でも更新できるので`response_url` が唯一の手段というわけではありませんが、こちらに対しても replace_original / delete_original は同様に動作します。

## まとめ

以上です！だいぶ長い記事になってしまいましたが、「**これを読んで response_url を理解できた！**」という方がいれば嬉しく思います。

そして、もし `response_url` でわからないことが出てきたら、この記事を読み返してみてください。私が思いつく限りの疑問には全て答える記事にしたつもりです。

最後に、ポイントを振り返っておきましょう。

`response_url` とは、メッセージの送信に使える URL で、以下のような特徴を持っています。

* Slack から送信されるペイロードに含まれうる
* 特定のユーザーとチャンネルに紐づいている
* 有効期限・利用回数上限を持っている
* 「あなただけに表示されています」なメッセージも送信可能な URL

そして、それを利用するメリットは以下の 3 点です。

* 非同期で返事をできる
* メンバーではない会話で返信ができる
* 「あなたにだけ表示されています」なメッセージの更新ができる

理解があいまいなところは、もう一度読み返してみてください。

フィードバックや質問があれば、気軽にコメントでお知らせください！それでは :wave: