Discord bot で ボタンを実装

# Discord interactions

[Discord Developer Portal](https://discord.com/developers/docs/intro) に文献があります。

APIバージョンは`https://discord.com/api/v8`です。

今回やりたいことは↓です
![ボタンのやつ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1563715/e6085d5b-149e-ec29-be8b-2f472321c6bc.png)


# 前提

すっ飛ばして、ボタンの表示方法知りたい人は[ボタンの表示](#ボタンメッセージの表示)を参照。

## Botの登録

[Discord Dev App](https://discord.com/developers/applications) へアクセスし、Botを作成してください。そして`TOKEN`を取得しておきましょう。
※方法はネット上に転がっているので省略します。

## Python3.8 の構築

本記事では`Python3.8`の環境で行っています。
また、あらかじめライブラリをインストールしておきましょう。

```bash
$ python3 -m pip install discord.py
```

一応、requirements.txtは以下の通りです。

```requirements.txt
discord~=1.0.1
requests~=2.25.1
aiohttp~=3.7.4.post0
```

# 基本的なBotプログラム

```discordbot.py

import discord
import requests
from discord.ext import commands
from pprint import pprint
import aiohttp

TOKEN = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"

AuthB = "Bot " + TOKEN

headers = {
    "Authorization": AuthB
}


def returnNormalUrl(channelId):
    return "https://discordapp.com/api/channels/" + str(channelId) + "/messages"


async def notify_callback(id, token):
    url = "https://discord.com/api/v8/interactions/{0}/{1}/callback".format(id, token)
    json = {
        "type": 6
    }
    async with aiohttp.ClientSession() as s:
        async with s.post(url, json=json) as r:
            if 200 <= r.status < 300:
                return


async def on_socket_response(msg):
    if msg["t"] != "INTERACTION_CREATE":
        return

    pprint(msg)


class MyBot(commands.Bot):

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.add_listener(on_socket_response)


bot = MyBot(command_prefix='$', description='discord bot')


@bot.event
async def on_message(msg):
    if msg.content == "hello":
        normal_url = returnNormalUrl(msg.channel.id)
        json = {
            "content": "Hello World"
        }
        r = requests.post(normal_url, headers=headers, json=json)


bot.run(oTOKEN)
```
これをコピペして`TOKEN`書き変えて実行すれば多分動きます。(ライブラリ不足のエラー等があればその都度`pip`でインストールしてください。Pycharm等のIDEで編集すれば自動的にインストールしてくれるかもしれません。)

# アクティビティの表示

```python
@bot.event
async def on_ready():
    print("Boot")
    await bot.change_presence(activity=discord.Game("Python")) #Pythonをプレイ中
```

を`on_message`関数の前に書けば、

- プログラム起動時にコンソールに`Boot`と表示
- Bot起動中にBotプロフィールに「Pythonをプレイ中」と表示

# ボタンメッセージの表示

```python
        normal_url = "https://discordapp.com/api/channels/" + str(message.channel.id) + "/messages"
        json = {
            "content": "Hello World",
            "components": [
                {
                    "type": 1,
                    "components": [
                        {
                            "type": 2,
                            "label": "1st",
                            "style": 1,
                            "custom_id": "click_one",
                        },
                        {
                            "type": 2,
                            "label": "2nd",
                            "style": 3,
                            "custom_id": "click_two",
                        },
                    ]

                }
            ]
        }
        r = requests.post(normal_url, headers=headers, json=json)

```
を`on_message()`関数内の`if`文内に記述等すればボタンが付いたメッセージを表示できます。
![ボタンが付いたメッセージ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1563715/86351a70-7e7c-2310-d3ce-d6f5be99df9f.png)

`"style"`要素では、[この文献](https://discord.com/developers/docs/interactions/message-components#buttons-button-styles)に書いてある`VALUE`を指定すればよいです。ボタンの色やデザインが変わります。

`"custom_id"`の説明は[ボタン押された時の処理](#ボタン押された時の処理)で説明します。

# ボタン押された時の処理

`on_socket_response`関数の引数となっている`msg`の`msg["d"]["data"]["custom_id"]`にボタンが押された際の`custom_id`が格納されているので、これの値をもとに処理を走らせればよいわけです。

例えば`on_socket_response`関数を以下のように変えてみましょう。

```python
async def on_socket_response(msg):
    if msg["t"] != "INTERACTION_CREATE":
        return

    pprint(msg)
    custom_id = msg["d"]["data"]["custom_id"]

    if custom_id == "click_one":
        normal_url = returnNormalUrl(msg["d"]["channel_id"]) #returnNormalUrl関数の定義はこの記事のどこかにあるよ
        json = {
            "content": "Push button_1"
        }
        r = requests.post(normal_url, headers=headers, json=json)
        await notify_callback(msg["d"]["id"], msg["d"]["token"]) #notify_callback関数は後で説明するよ
```
こうすると、ボタン`1st`が押された際に「Push Button_1」というメッセージが同じチャンネルに投稿されます。

## `notify_callback`関数の定義

通常、slash commandと同じように、ボタンが押されると「インタラクション」がサーバーに送信されます。このインタラクションに対しサーバーは`200`を含んだデータを返さなければなりません。(`200`は`HTTPステータスコード`の一種で、Successを意味します。)ここで普通にメッセージをインタラクションの成功返信として返してもよいのですが、ボタンが押されるたびにこの返信メッセージが帰ってくるので煩わしいこともあるでしょう。そのため、`notify_callback`関数を

```python
async def notify_callback(id, token):
    url = "https://discord.com/api/v8/interactions/{0}/{1}/callback".format(id, token)
    json = {
        "type": 6
    }
    async with aiohttp.ClientSession() as s:
        async with s.post(url, json=json) as r:
            if 200 <= r.status < 300:
                return
```
と定義しました。json変数の`"type"`で`6`を指定しているのは[この文献](https://discord.com/developers/docs/interactions/slash-commands#interaction-response-interactioncallbacktype)でもあるようにとりあえず成功返信をするためです。

# ボタン押すたびにメッセージを更新

ボタンを押すたびに、ボタンを押せなくしたり、メッセージの番号を+1していく、などしたい時があります。
その際には、更新後のメッセージ内容を格納したjson変数を用意し、

```python
r2 = requests.patch(normal_url2, headers=headers, json=json2)
```
で`PATCH`として送信します。

# サンプルコード

左右のボタンを押すたびにメッセージがRightとLeftで切り替わるコードです。ボタンを押すたびに片方のボタンが押せなくなります。
普通にコピペすれば動くはずです....

<url>https://gist.github.com/Narangelife/6f6f8b070dee4dfc0a8570aef2acdf1f</url>

# 最後に

間違ってたり、冗長だったらコメントを残していただけると幸いです。

- \[2021/5/30 1:21\] APIバージョンを追記(コメントありがとうございます。)