LoginSignup
8
7

More than 1 year has passed since last update.

discord botでドロップダウンメニューを作る

Last updated at Posted at 2022-12-14

前提

これはdiscord botからminecraft serverを動かしたいアドベントカレンダーの記事です.

また,

  • WSL, ubuntu 22.04.1
  • discord.py 2.1.0
  • Python 3.10.6

という環境で行っています.

Select

公式ドキュメントによると

Represents a UI select menu with a list of custom options. This is represented to the user as a dropdown menu.

みたいです.as a dropdown menuとあるようにドロップダウンメニュー(ドロップダウンリスト)を作成するものです.名称はリストの方でもメニューの方でもいいみたいなので,この記事ではドロップダウンメニューやメニューと言うことにします.

Selectには,複数の種類があります.

  • Select
    • 任意の選択肢を作成できる
  • ChannelSelect
    • チャンネルを選択肢として持つ
  • RoleSelect
    • ロールを選択肢として持つ
  • UserSelect
    • メンバーを選択肢として持つ
  • MentionableSelect
    • メンションが可能である,ロールとメンバーを選択肢を持つ

注意
Select以外のものはdiscord.pyのバージョン2.1が必要になります

Select以外は,特定のものに対するショートカットみたいなものだと思っています.そのため,ここではSelectについて説明します.

実装

SelectはUIですので,Viewに追加する必要があります.Viewのadd_itemというメソッドを使用して,メッセージに組み込むUIを作っていきます.
ただし,一つも選択肢がない場合エラーが発生するため,少なくとも一つは選択肢を追加する必要があります.選択肢を追加するためには,Selectのadd_item()メソッドを使います.

具体的にコードを見ていきましょう.

selectorbot.py
import os
import discord
from discord import Intents, Client, Interaction
from discord.ui import Select, View
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 somemenu(interaction: Interaction):
    select = Select(placeholder="this is placeholder")
    select.add_option(
        label="user can see this",
        value="user can not see this",
        description="this is description",
    )

    view = View()
    view.add_item(select)

    await interaction.response.send_message("menu", view=view)


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

実行し,メニューを広げてみると

image.png

こんな感じになります.
ちなみに現段階で選択してみると

image.png

失敗します.
選択されたときの処理を設定していないからですね.
というわけで設定しましょう.

選択されたときの処理

選択されたときの処理は,Selectクラスのcallbackメソッドをサブクラスで上書きし,そのサブクラスをViewに渡す方法があります.ただ,これは面倒(当人比)なので別の方法を利用します.
それは,discord.ui.selectデコレータを使って,ViewのサブクラスにSelectクラスのcallbackメソッドを実装する方法です.

discord.ui.selectデコレータの下に記述された関数は

  • Selectクラスとして扱われる
  • 関数内の処理はSelectクラスのcallbackメソッドとして解釈される
  • Viewに追加された状態になる

となります.
これにより,

  • Selectを継承したサブクラスを作成しcallbackをオーバーライドする
  • ViewにSelectを追加する

といった手間から解放されます.
コードを見ていきましょう.

selectorbot.py
import os
import discord
from discord import Intents, Client, Interaction
from discord.ui import Select, View
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)


+ class SelectView(View):
+     @discord.ui.select(
+         cls=Select,
+         placeholder="this is placeholder"
+     )
+     async def selectMenu(self, interaction: Interaction, select: Select):
+         await interaction.response.send_message(f"You selected {select.values}")


@client.tree.command()
async def somemenu(interaction: Interaction):
-    select = Select(placeholder="this is placeholder")
-    select.add_option(
-        label="user can see this",
-        value="user can not see this",
-        description="this is description",
-    )
-    view = View()
-    view.add_item(select)
+    view = SelectView()
+    view.selectMenu.add_option(
+        label="user can see this",
+        value="user can not see this",
+        description="this is description",
+    )

    await interaction.response.send_message("menu", view=view)


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

実行して選択してみると

image.png

と選択したものを表示してくれました.
ちなみに選択肢は,デコレータの引数にoptionsというパラメータがあり,そこに以下のように記述しても大丈夫です.

SelectView class
class SelectView(View):
    @discord.ui.select(
        cls=Select,
        placeholder="this is placeholder",
+       options=[
+           discord.SelectOption(label="one"),
+       ],
    )
    async def selectMenu(self, interaction: Interaction, select: Select):
        await interaction.response.send_message(f"You selected {select.values}")

image.png

このように選択肢が追加されました.
ただし,デコレータを利用して選択肢を追加しているため,デコレータ内で配列変数などを走査して選択肢を作るにはmapやfilterといったものを利用する必要があると思います.ですが,おそらくグローバル変数としておく必要があります.あやふやになってしまいましたが,Viewのインスタンスを生成した箇所で選択肢を追加するのが無難かなと思います.選択肢が明らかに少なければoptionsでもいいとも思います.

メニューの操作

さて,実はここまでの実装をしたコードだとあまり嬉しくない動作をします.どんな動作かわかりますでしょうか.
以下をご覧ください.これは3つの選択肢から1つを選択して選んでほしいときのメニューです.

どういう画質してんでしょうね

このように,何度も選択肢を選びなおせてしまいます.これができてしまうと過去に送られたSelectに反応できてしまいます.これへの対策としてViewにはtimeoutというプロパティを設定でき,設定された秒数が経過するとインタラクションを受け付けなくなります.また,on_timeoutメソッドにより,timeoutした際の処理を記述できます.しかし,on_timeoutメソッドではインタラクションを引数に取りませんので,選択されたことに対して返信するには別途,Selectが送られたチャンネルの情報が必要になります.Viewを作る際の引数に渡してしまえばできますが,やや面倒です.それよりも簡単な方法があります.それは,そもそも使えなくしてしまえばいい,という方法です.

disable

Selectにはdisableというプロパティがあります.disableをtrueにすると,メニューで選択ができなくなります.とりあえず使えなくしてみましょう.ここのサイトを参考にしました.

SelectView class
class SelectView(View):
    def __init__(self, *, timeout: float = 2):
        super().__init__(timeout=timeout)

    async def on_timeout(self) -> None:
        print("timeout")

    @discord.ui.select(
        cls=Select,
        placeholder="this is placeholder",
+       disabled=True,
    )
    async def selectMenu(self, interaction: Interaction, select: Select):
        await interaction.response.send_message(f"You selected {select.values}")

image.png

こんな感じで使えなくなります.

これを利用して,選択された後に使えなくしましょう.ただコード自体は単純なのですが,中身がやや複雑です.
とりあえずコードを見てみましょう.

SelectView class
class SelectView(View):
    def __init__(self, *, timeout: float = 2):
        super().__init__(timeout=timeout)

    async def on_timeout(self) -> None:
        print("timeout")

    @discord.ui.select(
        cls=Select,
        placeholder="this is placeholder",
-       disabled=True,
    )
    async def selectMenu(self, interaction: Interaction, select: Select):
-       await interaction.response.send_message(f"You selected {select.values}")
+       select.disabled = True
+       await interaction.response.edit_message(view=self)
+       await interaction.followup.send(f"You selected {select.values}")

実行してみると

selectmenu_disable.gif

このように,選択すると使えなくなりました.

followupについて軽く説明します.インタラクションのresponseは一度しか利用できないため,followupを利用してresonseに対して返信を行っています.つまり,インタラクションに対してresponseで何か処理を行い,そのresponseに対して連なる形でメッセージを送信しています.

続いて,インタラクションというものは,ユーザーがアプリに対して操作をしたメッセージ,と解釈して問題ありません.よって,selectMenuに対応するインタラクションはユーザーが選択肢を選んだメッセージと言えます.
従って,await interaction.response.edit_message(view=self)部分は,ドロップダウンメニューがあるメッセージに対してedit_messageを行っています.つまりメッセージを編集しています.今やりたいことはメニューを使えなくすることですので,編集するものはviewだけです.viewに対してselfを渡していますが,一旦ここは飛ばして一行前を見てください.
select.disabled = Trueという部分です.これはインタラクションが発生したメニューであるSelectのインスタンスが入っています.このインスタンスは,デコレータによってSelectクラスになったselectMenuによるインスタンスです.しかし,selectMenucallbackです.つまり,SelectViewのメソッドでもあります.引数のselectselect.disabled = Trueとしたにもかかわらずedit_message(view=self)で自分自身を渡してちゃんと動作していたのは,

  • 引数のselectは,SelectVeiwのインスタンスであるviewのselectMenuというSelectのインスタンスが入っている
  • このselectのdisabledをTrueにするとviewのselectMenuがdisableになる.
  • disableをTrueにしている処理はview内部である
  • つまり上記2つからselectMenuにおいて,メニューがdisableになったSeleceViewであるself(view)を渡しているから動作する

ということになります.

注意

徹夜してる頭で考えたものですので,違う可能性が大いにあります.

雑に言うと,selectに入ってるやつは自分だから,viewも自分の渡せばいいって感じだと思います.

何をやっていたか忘れました

これでメニューを選択できないようにしました.ややこしいものもありましたが,とりあえずそういうもんってしておくと生きやすいです.

以上となります.discord botでドロップダウンリストを作りました.
お疲れ様でした.

8
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
7