Slack Python SDK でチャンネルにメッセージを投稿しよう

## slack-sdk を使おう

「[Python で Slack API や Webhook を扱うなら公式 SDK（slack-sdk/slack-bolt）を使おう](https://qiita.com/seratch/items/8f93fd0bf815b0b1d557)」シリーズの記事です。公式 SDK についての概要、他にも記事がリンクされていますので、[こちらのエントリーページ](https://qiita.com/seratch/items/8f93fd0bf815b0b1d557)にもアクセスしてみてください。

## チャンネルにメッセージを投稿する

この記事では `slack-sdk` PyPI パッケージを使って Slack のチャンネル、DM にメッセージを投稿するコーディングについて解説します。以下の内容を説明していきます。

* `chat.postMessage` API でメッセージを投稿
    * とりあえず投稿してみる
    * ユーザーに成り代わって投稿
    * `text` / `attachments` / `blocks` の違い
* Incoming Webhooks でメッセージを投稿
* メッセージを更新・削除
* エフェメラルメッセージを投稿
* ユーザーインタラクションに対して返信

盛りだくさんになっていますが、いきなりすべてを理解する必要はありませんので、必要になったときにこの記事を読み返してみてください。


## `chat.postMessage` API でメッセージを投稿

それでは早速 [`chat.postMessage`](https://api.slack.com/methods/chat.postMessage) API の使い方から始めていきましょう。まずこの API を使うためには [`chat:write`](https://api.slack.com/scopes/chat:write) という OAuth スコープが必要となります。この権限を持つアプリの設定は以下のようになります。早速、[こちらのリンク](https://api.slack.com/apps?new_app=1&manifest_yaml=_metadata%3A%0A++major_version%3A+1%0A++minor_version%3A+1%0Adisplay_information%3A%0A++name%3A+chat.postMessage+App%0Afeatures%3A%0A++app_home%3A%0A++++messages_tab_enabled%3A+true%0A++++messages_tab_read_only_enabled%3A+false%0A++bot_user%3A%0A++++display_name%3A+chat.postMessage+Bot%0Aoauth_config%3A%0A++scopes%3A%0A++++bot%3A%0A++++++-+chat%3Awrite%0A++++++%23+%E3%81%93%E3%81%AE%E8%A8%98%E4%BA%8B%E3%81%A7%E3%81%AF%E8%A9%B3%E7%B4%B0%E3%82%92%E8%A7%A3%E8%AA%AC%E3%81%97%E3%81%BE%E3%81%9B%E3%82%93%E3%81%8C%E3%80%81%E3%81%93%E3%81%AE%E6%A8%A9%E9%99%90%E3%81%8C%E3%81%82%E3%82%8C%E3%81%B0+bot+user+%E3%81%AF+public+%E3%83%81%E3%83%A3%E3%83%B3%E3%83%8D%E3%83%AB%E3%81%AB%E9%99%90%E3%82%8A%E3%80%81%E3%83%81%E3%83%A3%E3%83%B3%E3%83%8D%E3%83%AB%E3%81%AB%E5%8F%82%E5%8A%A0%E3%81%9B%E3%81%9A%E3%81%AB%E3%83%A1%E3%83%83%E3%82%BB%E3%83%BC%E3%82%B8%E3%82%92%E6%8A%95%E7%A8%BF%E3%81%A7%E3%81%8D%E3%82%8B%E3%82%88%E3%81%86%E3%81%AB%E3%81%AA%E3%82%8A%E3%81%BE%E3%81%99%0A++++++-+chat%3Awrite.public%0A++++++%23+%E3%81%93%E3%81%AE%E8%A8%98%E4%BA%8B%E3%81%A7%E3%81%AF%E8%A9%B3%E7%B4%B0%E3%82%92%E8%A7%A3%E8%AA%AC%E3%81%97%E3%81%BE%E3%81%9B%E3%82%93%E3%81%8C%E3%80%81%E3%82%A2%E3%83%97%E3%83%AA%E3%81%8B%E3%82%89%E3%81%AE%E6%8A%95%E7%A8%BF%E3%81%A7%E3%81%AF%E3%81%82%E3%82%8A%E3%81%BE%E3%81%99%E3%81%8C+username+%2F+icon_emoji+%2F+icon_url+%E3%82%92%E5%A4%89%E6%9B%B4%E3%81%97%E3%81%A6%E8%A6%8B%E3%81%9F%E7%9B%AE%E3%82%92%E5%A4%89%E3%81%88%E3%82%8B%E3%81%93%E3%81%A8%E3%81%8C%E3%81%A7%E3%81%8D%E3%82%8B%E3%82%88%E3%81%86%E3%81%AB%E3%81%AA%E3%82%8A%E3%81%BE%E3%81%99%0A++++++-+chat%3Awrite.customize%0A)から作成してみてください。

```yaml
_metadata:
  major_version: 1
  minor_version: 1
display_information:
  name: chat.postMessage App
features:
  app_home:
    messages_tab_enabled: true
    messages_tab_read_only_enabled: false
  bot_user:
    display_name: chat.postMessage Bot
oauth_config:
  scopes:
    bot:
      - chat:write
      # この記事では詳細を解説しませんが、この権限があれば bot user は public チャンネルに限り、チャンネルに参加せずにメッセージを投稿できるようになります
      - chat:write.public
      # この記事では詳細を解説しませんが、アプリからの投稿ではありますが username / icon_emoji / icon_url を変更して見た目を変えることができるようになります
      - chat:write.customize
```

作成できたら「Install to Workspace」ボタンから Slack ワークスペースにインストールすると **Settings** > **Install App** のページ内の「Bot User OAuth Token」という名前で `xoxb-` で始まるトークンが発行されているかと思います。これを `SLACK_BOT_TOKEN` という名前で環境変数に設定しておいてください。

```bash
export SLACK_BOT_TOKEN=xoxb-111-222-xxxx
```

以上で、この記事のサンプルを動かすための Slack アプリの設定は完了です。Python プロジェクトを設定してコードを実行していきましょう。

この記事のコードは全て Python 3.6 以上で動作します。`python3 --version` でバージョンを確認してください。この記事では [Poetry](https://python-poetry.org/) を使って説明していきます。インストールしていない人は[こちら](https://python-poetry.org/docs/)を参考にしてください。

それではプロジェクトを作っていきましょう。

```bash
# 新しいプロジェクトを作成
poetry new chat_postMessage_app
cd chat_postMessage_app/

# 必要に応じて poetry env use 3.9 など

# virtualenv を有効化
poetry shell

# slack-sdk を追加
poetry add slack-sdk
```

これでひとまず完了です。

### とりあえず投稿してみる

まずはボットユーザーとしてメッセージを投稿してみましょう。以下のソースコードを `bot.py` として保存してください。

```python
import logging, os

# デバッグレベルのログを出力します
logging.basicConfig(level=logging.DEBUG)

# Web API クライアントを初期化します
from slack_sdk import WebClient
client = WebClient(os.environ["SLACK_BOT_TOKEN"])

# chat.postMessage API を呼び出します
response = client.chat_postMessage(
    channel="#random",
    text=":wave: こんにちは！",
)
```

実行が成功すると以下のような出力になるはずです。

```
(.venv) bash-3.2$ export SLACK_BOT_TOKEN=xoxb-111-222-xxx

(.venv) bash-3.2$ python bot.py
DEBUG:slack_sdk.web.base_client:Sending a request - url: https://www.slack.com/api/chat.postMessage, query_params: {}, body_params: {}, files: {}, json_body: {'text': ':wave: こんにちは！', 'channel': '#random'}, headers: {'Content-Type': 'application/json;charset=utf-8', 'Authorization': '(redacted)', 'User-Agent': 'Python/3.9.1 slackclient/3.5.1 Darwin/20.4.0'}
DEBUG:slack_sdk.web.slack_response:Received the following response - status: 200, headers: { ... }, body: {'ok': True, 'channel': 'C111', 'ts': '1622017093.007700', 'message': {'bot_id': 'B111', 'type': 'message', 'text': ':wave: こんにちは！', 'user': 'U111', 'ts': '1622017093.007700', 'team': 'T111', 'bot_profile': {'id': 'B111', 'deleted': False, 'name': 'chat.postMessage App', 'updated': 1622016343, 'app_id': 'A111', 'icons': {'image_36': 'https://a.slack-edge.com/111/img/plugins/app/bot_36.png', 'image_48': 'https://a.slack-edge.com/111/img/plugins/app/bot_48.png', 'image_72': 'https://a.slack-edge.com/111/img/plugins/app/service_72.png'}, 'team_id': 'T111'}}}
```

そして Slack ワークスペースの `#random` チャンネルに以下のようなメッセージが投稿されているはずです！
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/651399e4-6f7f-85e7-305c-c3a66a23263e.png" width=400 />
うまくいきましたか？おめでとうございます :tada: 

### ユーザーに成り代わって投稿する

次は、このアプリをインストールしてくれたユーザーに成り代わってメッセージを投稿してみましょう。これをするには先ほどの OAuth スコープだけでは不十分です。**Features** > **OAuth & Permissions** のページへ移動して **User Token Scope** に `chat:write` を追加してください。これは先程と名前は同じですが、ユーザーに成り代わるためのものです。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/cf23491d-729b-8fc3-3979-76a202764881.png" width=500 />

新しい権限が追加されたので、黄色いバーが上部に表示されています。リンクをクリックするか「Reinstall to Workspace」ボタンから再度インストールしてください。

なお、これによって App Manifest の当該部分は以下のように変わっています。

```yaml
oauth_config:
  scopes:
    user:
      - chat:write
    bot:
      - chat:write
      - chat:write.public
      - chat:write.customize
```

インストールが終わると、**Settings** > **Install App** のページ内では、これまでの「Bot User OAuth Token」に加えて「User OAuth Token」が追加されているかと思います。値は `xoxp-` から始まります。ここではこちらのトークンを使います。`SLACK_USER_TOKEN` という名前で環境変数に設定しましょう。

```bash
export SLACK_USER_TOKEN=xoxp-111-222-333-xxx
```

そして、以下のコードを `user.py` として保存しましょう。

```python
import logging, os

# デバッグレベルのログを出力します
logging.basicConfig(level=logging.DEBUG)

# Web API クライアントを初期化します
from slack_sdk import WebClient
client = WebClient(os.environ["SLACK_USER_TOKEN"])

# chat.postMessage API を呼び出します
response = client.chat_postMessage(
    channel="#random",
    text=":wave: こんにちは！",
)
```

同じように実行してみます。

```bash
export SLACK_USER_TOKEN=xoxp-111-222-333-xxx
python user.py
```

今回はユーザーとして投稿されているはずです！
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/1816999c-f931-300b-c027-1e84c72b68cc.png" width=400 />
なお、トークンはメソッド呼び出し単位で上書き・指定することができます。

```bash
# デフォルトはボットトークン
client = WebClient(os.environ["SLACK_BOT_TOKEN"])

response = client.chat_postMessage(
    # ここだけはユーザートークンを使う
    token=os.environ["SLACK_USER_TOKEN"],
    channel="#random",
    text=":wave: こんにちは！",
)
```

また、`WebClient` をインスタンス化するときにトークンは未指定でも構いません。この場合は必ずメソッド呼び出し時に `token` パラメーターを渡すようにしてください。

```python
client = WebClient()

# トークンを指定しないと {'ok': False, 'error': 'not_authed'} になる
client.auth_test()
```

### `text` / `blocks` / `attachments` の違い

ここまでの例では `text` でメッセージを指定してきました。ここからはもっとリッチなメッセージを投稿してみましょう。

|プロパティ名|説明|
|-|-|
|`text`|[こちらのテキストフォーマットルール](https://api.slack.com/reference/surfaces/formatting)を使用できるテキストフォーマット。`blocks` や `attachments` がある場合には指定しなくてもメッセージを投稿できますが、モバイル通知にはこちらのテキストが使われたり、Slack の検索インデックスへの登録などで使用されますので、設定することが推奨されています。また、[Bolt のメッセージのリスニング](https://slack.dev/bolt-python/ja-jp/concepts#message-listening)のコード例は、この `text` が適切に設定されていることが前提となっています（ユーザーからの投稿は必ず `text` が設定されています）。|
|`blocks`|[ブロックキット](https://api.slack.com/block-kit)と呼ばれるよりリッチなインターフェースを JSON 形式で簡単に定義するための仕様に則った「ブロック」の配列です。[ブロックキットビルダー](https://app.slack.com/block-kit-builder)を使えば、簡単に表示のプロトタイピングができ、完成した `blocks` はそのまま `chat.postMessage` や Incoming Webhooks に渡すことができます。また、`blocks` は多くの場合、チャットのメッセージだけでなく、[ホームタブ](https://api.slack.com/surfaces/tabs/using)や[モーダル](https://api.slack.com/surfaces/modals/using)の中にそのまま組み込むことができます。|
|`attachments`|[旧来のリッチインターフェース](https://api.slack.com/reference/messaging/attachments)で、**現在は `blocks` の利用を推奨しています**。多くのことはブロックキットの方が柔軟にできますが、カラーバーを使用するためにこちらを使用しているアプリを見かけることはあります。また、若干トリッキーですが、実は [`attachments` の中にブロックキットを含める](https://app.slack.com/block-kit-builder#%7B%22attachments%22:%5B%7B%22blocks%22:%5B%7B%22type%22:%22section%22,%22text%22:%7B%22type%22:%22mrkdwn%22,%22text%22:%22Hello,%20Assistant%20to%20the%20Regional%20Manager%20Dwight!%20*Michael%20Scott*%20wants%20to%20know%20where%20you'd%20like%20to%20take%20the%20Paper%20Company%20investors%20to%20dinner%20tonight.%5Cn%5Cn%20*Please%20select%20a%20restaurant:*%22%7D%7D,%7B%22type%22:%22divider%22%7D,%7B%22type%22:%22actions%22,%22elements%22:%5B%7B%22type%22:%22button%22,%22text%22:%7B%22type%22:%22plain_text%22,%22text%22:%22Farmhouse%22,%22emoji%22:true%7D,%22value%22:%22click_me_123%22%7D,%7B%22type%22:%22button%22,%22text%22:%7B%22type%22:%22plain_text%22,%22text%22:%22Kin%20Khao%22,%22emoji%22:true%7D,%22value%22:%22click_me_123%22,%22url%22:%22https://google.com%22%7D,%7B%22type%22:%22button%22,%22text%22:%7B%22type%22:%22plain_text%22,%22text%22:%22Ler%20Ros%22,%22emoji%22:true%7D,%22value%22:%22click_me_123%22,%22url%22:%22https://google.com%22%7D%5D%7D%5D%7D%5D%7D)こともできたりします。ブロックキットビルダーに加えて、元々存在する[メッセージビルダー](https://api.slack.com/docs/messages/builder)を使ってプレビューすることもできます。|

ここでは `text` と `blocks` を両方指定したメッセージを投稿してみましょう。[私の以前の記事](https://qiita.com/seratch/items/628751be65de9eb23a80)でホームタブやモーダルの日本語のメッセージテンプレートを紹介していましたが、今回も日本語で実際にありそうなメッセージを作ってみました。

```python
import logging, os

logging.basicConfig(level=logging.DEBUG)
from slack_sdk import WebClient

client = WebClient(os.environ["SLACK_BOT_TOKEN"])
response = client.chat_postMessage(
    channel="#random",
    text="はじめまして、田中さん :wave: このアプリは、Slack を離れることなく、ミーティング、イベントの予定を管理することをお手伝いします。このアプリを使えば以下のことが簡単>にできるようになります。\n• ミーティングを設定する\n • 参加者を管理する\n • ミーティングの変更通知を受け取る\nこれらの機能を利用するためには、まずあなたのカレンダーサービスとこのアプリを接続する必要があります。以下のボタンから設定を行ってください :bow:",
    blocks=[
        {"type": "section", "text": {"type": "mrkdwn", "text": "はじめまして、田中さん :wave:"}},
        {
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": "このアプリは、Slack を離れることなく、ミーティング、イベントの予定を管理することをお手伝いします。このアプリを使えば以下のことが簡単にできるようになります。",
            },
        },
        {
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": "• ミーティングを設定する\n • 参加者を管理する\n • ミーティングの変更通知を受け取る",
            },
        },
        {
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": "これらの機能を利用するためには、まずあなたのカレンダーサービスとこのアプリを接続する必要があります。以下のボタンから設定を行ってください :bow:",
            },
        },
        {
            "type": "actions",
            "block_id": "actions-block",
            "elements": [
                {
                    "type": "button",
                    "action_id": "connect_with_calendar",
                    "text": {"type": "plain_text", "text": "カレンダーアプリと連携する"},
                    "style": "primary",
                    "value": "configure",
                }
            ],
        },
    ],
)
```

以下のようなメッセージが無事投稿されたでしょうか？
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/e60337c9-46f2-7799-77ed-3cf5d8545df6.png" width=700 />
「カレンダーアプリと連携する」ボタンは今のままだと動作しません。動作させたい場合は、[こちらの Bolt のドキュメント](https://slack.dev/bolt-python/ja-jp/concepts#action-listening)を参考にリスナーを実装してみてください。上記の例そのままであれば `@app.action("connect_with_calendar")` というデコレーターのついたメソッドを実装すればイベントを受信できるでしょう。

## Incoming Webhooks でメッセージを投稿

[こちらの記事](https://qiita.com/seratch/items/03e224cc092e1a7dd764)を参考にしてください。

## メッセージを更新・削除

それでは API を使ってメッセージの更新・削除もやってみましょう。

```python
import logging, os, time

logging.basicConfig(level=logging.DEBUG)
from slack_sdk import WebClient

client = WebClient(os.environ["SLACK_BOT_TOKEN"])

# 新しいメッセージを投稿
new_message = client.chat_postMessage(
    channel="#random",
    text=":wave: こんにちは！",
)

# わかりやすくするために sleep
time.sleep(2)

# 自分が投稿したメッセージを更新
client.chat_update(
    channel=new_message["channel"],
    ts=new_message["ts"],
    text=":wave: こんばんは！",
)

# わかりやすくするために sleep
time.sleep(2)

# 自分が投稿したメッセージを削除
client.chat_delete(
    channel=new_message["channel"],
    ts=new_message["ts"],
)
````

Slack チャンネル内での動作は以下のようになります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/828be1e0-e8c9-993f-d905-2af54efe873e.gif" width=500 />


## エフェメラルメッセージを投稿

「エフェメラルメッセージ」とは、チャンネル内に投稿されていても特定のユーザーにだけ見えているメッセージです。Slack の画面上で「あなただけに表示されています（英語だと Only visible to you）」と表示されているメッセージを見たことがあるかもしれません。このメッセージは例えばスラッシュコマンドを実行したユーザーだけに「受け付けました！」のようなメッセージを表示したいときなどに便利です。では、このメッセージを API を使って送信してみましょう。

```python
import logging, os

logging.basicConfig(level=logging.DEBUG)
from slack_sdk import WebClient

client = WebClient(os.environ["SLACK_BOT_TOKEN"])

# 先程発行したユーザートークンを使ってあなたのユーザー ID を取得します
user_auth = client.auth_test(token=os.environ["SLACK_USER_TOKEN"])
user_id = user_auth["user_id"]

# 場所として channel を指定、誰に対して表示するかで user を指定します
response = client.chat_postEphemeral(
    channel="#random",
    user=user_id,
    text=":wave: あなただけにお知らせです！",
)
```

以下のようなメッセージが表示されたでしょうか？
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/54cba5c6-8781-a352-28ea-bbe7e8d811e4.png" width=400 />
割愛しますが、もちろん `blocks` も問題なく使えます。

## ユーザーインタラクションに対して返信

スラッシュコマンド、ショートカットなどへの応答でメッセージを返信したいという方は、ぜひこちらの `response_url` に関する詳細な記事を参考にしてみてください。

* [Slack ペイロードに含まれる response_url を完全に理解する](https://qiita.com/seratch/items/ed29acd565af36e65072)


## まとめ

この記事では Slack API を使ってチャットにメッセージを投稿する方法について公式 Python SDK のコード例とともに網羅的に紹介しました。

今回扱わなかった API に予約投稿のための [chat.scheduleMessage](https://api.slack.com/methods/chat.scheduleMessage) API というものもあります。ぜひ [API 一覧](https://api.slack.com/methods)の `chat.` から始まる API を色々と試してみてください。

それでは :wave: 

```bash
export SLACK_BOT_TOKEN=___; pip install slack-sdk; python -c 'import os; from slack_sdk import WebClient; WebClient(os.environ["SLACK_BOT_TOKEN"]).chat_postMessage(channel="#random", text="それでは :wave:")'
```