DiscordチャットボットをPythonでつくる (9) - メッセージにインタラクティブなボタンを設置する
今回は、チャットメッセージにインタラクティブなボタンを設置する方法を紹介します。
また、ボタンを押した際に実行可能な、様々な種類のレスポンスについて詳しく見ていきます。
今までの記事に比べて分量が多めです。
メッセージコンポーネントについて
Discordには、テキストメッセージの他に「メッセージコンポーネント」(または単に「コンポーネント」)
と呼ばれるインタラクティブな要素を追加できるフレームワークが用意されています。
(参照: Message Components)
この機能により、ボタンやセレクトメニュ等の様々なコンポーネントを
メッセージに設置することができるようになります。
discord.py
では、これらの機能はdiscord.ui
以下に実装されています。
(参照: Bot UIキット)
1. ボタンを設置する
それでは早速、discord.ui
を使用してメッセージにボタンを配置しましょう。
コンポーネントをメッセージに追加するには、はじめにdiscord.ui.View
インスタンスを作成します。
そしてadd_item
メソッドを用いてView
インスタンスにコンポーネントを追加します。
ここからは、「OK」と書かれたボタン付きのメッセージを出力する!button
コマンドを作っていきましょう。
-
commands/button.py
from discord.ext import commands from discord import ui @commands.command(name='button') async def button_command(ctx: commands.Context): """ボタン付きメッセージを表示する.""" view = ui.View() # Viewインスタンスを作成 button = ui.Button(label='OK') # Buttonインスタンスを作成 view.add_item(button) # ViewにButtonを追加 await ctx.send(view=view) # Viewを追加したメッセージを送信
-
chatbot.py
... from commands.quiz import quiz_command from commands.button import button_command BOT_COMMANDS = [omikuji_command, quiz_command, button_command] ...
上記のように設定後チャットボットを起動し、!button
コマンドを実行してみましょう。
すると、以下のようにボタンが表示されるはずです。
ボタンを押してみましょう。
するとまだ何も設定していないため「インタラクションに失敗しました」と表示されます。
2.ボタンを押した際の動作を定義する
それでは次に、ボタンを押した際の動作を定義しましょう。
2.1. ui.Button
を継承したクラスを作成
discord.py
には、ボタン押下時の動作に関していくつか設定方法が用意されていますが、
今回はui.Button
を継承した独自のクラスを作成する方法を紹介します。
はじめに、以下のようにui.Button
を継承したOKButton
クラスを作成します。
class OKButton(ui.Button):
...
2.2. __init__
メソッドを定義
次に、インスタンスの初期化を行う__init__
メソッドでは、
以下のようにlabel
の初期値として「OK」を代入しておきます。
class OKButton(ui.Button):
def __init__(self, *, label='OK', **kwargs):
super().__init__(label=label, **kwargs)
2.3. callback
メソッドを定義
次に、ボタンが押された際に実行されるcallback
メソッドを定義します(コルーチン関数です)。
callback
の第2引数には、発生したインタラクションを意味するInteraction
型の値interaction
が入ります。
(参照: APIリファレンス)
from discord import ui, Interaction
...
class OKButton(ui.Button):
...
async def callback(self, interaction: Interaction):
...
次に、レスポンスの種類について見ていきます。
callback
メソッドでは、発生したinteraction
に対して、レスポンスを1度だけ返す必要があります。
レスポンスは2度以上返すことは出来ないことに注意してください。
またレスポンスは3秒以内に返す必要があります。
2.3.1. interaction.response.send_message
でメッセージを返信
interaction.response.send_message
コルーチン関数を使用すると、
コンポーネントが配置されているメッセージに対する返信を行います。
以下では「OKボタンが押されました」というメッセージを返信します。
class OKButton(ui.Button):
...
async def callback(self, interaction: Interaction):
await interaction.response.send_message("OKボタンが押されました。")
(参照: 2.5.1 send_message
を使用した場合)
2.3.2 interaction.response.edit_message
でメッセージを編集
interaction.response.edit_message
コルーチン関数を使用すると、
コンポーネントが配置されているメッセージの編集を行います。
以下では、「OKボタンが押されました」というメッセージを設定します。
class OKButton(ui.Button):
...
async def callback(self, interaction: Interaction):
await interaction.response.edit_message(content="OKボタンが押されました。")
(参照: 2.5.2 edit_message
を使用した場合)
2.3.3 interaction.response.defer
で長時間作業の前にレスポンスを返す
interaction.response.defer
コルーチン関数は、長時間かかる作業を実施する際に使用されます。
上述の通りレスポンスは3秒以内に返す必要があるため、
長時間作業の前に一旦レスポンスを返しておくといった用途で使用されます。
また、thinking=True
とすると、チャット上に「<ボット名>が考え中…」と表示されるようになります。
(参照: APIリファレンス)
以下は、10秒経過後に「OKボタンが押されました。」というメッセージを返信する例です。
(interaction.followup
については後ほど紹介します。)
import asyncio
...
class OKButton(ui.Button):
...
async def callback(self, interaction: Interaction):
await interaction.response.defer(thinking=True)
await asyncio.sleep(10)
await interaction.followup.send(content="OKボタンが押されました。")
self.view.stop()
(参照: 2.5.3. defer
を使用した場合)
2.4. 作成したボタンを使用するようにコマンドを調整
最後に、作成したOKButton
を使用するようにbutton_command
を変更します。
@commands.command(name='button')
async def button_command(ctx: commands.Context):
"""ボタン付きメッセージを表示する."""
view = ui.View()
button = OKButton()
view.add_item(button)
await ctx.send(view=view)
2.5. 動作確認
チャットボットを起動して!button
コマンドを入力し、表示されるボタンを押してみましょう。
2.5.1 send_message
を使用した場合
interaction.response.send_message
を使用して2.3.1の設定を行った場合、
ボタンを押す度にメッセージが返信されることがわかります。
しかし、しばらくしてから再度ボタンを押すと「インタラクションに失敗しました」と表示され、
返信されなくなります。
2.5.1.1 複数回のインタラクションを禁止したい場合
ボタンを押して実行されるのは1回限りとしたい場合、ボタンのcallback
メソッドでView
を停止させます。
self.view
でボタンが設置されているView
が取得でき、
self.view.stop()
でインタラクションの受け付けを停止することが出来ます。
class OKButton(ui.Button):
...
async def callback(self, interaction: Interaction):
await interaction.response.send_message("OKボタンが押されました。")
self.view.stop()
変更後試してみると、2回目以降は「インタラクションに失敗しました」と表示され失敗するようになります。
2.5.1.2 一定時間経過後に「インタラクションに失敗しました」とならないようにしたい場合
一定時間経過後にインタラクションが失敗するようになるのは、ui.View
インスタンスの初期化時に、
timeout
オプションがデフォルトで180秒(3分)に設定されていることが原因です。
無期限にしたい場合はNone
に設定してください。
@commands.command(name='button')
async def button_command(ctx: commands.Context):
"""ボタン付きメッセージを表示する."""
view = ui.View(timeout=None)
...
2.5.2 edit_message
を使用した場合
interaction.response.send_message
を使用して2.3.2の設定を行った場合、
ボタンが設置してあるメッセージのテキストが「OKボタンが押されました。」に変更されます。
2.5.2.1 ボタンを消したい場合
実行後にボタンをメッセージから消去したい場合、edit_message
のview
オプションをNone
にすると実現できます。
class OKButton(ui.Button):
...
async def callback(self, interaction: Interaction):
await interaction.response.edit_message(content="OKボタンが押されました。", view=None)
2.5.2.2 ボタンを無効化したい場合
実行後にボタンを無効化したい場合、以下のようにself.disabled = True
としてボタンを無効化後、
edit_message
の引数でview=self.view
として実行し、メッセージに反映することで実現できます。
class OKButton(ui.Button):
...
async def callback(self, interaction: Interaction):
self.disabled = True
await interaction.response.edit_message(content="OKボタンが押されました。", view=self.view)
2.5.3. defer
を使用した場合
interaction.response.defer
を使用して2.3.3の設定を行った場合、
以下のように10秒間「考え中…」となった後にメッセージが返信されます。
2.6. レスポンスを複数回行いたい場合
2.3節で以下のように書いた通り、1つのインタラクションに対するレスポンスは1つのみ可能です。
callback
メソッドでは、発生したinteraction
に対して、レスポンスを1度だけ返す必要があります。
レスポンスは2度以上返すことは出来ないことに注意してください。
しかし、「元のメッセージのボタンを無効化しつつ、メッセージを返信する」といったような
複数の操作を行いたい場合があります。これを実現するにはどうすればよいでしょうか?
これは、interaction.followup
を使用することで実現できます。
(参照: Followup Messages, APIリファレンス)
interaction.followup
はインタラクション発生後15分間有効です。
それ以上経過後に実行すると、以下のようなエラーが発生し失敗します。
discord.errors.HTTPException: 401 Unauthorized (error code: 50027): Invalid Webhook Token
(参照: Followup Messages)
2.6.1 interaction.followup.send
でメッセージを後から返信する
interaction.followup.send
を使用すると、レスポンスを返答後でもメッセージを返信することが出来ます。
これを利用すると、例えば以下のようにして、
「元のメッセージのボタンを無効化しつつ、メッセージを返信する」といった動作が実現できます。
class OKButton(ui.Button):
...
async def callback(self, interaction: Interaction):
self.disabled = True
await interaction.response.edit_message(view=self.view)
await interaction.followup.send(content="OKボタンが押されました。")
2.7. レスポンスメッセージを編集したい場合
interaction.edit_original_response
を用いると、
インタラクションのレスポンスとして送信されたメッセージをあとから編集することが出来ます。
(参照: APIリファレンス)
例えば以下のように設定すると、
「10秒待機します。」と返信されたメッセージが、10秒後に「10秒経過しました。」に変化します。
import asyncio
...
class OKButton(ui.Button):
...
async def callback(self, interaction: Interaction):
await interaction.response.send_message(content="10秒待機します。")
await asyncio.sleep(10)
await interaction.edit_original_response(content="10秒経過しました。")
-
ボタン押下時
-
10秒経過後
まとめ
今回は、メッセージにボタンを追加する方法と、ボタンを押した際の動作について詳しく見てきました。
次回はこれを利用して、「クイズ」機能の出題メッセージに回答ボタンを設置します。