discord pyのcontext menuとbutton

# 前提

この記事ではdiscord.pyによるdiscord bot制作において，context menuとbuttonの作成について説明していきます．
以下のサンプルコードを用いて発展させていきます．

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

```python: discordbot.py
import os
from discord import Intents, Client
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.run(os.getenv("TOKEN"))
```

</details>


また，今回はCommandTreeの`command`デコレータを使ってコマンドを登録していきますが，`add_command()`を使って登録してもいいと思います．
そして上記のサンプルコード，および記事の作成にあたって[公式のサンプル](https://github.com/Rapptz/discord.py/blob/master/examples/app_commands/basic.py)を参考にしてます．あともちろん[APIリファレンス](https://discordpy.readthedocs.io/ja/latest/index.html)も見てます．

# ContextMenu

まず，ContextMenuのコマンドは，

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953650/79cb91c9-6885-2802-8b03-32d77aa6de67.png)

これです．ContextMenuを登録すると右クリックで表示されるメニューに「アプリ」が追加され，それをマウスオーバーするとコマンドが表示されるようになります．右クリックのメニューがコンテキストメニューのことです．

ちなみにクリックして実行すると
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953650/7ee865bb-4fa6-ec92-be15-67bb1240cdf0.png)

こんな感じのことを言うように設定しています．コードは以下の通りです．

```diff_python: discordbot.py
import os
- from discord import Intents, Client
+ from discord import Intents, Client, Interaction, Member
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.context_menu()
+ async def greeting(interaction: Interaction, member: Member):
+     await interaction.response.send_message(f"this is {member.nick} !!!")

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

`context_menu()`のデコレータを利用してコマンドを作ります．引数にはスラッシュコマンドを利用したときに発生する`Interaction`と，どこを右クリックしたときにコマンドが表示されるかの変数を記述する必要があります．[リファレンス](https://discordpy.readthedocs.io/ja/latest/interactions/api.html#discord.app_commands.context_menu)によると，
> 関数は第一パラメータとして Interaction を取り、第二パラメータとして Member 、 User 、 Message 、または Member と User の typing.Union を取らないといけません。

MemberとUserの違いは，Memberがギルド，つまりそのサーバーに参加しているユーザーで，Userはサーバーに参加しているかに依存しないユーザーです．例えばニックネームはサーバー依存ですのでUserでは取れずMemberである必要があります．
まとめると右クリックしてコマンドを実行できる場所は，誰かのアカウントのところかメッセージのところです．

# Button
Buttonは，

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953650/e7c4352f-4d79-279f-ed90-c6a41878c309.png)

こういうやつです．ボタンにcallback関数を追加することで固有の処理を追加できます．コードは以下の通りです．

```diff_python: discordbot.py
import os
import discord
- from discord import Intents, Client, Interaction, Member
+ from discord import Intents, Client, Interaction, Member, ButtonStyle
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.context_menu()
async def greeting(interaction: Interaction, member: Member):
    await interaction.response.send_message(f"this is {member.nick} !!!")


+ class YesButton(discord.ui.Button):
+     def __init__(self, *, style: ButtonStyle = ButtonStyle.secondary, label: str = "yes"):
+         super().__init__(style=style, label=label)
+ 
+     async def callback(self, interaction: Interaction):
+         await interaction.response.send_message("clicked")
+         await interaction.followup.send("you may push yes......")


+ class NoButton(discord.ui.Button):
+     def __init__(self, *, style: ButtonStyle = ButtonStyle.secondary, label: str = "no"):
+         super().__init__(style=style, label=label)
+ 
+     async def callback(self, interaction: Interaction):
+         await interaction.response.send_message("clicked")
+         await interaction.followup.send("you may push nooooooo!!!!")


+ @client.tree.command()
+ async def button(interaction: Interaction):
+     view = discord.ui.View()
+     view.add_item(YesButton(style=discord.ButtonStyle.primary))
+     view.add_item(NoButton(style=discord.ButtonStyle.grey))

    await interaction.response.send_message("do you want to button?", view=view)

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

```

ボタンのUIを作成している部分は`button`関数です．discordにおいてUIを作成するためには`View`クラスのインスタンスにUIアイテムを追加していきます．今回はボタンを追加することになります．
そしてボタンを押されたときに実行する関数を設定するために，discord.ui.Buttonクラスを継承したクラスにおいて`callback`メソッドをオーバーライドします．`callback`メソッド内に書かれた処理がボタンを押したときに実行されます．

このようなボタンを作成して実行してみた結果が以下の通りです．初めてgifを作ったのでので荒いのは見逃してください．とりあえず，ボタンを押したら返信したなってことがわかれば大丈夫です．

![discordbot-button.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953650/5016928c-39ff-feec-8585-a8d974d7ef13.gif)


以上がcontext menuとbuttonの作成について説明となります．context menuとbuttonだけでなく，ドロップダウンリストやテキスト入力もできますので，[リファレンス](https://discordpy.readthedocs.io/ja/latest/interactions/api.html#bot-ui-kit)を読んでみてください．
お疲れ様でした．