LoginSignup
3
1

More than 1 year has passed since last update.

discord.py 2.0.0aで、view.from_messageでViewを取得した時にcallbackが実装されない問題を解決する

Posted at

最近、趣味で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. 図1ようなボタンがあって、クリックされるとボタンの色が変わり、図2のような"キャンセル"ボタンが出現します。
  2. キャンセルボタンがクリックされると、図1の状態に戻り、1.の動作が再び行えるようにする

image.png
図1: ボタンがクリックされる前の状態

image.png
図2: ボタンがクリックされた後の状態

これを以下のように実装しました。

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.を行うと以下のようにインタラクションに失敗してしまいます。

image.png

この原因が分からず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.を永遠にループ出来るようになりました。
また、メッセージが投稿されるわけではないので、ボタンがついているメッセージが複数あっても順番が入れ替わることもありません。

参考

3
1
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
3
1