@@ -0,0 +1,79 @@
|
||||
package com.spigotchatsync;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import java.time.Instant;
|
||||
|
||||
public final class ChatSyncMessage {
|
||||
@SerializedName("type")
|
||||
private String type;
|
||||
|
||||
@SerializedName("server_id")
|
||||
private String serverId;
|
||||
|
||||
@SerializedName("server")
|
||||
private String server;
|
||||
|
||||
@SerializedName("player_uuid")
|
||||
private String playerUuid;
|
||||
|
||||
@SerializedName("player_name")
|
||||
private String playerName;
|
||||
|
||||
@SerializedName("message")
|
||||
private String message;
|
||||
|
||||
@SerializedName("timestamp")
|
||||
private long timestamp;
|
||||
|
||||
public static ChatSyncMessage forChat(String serverId, String server, String playerUuid, String playerName, String message) {
|
||||
ChatSyncMessage payload = new ChatSyncMessage();
|
||||
payload.type = "chat";
|
||||
payload.serverId = serverId;
|
||||
payload.server = server;
|
||||
payload.playerUuid = playerUuid;
|
||||
payload.playerName = playerName;
|
||||
payload.message = message;
|
||||
payload.timestamp = Instant.now().getEpochSecond();
|
||||
return payload;
|
||||
}
|
||||
|
||||
public static ChatSyncMessage forDeath(String serverId, String server, String playerUuid, String playerName, String message) {
|
||||
ChatSyncMessage payload = new ChatSyncMessage();
|
||||
payload.type = "death";
|
||||
payload.serverId = serverId;
|
||||
payload.server = server;
|
||||
payload.playerUuid = playerUuid;
|
||||
payload.playerName = playerName;
|
||||
payload.message = message;
|
||||
payload.timestamp = Instant.now().getEpochSecond();
|
||||
return payload;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getServerId() {
|
||||
return serverId;
|
||||
}
|
||||
|
||||
public String getServer() {
|
||||
return server;
|
||||
}
|
||||
|
||||
public String getPlayerUuid() {
|
||||
return playerUuid;
|
||||
}
|
||||
|
||||
public String getPlayerName() {
|
||||
return playerName;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package com.spigotchatsync;
|
||||
|
||||
import java.util.Objects;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.PlayerDeathEvent;
|
||||
import org.bukkit.event.player.AsyncPlayerChatEvent;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
public final class ChatSyncPlugin extends JavaPlugin implements Listener {
|
||||
private NatsBridge natsBridge;
|
||||
private String serverId;
|
||||
private String serverName;
|
||||
private String natsUrl;
|
||||
private String chatSubject;
|
||||
private String deathSubject;
|
||||
private String chatFormat;
|
||||
private String deathFormat;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
saveDefaultConfig();
|
||||
if (!loadSettings()) {
|
||||
return;
|
||||
}
|
||||
|
||||
natsBridge = new NatsBridge(getLogger(), natsUrl);
|
||||
if (!natsBridge.connect(this::handleIncoming, chatSubject, deathSubject)) {
|
||||
getLogger().severe("Failed to connect to NATS; disabling plugin.");
|
||||
Bukkit.getPluginManager().disablePlugin(this);
|
||||
return;
|
||||
}
|
||||
|
||||
Bukkit.getPluginManager().registerEvents(this, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
if (natsBridge != null) {
|
||||
natsBridge.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean loadSettings() {
|
||||
FileConfiguration config = getConfig();
|
||||
serverId = config.getString("server.id", "").trim();
|
||||
serverName = config.getString("server.name", "").trim();
|
||||
if (serverId.isEmpty()) {
|
||||
getLogger().severe("server.id is required in config.yml");
|
||||
Bukkit.getPluginManager().disablePlugin(this);
|
||||
return false;
|
||||
}
|
||||
if (serverName.isEmpty()) {
|
||||
serverName = serverId;
|
||||
getLogger().warning("server.name missing; defaulting to server.id");
|
||||
}
|
||||
|
||||
chatSubject = config.getString("subjects.chat", "mc.chat.sync");
|
||||
deathSubject = config.getString("subjects.death", "mc.death.sync");
|
||||
chatFormat = config.getString("format.chat", "[{server}] {player}: {message}");
|
||||
deathFormat = config.getString("format.death", "[{server}] {message}");
|
||||
natsUrl = config.getString("nats.url", "nats://localhost:4222").trim();
|
||||
if (natsUrl.isEmpty()) {
|
||||
getLogger().severe("nats.url is required in config.yml");
|
||||
Bukkit.getPluginManager().disablePlugin(this);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
@SuppressWarnings("deprecation")
|
||||
public void onChat(AsyncPlayerChatEvent event) {
|
||||
if (event.isCancelled() || natsBridge == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ChatSyncMessage message = ChatSyncMessage.forChat(
|
||||
serverId,
|
||||
serverName,
|
||||
event.getPlayer().getUniqueId().toString(),
|
||||
event.getPlayer().getName(),
|
||||
event.getMessage()
|
||||
);
|
||||
natsBridge.publish(chatSubject, message);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onDeath(PlayerDeathEvent event) {
|
||||
if (natsBridge == null) {
|
||||
return;
|
||||
}
|
||||
String deathMessage = event.getDeathMessage();
|
||||
if (deathMessage == null || deathMessage.isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ChatSyncMessage message = ChatSyncMessage.forDeath(
|
||||
serverId,
|
||||
serverName,
|
||||
event.getEntity().getUniqueId().toString(),
|
||||
event.getEntity().getName(),
|
||||
deathMessage
|
||||
);
|
||||
natsBridge.publish(deathSubject, message);
|
||||
}
|
||||
|
||||
private void handleIncoming(String subject, ChatSyncMessage message) {
|
||||
if (message == null || Objects.equals(serverId, message.getServerId())) {
|
||||
return;
|
||||
}
|
||||
|
||||
String format = subject.equals(chatSubject) ? chatFormat : deathFormat;
|
||||
String formatted = MessageFormatter.format(format, message);
|
||||
String broadcastMessage = ChatColor.translateAlternateColorCodes('&', formatted);
|
||||
|
||||
Bukkit.getScheduler().runTask(this, () -> Bukkit.broadcastMessage(broadcastMessage));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.spigotchatsync;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
public final class MessageFormatter {
|
||||
private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm");
|
||||
|
||||
private MessageFormatter() {
|
||||
}
|
||||
|
||||
public static String format(String template, ChatSyncMessage message) {
|
||||
String output = template;
|
||||
output = output.replace("{server}", nullSafe(message.getServer()));
|
||||
output = output.replace("{player}", nullSafe(message.getPlayerName()));
|
||||
output = output.replace("{message}", nullSafe(message.getMessage()));
|
||||
output = output.replace("{time}", formatTime(message.getTimestamp()));
|
||||
return output;
|
||||
}
|
||||
|
||||
private static String nullSafe(String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
|
||||
private static String formatTime(long epochSeconds) {
|
||||
if (epochSeconds <= 0) {
|
||||
return "";
|
||||
}
|
||||
LocalTime time = Instant.ofEpochSecond(epochSeconds).atZone(ZoneId.systemDefault()).toLocalTime();
|
||||
return TIME_FORMAT.format(time);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.spigotchatsync;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import io.nats.client.Connection;
|
||||
import io.nats.client.Dispatcher;
|
||||
import io.nats.client.Message;
|
||||
import io.nats.client.Nats;
|
||||
import io.nats.client.Options;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public final class NatsBridge {
|
||||
private final Logger logger;
|
||||
private final String url;
|
||||
private final Gson gson = new Gson();
|
||||
private Connection connection;
|
||||
private Dispatcher chatDispatcher;
|
||||
private Dispatcher deathDispatcher;
|
||||
|
||||
public NatsBridge(Logger logger, String url) {
|
||||
this.logger = Objects.requireNonNull(logger, "logger");
|
||||
this.url = Objects.requireNonNull(url, "url");
|
||||
}
|
||||
|
||||
public boolean connect(BiConsumer<String, ChatSyncMessage> handler, String chatSubject, String deathSubject) {
|
||||
try {
|
||||
Options options = new Options.Builder().server(url).build();
|
||||
connection = Nats.connect(options);
|
||||
} catch (IOException | InterruptedException ex) {
|
||||
logger.log(Level.SEVERE, "Failed to connect to NATS", ex);
|
||||
return false;
|
||||
}
|
||||
|
||||
chatDispatcher = connection.createDispatcher(message -> handleMessage(handler, chatSubject, message));
|
||||
chatDispatcher.subscribe(chatSubject);
|
||||
|
||||
deathDispatcher = connection.createDispatcher(message -> handleMessage(handler, deathSubject, message));
|
||||
deathDispatcher.subscribe(deathSubject);
|
||||
|
||||
logger.info("Connected to NATS at " + url);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void publish(String subject, ChatSyncMessage message) {
|
||||
if (connection == null || connection.getStatus() != Connection.Status.CONNECTED) {
|
||||
logger.warning("NATS is not connected; skipping publish to " + subject);
|
||||
return;
|
||||
}
|
||||
String json = gson.toJson(message);
|
||||
connection.publish(subject, json.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
if (connection == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
connection.drain(Duration.ofSeconds(2));
|
||||
connection.close();
|
||||
} catch (InterruptedException ex) {
|
||||
logger.log(Level.WARNING, "Interrupted while shutting down NATS connection", ex);
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (TimeoutException ex) {
|
||||
logger.log(Level.WARNING, "Timed out while draining NATS connection", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMessage(BiConsumer<String, ChatSyncMessage> handler, String subject, Message message) {
|
||||
String json = new String(message.getData(), StandardCharsets.UTF_8);
|
||||
try {
|
||||
ChatSyncMessage payload = gson.fromJson(json, ChatSyncMessage.class);
|
||||
handler.accept(subject, payload);
|
||||
} catch (JsonSyntaxException ex) {
|
||||
logger.log(Level.WARNING, "Invalid JSON from NATS on " + subject + ": " + json, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
server:
|
||||
id: "server-1"
|
||||
name: "Server 1"
|
||||
|
||||
nats:
|
||||
url: "nats://localhost:4222"
|
||||
|
||||
subjects:
|
||||
chat: "mc.chat.sync"
|
||||
death: "mc.death.sync"
|
||||
|
||||
format:
|
||||
chat: "&7[&b{server}&7] &f{player}&7: &f{message}"
|
||||
death: "&7[&b{server}&7] &f{message}"
|
||||
@@ -0,0 +1,5 @@
|
||||
name: SpigotChatSync
|
||||
main: com.spigotchatsync.ChatSyncPlugin
|
||||
version: 1.0.0
|
||||
api-version: 1.21
|
||||
description: Sync chat and death events via NATS
|
||||
Reference in New Issue
Block a user