Edited at

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


はじめに

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

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

MinecraftBEのサーバーソフトウェア

Nukkit のプラグイン拡張で

サーバー内のチャットをDiscordのBotでリアルタイムに表示する機能を実装までを書きました。


Mavenでライブラリを落とす

pom.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>
...


手動で落とす方はこちら


ライブラリを使ってみる


ログインさせる


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();
}
}



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


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の実装


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の実装

これが難しかった。

しかし、このコードには問題があります。

非同期でチャットが送信されないため、サーバーが一瞬フリーズします。


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);
});
}



改善版

ここでオレオレ実装クラスの登場



オレオレ実装クラス


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;
}
}
}






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


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のログを消す

個人的に邪魔だったので


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();
}
}



ダミークラス


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) {

}
}






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


まとめ

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