最近、趣味でdiscord.py
を用いてちょっとしたdiscord botを作っています。
UIに少し凝って作り込んだ時にハマったので、記事にしたいと思います。
また、現在discord.py
はalpha版であり、安定していないことから、これが変わる可能性もあります。
また、このことから現在はサードパーティライブラリを使わないで実装しております(サードパーティを使ってみたら動かなかったため)。
今回用いた環境を書いておきます。
Python 3.9.2
discord.py-2.0.0a4070
TL;DR
-
View.from_message()
ではViewのコピーしか行わなく、ButtonのCallbackなどはコピーしてくれない。 - 自分のViewクラスを作ったとき、
from_message()
もオーバーライドしてViewをコンポーネントから再構築する必要がある
起こった事象
今回作りたかったボットの仕様の一部として、
- 図1ようなボタンがあって、クリックされるとボタンの色が変わり、図2のような"キャンセル"ボタンが出現します。
- キャンセルボタンがクリックされると、図1の状態に戻り、1.の動作が再び行えるようにする
これを以下のように実装しました。
CHANNEL_ID = (チャンネルID)
TOKEN = (トークン)
intent = discord.Intents.all()
bot = commands.Bot(command_prefix=commands.when_mentioned_or('$'), intents=intent)
@bot.event
async def on_ready():
print('login successfully')
############### begin of UI ###############
# メインとなるボタン
class MainButton(Button):
def __init__(self):
super().__init__(label='Button',style=discord.ButtonStyle.primary)
async def callback(self, interaction: Interaction):
assert self.view is not None
view: View = self.view
print(view.is_finished())
self.style = discord.ButtonStyle.danger
await interaction.response.edit_message(view=view)
await interaction.followup.send(content=f'clicked',view=ReplyView())
print(f'{interaction.user.name} clicked')
# ボタンを並べるView
class RowView(View):
def __init__(self):
super().__init__(timeout=None)
self.add_item(MainButton())
#MainButtonが押された時に返信されるView
class ReplyView(View):
def __init__(self):
super().__init__(timeout=None)
self.add_item(CancelButton())
# キャンセルボタン
class CancelButton(Button):
def __init__(self):
super().__init__(label=f'Cancel', style=discord.ButtonStyle.secondary, emoji='❌')
async def callback(self, interaction: Interaction):
assert self.view is not None
this_view: View = self.view
message = await bot.get_channel(CHANNEL_ID).fetch_message(message_id_list[0])
view: View = View.from_message(message)
view.children[0].style = discord.ButtonStyle.primary
await message.edit(view=view)
print(f'{interaction.user.name} canceled')
await interaction.message.delete()
this_view.stop()
############### end of UI ###############
message_id_list = list()
@bot.command()
async def execute(ctx):
message = await ctx.send(view=RowView())
message_id_list.append(message.id)
bot.run(TOKEN)
これを実行し、上の手順1.,2.を行ったあと、もう一度手順1.を行うと以下のようにインタラクションに失敗してしまいます。
この原因が分からず1日ほど悩み、Bot作っている知り合いに聞いてみたところ、原因が判明しました。
原因
どうやらこのView.from_message()
関数は、View(コンポーネントなどの外見)だけをコピーしているだけのようで、中身のボタンのインスタンスまではコピーしていないようです。なので、ボタンのインスタンスに含まれるCallback関数が呼ばれないとのこと。
解決法
それなら、View.from_message()
関数を自分が継承して作ったRowView
でオーバーライドしてしまえば良くね?ってなり、このような感じにしました。
class RowView(View):
# initは同様
# ......
# from_message()をオーバーライド
@classmethod
def from_message(cls,message):
cls = super().from_message(message)
cls.clear_items()
cls.add_item(MainButton())
return cls
View.from_message()
はクラスメソッドなので、このように書けばOK。返すクラスでViewを消して、MainButton()
を入れ直してあげます。
次にCancelButton
クラス内のコールバックで、
view: View = View.from_message(message)
を
view: RowView = RowView.from_message(message)
と、自分で作ったサブクラスのfrom_message()
を呼べばOK。
これで手順1.,2.を永遠にループ出来るようになりました。
また、メッセージが投稿されるわけではないので、ボタンがついているメッセージが複数あっても順番が入れ替わることもありません。
参考