DiscordAPIの新機能 Slash Commandを試す

# はじめに

この記事は[DiscordBotアドベントカレンダー2020](https://qiita.com/advent-calendar/2020/discordbot)の19日目の記事です。

こんにちは。この記事では、[つい先日オープンベータになった](https://support.discord.com/hc/ja/articles/1500000368501-Slash-Commands-FAQ)、スラッシュコマンドを試してみたいと思います。

スラッシュコマンドは、公式にサポートされたコマンド機能です。組み込みのコマンドもいくつかあり、例えば `/tableflip おりゃー`と送信すると、**おりゃー (╯°□°）╯︵ ┻━┻**と変換してくれるコマンドなどがあります。

スラッシュコマンドは利用するユーザーにやさしいのが特徴です。/を打てば、コマンドの名前と機能の説明が表示されるだけでなく、コマンドをTabで補完したり、引数をユーザー型やチャンネル型などに限定することも出来ます。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234877/b075018b-aac8-ae19-8c6a-76ce58c573a7.png)

なお、この機能はベータ版であり、今後動かなくなる可能性があることに注意してください。

# コマンドの登録


##  Botの権限を変更する

まず、コマンドを追加するためにBotに権限が必要なので、[Discord Developer Portal — My Applications](https://discord.com/developers/applications)から新しくサーバーへの招待リンクを作り直します。

アプリケーションを選択👉OAuth2👉OAuth2 URL Generator

から`applications.commands`にチェックを入れ、生成されたURLを踏んで、Botのサーバー権限を上書きします。

## コマンドを追加する

いよいよ、コマンドを追加していきます。コマンドの追加は、以下のURLにPOSTでリクエストすることで可能です。

```
https://discord.com/api/v8/applications/{application.id}/commands
```

[公式チュートリアル](https://discord.com/developers/docs/interactions/slash-commands)を少し弄ったものをリクエストします。例はPythonです。

```python
import requests

url = "https://discord.com/api/v8/applications/<APPLICATION_ID>/commands"

json = {
    "name": "blep",
    "description": "Send a random adorable animal photo",
    "options": [
        {
            "name": "animal",
            "description": "The type of animal",
            "type": 3,
            "required": True,
            "choices": [
                {
                    "name": "Dog",
                    "value": "animal_dog"
                },
                {
                    "name": "Cat",
                    "value": "animal_cat"
                },
                {
                    "name": "Penguin",
                    "value": "animal_penguin"
                }
            ]
        },
        {
            "name": "only_smol",
            "description": "Whether to show only baby animals",
            "type": 5,
            "required": False
        }
    ]
}

# For authorization, you can use either your bot token 
headers = {
    "Authorization": "Bot <BOT_TOKEN>"
}

r = requests.post(url, headers=headers, json=json)
print(r.json())
```

レスポンスに登録したコマンドの情報に加えて、IDなどが帰ってきていれば登録成功です。

ここまで上手くいっていれば`/blep`でコマンドが表示されるはずです。ただし、この時点では送信しても特に何も起こりません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234877/44ae17ed-0003-3ade-d0dc-57b928dfb28a.png)


# コマンドに返信する

方法は二つあります。一つはGateway EventのInteractionEventをキャッチする方法。もう一つはwebhookを用意する方法です。今回は一つ目で行きます。

新しい機能なのでどのフレームワークにも当然実装されていません。ということで、生のDiscord Gatewayを触る...のは流石に面倒なのでdiscordpyを拡張しながら実装していきます。 

## コマンドをキャッチする

```python

from discord.ext import commands
from pprint import pprint

TOKEN = "<BOT TOKEN>"

class MyBot(commands.Bot):

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

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

        pprint(msg)

if __name__ == '__main__':
    bot = MyBot(command_prefix='!',
                   description='slash test')
    bot.run(TOKEN)
```


これで、スラッシュコマンドのEventにのみ反応し、Eventの内容は以下のような辞書で手に入ります。

```python
{'d': {'channel_id': '<CHANNEL ID>',
       'data': {'id': '<COMMAND ID>',
                'name': 'blep',
                'options': [{'name': 'animal', 'value': 'animal_dog'},
                            {'name': 'only_smol', 'value': True}]},
       'guild_id': '<GUILD ID>',
       'id': '<COMMAND INTERACTION ID>',
       'member': {'deaf': False,
                  'is_pending': False,
                  'joined_at': '2020-01-13T14:08:23.570000+00:00',
                  'mute': False,
                  'nick': None,
                  'pending': False,
                  'permissions': '11111111',
                  'premium_since': None,
                  'roles': [],
                  'user': {'avatar': '1111111111111111111111',
                           'discriminator': '1111',
                           'id': '11111111111111',
                           'public_flags': 111,
                           'username': 'Pishiko'}},
       'token': '<COMMAND INTERACTION TOKEN>',
       'type': 2,
       'version': 1},
 'op': 0,
 's': 3,
 't': 'INTERACTION_CREATE'}
```

## コマンドに返信する

これで、返信するのに必要なコマンドのアクション（=Interaction）のIDとトークンが分かったので、以下のURLにPOSTでリクエストします。

```
https://discord.com/api/v8/interactions/{interaction.id}/{interaction.token}/callback
```

discordpyでリクエストするにはaiohttpモジュールを利用します。以下のような関数を用意して返信します。

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

最終的に以下のようなコードになりました。

```python
from discord.ext import commands
from pprint import pprint
import aiohttp

TOKEN = "<BOT TOKEN>"

class MyBot(commands.Bot):

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

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

        await reply(msg["d"]["id"], msg["d"]["token"],str(msg["d"]["data"]["options"]))

async def reply(id, token, content):

    url = "https://discord.com/api/v8/interactions/{0}/{1}/callback".format(
        id, token)

    json = {
        "type": 4,
        "data": {
            "content": content
        }
    }

    async with aiohttp.ClientSession() as s:
        async with s.post(url, json=json) as r:
            if 200 <= r.status < 300:
                return

if __name__ == '__main__':
    bot = MyBot(command_prefix='!',
                   description='slash test')
    bot.run(TOKEN)
```

うまくいけば、このように表示されるはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234877/1fa49903-ef00-d054-5011-c8e4e544c229.png)


# おわりに

この他にも、サブコマンド、ユーザーやチャンネルの指定、返信の編集や表示の変更など、出来ることはまだあるので、以下を参考にしながらいろいろ遊んでみてください。

[Discord Developer Portal — Documentation — Slash Commands](https://discord.com/developers/docs/interactions/slash-commands)