【Java/Kotlin】Discord Bot のスラッシュコマンドを実装する-2022

# はじめに
この記事は、Java で Discord Bot のスラッシュコマンド実装方法を解説していきます。
私が嵌った点を含めて、備忘録的に書いていこうと思います。
Discord Bot の作成は、他に記事がたくさんあるので、作成されている前提で話を進めていきます。
リポジトリを使用する際は、プロジェクトに依存関係を追加することを忘れないようにしましょう。

ソースコードは、Java と Kotlin の両方を掲載します。
私が Kotlin 推しなので、、、。
Kotlin すごく楽で便利でいいですよ！

この記事で実際に使用したソースコードは、以下の GitHub に push しておくので全体像を見たい場合はそちらを確認してください。

https://github.com/waigoma/TutorialSlashCommandDiscordBot

# 目次
[1. はじめに](#はじめに)
[2. 目次](#目次)
[3. 使用するライブラリ](#使用するライブラリ)
[4. DiscordBotの招待](#discordbotの招待)
[5. Botにログインする](#botにログインする)
[6. スラッシュコマンドを登録する](#スラッシュコマンドを登録する)
[7. コマンドの処理を実装する](#コマンドの処理を実装する)
[8. 最後に](#最後に)
[9. 参考文献](#参考文献)

# 使用するライブラリ
- **JDA - 5.0.0-alpha.11**
### GitHub
https://github.com/DV8FromTheWorld/JDA
### JavaDoc
https://javadoc.io/doc/net.dv8tion/JDA/latest/index.html

# DiscordBotの招待
https://discord.com/developers/applications

1. 自分で作成した Bot を選択し、OAuth2 > URL Generator を開きます。
2. チェック欄を以下の画像のようにします。
3. **applications.commands** にチェックを入れます。  
**※ このチェックを入れないと、スラッシュコマンドを作成することができません！ (私はここで嵌りました。)**
4. 最下部に表示されている URL を使用し、自分のサーバーに Bot を招待します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348896/8c591b4d-3e9b-ecbe-9165-117e8f5b6b75.png)

# Botにログインする
TOKEN という変数に、Discord Bot のトークンを入力します。
次に、ログイン処理内の `.setActivity(Activity.playing("作業"))` の "作業" を好きな文字列に置き換えましょう。
これは、Discord のオンライン欄に表示される、"○○をプレイ中" の部分のことです。

ログイン処理では、ログインできなかった時に `LoginException` が発生するので、`try-catch` で囲っておきます。
ログイン処理内の、 `GatewayIntent` は、種類がいくつかあり、監視するイベントを設定することができます。
詳細は、[JavaDoc](https://ci.dv8tion.net/job/JDA5/javadoc/net/dv8tion/jda/api/requests/GatewayIntent.html) を確認してみてください。

これを実行すると、Bot にログインすることができます。
そして、サーバーに招待した Bot が、オンラインになっていることが確認できると思います。
```Main.java
import net.dv8tion.jda.api.JDABuilder;
import net.dv8tion.jda.api.entities.Activity;
import net.dv8tion.jda.api.requests.GatewayIntent;

import javax.security.auth.login.LoginException;

public class Main {
    private static final String TOKEN = "Discord bot のトークンを記述";

    public static void main(String[] args) {
        try {
            // Login 処理
            JDABuilder.createLight(TOKEN, GatewayIntent.GUILD_MESSAGES, GatewayIntent.DIRECT_MESSAGES)
                    .setActivity(Activity.playing("作業")) // "～をプレイ中" の ～の部分
                    .build();
        }catch (LoginException e){
            e.printStackTrace();
        }
    }
}
```
```MainKt.kt
import net.dv8tion.jda.api.JDABuilder
import net.dv8tion.jda.api.entities.Activity
import net.dv8tion.jda.api.requests.GatewayIntent

import javax.security.auth.login.LoginException

class MainKt {
    companion object {
        private const val TOKEN = "Discord bot のトークンを記述"
    }

    fun first() {
        try {
            // Login 処理
            JDABuilder.createLight(TOKEN, GatewayIntent.GUILD_MESSAGES, GatewayIntent.DIRECT_MESSAGES)
                .setActivity(Activity.playing("作業")) // "～をプレイ中" の ～の部分
                .build()
        } catch (e: LoginException) {
            e.printStackTrace()
        }
    }
}

fun main(){
    MainKt().first()
}
```

# スラッシュコマンドを登録する
実際にスラッシュコマンドを追加します。
スラッシュコマンドを実装する場所を全ての場所にすると、コマンド登録に最大１時間かかるようです。
そのため、コマンドをテストしたい場合は特定サーバーのみ使用可能な形にすると良いそうです。
[参考 JavaDoc](https://ci.dv8tion.net/job/JDA5/javadoc/net/dv8tion/jda/api/JDA.html#updateCommands())

まず、`JDABuilder` を `build()` した時に戻り値である `JDA` を使用するので、変数 `jda` に置きます。
次に、`jda.awaitReady()` を記述し、ログイン処理が完了するまで待ちます。
**この処理を書かないと、サーバーが見つからず `null` になってしまいます。 (私はここで嵌りました。)**

次に、サーバー ID からサーバー情報を取得して、変数 `guild` に置きます。

次に、登録するスラッシュコマンドを作成します。
作成方法は、`Commands.slash("コマンド名", "コマンドの説明")` を `SlashCommandData` 型で変数に置きます。

次に、サーバーにコマンドを登録します。
`guild.updateCommands().addCommands(作成したコマンドの変数).queue();`
これを記述することで、登録することができます。
全ての場所に登録したい場合は、`guild` の部分を `jda` に変更しましょう。

ソースコードが長くなるため、import 文は追加分だけ書きます。
```Main.java
import net.dv8tion.jda.api.JDA;

import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData;

public class Main {
    private static final String TOKEN = "Discord bot のトークンを記述";
    private static final String GUILD_ID = "Discord サーバーの ID を記述";

    public static void main(String[] args) {
        try {
            // Login 処理
            JDA jda = JDABuilder.createLight(TOKEN, GatewayIntent.GUILD_MESSAGES, GatewayIntent.DIRECT_MESSAGES)
                    .setActivity(Activity.playing("作業")) // "～をプレイ中" の ～の部分
                    .build();

            // ログインが完了するまで待つ
            jda.awaitReady();

            // 参加しているサーバーを ID から取得
            Guild guild = jda.getGuildById(GUILD_ID);

            // 登録するコマンドを作成
            SlashCommandData testCommand = Commands.slash("test", "テストコマンド");

            // コマンドを指定したサーバーに登録
            guild.updateCommands()
                    .addCommands(testCommand)
                    .queue();
            
        }catch (LoginException | InterruptedException e){
            e.printStackTrace();
        }
    }
}
```
```MainKt.kt
import net.dv8tion.jda.api.interactions.commands.build.Commands

class MainKt {
    companion object {
        private const val TOKEN = "Discord bot のトークンを記述"
        private const val GUILD_ID = "Discord サーバーの ID を記述"
    }

    fun first() {
        try {
            // Login 処理
            val jda = JDABuilder.createLight(TOKEN, GatewayIntent.GUILD_MESSAGES, GatewayIntent.DIRECT_MESSAGES)
                .setActivity(Activity.playing("作業")) // "～をプレイ中" の ～の部分
                .build()

            // ログインが完了するまで待つ
            jda.awaitReady()

            // 参加しているサーバーを ID から取得
            val guild = jda.getGuildById(GUILD_ID)!!

            // 登録するコマンドを作成
            val testCommand = Commands.slash("test", "テストコマンド")

            // 指定したサーバーにコマンドを登録
            guild.updateCommands()
                .addCommands(testCommand)
                .queue()

        } catch (e: LoginException) {
            e.printStackTrace()
        }
    }
}

fun main(){
    MainKt().first()
}
```

# コマンドの処理を実装する
現在は、コマンドを登録だけして中身を書いていないので、コマンドを打つと、応答していないと言われます。
なので、これから処理するクラスを実装していきます。

## 処理するクラスを作る
実装方法は、`ListenerAdapter` を継承したクラスを使用します。
このクラス内で、`onSlashCommandInteraction` メソッドを `override` して、その中にコマンドの中身を書きます。

`event.getName()` を使用することで、コマンド名を取得できます。
ここでは、`test` コマンドであれば処理をするようにしています。

`event.reply("リプライメッセージ")` を使用することで、コマンド送信者に対して返信という形でメッセージを返すことができます。
`.setEphemeral(boolean)` を true に設定することで、コマンドを使用した人のみにしか見えないように返信されます。

```SlashCommandListener.java
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.jetbrains.annotations.NotNull;

public class SlashCommandListener extends ListenerAdapter {
    @Override
    public void onSlashCommandInteraction(@NotNull SlashCommandInteractionEvent event) {
        // test コマンドなら処理をする。
        if (event.getName().equals("test")) {
            // コマンド送信者に対して、その人にだけ見えるメッセージとして返信する。
            event.reply("This is test command!").setEphemeral(true).queue();
        }
    }
}
```
```SlashCommandListener.kt
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent
import net.dv8tion.jda.api.hooks.ListenerAdapter

class SlashCommandListener : ListenerAdapter() {
    override fun onSlashCommandInteraction(event: SlashCommandInteractionEvent) {
        // test コマンドなら処理をする。
        if (event.name == "test") {
            // コマンド送信者に対して、その人にだけ見えるメッセージとして返信する。
            event.reply("This is test command!").setEphemeral(true).queue()
        }
    }
}
```

## 処理するクラスをBotに登録する
コマンドの処理を行うクラスを Bot に登録します。
Main クラスで行っているログイン処理の部分に追加の記述をします。

`.addEventListeners()` を使用して、コマンドの処理を Bot に登録します。
具体的には、`.addEventListeners(new 処理するクラス())` と記述すれば ok です。
処理するクラスが複数ある場合は、`.addEventListeners(new C1(), new C2())` のようにカンマ区切りで記述できます。
```Main.java
// Login 処理
JDA jda = JDABuilder.createLight(TOKEN, GatewayIntent.GUILD_MESSAGES, GatewayIntent.DIRECT_MESSAGES)
    .addEventListeners(new SlashCommandListener())
    .setActivity(Activity.playing("作業")) // "～をプレイ中" の ～の部分
    .build();
```
```MainKt.kt
// Login 処理
val jda = JDABuilder.createLight(TOKEN, GatewayIntent.GUILD_MESSAGES, GatewayIntent.DIRECT_MESSAGES)
    .addEventListeners(SlashCommandListener())
    .setActivity(Activity.playing("作業")) // "～をプレイ中" の ～の部分
    .build()
```

# 最後に
JDA を使ってスラッシュコマンドを実装するときに、最新の情報がほとんど見つからなかったので、備忘録的に書いてみました。
ほとんど JavaDoc から解読して手探り状態だったのでとても大変でした。
しかし、スラッシュコマンドを実装すると、tab 補完でコマンドを打つことができるようになるので、とても楽することができます。

わかり辛いところも多々あったかと思いますが、皆様のお役に立てば幸いです。

なにか間違い等があれば指摘の方をお願いします。
ありがとうございました。

# 参考文献

https://qiita.com/rito528/items/6436c419c11c58caa01a