DiscordAPIでMinecraftのサーバーとチャットをリンクしてみた

##はじめに
**初めてのqiita投稿なので読みづらい場所等がありますが、ご了承下さい。**

**また、DiscordのBotのAPIトークンの取得方法は今回は紹介しないので予めご了承ください。**

MinecraftBEのサーバーソフトウェア
[Nukkit](https://github.com/hiroki19990625/Nukkit) のプラグイン拡張で
サーバー内のチャットをDiscordのBotでリアルタイムに表示する機能を実装までを書きました。

##Mavenでライブラリを落とす

pom.xmlにこれらを追加して下さい。

```xml:pom.xml
...
<repositories>
    ...
    <repository>
        <id>jcenter</id>
        <url>http://jcenter.bintray.com</url>
    </repository>
    <repository>
        <id>jitpack.io</id>
        <url>https://jitpack.io</url>
    </repository>
</repositories>
...

...
<dependencies>
    ...
    <dependency>
        <groupId>com.github.austinv11</groupId>
        <artifactId>Discord4J</artifactId>
        <version>2.10.1</version>
    </dependency>
</dependencies>
...
```

手動で落とす方は[こちら](https://discord4j.com/downloads.html)

##ライブラリを使ってみる

###ログインさせる

```java:DiscordChatService.java
public class DiscordChatService implements Listener {
    private final String Token = "取得したAPIトークン";
    private IDiscordClient client;
    private IChannel chatChannel;

    // PluginBaseを継承したクラスをコンストラクタにぶち込んでください。
    // 今回は例えとして MyPlugin にしています。
    public DiscordChatService(MyPlugin plugin) {
        this.login();

        Server.getInstance().getPluginManager().registerEvents(this, plugin);
    }

    private void login() {
        // Discordから取得したAPIトークンを使ってログイン。
        this.client = new ClientBuilder().withToken(TOKEN).build();

        // これを書くと @EventSubscriber ついたイベントをコールバックしてくれる。
        this.client.getDispatcher().registerListener(this);

        // ログイン
        this.client.login();
    }
}
```

###ログイン完了のコールバックを実装する

```java:DiscordChatService.java
@EventSubscriber
public synchronized void onReady(ReadyEvent event) {
    Server.getInstance().getLogger().info("Botがログインしました。");

    // ここでBotが投稿するチャンネルを取得しておく。
    this.chatChannel = this.client.getChannels().stream().filter(ch -> ch.getName().equals("chat")
        && ch.getCategory().getName().equals("SERVER")).findFirst().get();
}
```

###ここから本題(Discord -> Server)Chatの実装

```java:DiscordChatService.java
@EventSubscriber
public synchronized void onMessage(MessageReceivedEvent event) throws RateLimitException, DiscordException, MissingPermissionsException {
    IMessage message = event.getMessage();
    IUser user = message.getAuthor();

    // Botのチャットをブロック。
    if (user.isBot()) return;

    // メッセージからチャンネルを取得。
    IChannel channel = message.getChannel();

    // Discordのチャンネルとカテゴリーを指定(これが無いと全てのチャンネルからメッセージを受信することになります)
    // 特に指定が無い場合は消して構いません。
    if (channel.getCategory().getName().equals("SERVER")// "SERVER" という名前のカテゴリーであるか
            && channel.getName().equals("chat")) {// "chat" という名前のチャンネルであるか

        // メッセージ本体。
        String mes = message.getContent();

        // ユーザー名を取得。
        String name = user.getName();

        // 文字列を連結してサーバー内にチャットをブロードキャスト。
        Server.getInstance().broadcastMessage("[" + name + "] " + mes);
    }
}
```

###(Server -> Discord)Chatの実装
これが難しかった。
しかし、このコードには問題があります。
非同期でチャットが送信されないため、サーバーが一瞬フリーズします。

```java:DiscordChatService.java
@EventHandler
public void onPlayerChatEvent(PlayerChatEvent event) {
    Player player = event.getPlayer();
    String name = player.getName();
    String msg = "[" + name + "] " + event.getMessage();

    // レート制限に配慮した実装。(勝手再送してくれる)
    // 普通に実装すると RateLimitException が出てしまう。
    RequestBuffer.request(() -> {
        // チャンネルに投稿。
        this.chatChannel.sendMessage(msg);
    });
}
```

###改善版
ここでオレオレ実装クラスの登場

<details>
<summary>オレオレ実装クラス</summary>
<div>

```java:ThreadPool.java

import java.util.HashMap;
import java.util.concurrent.ConcurrentLinkedQueue;

public final class ThreadPool {
    private static final HashMap<String, ActionThread> pool = new HashMap<>();

    static {
        registerThread("DiscordMassage");
    }

    public static void registerThread(String name) {
        Safe.nullCheck(name);

        ActionThread thread = new ActionThread(name);
        pool.put(name, thread);
        thread.start();
    }

    public static void addAction(String threadName, Runnable action) {
        ActionThread thread = pool.get(threadName);
        thread.addAction(action);
    }

    public static void close(String threadName) {
        ActionThread thread = pool.remove(threadName);
        thread.close();
    }

    private static class ActionThread extends Thread {
        private ConcurrentLinkedQueue<Runnable> actions = new ConcurrentLinkedQueue<>();
        private boolean isClose;

        public ActionThread(String name) {
            this.setName(name);
        }

        @Override
        public void run() {
            while (!this.isClose) {
                if (this.actions.isEmpty())
                    continue;
                this.actions.poll().run();
            }
        }

        public void addAction(Runnable action) {
            this.actions.offer(action);
        }

        public void close() {
            this.isClose = true;
        }
    }
}
```

</div>
</details>

そしてオレオレ実装クラスを使う

```java:DiscordChatService.java
@EventHandler
public void onPlayerChatEvent(PlayerChatEvent event) {
    Player player = event.getPlayer();
    String name = player.getName();
    String msg = "[" + name + "] " + event.getMessage();

    // オレオレ実装クラスを使う
    ThreadPool.addAction("DiscordMassage", () -> {
        // レート制限に配慮した実装。(勝手再送してくれる)
        RequestBuffer.request(() -> {
            // チャンネルに投稿。
            this.chatChannel.sendMessage(msg);
        });
    }
}
```

###黒魔術を使ってDiscord4Jのログを消す
個人的に邪魔だったので

```java:DiscordChatService.java
public DiscordChatService(MyPlugin plugin) {
...
    try {
        Field f = Discord4J.class.getDeclaredField("LOGGER");// Loggerというフィールドを取得
        Field modifiersField = Field.class.getDeclaredField("modifiers");// アクセシビリティを管理している闇クラス
        modifiersField.setAccessible(true);
        modifiersField.setInt(f, f.getModifiers() & ~Modifier.PRIVATE & ~Modifier.FINAL);// 改ざん
        f.set(null, new DummyLogger());// ダミーのLoggerをセット
    } catch (NoSuchFieldException | IllegalAccessException e) {
        e.printStackTrace();
    }
}
```

<details>
<summary>ダミークラス</summary>
<div>

```java:DummyLogger
import org.slf4j.Logger;
import org.slf4j.Marker;

public class DummyLogger implements Logger {
    @Override
    public String getName() {
        return null;
    }

    @Override
    public boolean isTraceEnabled() {
        return false;
    }


    @Override
    public void trace(String s) {

    }

    @Override
    public void trace(String s, Object o) {

    }

    @Override
    public void trace(String s, Object o, Object o1) {

    }

    @Override
    public void trace(String s, Object... object) {

    }

    @Override
    public void trace(String s, Throwable throwable) {

    }

    @Override
    public boolean isTraceEnabled(Marker marker) {
        return false;
    }

    @Override
    public void trace(Marker marker, String s) {

    }

    @Override
    public void trace(Marker marker, String s, Object o) {

    }

    @Override
    public void trace(Marker marker, String s, Object o, Object o1) {

    }

    @Override
    public void trace(Marker marker, String s, Object... object) {

    }

    @Override
    public void trace(Marker marker, String s, Throwable throwable) {

    }

    @Override
    public boolean isDebugEnabled() {
        return false;
    }

    @Override
    public void debug(String s) {

    }

    @Override
    public void debug(String s, Object o) {

    }

    @Override
    public void debug(String s, Object o, Object o1) {

    }

    @Override
    public void debug(String s, Object... object) {

    }

    @Override
    public void debug(String s, Throwable throwable) {

    }

    @Override
    public boolean isDebugEnabled(Marker marker) {
        return false;
    }

    @Override
    public void debug(Marker marker, String s) {

    }

    @Override
    public void debug(Marker marker, String s, Object o) {

    }

    @Override
    public void debug(Marker marker, String s, Object o, Object o1) {

    }

    @Override
    public void debug(Marker marker, String s, Object... object) {

    }

    @Override
    public void debug(Marker marker, String s, Throwable throwable) {

    }

    @Override
    public boolean isInfoEnabled() {
        return false;
    }

    @Override
    public void info(String s) {

    }

    @Override
    public void info(String s, Object o) {

    }

    @Override
    public void info(String s, Object o, Object o1) {

    }

    @Override
    public void info(String s, Object... object) {

    }

    @Override
    public void info(String s, Throwable throwable) {

    }

    @Override
    public boolean isInfoEnabled(Marker marker) {
        return false;
    }

    @Override
    public void info(Marker marker, String s) {

    }

    @Override
    public void info(Marker marker, String s, Object o) {

    }

    @Override
    public void info(Marker marker, String s, Object o, Object o1) {

    }

    @Override
    public void info(Marker marker, String s, Object... object) {

    }

    @Override
    public void info(Marker marker, String s, Throwable throwable) {

    }

    @Override
    public boolean isWarnEnabled() {
        return false;
    }

    @Override
    public void warn(String s) {

    }

    @Override
    public void warn(String s, Object o) {

    }

    @Override
    public void warn(String s, Object... object) {

    }

    @Override
    public void warn(String s, Object o, Object o1) {

    }

    @Override
    public void warn(String s, Throwable throwable) {

    }

    @Override
    public boolean isWarnEnabled(Marker marker) {
        return false;
    }

    @Override
    public void warn(Marker marker, String s) {

    }

    @Override
    public void warn(Marker marker, String s, Object o) {

    }

    @Override
    public void warn(Marker marker, String s, Object o, Object o1) {

    }

    @Override
    public void warn(Marker marker, String s, Object... object) {

    }

    @Override
    public void warn(Marker marker, String s, Throwable throwable) {

    }

    @Override
    public boolean isErrorEnabled() {
        return false;
    }

    @Override
    public void error(String s) {

    }

    @Override
    public void error(String s, Object o) {

    }

    @Override
    public void error(String s, Object o, Object o1) {

    }

    @Override
    public void error(String s, Object... object) {

    }

    @Override
    public void error(String s, Throwable throwable) {

    }

    @Override
    public boolean isErrorEnabled(Marker marker) {
        return false;
    }

    @Override
    public void error(Marker marker, String s) {

    }

    @Override
    public void error(Marker marker, String s, Object o) {

    }

    @Override
    public void error(Marker marker, String s, Object o, Object o1) {

    }

    @Override
    public void error(Marker marker, String s, Object... object) {

    }

    @Override
    public void error(Marker marker, String s, Throwable throwable) {

    }
}
```
</div>
</details>

そして平和が訪れた...

##まとめ
この他にも長さ制限やまとめてチャット送信などの機能も実装する必要がありますが今後時間があれば書きます。