Skip to content
This repository has been archived by the owner on Nov 12, 2023. It is now read-only.

Slash CommandおよびButtonへの対応 #17

Closed
book000 opened this issue Jun 4, 2021 · 26 comments · Fixed by #301
Closed

Slash CommandおよびButtonへの対応 #17

book000 opened this issue Jun 4, 2021 · 26 comments · Fixed by #301
Labels
♻improvement 強化・改善・リファクタリング

Comments

@book000
Copy link
Member

book000 commented Jun 4, 2021

とはいえ、cloudが対応する前は別の方法でなんか考えることになるだろうけど。

⚠️This repository uses JDA, but has nothing to do with the JDA developers.
According to the policy of the repository, conversations are conducted in Japanese in principle. Please use translation etc. for reference.

参考: discord-jda/JDA#1501

@book000 book000 added the ♻improvement 強化・改善・リファクタリング label Jun 4, 2021
@book000
Copy link
Member Author

book000 commented Jun 4, 2021

https://discord.com/developers/docs/interactions/slash-commands

「コマンド」の引数として、「サブコマンド」と「サブコマンドグループ」を追設定できる
サブコマンドグループはその名の通り「サブコマンドをグルーピングする」もの

  • /perm role add GROUP PERMISSION: 指定したロールに対してパーミッションを設定する
  • /perm role remove GROUP PERMISSION: 指定したロールからパーミッションを削除する
  • /perm role status: すべてのロールのステータスを表示する
  • /perm role status GROUP: 指定したロールのステータスを表示する
  • /perm user add USER PERMISSON: 指定したユーザーに対してパーミッションを設定する
  • /perm user remove USER PERMISSION: 指定したユーザーからパーミッションを削除する
  • /perm user status USER: 指定したユーザーのステータスを表示する

という7つの引数ケースを持つコマンドを実装する場合(実際に実装するわけではない)、

Command.Choice[] permissions = new Command.Choice[]{
    new Command.Choice("Command: perm", "cmd-perm"), // 第1引数: ユーザーに見える説明、第2引数: プログラムに渡される判定用文字列
    new Command.Choice("Command: test", "cmd-test"),
    // などなど
};

CommandData perm = new CommandData("perm", "パーミッションに関する操作を行う")
    .addSubcommandGroups(
        new SubcommandGroupData("role", "ロール")
            .addSubcommands(
                new SubcommandData("add", "ロールに対してパーミッションを設定")
                    .addOptions(
                        new OptionData(OptionType.ROLE, "target", "対象ロール", true),
                        new OptionData(OptionType.STRING, "permission", "パーミッション", true)
                            .addChoices(permissions)
                    ),
                new SubcommandData("remove", "ロールからパーミッションを削除")
                    .addOptions(
                        new OptionData(OptionType.ROLE, "target", "対象ロール", true),
                        new OptionData(OptionType.STRING, "permission", "パーミッション", true)
                            .addChoices(permissions)
                    ),
                new SubcommandData("status", "ロールのステータスを表示")
                    .addOptions(
                        new OptionData(OptionType.ROLE, "target", "対象ロール", false)
                    )
            ),
        new SubcommandGroupData("user", "ユーザー")
            .addSubcommands(
                new SubcommandData("add", "ユーザーに対してパーミッションを設定")
                    .addOptions(
                        new OptionData(OptionType.USER, "target", "対象ユーザー", true),
                        new OptionData(OptionType.STRING, "permission", "パーミッション", true)
                            .addChoices(permissions)
                    ),
                new SubcommandData("remove", "ユーザーからパーミッションを削除")
                    .addOptions(
                        new OptionData(OptionType.USER, "target", "対象ユーザー", true),
                        new OptionData(OptionType.STRING, "permission", "パーミッション", true)
                            .addChoices(permissions)
                    ),
                new SubcommandData("status", "ユーザーのステータスを表示")
                    .addOptions(
                        new OptionData(OptionType.STRING, "target", "対象ユーザー", true)
                    )
            )
    );

Objects.requireNonNull(event.getJDA().getGuildById(597378876556967936L))
    .upsertCommand(perm).queue();

のようになるのかなあ。ユーザーには以下画像のように見える
image

OptionData の第4引数 isRequired はデフォ値 false で指定することがオプションではありますが、明示したほうが複数人開発においては分かりやすいでしょう…。

参考までに、JDA本家が置いているサンプルソースコード: https://github.com/DV8FromTheWorld/JDA/blob/development/src/examples/java/SlashBotExample.java

@book000
Copy link
Member Author

book000 commented Jun 4, 2021

ユーザーが選択・入力した項目を取得するには以下の通り

  • onSlashCommand イベント (SlashCommandEvent) で受け取る
  • event.getName() でコマンド名 (e.g. perm)
  • event.getSubcommandName() でサブコマンド名 (e.g. add, remove) サブコマンドグループ名ではないので注意
  • event.getCommandPath() でサブコマンドまでのパス (e.g. /perm role add TARGET PERMISSION の場合 perm/role/add) これで振り分けたりすることになるんだろうと思う
  • オプションは event.getOption("オプション名") で受け取る。但し、getOption が返すのは OptionMapping なので、String とか booleanint を取得するには、event.getOption("オプション名").getAsString() のように getAsString で取得しなければならない (@Nullable なので null が入る可能性を考慮して実装すること)

@book000
Copy link
Member Author

book000 commented Jun 4, 2021

ユーザーが実行したコマンド(インタラクト)には3秒以内に応答を返す必要がある。応答は event.reply("メッセージ").queue(); で返すことができる
3秒以内に何らかの応答を返さなければならない制約があるので、それ以上かかる可能性がある場合は event.deferReply() で応答を延期できる。この際、ユーザーには Bot は考え中... というメッセージが表示される。延期は最大15分

setEphemeral メソッドを使用することで、応答が一時的に自分だけに表示されるかどうかを制御できる(Ephemeral = 一時的)。true を設定して応答を自分だけに表示する場合、ユーザーのクライアントが再起動した時点でこのメッセージは削除される

event.getHook().editOriginal("メッセージ").queue(); によって、一度送信した何らかの応答(reply, deferReply)を編集し、本来の応答を返すことができる。
String.format のような文字列フォーマッティングを使用する場合は editOriginalFormat が使用できる
Embedを返す場合は editOriginalEmbeds が使用可能

@book000
Copy link
Member Author

book000 commented Jun 4, 2021

今までのDiscordBotの利用形態からして、Ephemeral は無条件で false を指定すること(ほかのユーザーにも閲覧でき、恒久的)が望まれると思う

@book000
Copy link
Member Author

book000 commented Jun 4, 2021

ボタンに関して

登場人物は ActionRowButton 、そして MessageBuilderSlashCommandEventeditOriginal でメソッドチェーンに使える setActionRows

まず、Button.primaryButton.secondary でボタンを作る。Button.of で細かく指定できたりするが、色などをカラーコードなどで細かく指定すること等はできない。ボタンには絵文字などが利用できたりするが、ここら辺は公式ドキュメントを参照。これらボタンをここではコンポーネントと呼ぶ(今後このコンポーネントに該当するものは増えるのかも)

次に、ActionRow.of でこれらコンポーネントをまとめる。具体的には ActionRow.of(Button.primary("btn-test", "Label"), Button.primary("btn-test", "Label"))ActionRow を受け取る

最後に、 MessageBuilderSlashCommandEventeditOriginal で返ってくる WebhookMessageUpdateAction にある setActionRowsActionRow を渡すことでボタンを表示できる。

結論として、以下のようにプログラミングすれば実装できる:

// MessageBuilder を使う場合 (スラッシュメッセージ駆動でない場合)

jda.getTextChannelById("チャンネルID")
    .sendMessage(new MessageBuilder("メッセージ")
        .setActionRows(ActionRow.of(
            Button.primary("btn-one", "One"),
            Button.primary("btn-two", "Two")))
        .build());

// スラッシュメッセージの場合
@Override
public void onSlashCommand(SlashCommandEvent event) {
    // event.reply時点(ReplyAction)でボタンを表示する場合
    event.reply("Pong!")
        .setEphemeral(false)
        .addActionRow(
            Button.primary("btn-one", "One"),
            Button.primary("btn-two", "Two"))
        .queue();

    // 一度応答した後、editOriginalでメッセージを編集するときにボタンを設定・表示する場合
    event.reply("Pong!").setEphemeral(false)
        .flatMap(v ->
            event.getHook().editOriginal("メッセージ")
                .setActionRows(ActionRow.of(
                    Button.primary("btn-one", "One"),
                    Button.primary("btn-two", "Two")))
        ).queue();
}

@book000
Copy link
Member Author

book000 commented Jun 4, 2021

ボタンが押されたときのイベント駆動については onButtonClick(ButtonClickEvent) で受付・取得する。

  • チャンネル情報: event.getChannel()
  • メッセージ情報: event.getMessage()
  • ボタン押下者情報: event.getUser()
  • コンポーネント(ボタンなど)ID: event.getComponentId()
  • ボタン情報: getButton()

でそれぞれ取得できる

本家のサンプル: https://github.com/DV8FromTheWorld/JDA/blob/development/src/examples/java/SlashBotExample.java#L107-L132

@book000
Copy link
Member Author

book000 commented Jun 4, 2021

以下、Javajaotan2での方針

  • 全てのコマンドは Slash Command に対応し、メッセージによるコマンド実行(/imoとかをそのまま送信するもの)は非対応とする (動作しない)
  • 応答はエラー時を除き、全て実行者以外のユーザにも表示され恒久的に残ることを前提とする。但しエラー時でも他ユーザの表示・恒久化を制限するわけではない
  • ボタンは各コマンドの実行クラス上で処理するように実装する。ボタン・コンポーネントIDは コマンド名-コマンド実行者ID-コマンド側の独自名-その他情報 でセットし、- までの値によって分岐・コマンドクラスのメソッド呼び出しを行う
  • コマンドクラスには onButtonClicked メソッドの実装を義務づける。引数は ButtonClickEvent
  • コマンドフレームワークは使えないので、置き換える。
  • 自作コマンドフレームワークを作ることも考える

@ghost

This comment has been minimized.

@book000

This comment has been minimized.

@ghost

This comment has been minimized.

@yuuahp
Copy link
Member

yuuahp commented Jun 5, 2021

うへ〜
めっちゃ面白そうだし使いたいけどむずそう...

@yuuahp
Copy link
Member

yuuahp commented Jun 5, 2021

Cmd_????から引数を自動で取得->登録
とかも出来たりするのかな?

@book000
Copy link
Member Author

book000 commented Jun 5, 2021

引数を自動で、というのがどこまで何を示しているのかわからんのでアレだけど、流石に難しくないか…?明示的にどの場合どうやるって割り当てないと厳しそう

@yuuahp
Copy link
Member

yuuahp commented Jun 5, 2021

コマンド自体をJavajaotanは全部自動で登録してたから、そんな風に出来たら便利じゃないかな~とか思った😔

@yuuahp
Copy link
Member

yuuahp commented Jun 5, 2021

あれとは全然違うんだろうけど

@yuuahp
Copy link
Member

yuuahp commented Jun 5, 2021

.

@book000
Copy link
Member Author

book000 commented Jun 5, 2021

頭になるコマンドだけを判別してそれで振り分けるのはなんら難しいことではない
ただ、引数を判別して云々…となると流石にコマンドフレームワーク使ったりしないことにはあまりにも難しすぎる…

@yuuahp
Copy link
Member

yuuahp commented Jun 5, 2021

そっかコマンドフレームワーク使えないのか...

@book000
Copy link
Member Author

book000 commented Jun 5, 2021

まあ、機能削減したりJDAが今回実装したOptionType, OptionDataとかをうまく流用してコマンドフレームワークっぽいものを作るとかのやり方をするしかないんだろうなあ
cloudがすぐここで対応してくれるとは思えん(JDA特化ではないから)

@book000
Copy link
Member Author

book000 commented Jun 5, 2021

構想が全然練れてないし、どうしたらいいか正直難しいので案とかあればください(そもそも時間をどこまでとれるか微妙なので案として実装してくれてもいい)
registerたたいたらコマンド情報纏めて云々とかそういう感じにはなると思うんだけど

@yuuahp
Copy link
Member

yuuahp commented Jun 5, 2021

コマンドフレームワークっぽく出来るように色々やってみてるけど、さっきから1時間経ってもSlashCommandが全く反映されない...
こんな風に動かせるかな...的な構想は出来てます

@book000
Copy link
Member Author

book000 commented Jun 5, 2021

Bot(Guild)にslash commandの利用可能許可は出した?GuildにBot追加するときみたいに認証かまさないと駄目なはず

@yuuahp
Copy link
Member

yuuahp commented Jun 5, 2021

ほんとだ..................................................................
出来た..........................................................................

やっとまともにテストできるようになったので色々やってみます💪

@yuuahp
Copy link
Member

yuuahp commented Jun 5, 2021

subCommandの判定って
image
↑みたいにswitchを重ねるか
image
↑みたいに1段?にまとめるか
どっちのほうがいいですか

@book000
Copy link
Member Author

book000 commented Jun 6, 2021

まあ前者かなあ…

あとは今のコマンドフレームワークみたいにこれこれこういうサブコマンドだったらこの関数を呼び出してね、みたいにした方が分かりやすいのかなあと思ったり(switchとかifをコマンド実装側でやるとバグの温床にならざるを得ないし何より読みにくい…)

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
♻improvement 強化・改善・リファクタリング
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants