【Java】DiscordBotの作り方 2021年版

#はじめに
初投稿です。
この記事では2021年版のDiscordBotの作り方を解説していきます。
なお、DiscordBotをグループに招待する方法等はたくさん記事があるので、省略させていただきます。

#目次
1.[はじめに](#はじめに)
2.[使用するライブラリ](#使用するライブラリ)
3.[プロジェクトに依存関係を追加する](#プロジェクトに依存関係を追加する)
4.[BOTにログインできるようにする](#botにログインできるようにする)
5.[チャットを検知する](#チャットを検知する)
6.[スラッシュコマンドを実装する](#スラッシュコマンドを実装する)
7.[ユーザーID(Long)からユーザ名(String)を取得する](#ユーザーid(long)からユーザ名(string)を取得する)
8.[チャンネルID(Long)からテキストチャンネルを取得する](#チャンネルid(long)からテキストチャンネルを取得)
9.[BOTがメンションされたことを検知する](#botがメンションされたことを検知する)
10.[BOTを正常に終了させる](#botを正常に終了させる)
11.[最後に](#最後に)


#使用するライブラリ
・JDA
###GitHub
https://github.com/DV8FromTheWorld/JDA
###JavaDoc
https://ci.dv8tion.net/job/JDA/javadoc/index.html
###この記事で利用するバージョン
4.3.0_277

#プロジェクトに依存関係を追加する
プロジェクトを作成し、pom.xmlやbuild.gradleに記載を行います。

##Mavenの場合
```xml:pom.xml
<dependency>
    <groupId>net.dv8tion</groupId>
    <artifactId>JDA</artifactId>
    <version>4.3.0_277</version>
</dependency>

<repository>
    <id>dv8tion</id>
    <name>m2-dv8tion</name>
    <url>https://m2.dv8tion.net/releases</url>
</repository>
```
##Gradleの場合
```gradle:build.gradle
dependencies {
    implementation("net.dv8tion:JDA:4.3.0_277")
}

repositories {
    mavenCentral()
    maven {
      name 'm2-dv8tion'
      url 'https://m2.dv8tion.net/releases'
    }
}
```

#botにログインできるようにする

BOT_TOKENという変数にご自身のDiscordBOTのトークンを入力します。
その後、Activity.playing("")という欄に好きな文字列を入れてください。
※設定しないことも可能です。

ログインに失敗した場合はLoginExceptionが発生するので、例外処理も忘れないようにしましょう。

また、今回はグループ内でのBotの使用を目的としているので解説はしませんが、GatewayIntent.GUILD_MESSAGESを
GatewayIntent.DIRECT_MESSAGESに変更することでダイレクトメッセージのイベントを監視することができます。


```java:Main.java
import net.dv8tion.jda.api.JDA;
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 JDA jda = null;
    private static final String BOT_TOKEN = "BOTのTokenを入力してください。";
    
    public static void main(String[] args) {
        try {
            jda = JDABuilder.createDefault(BOT_TOKEN, GatewayIntent.GUILD_MESSAGES)
                    .setRawEventsEnabled(true)
                    .setActivity(Activity.playing("設定したいステータスを入力してください"))
                    .build();
        } catch (LoginException e) {
            e.printStackTrace();
        }
    }
    
}

```

###Activityの種類について
Activityには種類があり、様々な文字列に変更することができます。
・Activity.playing("好きな文字列")
〜〜をプレイ中

・Activity.competing("好きな文字列")
〜〜に参戦中です

・Activity.listening("好きな文字列")
〜〜を再生中

・Activity.streaming("好きな文字列","url")
〜〜を配信中(ユーザーをクリックすることで設定したURLに飛ぶようです)

・Activity.watching("好きな文字列")
〜〜を視聴中

#チャットを検知する
JDAのListenerAdapterを継承させ、イベントを記載するクラスのインスタンスを生成し、JDAに追加します。

※extends ListenerAdapterと.addEventListeners(new Main())を追加しました。

```java:Main.java
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.JDABuilder;
import net.dv8tion.jda.api.entities.Activity;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import net.dv8tion.jda.api.requests.GatewayIntent;

import javax.security.auth.login.LoginException;

public class Main extends ListenerAdapter { //追加部分

    private static JDA jda = null;
    private static final String BOT_TOKEN = "BOTのTokenを入力してください。";

    public static void main(String[] args) {
        try {
            jda = JDABuilder.createDefault(BOT_TOKEN, GatewayIntent.GUILD_MESSAGES)
                    .setRawEventsEnabled(true) 
                    .addEventListeners(new Main()) //追加部分
                    .setActivity(Activity.playing("設定したい文字列"))
                    .build();
        } catch (LoginException e) {
            e.printStackTrace();
        }
    }
}
```

##メッセージを受け取るイベントの定義
下記記載のイベント(MessageReceivedEvent)を上で登録したクラスに記載すると、BOTがメッセージを送信されたことを検知することができるようになります。

また、e.getMessage().getContentRaw()を利用することで入力されたメッセージを取得できます。

```java:Main.java
@Override 
public void onMessageReceived(MessageReceivedEvent e) {
    String msg = e.getMessage().getContentRaw(); //入力されたメッセージを取得
}
```

##メッセージを送信する

###通常メッセージを送信する
次は、Botから通常メッセージを送信します。
Botからメッセージを送信するには

```java:Main.java
e.getTextChannel().sendMessage("送信したいメッセージ").queue();
```

を記載することでイベント発生後にメッセージを送信します。

ただ、これを記載するだけだと無限ループしてしまうので以下のようにします。
※BOTのメッセージだった場合は、メッセージを送信しないようにしています。

```java:Main.java
String msg = e.getMessage().getContentRaw();
if (!e.getAuthor().isBot()) {
    e.getTextChannel().sendMessage("送信したいメッセージ").queue();
}

```

###埋め込みメッセージを送信する
書いてある説明を参考にしてください。

また、タイトルや色、説明、フッター等はなくても動きます。

```java:Main.java
@Override
public void onMessageReceived(MessageReceivedEvent e) {
    if (!e.getAuthor().isBot()) {
        EmbedBuilder eb = new EmbedBuilder();
        eb.addField("名前1","説明1",false);
        eb.addField("名前2","説明2",false);
        eb.setTitle("タイトル");
        eb.setDescription("埋め込みメッセージの説明");
        eb.setThumbnail("サムネイル[リンク]");
        eb.setFooter("フッター画像[リンク]");
        eb.setColor(Color.WHITE); //埋め込みメッセージの横の色
        eb.setImage("横に表示する画像[リンク]");
        eb.setAuthor("名前","アイコン[リンク](任意)");
        e.getTextChannel().sendMessage(eb.build()).queue(); //送信
    }
}
```


##メッセージを削除する
メッセージを削除するには、過去のメッセージを取得し削除を行います。
今回は、/clearというメッセージを検知した際に、メッセージを3つListとして取得し、
取得したメッセージをdelete()で消しています。

※メッセージを削除できる数に制限はないようですが、一度にやるとDiscordに怒られるので、大量に行う場合は遅延を入れましょう。

```java:Main.java
@Override
public void onMessageReceived(MessageReceivedEvent e) {
    String msg = e.getMessage().getContentRaw();
    if (msg.equalsIgnoreCase("/clear")) {
        //メッセージが/clearだったら過去のメッセージを3つ取得
        List<Message> messages = e.getTextChannel().getHistory().retrievePast(3).complete();
        for (Message getMsg : messages) {
            getMsg.delete().queue();
        }
    }
}
```

#スラッシュコマンドを実装する
最近Discordにスラッシュコマンドとやらが追加されましたね。
JavaのJDAを利用したスラッシュコマンドの追加方法(特にサブコマンド)はほぼ見つからなかったので、Docs等を読み漁りなんとか実装したものです。(ミスがあれば指摘の方お願いしますm(_ _)m)

また、スラッシュコマンドはクライアント側で使えるようになるまで最大1時間程度かかることがあります。
※スラッシュコマンドを追加するには、Botを招待する際に、OAuth2からapplications.commandsの権限を与える必要があります。

※スラッシュコマンドはまだ不安定なようだったので、どうしてもうまく行かないものはメッセージの検知で実装を行うことをオススメします。

##スラッシュコマンドをJDAに登録
OptionTypeについては入力を求める型名を入力してください。(今回は文字列のためSTRING)
※数字はINTEGER

※サブコマンドはいくつでも追加できます。
※ヒントに表示させるで、使用できる文字が制限されているようです。
※また、setRequired(false)で入力を任意にすることができます。

```java:Main.java
public static void main(String[] args) {
    try {
        jda = JDABuilder.createDefault(BOT_TOKEN, GatewayIntent.GUILD_MESSAGES)
                .setRawEventsEnabled(true)
                .addEventListeners(new Main())
                .setActivity(Activity.playing("てすとBot"))
                .build();
    } catch (LoginException e) {
        e.printStackTrace();
    }

    //登録しているコマンドを最新のものに更新する(一度リセットを行う)
    //updateを行わないと過去のコマンド等が残り続けます。
    jda.updateCommands().queue();
    
    //コマンドの登録
    // /testコマンドの実装
    jda.upsertCommand("test1","テストコマンド2").queue();
    
    //サブコマンドの実装
    // /test2 [subCommand(必須)]コマンドの実装
    jda.upsertCommand("test2","テストコマンド2")
            .addOptions(new OptionData(OptionType.STRING,"subCommand1","subCommand1の説明").setRequired(true))
            .queue();
}
```

また、このままだと登録だけしてコマンドが動作しないので動作について実装します。

##コマンドの動作を書く

下記プログラムを実装することによってスラッシュコマンドの動作を実装することができます。
また、スラッシュコマンドは何も反応がなかった場合に、ユーザーにエラーを表示してしまうので、必ず反応を行うようにします。
※今回の場合は、/test2 subCommand以外を受け付けないため、/test2 test2　等入力されたときのエラーを防ぐためにelseを記述

スラッシュコマンドは実行した人以外に結果を表示するかどうかの設定も可能

```java:Main.java
@Override
public void onSlashCommand(SlashCommandEvent e) {
    if (e.getName().equalsIgnoreCase("test1")) {
        //setEphemeral()でコマンドを実行したユーザー以外にリプライを見えないようにするかを指定します。
        e.reply("/test1を入力しました！").setEphemeral(false).queue(); 
    } else if (e.getName().equalsIgnoreCase("test2") && e.getOptions().get(0).getAsString().equalsIgnoreCase("subCommand")) {
        e.reply("/test2 subCommand を入力しました！").setEphemeral(false).queue();
    } else {
        e.reply("不明なコマンドです。").setEphemeral(false).queue();
    }
}
```

#ユーザーid(long)からユーザー名(string)を取得する
ユーザーID(Long)からユーザー名(String)を取得する方法です。
以下の記述で取得することができます。

```java:Main.java
jda.retrieveUserById(/*ユーザーID(Long)*/).queue(userName -> {
    //userNameにユーザー名(String)が代入されます。(非同期処理なので注意!!)
});
```

#チャンネルid(long)からテキストチャンネルを取得する
チャンネルID(Long)からテキストチャンネルを取得する方法です。
以下の記述で取得することができます。

```java:Main.java
TextChannel tc = jda.getTextChannelById(/*チャンネルID(Long)*/);
```

#botがメンションされたことを検知する
下記の処理を記述し、isMentionがtrueになったときの処理をif分等で実装すればよいです。

```java:Main.java
String message = e.getMessage().getContentDisplay();
boolean isMention = false;
for (Object s : e.getMessage().getMentionedMembers()) {
    //メッセージを取得して、取得したメッセージに自分のIDが含まれていればisMentionをtrueにする
    isMention = s.toString().contains(jda.getSelfUser().getId());
}
```


#botを正常に終了させる
上記のコマンド等を利用してBotを正常に終了させるには

```java:Main.java
jda.shutdownNow();
```
を実行するコマンド等を実装することで正常にBotを終了するようにできます。


#最後に
DiscordBotを作る際に、最新のものがなくてすごく大変な思いをしたので皆さんに共有できればと思いこの記事を書きました。
なにか間違い等があれば指摘の方をお願いしますm(_ _)m