LambdaでSlackからのButton Actionを受け取って、それに応じて元のメッセージをupdateする

# やりたいこと

何らかのSlack app（Bot）からボタン付きのメッセージをPOSTし、押されたボタンに応じて元のメッセージを更新する。
![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119757/8e2aadd1-1343-0e33-1719-3fd13f140ff8.gif)
個人的には機械学習のアノテーションに使おうと思っていて、ボタンに応じた値をDBに保存して、元のメッセージに`アノテーション済みです`みたいな文言を出したいなと考えてます。

# 元のボタン付きメッセージの作り方
まず、元のメッセージはこのように作りました。
例は2択ですが、`elements`内の要素を増やせば何択でもできると思います。

また、`SLACK_TOKEN`はSlackアプリであればアプリの管理画面上の`OAuth & Permissions > OAuth Tokens for Your Workspace`から取得できるはずです。

```python
# !pip install slackclient
from slack import WebClient

SLACK_TOKEN = "xoxb-xxxxxxxxxxxx"
client = WebClient(token=SLACK_TOKEN)
blocks = [
    {
        "type": "section",
        "text": {
            "type": "mrkdwn",
            "text": "元気ですか？",
        },
    },
    {
        "type": "actions",
        "block_id": "button_block",
        "elements": [
            {
                "type": "button",
                "text": {"type": "plain_text", "text": "元気です！"},
                "style": "primary",
                "value": "1",
                "action_id": "yes",
            },
            {
                "type": "button",
                "text": {"type": "plain_text", "text": "そうでもない…"},
                "style": "danger",
                "value": "0",
                "action_id": "no",
            },
        ],
    },
]

response = client.chat_postMessage(
    channel="チャンネル名",
    blocks=blocks,
)
)
```

# メッセージを更新するLambdaの作り方
ざっくり以下の2ステップが必要なので、順に説明します。

1. Lambdaの実装
1. Lambda（API Gateway）のURLをSlack appの`Interactivity & Shortcuts > Interactivity > Request URL`に登録する
1. Slack appの`OAuth & Permissions > Scopes`でメッセージを送る権限をつける


## Lambdaの実装
下のようにすればButton Actionを受け取り、値に応じてメッセージを更新できます。
今回は単純にボタンのテキストをそのまま返していますが、もちろん条件分岐などでもっと複雑なこともできるはずです。

```python
import base64
import json
from urllib.parse import parse_qs

from slack import WebClient

SLACK_TOKEN = "xoxb-xxxxxxxxxxxx"
client = WebClient(token=SLACK_TOKEN)

payload = json.loads(
        parse_qs(base64.b64decode(event["body"]).decode("utf-8"))["payload"][0]
    )
response = client.chat_update(
        channel=payload["container"]["channel_id"],
        ts=payload["message"]["ts"],
        attachments=[
            {"text": f":white_check_mark: {payload['actions'][0]['text']['text']}"}
        ],
    )
```

個人的にハマったのは下のあたりです。

- SlackからのPOSTはbase64エンコードかつ`application/x-www-form-urlencoded`なので、適切にデコードしてパースする必要がある
- 元のメッセージを特定するためには下の2つが必要なので、リクエスト内から取得する
    - channel_id（チャンネル名ではNG）
    - ts（タイムスタンプ）
- メッセージを更新するやり方はいくつかあるが、今回の`slackclient`を使って更新するやり方では、`slackclient`を使ってPOSTされたメッセージしか更新できない
    - 例）`requests.post()`で投稿したメッセージは更新できない
- Slack APIでメッセージ内容の記述方法は`text`, `attachments`, `blocks`の3つあるが、「`text`, `blocks`が両方あるときは`blocks`しか表示されない」のような関係があるので
    - 元のメッセージが`blocks`の場合、更新を`text`でやろうとしてもできない
    - 元のメッセージが`blocks`の場合、`attachments`で更新すると元の`blocks`は残ったままで`attachments`が追加される
        - 今回はこれを利用して、元のメッセージに追記する形で更新した

なお、`text`, `attachments`, `blocks`の関係はこのようになってるようです。

https://qiita.com/yoshitake_1201/items/9ae3a48373bfafa89e97#text-blocks-attachments%E3%81%AE%E9%96%A2%E4%BF%82

## Lambda（API Gateway）をSlack appに登録
実装したLambda関数やAPI GatewayのURLをSlack appの管理画面から登録します。
`Interactivity & Shortcuts > Interactivity > Request URL`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119757/df06d59c-ef42-6a9c-b7f7-14b9856ca347.png)

## Slack appにメッセージを送る権限をつける
同じくSlack appの管理画面上の`OAuth & Permissions > Scopes`に`chat:write`, `chat:write:customize`を付与します。
そもそもこれがないと元のボタン付きのメッセージも送れないと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119757/10a6ff2f-19ee-d4f3-ccea-529fceb54c22.png)

以上の手順で、最初に書いた「何らかのSlack app（Bot）からボタン付きのメッセージをPOSTし、押されたボタンに応じて元のメッセージを更新する」ができるはずです。