asyncio アプリ内から Slack にメッセージを投稿しよう

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

## Slack 公式 SDK の asyncio サポート

このページに辿り着いたということは [asyncio](https://docs.python.org/3/library/asyncio.html) を活用してアプリケーション開発をされていますか？

Slack 公式 SDK である [slack-sdk](https://pypi.org/project/slack-sdk/) は [AIOHTTP](https://docs.aiohttp.org/en/stable/client.html) を HTTP クライアントとして利用する形で [asyncio](https://docs.python.org/3/library/asyncio.html) をサポートしています。

この記事ではそのモジュールの使用方法をコード例とともに紹介していきます。

なお、こちらの記事は[「Slack Python SDK でチャンネルにメッセージを投稿しよう」](https://qiita.com/seratch/items/93714b5cf3974c2f5327)という記事をベースにしています。もし asyncio ではない一般的な Python コードの例を探している場合は、そちらの記事を参考にしてください。


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

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

* `chat.postMessage` API でメッセージを投稿
* 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 と aiohttp を追加
poetry add slack-sdk aiohttp
```

requirements.txt であれば

```
slack-sdk>=3.13,<4
aiohttp>=3.7.3,<4
```

のようにしてください（バージョンは使用したいものに合わせてください）。

asyncio は Python の標準モジュールですが、slack-sdk が利用する aiohttp はあくまで広く利用されている 3rd party ライブラリです。slack-sdk をインストールするだけでは aiohttp は追加されませんので、必ず明にインストールするようにしてください。これでひとまず完了です。


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

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

```python
import asyncio, logging, os

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

from slack_sdk.web.async_client import AsyncWebClient
from slack_sdk.errors import SlackApiError

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

async def main():
    try:
        # chat.postMessage API を呼び出します
        # メソッド呼び出しに await キーワードをつけるのを忘れずに！
        response = await client.chat_postMessage(
            channel="#random",
            text=":wave: こんにちは！",
        )
        print(response)
    except SlackApiError as e:
        assert e.response["ok"] is False
        assert e.response["error"]  # str like 'invalid_auth', 'channel_not_found'
        print(f"Got an error: {e.response['error']}")

# asyncio.run は簡単に async 関数を実行するためのユーティリティです
asyncio.run(main())
```

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

```
(.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: 

ユーザーに成り代わっての投稿や `blocks`、`attachments` の利用方法など、`WebClient` と何ら関わるところはありません。それらについては[こちらの記事](https://qiita.com/seratch/items/93714b5cf3974c2f5327#%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E3%81%AB%E6%88%90%E3%82%8A%E4%BB%A3%E3%82%8F%E3%81%A3%E3%81%A6%E6%8A%95%E7%A8%BF%E3%81%99%E3%82%8B)を参考にしてください。


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

基本的には[こちらの記事](https://qiita.com/seratch/items/03e224cc092e1a7dd764)と同様なのですが、コード例が異なりますので、最初のコード例のみこちらに載せておきます。そこからのカスタマイズは特に変わるところはありません。

```python
import asyncio, logging, os

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

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

async def main():
    # メッセージを送信します
    # メソッド呼び出しに await キーワードをつけるのを忘れずに！
    response = await client.send(text=":wave: こんにちは！")
    print(f"status: {response.status_code} body: {response.body}")

# asyncio.run は簡単に async 関数を実行するためのユーティリティです
asyncio.run(main())
```

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

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

```python
import asyncio, logging, os

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

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

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

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

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

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

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

# asyncio.run は簡単に async 関数を実行するためのユーティリティです
asyncio.run(main())
````

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 asyncio, logging, os

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

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

async def main():
    # TODO: 実際のユーザー ID に変更してください
    user_id = "U03E94MK0"

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

# asyncio.run は簡単に async 関数を実行するためのユーティリティです
asyncio.run(main())
```

以下のようなメッセージが表示されたでしょうか？
<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` に関する詳細な記事を参考にしてみてください。`response_url` への HTTP リクエストには `AsyncWebhookClient` を使うことができます。

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


## まとめ

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

繰り返しとなりますが、こちらの記事は[「Slack Python SDK でチャンネルにメッセージを投稿しよう」](https://qiita.com/seratch/items/93714b5cf3974c2f5327)という記事をベースにしています。もし asyncio ではない一般的な Python コードの例を探している場合は、そちらの記事を参考にしてください。

それでは :wave: