discord botへのコマンド登録

# 前提


- 想定環境：linux (ubuntu)
- pythonによる開発，つまりdiscord.pyでのdicord botの作成をする
- botのアカウントがある
- botがログインできる

要するに
```python: discordbot.py
import os
from discord import Intents, Client
from dotenv import load_dotenv
load_dotenv()


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

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


intents = Intents.default()
client = MyClient(intents=intents)
client.run(os.getenv("TOKEN"))
```

こんな感じのプログラムが正しく動けば大丈夫です．

# コマンド
まずコマンドは，discord上でチャットにスラッシュ(/)を入力すると表示されるやつです．コマンドに応じて様々な処理が可能です．
以下の画像は，デフォルトで使用できるコマンド`/tableflip`を実行してみたものです．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953650/6e6f9945-7f9d-fc61-9dd1-c1aa0e848005.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953650/1a684b49-418f-1608-15cf-b2d6c0f82c92.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953650/7ed71f5f-02f3-f371-43de-ebbedab96feb.png)

ちなみに，スラッシュから入力するコマンドはスラッシュコマンドと言います．コマンドは，任意の文字から始めたり，特定の文字列に反応したコマンドなどのテキストベースのものもあります．それと区別してスラッシュコマンドと言う，んだと思ってます．

そしてテキストベースのコマンドは，[動作を保証されなくなったはず](https://discord.com/blog/welcome-to-the-new-era-of-discord-apps/?ref=message_content_notification)です．
> September 1, 2022 is the final switchover date. This means that, if the developer of one of the apps you’re using has not made the switch yet, parts of the app might not work after that time.

とのことですので，スラッシュコマンドを使っていきましょう．

スラッシュコマンドのほかにもボタンを使ったGUI操作もできます．これらは総称してアプリケーションコマンドというみたいです．詳しくは[ドキュメント](https://discord.com/developers/docs/interactions/application-commands#authorizing-your-application)を読んでください．

アプリケーションコマンドを使うには，botのscopeに`applications.commands`を含める必要がありますので注意してください．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953650/fda1ee71-6943-c2fb-5c25-6a1396e56d09.png)



# スラッシュコマンド

スラッシュコマンドは[discord.app_commandsにあるCommandTree](https://discordpy.readthedocs.io/ja/latest/interactions/api.html?highlight=app_command#commandtree)を利用します．このCommandTreeのインスタンスに対しコマンドを追加していきます．そして，CommandTreeのデコレータ`command()`の下に関数を書くことでコマンドの追加ができます．

```diff_python: discordbot.py
import os
- from discord import Intents, Client
+ 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 on_ready(self):
        print(f"login: {self.user.name} [{self.user.id}]")


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"))

```

しかし今のままではスラッシュコマンドが使えません．CommandTreeに追加したコマンドはdiscordに同期しないと表示，使用することができないからです．そのため同期をさせる処理を追加します．そのタイミングはbotがログインするタイミングが一番良いでしょう．また，一度同期させれば問題ないため，今回は`setup_hook()`内に処理を追加します．

```diff_python: discord.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}]")


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"))

```

チャットにスラッシュを打ち込むと
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953650/0b474da2-e803-d57f-ab9e-9258f53ea389.png)

実行すると

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953650/a665d2dd-1523-3bb3-175a-9c5147c5b6aa.png)

こんな風になりました．

コードの具体的なところを見ていきましょう．
```python
@client.tree.command()
async def hello(interaction: Interaction):
    await interaction.response.send_message(f'Hello, {interaction.user.mention}')
```

実行されたときの画像を見てもらうとわかりますが，コマンドを使用するとそのときのメッセージ「/hello」はチャットに残りません．その代わりにどのコマンドを使用したかのメッセージに対しての返信がbotから送信されます．つまりコードは

- どのコマンドを使用したか
    - interaction
- 反応をするためのオブジェクト
    - response
- 反応への返信
    - send_message(message)

という感じです．ただし，リファレンスに書いてありますが応答は一度しかできません．そのため複数回にわたってメッセージを送りたい場合，[リファレンス](https://discordpy.readthedocs.io/ja/latest/interactions/api.html#discord.Interaction.response)によると
> 応答は一度だけ行うことができます。複数回にわたってメッセージを送信する必要がある場合は、代わりに followup を使用することを検討してください。

とありますが，[followup](https://discordpy.readthedocs.io/ja/latest/interactions/api.html#discord.Interaction.followup)は[webhook](https://discordpy.readthedocs.io/ja/latest/api.html#discord.Webhook)です．現状ではこれを使えません．これを使うには．．．という話は正直，私には現状わかりません．気にせず使ってみたらエラー[^web_error]吐きました．[リファレンスのwebhook](https://discordpy.readthedocs.io/ja/latest/api.html#discord.Webhook)のところを読んでみてください．少なくとも，サーバーにwebhookを登録するには，サーバーのアイコンを右クリックして，サーバー設定から連携サービスを選び，そこからwebhookが作成できるみたいです．この先は知りません．申し訳ありません．

複数回メッセージを送信したい場合はchannelを取得して，そこからメッセージを送ればよいと思います．
```diff_python
@client.tree.command()
async def hello(interaction: Interaction):
+   await interaction.channel.send("use interaction.channel.send")
+   await interaction.channel.send("おはようございます")
    await interaction.response.send_message(f'Hello, {interaction.user.mention}')
```


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953650/ce15deea-ed8c-4f62-3750-21c837d0bf1a.png)

ただ，書いたコードの順番ではないようですね．インタラクション自体はコマンドを打った時ですので，それがresponceによって書き換わっているためこのような順番になるのだと思います．そのため，responceを利用する場合は先頭に書いた方がよさそうです．

ともかく，これにてbotにスラッシュコマンドを登録することができました．別の処理をするコマンドも同様に追加することができます．
お疲れ様でした．

[^web_error]: こんなエラー吐きました．`discord.app_commands.errors.CommandInvokeError: Command 'hello' raised an exception: NotFound: 404 Not Found (error code: 10015): Unknown Webhook`．やはりチャンネルにwebhookを登録していないからですね．