discord pyでbotのコマンドをadd_command()で登録する

# 前提

この記事ではdiscord.pyを使ったdiscord botにおいて，command treeの`add_command()`を利用したコマンド登録を説明します．これに合わせて，コマンドのグループ化について少し触れます．

また，説明には以下のコードを使用し，これを発展させていきます．

<details><summary>サンプルコード</summary>

```python: discordbot.py
import os
from discord import Intents, Client, Interaction
from discord.app_commands import CommandTree
from dotenv import load_dotenv
load_dotenv()


class MyClient(Client):
    def __init__(self, intents: Intents) -> None:
        super().__init__(intents=intents)
        self.tree = CommandTree(self)

    async def setup_hook(self) -> None:
        await self.tree.sync()

    async def on_ready(self):
        print(f"login: {self.user.name} [{self.user.id}]")
        print(self.get_guild(1047700283217686719))


intents = Intents.default()
client = MyClient(intents=intents)


@client.tree.command()
async def hello(interaction: Interaction):
    await interaction.response.send_message(f'Hello, {interaction.user.mention}')


client.run(os.getenv("TOKEN"))
```
</details>


# CommandTree.add_command()

コマンドを追加する方法はサンプルコードにあるような`@client.tree.command()`の下にコマンドの下に関数を記述するだけでなく，CommandTreeのインスタンスから`add_command()`を呼び出し，引数にコマンドを与えるという方法があります．

[リファレンス](https://discordpy.readthedocs.io/ja/latest/interactions/api.html#discord.app_commands.CommandTree.add_command)を見てみると，
> command (Union[Command, Group]) -- 追加するアプリケーションコマンドまたはグループ。

とあります．Union型は指定された型のどれかであればいいというものなので，`Command`か`Group`のどちらかを指定すれば大丈夫です．

## Command
Commandについて見ていきましょう．[リファレンス](https://discordpy.readthedocs.io/ja/latest/interactions/api.html#command)によると，
> これは通常手動で作成されず、代わりに以下のデコレータを使用して作成されます。
> - command()
> - Group.command
> - CommandTree.command

とあります．このどれもが結局[discord.app_commands.command](https://discordpy.readthedocs.io/ja/latest/interactions/api.html#discord.app_commands.command)によるデコレータです．デコレータなのでわかりずらいですが，デコレータの下に記述した関数がクラスになります．[GitHubにある基のコード](https://github.com/Rapptz/discord.py/blob/master/discord/app_commands/commands.py#L1976)を見るほうがわかりやすいかもしれません．デコレータ部分で`Command`クラスが返されています．つまり`app_command(function)`とすればコマンドが登録されます．また，特に指定しなければ関数名が実行する際のコマンド名になります．

この`@discord.app_commands.command(Command)`を利用したサンプルコードを下記に置いておきます．

<details><summary>サンプルコード</summary>

```diff_python: discordbot.py
import os
- from discord import Intents, Client, Interaction
+ from discord import Intents, Client, Interaction, app_commands
from discord.app_commands import CommandTree
from dotenv import load_dotenv
load_dotenv()


+ @app_commands.command()
+ async def hello(interaction: Interaction):
+     await interaction.response.send_message(f'Hello, {interaction.user.mention}')


class MyClient(Client):
    def __init__(self, intents: Intents) -> None:
        super().__init__(intents=intents)
        self.tree = CommandTree(self)
+       self.tree.add_command(hello)

    async def setup_hook(self) -> None:
        await self.tree.sync()

    async def on_ready(self):
        print(f"login: {self.user.name} [{self.user.id}]")
        print(self.get_guild(1047700283217686719))


intents = Intents.default()
client = MyClient(intents=intents)


client.run(os.getenv("TOKEN"))

```
</details>

ただ，注意するべきことが一つあります．それは，**コマンドの登録の反映がすぐには行われない**ということです．当たり前といえば当たり前ですが，私は散々これで悩んだので共有しておきます．エラーなく実行できてるのに期待した通りにならなくて，でも時折できてて，とても悩みました......
そのコマンドを実行しようとすれば更新がかかると思います．でも更新されないときがあるのでそのときは空でも眺めましょう．

## Group
続いて，Groupを見ていきましょう．
そもそも，Groupによるコマンドとはコマンドの階層構造のようなものです．
```
MyGroup
├── subcommand
└── SubGroup
    └── subsubcommand
```
上記のようなコマンドグループがあったとして，subcommadを実行するには`/MyGroup subcommand`のように記述します．subsubcommandであれば`/MyGroup SubGroup subsubcommand`です．
このようにコマンドをカテゴリ別に分類することができます．

[リファレンス](https://discordpy.readthedocs.io/ja/latest/interactions/api.html#group)によると，継承して使うクラスなので，継承したクラスを作りましょう．

コードは以下のような感じです．本当は上に示したような構成のグループにしたかったのですが，更新が思っているより遅くて億劫なので事前に実験したものを載せます．

<details><summary>サンプルコード</summary>

```diff_python: discordbot.py
import os
from discord import Intents, Client, Interaction, app_commands
from discord.app_commands import CommandTree
from dotenv import load_dotenv
load_dotenv()


@app_commands.command()
async def hello(interaction: Interaction):
    await interaction.response.send_message(f'Hello, {interaction.user.mention}')

+ class MyGroup(app_commands.Group):
+     def __init__(self, name: str):
+         super().__init__(name=name)
+
+     @app_commands.command()
+     async def hello(self, interaction: Interaction):
+         await interaction.response.send_message(f'Hello, {interaction.user.mention}')

class MyClient(Client):
    def __init__(self, intents: Intents) -> None:
        super().__init__(intents=intents)
        self.tree = CommandTree(self)
        self.tree.add_command(hello)
        self.tree.add_command(MyGroup("group"))

    async def setup_hook(self) -> None:
        await self.tree.sync()

    async def on_ready(self):
        print(f"login: {self.user.name} [{self.user.id}]")
        print(self.get_guild(1047700283217686719))


intents = Intents.default()
client = MyClient(intents=intents)


client.run(os.getenv("TOKEN"))
```
</details>

現在のサンプルコードでは一つのファイルにまとめていますが，別ファイルに作りそれをimportすることでコードの見通しが良くなると思います．

以上がdiscord pyでbotのコマンドをadd_command()での登録でした．
お疲れ様でした．