Slackアプリの開発環境について深堀りしてみた:Bolt for Python

今年はSlackの次世代プラットフォームがオープンベータになったということで @seratch が刺激的な記事をpostし続けてくれていますね。
こちらの記事は現行のイケてるSlackアプリ開発用フレームワークである、Bolt for Pythonについて深堀りしていきたいと思います。

昨年の[Advent Calendar](https://qiita.com/advent-calendar/2021/slack)では実際に使ってみた系の記事をpostしていましたが、そこからSlack-SDK及びBoltにもアップデートが(頻繁に)入っていますので、自分の知識のアップデートも兼ねて、深堀りしていきたいと思います。

なぜそんなことを思ったかといいますと、先日書いたこちらの記事のように、昔はなかったけど、今はBoltが自動的にハンドリングしてくれますよみたいなアプデが入ってるからです！

https://qiita.com/geeorgey/items/eff73ea4c045ef4633d1

単純にリリースノート読み込んでないだけだろと言われてしまうとそのとおりなのですが、反省して振り返って行きたいと思います。それでは、[Slack Advent Calendar 2022](https://qiita.com/advent-calendar/2022/slack) 7日目、スタートしていきたいと思います。Let's go🎉

## Bolt 入門ガイドを読む
https://slack.dev/bolt-python/ja-jp/tutorial/getting-started

ありがたいことに日本語化されています。
はじめに目につくのはソケットモードでしょうか。これは2021年の1月にGAになっていますが、開発初期はこのモードを使うことで最短で開発に挑むことができるようになります。詳細については既に @seratch さんが書いていますのでそちらを参考にしましょう。

https://qiita.com/seratch/items/1a460c08c3e245b56441

開発環境ではCloud Flare Tunnelsを使ってローカル環境とSlack間をつなげているのですが、面倒なので開発環境の場合はソケットモードで接続するようにしようかなと思いつつありますが、それはそれで環境が違うのでだめっぽいか。

### メッセージイベントをサブタイプでフィルタする

[出典](https://slack.dev/bolt-python/ja-jp/concepts#event-listening)

```python
# 変更されたすべてのメッセージに一致
@app.event({
    "type": "message",
    "subtype": "message_changed"
})
def log_message_change(logger, event):
    user, text = event["user"], event["text"]
    logger.info(f"The user {user} changed the message to {text}")
```

メッセージイベント取得時に、サブタイプ属性を付けておくことで処理の分岐に使うことができるようです、知らなかった。
これまで作ったアプリでは、 ```@app.event("message")``` で全部受け付けて、その中で分岐処理を書いていた！分けちゃうとうまく書けない部分が結構あったのでまだ使ってないけど、こうやって処理を分けておくとboltがハンドリングする量が減るという可能性は考えられる。
メッセージイベント取得しはじめると、全部のpostに反応する事になるので、処理量削減したい場合は検討してもいいかも。

### アクションのリスニング時にブロックIDでフィルタできる
[出典](https://slack.dev/bolt-python/ja-jp/concepts#action-listening)

```Python:
# この関数は、block_id が 'assign_ticket' に一致し
# かつ action_id が 'select_user' に一致する場合にのみ呼び出される
@app.action({
    "block_id": "assign_ticket",
    "action_id": "select_user"
})
def update_message(ack, body, client):
    ack()

    if "container" in body and "message_ts" in body["container"]:
        client.reactions_add(
            name="white_check_mark",
            channel=body["channel"]["id"],
            timestamp=body["container"]["message_ts"],
        )
```
この組み合わせ使ったことなかった。普段は```@app.action("approve_button")```と書くのですが、block_idと組み合わせることでaction_id共通にして処理を分岐できるようだ。

### Lazy リスナーについて
[出典](https://slack.dev/bolt-python/ja-jp/concepts#lazy-listeners)
これは既に当アドカレで @seratch さんより詳細書いていただいていますが

https://qiita.com/seratch/items/12b39d636daf8b1e5fbf

たまにしか発生しないアクションに対する反応を行うbotなんかはこれがいいよねとずっと思ってきたのですが、実装例が公開されたことでより使いやすくなったのではないかなと思います。

### アダプター
[出典](https://slack.dev/bolt-python/ja-jp/concepts#adapters)
私の環境ではFlaskを使っているのですが、どうやって組み込めば…！？みたいに思っていたことがあります。こちらを見れば書き方がわかるので良いですね。
[Bolt for Python](https://github.com/slackapi/bolt-python/tree/main/slack_bolt/adapter)のアダプターページに、様々な事例が掲載されていますので、自分の環境に合わせたサンプルを見つけられると思います。

### リスナーミドルウェア
[出典](https://slack.dev/bolt-python/ja-jp/concepts#listener-middleware)
上述したサブタイプやblock_idによるフィルタリングに似ていますが、これを関数にまとめた形で設定することができます。

```Python:
# "bot_message" サブタイプのメッセージを抽出するリスナーミドルウェア
def no_bot_messages(message, next):
    subtype = message.get("subtype")
    if subtype != "bot_message":
       next()

# このリスナーは人間によって送信されたメッセージのみを受け取ります
@app.event(event="message", middleware=[no_bot_messages])
def log_message(logger, event):
    logger.info(f"(MSG) User: {event['user']}\nMessage: {event['text']}")

# リスナーマッチャー： 簡略化されたバージョンのリスナーミドルウェア
def no_bot_messages(message) -> bool:
    return message.get("subtype") != "bot_message"

@app.event(
    event="message", 
    matchers=[no_bot_messages]
    # or matchers=[lambda message: message.get("subtype") != "bot_message"]
)
def log_message(logger, event):
    logger.info(f"(MSG) User: {event['user']}\nMessage: {event['text']}")
```

Slackアプリでは最初はメッセージイベント全体を受け取って処理するみたいな形で書き始めてしまいますが、アプリが大きくなってきた際にはミドルウェアを書く事によってより低負荷なアプリケーションにすることができそうです。
私自身が書くアプリはメッセージイベントを多用しているので、うまくフィルタできないのかなと思っていたのですが、今後はこれらを使ってみるのも検討してみると恩恵がありそうです。

### コンテキストの追加
[出典](https://slack.dev/bolt-python/ja-jp/concepts#context)
ミドルウェアでcontextにデータ追加するサンプルが掲載されています。
サンプルみたいにblocksを構成するというのはよくやるのでリスナーの実行前に処理を寄せてしまうというのもありなのかもしれない。

## Bolt for Pythonのリリースノートを読むには
https://github.com/slackapi/bolt-python/releases

昨年のクリスマスイブ(2021.12.24)は[ver.1.11.1](https://github.com/slackapi/bolt-python/releases/tag/v1.11.1)だったようです。現在のバージョンが[1.15.5](https://github.com/slackapi/bolt-python/releases/tag/v1.15.5)ですからバージョンアップ頻度が多くて安心できます。

### ignoring_self_events_enabled=True について
デフォルトでTrueになっています。アプリの初期化時に渡す引数です。こんな形になります。

```Python:
bolt_app = App(
    logger=logger,
    signing_secret=os.environ.get("SIGNING_SECRET"),
    installation_store=installation_store,
    raise_error_for_unhandled_request=True,
    client=WebClient(retry_handlers = retry_handlers),
    ignoring_self_events_enabled=True,
    oauth_settings=OAuthSettings(
        client_id=client_id,
        client_secret=client_secret,
        state_store=oauth_state_store,
        scopes=os.environ.get("SLACK_SCOPES"),
        user_scopes=os.environ.get("SLACK_USER_SCOPES"),
        callback_options=callback_options,
    ),
)
```

設定しなくても良いのですが、なぜこれをピックアップしたかというと、昔はこれなかったんですよね。
例えば、「Hello」というワードに「Hello」とbotが返すみたいな実装をした場合、レスポンスにもHelloが入っているのでループする事になります。
実は、少し前に現在pilotであるSalesforce Apex SDK for Slackを試用してみたのですが、そこにはまだ入っていませんでした。

https://qiita.com/geeorgey/items/65a512abbcce2a39e49e

>こうすることで、チャンネルに投げられたメッセージをそのチャンネルにリポストしてくれるようにすることができるのですが、無限ループになるのでこの実装はやめましょう(笑

その他、初期化時に使える機能についてはここを参照しましょう

https://slack.dev/bolt-python/api-docs/slack_bolt/app/app.html

>引数
>logger
>　このアプリで使用できるカスタムロガーです。
>name
>　ロギングに使用するアプリケーション名。省略した場合は、ソースファイル名が使用されます。
>process_before_response
>　このアプリがFunction as a Serviceで動作している場合はTrueを指定します。(デフォルト: False)
>raise_error_for_unhandled_request
>　処理されないリクエストに対して例外を発生させ、警告ログをピントして Slack に 404 を返す組み込みハンドラではなく @app.error リスナーを使用したい場合は True を指定します (デフォルト: False)
>signing_secret
>　Slackからのリクエストを検証するために使用されるSigning Secretの値。
>token
>　単一ワークスペースのアプリにのみ必要なボット/ユーザーアクセストークン。
>token_verification_enabled
>　True の場合、指定されたトークンの有効性を検証します。
>client
>　このアプリのシングルトン slack_sdk.WebClient インスタンスです。
>authorize
>  インストールデータにチーム/ユーザーがあるかどうかを確認し、Slackからの着信リクエストを承認する関数です。
>installation_store
>　インストールデータの保存/検索操作を提供するモジュール
>installation_store_bot_only
>　Trueの場合、InstallationStore#find_bot()を使用する（デフォルト：False)
>request_verification_enabled
>　組み込みミドルウェアを無効にしたい場合はFalse (デフォルト: True)。RequestVerificationは、HTTP Modeのリクエストに含まれる署名を検証する組み込みミドルウェアです。内蔵ミドルウェアをオフにする場合は、十分に安全であることを確認してください。より安全性を高めるために、RequestVerificationの使用を強く推奨します。もし、Boltアプリの前でリクエストの署名を検証するプロキシがある場合は、作業の重複を避けるためにRequestVerificationを無効にしても全く問題ないでしょう。開発のしやすさだけを考えてオフにしないようにしましょう。
>request_verification_enabled
>　組み込みのミドルウェアを無効にしたい場合は False を指定します (デフォルト: True)。IgnoringSelfEventsは、Boltアプリがこのアプリのボットユーザーが生成したイベントを簡単にスキップできるようにする内蔵ミドルウェアです（無限ループを引き起こすコードエラーを回避するのに有効です）。
>url_verification_enabled
>　組み込みのミドルウェアを無効にしたい場合は、Falseを指定します（デフォルト：True）。UrlVerificationは、HTTP ModeリクエストでEvent APIのエンドポイントを検証するurl_verificationリクエストを処理する組み込みのミドルウェアです。
>ssl_check_enabled
>　bool = 組み込みミドルウェアを無効にしたい場合はFalse (Default: True)。SslCheckは、Slackからのssl_checkリクエストを処理する組み込みのミドルウェアです。
>oauth_settings
>　Slackアプリのインストールフロー（OAuthフロー）に関する設定です。
>oauth_flow
>　インスタンス化されたOAuthFlow。oauth_settingsよりも常に優先される。
>verification_token
>　非推奨の検証メカニズム。ssl_check リクエストにのみ使用可能です。
>listener_executor
>　バックグラウンドタスクを実行するためのカスタムエグゼキュータ。指定しなかった場合は、デフォルトの ThreadPoolExecutor が使われます。

## Slack APIのChange logを見る

https://api.slack.com/changelog

こちらはAPI周りの変更点について書かれています。Slackのアプリからは頻繁にAPIを呼び出しますので、新しい変更点が気になる場合はこのページが外せません。

どんなAPIが使えるのかについてはここを参照しましょう

https://api.slack.com/methods

個人的にはhaddleに関するAPIがはえてこないかなと期待しているのですがまだ無いですね。

### まとめ
ということで、改めてドキュメントを読み直してみたり、Githubみたり[API-Docs](https://slack.dev/bolt-python/api-docs/slack_bolt/)みてみたりと、改めて深堀りをしてみました。
あらゆるリリースを頭に入れておくというのはなかなか難しいのですが、実はSlackにはコミュニティ向けのワークスペースが存在します。

https://slackcommunity.com/

こちらからサインアップするとSlack Communityという名前のワークスペースに参加することができます。
#lang-japanese チャンネルもありますので、日本語でのコミュニケーションも可能です。上述の @seratch がリリース内容を必ずpostしてくれるので、それを追いかけるだけでも十分な情報が手に入るかなと思います。
Slackアプリ開発に躓いたときは、Twitterに流すのも良いのですが、こちらのSlackワークスペースでやり取りするほうがやりやすいのではないかなと思います。
無料で参加できますので是非ご参加ください！

以上、Slack Advent Clendar 2022のDAY7の記事となります。読んで頂きありがとうございました！