前提
これは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()メソッド
を使います.
具体的にコードを見ていきましょう.
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"))
実行し,メニューを広げてみると
こんな感じになります.
ちなみに現段階で選択してみると
失敗します.
選択されたときの処理を設定していないからですね.
というわけで設定しましょう.
選択されたときの処理
選択されたときの処理は,Selectクラスのcallback
メソッドをサブクラスで上書きし,そのサブクラスをViewに渡す方法があります.ただ,これは面倒(当人比)なので別の方法を利用します.
それは,discord.ui.select
デコレータを使って,ViewのサブクラスにSelectクラスのcallback
メソッドを実装する方法です.
discord.ui.select
デコレータの下に記述された関数は
- Selectクラスとして扱われる
- 関数内の処理はSelectクラスの
callback
メソッドとして解釈される - Viewに追加された状態になる
となります.
これにより,
- Selectを継承したサブクラスを作成し
callback
をオーバーライドする - ViewにSelectを追加する
といった手間から解放されます.
コードを見ていきましょう.
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"))
実行して選択してみると
と選択したものを表示してくれました.
ちなみに選択肢は,デコレータの引数にoptions
というパラメータがあり,そこに以下のように記述しても大丈夫です.
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}")
このように選択肢が追加されました.
ただし,デコレータを利用して選択肢を追加しているため,デコレータ内で配列変数などを走査して選択肢を作るにはmapやfilterといったものを利用する必要があると思います.ですが,おそらくグローバル変数としておく必要があります.あやふやになってしまいましたが,Viewのインスタンスを生成した箇所で選択肢を追加するのが無難かなと思います.選択肢が明らかに少なければoptions
でもいいとも思います.
メニューの操作
さて,実はここまでの実装をしたコードだとあまり嬉しくない動作をします.どんな動作かわかりますでしょうか.
以下をご覧ください.これは3つの選択肢から1つを選択して選んでほしいときのメニューです.
どういう画質してんでしょうね
このように,何度も選択肢を選びなおせてしまいます.これができてしまうと過去に送られたSelectに反応できてしまいます.これへの対策としてViewにはtimeoutというプロパティを設定でき,設定された秒数が経過するとインタラクションを受け付けなくなります.また,on_timeout
メソッドにより,timeoutした際の処理を記述できます.しかし,on_timeout
メソッドではインタラクションを引数に取りませんので,選択されたことに対して返信するには別途,Selectが送られたチャンネルの情報が必要になります.Viewを作る際の引数に渡してしまえばできますが,やや面倒です.それよりも簡単な方法があります.それは,そもそも使えなくしてしまえばいい,という方法です.
disable
Selectにはdisable
というプロパティがあります.disable
をtrueにすると,メニューで選択ができなくなります.とりあえず使えなくしてみましょう.ここのサイトを参考にしました.
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}")
こんな感じで使えなくなります.
これを利用して,選択された後に使えなくしましょう.ただコード自体は単純なのですが,中身がやや複雑です.
とりあえずコードを見てみましょう.
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}")
実行してみると
このように,選択すると使えなくなりました.
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
によるインスタンスです.しかし,selectMenu
はcallback
です.つまり,SelectViewのメソッドでもあります.引数のselect
をselect.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でドロップダウンリストを作りました.
お疲れ様でした.