Slackのスラッシュコマンドを作成する

本記事は以前投稿した[Slackで自分の投稿にリアクションされたら通知する](https://qiita.com/ytamu2/items/ec6cc636f262d7a6d8be)にスラッシュコマンドを追加する内容となります。
前述の記事はSlackアプリを作成する内容となっています。

# コマンドの概要
リアクションが追加された時の通知対象を変更出来るように選択式のフォームを表示させ、
設定内容はDBに保管するというものです。

# DBの作成
DBはAzure Cosmos DBを使用してみました。
格納するデータはチャンネルID、ユーザID、通知させる投稿対象の設定値のみです。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604991/f3fb166c-9a56-1714-e8b4-6edce4cc7509.png" width=65%>

受信許可するIPアドレスをApp Serviceと自分の端末（グローバルIP）のみに設定します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604991/9e7eb27e-b000-ed98-dfe2-9f5b4613696e.png" width=80%>

App Serviceの送信IPアドレスはApp Serviceのプロパティで見れます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604991/63681f7b-026d-764f-a528-eb7dbc6e3a9d.png" width=80%>

# コマンドの作成
アプリ設定のSlash Commands画面で`Create New Command`ボタンを押下してコマンドを作成します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604991/317cde7f-50a5-0fdd-ee37-203bf1666a0a.png" width=60%>

名前や説明等記入します。
Request URLはChallengeパラメータを返すURLと同じです。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604991/d2b0011e-8fe0-55b8-bcdf-7a44a7ca34f9.png" width=60%>
保存すると`commands`スコープが追加されます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604991/0b375050-9b62-2ff3-4a39-c35770c6eb8d.png" width=60%>
スコープの追加がある場合はアプリの再インストールを行って反映します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604991/c9c55c97-37d3-4741-4b2e-c679a3df878b.png" width=60%>
Interactivity & Shortcuts画面で`Interactivity`を有効化します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604991/85143d67-65ce-9e0d-f181-5b2a9cf67601.png" width=60%>

# App Serviceの設定を追加
Pythonアプリから読み取るようにするためDB名、エンドポイント、コンテナー名、およびアクセスキーをアプリケーション設定に追加します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604991/a434ff65-08e0-3fe9-b211-e3546e1d5353.png" width=90%>

# ソースコード修正
スラッシュコマンド実行時の画面とボタン選択時の処理を追加します。
画面のコードは[Block Kit Builder](https://app.slack.com/block-kit-builder/)を使うと簡単に作成できます。
修正後はApp Serviceにデプロイします。

```python:app.py

from slack_bolt import App, Ack, BoltResponse, BoltContext, Respond, Say
from slack_sdk import WebClient
import azure.cosmos.cosmos_client as cosmos_client
from enum import IntEnum, auto
import copy

# 通知モード種別
class Notification(IntEnum):
    NONE = 0
    SPOT = auto()
    ALL = auto()


USER_ID = "user_id"
NOTIFICATION_MODE = "notification_mode"
template_doc_main = {"id": "", "channel_id": "", "user": []}

# DBクライアント
cosmosclient = cosmos_client.CosmosClient(
    os.environ["DB_ENDPOINT"],
    os.environ["DB_KEY"],
)

db_id = os.environ["DB_ID"]
container_id = os.environ["DB_CONTAINER_ID"]
dbclient = cosmosclient.get_database_client(db_id)
containerclient = dbclient.get_container_client(container_id)
query_user = "SELECT u.user_id, u.notification_mode FROM config c JOIN u IN c.user WHERE c.channel_id = @channel_id AND u.user_id = @user_id"


@app.command("/rewatch")
def notification(ack: Ack, body: dict, respond: Respond):
    ack(
        blocks=[
            {
                "type": "section",
                "text": {
                    "type": "mrkdwn",
                    "text": f"通知タイプを選択してください\n\n*投稿毎* ：:{target_reaction}:のリアクションがある場合のみ通知します\n*全て* ：すべての投稿のリアクションを通知します\n*なし* ：通知しません",
                },
            },
            {
                "type": "actions",
                "elements": [
                    {
                        "type": "button",
                        "action_id": "notification_spot",
                        "text": {"type": "plain_text", "text": "投稿毎", "emoji": True},
                        "value": f"{Notification.SPOT}",
                    },
                    {
                        "type": "button",
                        "action_id": "notification_all",
                        "text": {"type": "plain_text", "text": "全て", "emoji": True},
                        "value": f"{Notification.ALL}",
                    },
                    {
                        "type": "button",
                        "action_id": "notification_none",
                        "text": {"type": "plain_text", "text": "なし", "emoji": True},
                        "value": f"{Notification.NONE}",
                    },
                ],
            },
        ],
    )


@app.action("notification_spot")
def start(ack: Ack, body: dict, logger, respond):
    ack()
    logger.info(body)
    upsert_notification(body, logger, respond)


@app.action("notification_all")
def start(ack: Ack, body: dict, logger, respond):
    ack()
    logger.info(body)
    upsert_notification(body, logger, respond)


@app.action("notification_none")
def start(ack: Ack, body: dict, logger, respond):
    ack()
    logger.info(body)
    upsert_notification(body, logger, respond)


def upsert_notification(body: dict, logger, respond):
    exist_channel = list(
        containerclient.query_items(
            query="SELECT c.id FROM config c WHERE c.id=@id",
            parameters=[{"name": "@id", "value": body["channel"]["id"]}],
            enable_cross_partition_query=True,
        )
    )

    channel = body["channel"]["id"]
    user = body["user"]["id"]
    notification_mode = int(body["actions"][0]["value"])

    # チャンネル追加
    upsert_item = copy.deepcopy(template_doc_main)
    logger.info(upsert_item)
    upsert_item["id"] = channel
    upsert_item["channel_id"] = channel
    upsert_item["user"] += [{USER_ID: user, NOTIFICATION_MODE: notification_mode}]
    logger.info(upsert_item)

    res_message = f"通知モードを「" + body["actions"][0]["text"]["text"] + f"」に設定しました。"

    if len(exist_channel) == 0:
        containerclient.upsert_item(body=upsert_item)
        respond(res_message)
        return

    # 通知モードの更新
    channel_items = containerclient.read_item(item=channel, partition_key=channel)
    for channel_item in channel_items["user"]:
        if channel_item["user_id"] == user:
            channel_item["notification_mode"] = notification_mode
            containerclient.upsert_item(body=channel_items)
            respond(res_message)
            return

    # ユーザ追加
    channel_items["user"] += [{USER_ID: user, NOTIFICATION_MODE: notification_mode}]
    containerclient.upsert_item(body=channel_items)
    respond(res_message)
    return

```

# 動作確認
アプリをインストールしたチャンネルで設定したコマンド`/rewatch`を実行します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604991/7be63fce-0b7f-cacd-dba0-27d175083c09.png" width=50%>
画面が表示され3つの選択肢が出ました。
`全て`を選択すると…
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604991/c5f93685-c5fb-7dc5-d510-4b880ec41d68.png" width=50%>
設定完了のメッセージが表示されます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604991/53992d89-9ea1-df42-c7a6-7b75c38ad131.png" width=50%>

コマンドの動作はこのようになります。
![rewatch_command.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604991/1c240c5f-467c-9cea-2d77-ca6f541c41a0.gif)

これでスラッシュコマンドの作成完了です。
後はリアクション追加の通知処理の中でDBの値を見るように変更すればユーザ毎に通知タイプを変更できます。