@@ -0,0 +1,119 @@
|
|||||||
|
name: Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Java
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: temurin
|
||||||
|
java-version: 21
|
||||||
|
|
||||||
|
- name: Set up Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22.14.0
|
||||||
|
|
||||||
|
- name: Install Node dependencies
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
- name: Lint commits
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event.before }}" = "0000000000000000000000000000000000000000" ]; then
|
||||||
|
FROM=$(git rev-list --max-parents=0 HEAD)
|
||||||
|
else
|
||||||
|
FROM="${{ github.event.before }}"
|
||||||
|
fi
|
||||||
|
npx commitlint --from "$FROM" --to "${{ github.sha }}"
|
||||||
|
|
||||||
|
- name: Release version
|
||||||
|
env:
|
||||||
|
GIT_AUTHOR_NAME: "automation"
|
||||||
|
GIT_AUTHOR_EMAIL: "automation@local"
|
||||||
|
GIT_COMMITTER_NAME: "automation"
|
||||||
|
GIT_COMMITTER_EMAIL: "automation@local"
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
run: npx semantic-release
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: mvn -B package
|
||||||
|
|
||||||
|
- name: Name build artifact
|
||||||
|
run: |
|
||||||
|
if [ -f release-version.txt ]; then
|
||||||
|
VERSION=$(tr -d '\n' < release-version.txt)
|
||||||
|
else
|
||||||
|
VERSION=dev-${GITHUB_SHA:0:7}
|
||||||
|
fi
|
||||||
|
JAR_PATH=$(ls target/*.jar | head -n 1)
|
||||||
|
cp "$JAR_PATH" "target/spigot-chat-sync-${VERSION}.jar"
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: spigot-chat-sync-jar
|
||||||
|
path: target/spigot-chat-sync-*.jar
|
||||||
|
|
||||||
|
- name: Create release
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
GITEA_API: ${{ vars.GITEA_API_URL }}
|
||||||
|
GITEA_SERVER_URL: ${{ vars.GITEA_SERVER_URL }}
|
||||||
|
GITHUB_SERVER_URL: ${{ github.server_url }}
|
||||||
|
run: |
|
||||||
|
TAG=$(git describe --tags --exact-match 2>/dev/null || true)
|
||||||
|
if [ -z "$TAG" ]; then
|
||||||
|
echo "No release tag on HEAD; skipping release."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [ -z "$GITEA_API" ]; then
|
||||||
|
if [ -n "$GITEA_SERVER_URL" ]; then
|
||||||
|
GITEA_API="${GITEA_SERVER_URL}/api/v1"
|
||||||
|
elif [ -n "$GITHUB_SERVER_URL" ]; then
|
||||||
|
GITEA_API="${GITHUB_SERVER_URL}/api/v1"
|
||||||
|
else
|
||||||
|
echo "GITEA_API_URL is not set and server URL is unavailable."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
VERSION=${TAG#v}
|
||||||
|
JAR_PATH=$(ls target/spigot-chat-sync-*.jar | head -n 1)
|
||||||
|
RELEASE_RESPONSE=$(curl -sS -X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${GITEA_API}/repos/${GITHUB_REPOSITORY}/releases" \
|
||||||
|
-d "{\"tag_name\":\"${TAG}\",\"name\":\"${TAG}\",\"draft\":false,\"prerelease\":false}")
|
||||||
|
RELEASE_ID=$(python3 - <<'PY'
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
try:
|
||||||
|
payload = json.load(sys.stdin)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
payload = {}
|
||||||
|
|
||||||
|
value = payload.get("id", "")
|
||||||
|
sys.stdout.write(str(value) if value is not None else "")
|
||||||
|
PY
|
||||||
|
)
|
||||||
|
if [ -z "$RELEASE_ID" ]; then
|
||||||
|
echo "Failed to create release."
|
||||||
|
echo "$RELEASE_RESPONSE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
curl -sS -X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
--data-binary "@${JAR_PATH}" \
|
||||||
|
"${GITEA_API}/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets?name=spigot-chat-sync-${VERSION}.jar"
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
target/
|
||||||
|
.classpath
|
||||||
|
.project
|
||||||
|
.settings/
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
.DS_Store
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"branches": ["main"],
|
||||||
|
"tagFormat": "v${version}",
|
||||||
|
"plugins": [
|
||||||
|
[
|
||||||
|
"@semantic-release/commit-analyzer",
|
||||||
|
{
|
||||||
|
"releaseRules": [
|
||||||
|
{ "type": "chore", "scope": "deps", "release": "patch" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@semantic-release/release-notes-generator",
|
||||||
|
[
|
||||||
|
"@semantic-release/exec",
|
||||||
|
{
|
||||||
|
"prepareCmd": "node -e \"const fs=require('fs');fs.writeFileSync('release-version.txt', process.env.RELEASE_VERSION + '\\n');\""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@semantic-release/git",
|
||||||
|
{
|
||||||
|
"assets": ["release-version.txt"],
|
||||||
|
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
FROM maven:3.9.9-eclipse-temurin-21
|
||||||
|
|
||||||
|
WORKDIR /workspace
|
||||||
|
|
||||||
|
COPY pom.xml /workspace/
|
||||||
|
|
||||||
|
RUN mvn -q -e package || true
|
||||||
|
|
||||||
|
COPY src /workspace/src
|
||||||
|
|
||||||
|
RUN mvn -q -e package
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
IMAGE_NAME ?= prometheus-spigot-build
|
||||||
|
CONTAINER_NAME ?= prometheus-spigot-build
|
||||||
|
|
||||||
|
.PHONY: build docker-build docker-jar clean-docker
|
||||||
|
|
||||||
|
build:
|
||||||
|
mvn package
|
||||||
|
|
||||||
|
docker-build:
|
||||||
|
docker build -t $(IMAGE_NAME) .
|
||||||
|
|
||||||
|
docker-jar: docker-build
|
||||||
|
docker rm -f $(CONTAINER_NAME) 2>/dev/null || true
|
||||||
|
docker create --name $(CONTAINER_NAME) $(IMAGE_NAME)
|
||||||
|
docker cp $(CONTAINER_NAME):/workspace/target ./target
|
||||||
|
docker rm -f $(CONTAINER_NAME)
|
||||||
|
|
||||||
|
clean-docker:
|
||||||
|
docker rm -f $(CONTAINER_NAME) 2>/dev/null || true
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "spigot-chat-sync",
|
||||||
|
"private": true,
|
||||||
|
"devDependencies": {
|
||||||
|
"@commitlint/cli": "^20.0.0",
|
||||||
|
"@commitlint/config-conventional": "^20.0.0",
|
||||||
|
"@semantic-release/exec": "^7.0.0",
|
||||||
|
"@semantic-release/git": "^10.0.1",
|
||||||
|
"semantic-release": "^25.0.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"semantic-release": "semantic-release"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>com.spigotchatsync</groupId>
|
||||||
|
<artifactId>spigot-chat-sync</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<name>Spigot Chat Sync</name>
|
||||||
|
<description>Sync chat and death events via NATS</description>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.release>21</maven.compiler.release>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<id>spigot-repo</id>
|
||||||
|
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.spigotmc</groupId>
|
||||||
|
<artifactId>spigot-api</artifactId>
|
||||||
|
<version>1.21.1-R0.1-SNAPSHOT</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.nats</groupId>
|
||||||
|
<artifactId>jnats</artifactId>
|
||||||
|
<version>2.20.5</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.code.gson</groupId>
|
||||||
|
<artifactId>gson</artifactId>
|
||||||
|
<version>2.10.1</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<version>3.5.1</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>shade</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<relocations>
|
||||||
|
<relocation>
|
||||||
|
<pattern>io.nats</pattern>
|
||||||
|
<shadedPattern>com.spigotchatsync.shaded.io.nats</shadedPattern>
|
||||||
|
</relocation>
|
||||||
|
<relocation>
|
||||||
|
<pattern>com.google.gson</pattern>
|
||||||
|
<shadedPattern>com.spigotchatsync.shaded.com.google.gson</shadedPattern>
|
||||||
|
</relocation>
|
||||||
|
</relocations>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
@@ -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