diff --git a/.gitignore b/.gitignore
index 0dd4411..20b5546 100755
--- a/.gitignore
+++ b/.gitignore
@@ -40,3 +40,5 @@ target/
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid
!gradle/wrapper/gradle-wrapper.jar
+
+*.bat
diff --git a/api/build.gradle.kts b/api/build.gradle.kts
index 72a3879..efd3670 100644
--- a/api/build.gradle.kts
+++ b/api/build.gradle.kts
@@ -4,7 +4,7 @@ plugins {
dependencies {
// Cosmos
- compileOnly("com.alttd.cosmos:cosmos-api:1.21.8-R0.1-SNAPSHOT") {
+ compileOnly("com.alttd.cosmos:cosmos-api:1.21.10-R0.1-SNAPSHOT") {
isChanging = true
}
compileOnly("org.spongepowered:configurate-yaml:4.2.0") // Configurate
diff --git a/api/src/main/java/com/alttd/chat/config/Config.java b/api/src/main/java/com/alttd/chat/config/Config.java
index 2f7bfc0..29a8830 100755
--- a/api/src/main/java/com/alttd/chat/config/Config.java
+++ b/api/src/main/java/com/alttd/chat/config/Config.java
@@ -38,9 +38,9 @@ public final class Config {
CONFIGPATH = new File(File.separator + "mnt" + File.separator + "configs" + File.separator + "ChatPlugin");
CONFIG_FILE = new File(CONFIGPATH, "config.yml");
configLoader = YamlConfigurationLoader.builder()
- .file(CONFIG_FILE)
- .nodeStyle(NodeStyle.BLOCK)
- .build();
+ .file(CONFIG_FILE)
+ .nodeStyle(NodeStyle.BLOCK)
+ .build();
if (!CONFIG_FILE.getParentFile().exists()) {
if (!CONFIG_FILE.getParentFile().mkdirs()) {
return;
@@ -182,12 +182,12 @@ public final class Config {
private static void settings() {
PREFIXGROUPS = getList("settings.prefix-groups",
- Lists.newArrayList("discord", "socialmedia", "eventteam", "eventleader", "youtube", "twitch",
- "developer"));
+ Lists.newArrayList("discord", "socialmedia", "eventteam", "eventleader", "youtube", "twitch",
+ "developer"));
CONFLICTINGPREFIXGROUPS = getList("settings.prefix-conflicts-groups",
- Lists.newArrayList("eventteam", "eventleader"));
+ Lists.newArrayList("eventteam", "eventleader"));
STAFFGROUPS = getList("settings.staff-groups",
- Lists.newArrayList("trainee", "moderator", "headmod", "admin", "manager", "owner"));
+ Lists.newArrayList("trainee", "moderator", "headmod", "admin", "manager", "owner"));
CONSOLENAME = getString("settings.console-name", CONSOLENAME);
CONSOLEUUID = UUID.fromString(getString("settings.console-uuid", CONSOLEUUID.toString()));
MINIMIUMSTAFFRANK = getString("settings.minimum-staff-rank", MINIMIUMSTAFFRANK);
@@ -282,7 +282,7 @@ public final class Config {
public static String NO_PERMISSION = "You don't have permission to use this command.";
public static String NO_CONSOLE = "This command can not be used by console";
public static String CREATED_PARTY = "You created a chat party called: " +
- "'' with the password: ''";
+ "'' with the password: ''";
public static String NOT_IN_A_PARTY = "You're not in a chat party.";
public static String NOT_YOUR_PARTY = "You don't own this chat party.";
public static String NOT_A_PARTY = "This chat party does not exist.";
@@ -307,14 +307,14 @@ public final class Config {
public static String ALREADY_IN_THIS_PARTY = "You're already in !";
public static String SENT_PARTY_INV = "You send a chat party invite to !";
public static String JOIN_PARTY_CLICK_MESSAGE = " '>" +
- "You received an invite to join , click this message to accept.";
+ "You received an invite to join , click this message to accept.";
public static String PARTY_MEMBER_LOGGED_ON = "[ChatParty] joined Altitude...";
public static String PARTY_MEMBER_LOGGED_OFF = "[ChatParty] left Altitude...";
public static String RENAMED_PARTY =
"[ChatParty] changed the party name from to !";
public static String CHANGED_PASSWORD = "Password was set to ";
public static String DISBAND_PARTY_CONFIRM = "Are you sure you want to disband your party? " +
- "Type /party disband confirm to confirm.";
+ "Type /party disband confirm to confirm.";
public static String DISBANDED_PARTY =
"[ChatParty] has disbanded , everyone has been removed.";
public static String PARTY_INFO = """
@@ -413,7 +413,7 @@ public final class Config {
ConfigurationNode node = getNode("chat-channels");
if (node.empty()) {
getString("chat-channels.ac.format",
- " >to : ");
+ " >to : ");
getList("chat-channels.ac.servers", List.of("lobby"));
getBoolean("chat-channels.ac.proxy", false);
node = getNode("chat-channels");
@@ -423,11 +423,11 @@ public final class Config {
String channelName = Objects.requireNonNull(configurationNode.key()).toString();
String key = "chat-channels." + channelName + ".";
new CustomChannel(channelName,
- getString(key + "format", ""),
- getList(key + "servers", Collections.EMPTY_LIST),
- getList(key + "alias", Collections.EMPTY_LIST),
- getBoolean(key + "proxy", false),
- getBoolean(key + "local", false)
+ getString(key + "format", ""),
+ getList(key + "servers", Collections.EMPTY_LIST),
+ getList(key + "alias", Collections.EMPTY_LIST),
+ getBoolean(key + "proxy", false),
+ getBoolean(key + "local", false)
);
}
@@ -616,12 +616,12 @@ public final class Config {
NICK_REQUESTS_ON_LOGIN = getString("nicknames.messages.nick-reauests-on-login", NICK_REQUESTS_ON_LOGIN);
NICK_WAIT_TIME = getLong("nicknames.wait-time", NICK_WAIT_TIME);
NICK_ITEM_LORE = getList("nicknames.item-lore",
- List.of("New nick: ", "Old nick: ", "Last changed: ",
- "Left click to Accept | Right click to Deny"));
+ List.of("New nick: ", "Old nick: ", "Last changed: ",
+ "Left click to Accept | Right click to Deny"));
NICK_BLOCKED_COLOR_CODESLIST = getList("nicknames.blocked-color-codes", List.of("&k", "&l", "&n", "&m", "&o"));
NICK_ALLOWED_COLOR_CODESLIST = getList("nicknames.allowed-color-codes",
- List.of("&0", "&1", "&2", "&3", "&4", "&5", "&6", "&7", "&8", "&9", "&a", "&b", "&c", "&d", "&e", "&f",
- "&r"));
+ List.of("&0", "&1", "&2", "&3", "&4", "&5", "&6", "&7", "&8", "&9", "&a", "&b", "&c", "&d", "&e", "&f",
+ "&r"));
NICK_CURRENT = getString("nicknames.messages.nick-current", NICK_CURRENT);
}
@@ -641,4 +641,14 @@ public final class Config {
CHAT_LOG_DELETE_OLDER_THAN_DAYS = getLong("chat-log.delete-older-than-days", CHAT_LOG_DELETE_OLDER_THAN_DAYS);
CHAT_LOG_SAVE_DELAY_MINUTES = getLong("chat-log.save-delay-minutes", CHAT_LOG_SAVE_DELAY_MINUTES);
}
+
+ public static String MATRIX_SERVER = "http://127.0.0.1:8008";
+ public static String MATRIX_AS_TOKEN = "";
+ public static String MATRIX_HS_TOKEN = "";
+
+ private static void matrixSettings() {
+ MATRIX_SERVER = getString("matrix.server", MATRIX_SERVER);
+ MATRIX_AS_TOKEN = getString("matrix.as_token", MATRIX_AS_TOKEN);
+ MATRIX_HS_TOKEN = getString("matrix.hs_token", MATRIX_HS_TOKEN);
+ }
}
diff --git a/galaxy/build.gradle.kts b/galaxy/build.gradle.kts
index 7f4525c..21b02d7 100644
--- a/galaxy/build.gradle.kts
+++ b/galaxy/build.gradle.kts
@@ -5,7 +5,7 @@ plugins {
dependencies {
implementation(project(":api")) // API
- compileOnly("com.alttd.cosmos:cosmos-api:1.21.8-R0.1-SNAPSHOT") {
+ compileOnly("com.alttd.cosmos:cosmos-api:1.21.10-R0.1-SNAPSHOT") {
isChanging = true
}
compileOnly("com.gitlab.ruany:LiteBansAPI:0.6.1") // move to proxy
diff --git a/matrix/build.gradle.kts b/matrix/build.gradle.kts
new file mode 100644
index 0000000..bcf8ee7
--- /dev/null
+++ b/matrix/build.gradle.kts
@@ -0,0 +1,17 @@
+plugins {
+ `maven-publish`
+}
+
+dependencies {
+ api(project(":api"))
+ compileOnly("com.velocitypowered:velocity-api:3.2.0-SNAPSHOT")
+
+ api("com.fasterxml.jackson.core:jackson-databind:2.17.2")
+ api("com.fasterxml.jackson.core:jackson-core:2.17.2")
+ api("com.fasterxml.jackson.core:jackson-annotations:2.17.2")
+}
+
+tasks.withType().configureEach {
+ options.encoding = "UTF-8"
+ options.release.set(21)
+}
diff --git a/matrix/src/main/java/com/alttd/matrix/MatrixConfig.java b/matrix/src/main/java/com/alttd/matrix/MatrixConfig.java
new file mode 100644
index 0000000..0fb2fd7
--- /dev/null
+++ b/matrix/src/main/java/com/alttd/matrix/MatrixConfig.java
@@ -0,0 +1,12 @@
+package com.alttd.matrix;
+
+public record MatrixConfig(
+ String homeserver, // e.g. "http://127.0.0.1:8008"
+ String asToken, // from as registration yaml
+ String hsToken, // from as registration yaml
+ String domain, // "matrix.alttd.com"
+ String userPrefix, // "mc_"
+ String bindHost, // "127.0.0.1"
+ int bindPort // 9000
+) {
+}
diff --git a/matrix/src/main/java/com/alttd/matrix/MatrixRuntime.java b/matrix/src/main/java/com/alttd/matrix/MatrixRuntime.java
new file mode 100644
index 0000000..ed6b38e
--- /dev/null
+++ b/matrix/src/main/java/com/alttd/matrix/MatrixRuntime.java
@@ -0,0 +1,29 @@
+package com.alttd.matrix;
+
+import com.alttd.matrix.app_service.MatrixAppServiceHttpServer;
+import com.alttd.matrix.client.HttpMatrixClient;
+import com.alttd.matrix.interfaces.MatrixAppServiceServer;
+import com.alttd.matrix.interfaces.MatrixClient;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+
+public final class MatrixRuntime {
+
+ private MatrixRuntime() {
+ }
+
+ public static MatrixAppServiceServer startAppService(MatrixConfig cfg) throws IOException {
+ var server = new MatrixAppServiceHttpServer(
+ new InetSocketAddress(cfg.bindHost(), cfg.bindPort()),
+ cfg.hsToken()
+ );
+ server.start();
+ return server;
+ }
+
+ public static MatrixClient createClient(MatrixConfig cfg) {
+ return new HttpMatrixClient(cfg.homeserver(), cfg.asToken());
+ }
+}
+
diff --git a/matrix/src/main/java/com/alttd/matrix/app_service/MatrixAppServiceHttpServer.java b/matrix/src/main/java/com/alttd/matrix/app_service/MatrixAppServiceHttpServer.java
new file mode 100644
index 0000000..485ec07
--- /dev/null
+++ b/matrix/src/main/java/com/alttd/matrix/app_service/MatrixAppServiceHttpServer.java
@@ -0,0 +1,153 @@
+package com.alttd.matrix.app_service;
+
+import com.alttd.matrix.interfaces.MatrixAppServiceServer;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpServer;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.InetSocketAddress;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.function.Consumer;
+
+public final class MatrixAppServiceHttpServer implements MatrixAppServiceServer {
+
+ private final HttpServer server;
+ private final String hsToken;
+ private final ObjectMapper om = new ObjectMapper();
+
+ private volatile Consumer txListener;
+
+ public MatrixAppServiceHttpServer(InetSocketAddress bind, String hsToken) throws IOException {
+ this.server = HttpServer.create(bind, 0);
+ this.hsToken = hsToken;
+
+ server.createContext("/_matrix/app/v1/transactions", this::handleTransactions);
+ server.createContext("/_matrix/app/v1/users", this::handleUsers);
+
+ server.setExecutor(Executors.newCachedThreadPool(r -> {
+ Thread t = new Thread(r, "matrix-as-http");
+ t.setDaemon(true);
+ return t;
+ }));
+ }
+
+ public void start() {
+ server.start();
+ }
+
+ @Override
+ public void setTransactionListener(Consumer listener) {
+ this.txListener = listener;
+ }
+
+ @Override
+ public void close() {
+ server.stop(0);
+ }
+
+ private void handleUsers(HttpExchange ex) throws IOException {
+ if (!"GET".equals(ex.getRequestMethod())) {
+ send(ex, 405, "{}");
+ return;
+ }
+ if (!validHsToken(ex)) {
+ send(ex, 401, "{}");
+ return;
+ }
+
+ // Synapse asks "does this user exist?" -> for AS, answer 200 so Synapse will proceed.
+ // You can optionally restrict to your namespace by inspecting the path.
+ send(ex, 200, "{}");
+ }
+
+ private void handleTransactions(HttpExchange ex) throws IOException {
+ if (!"PUT".equals(ex.getRequestMethod())) {
+ send(ex, 405, "{}");
+ return;
+ }
+ if (!validHsToken(ex)) {
+ send(ex, 401, "{}");
+ return;
+ }
+
+ JsonNode root;
+ try (InputStream in = ex.getRequestBody()) {
+ root = om.readTree(in);
+ } catch (Exception e) {
+ send(ex, 400, "{}");
+ return;
+ }
+
+ Consumer listener = txListener;
+ if (listener != null) {
+ JsonNode events = root.path("events");
+ if (events.isArray()) {
+ for (Iterator it = events.elements(); it.hasNext(); ) {
+ JsonNode ev = it.next();
+
+ // only handle m.room.message text
+ if (!"m.room.message".equals(ev.path("type").asText())) {
+ continue;
+ }
+ if (!"m.text".equals(ev.path("content").path("msgtype").asText())) {
+ continue;
+ }
+
+ String roomId = ev.path("room_id").asText(null);
+ String sender = ev.path("sender").asText(null);
+ String eventId = ev.path("event_id").asText(null);
+ String body = ev.path("content").path("body").asText(null);
+
+ if (roomId != null && body != null) {
+ listener.accept(new TransactionEvent(roomId, sender, body, eventId));
+ }
+ }
+ }
+ }
+
+ send(ex, 200, "{}");
+ }
+
+ private boolean validHsToken(HttpExchange ex) {
+ String q = ex.getRequestURI().getRawQuery();
+ if (q == null) {
+ return false;
+ }
+ Map params = parseQuery(q);
+ String token = params.get("access_token");
+ return token != null && token.equals(hsToken);
+ }
+
+ private static Map parseQuery(String raw) {
+ var map = new java.util.HashMap();
+ for (String part : raw.split("&")) {
+ int i = part.indexOf('=');
+ if (i <= 0) {
+ continue;
+ }
+ String k = urlDecode(part.substring(0, i));
+ String v = urlDecode(part.substring(i + 1));
+ map.put(k, v);
+ }
+ return map;
+ }
+
+ private static String urlDecode(String s) {
+ return URLDecoder.decode(s, StandardCharsets.UTF_8);
+ }
+
+ private static void send(HttpExchange ex, int code, String body) throws IOException {
+ byte[] b = body.getBytes(StandardCharsets.UTF_8);
+ ex.getResponseHeaders().set("Content-Type", "application/json");
+ ex.sendResponseHeaders(code, b.length);
+ ex.getResponseBody().write(b);
+ ex.close();
+ }
+}
diff --git a/matrix/src/main/java/com/alttd/matrix/bridge/DefaultMatrixBridge.java b/matrix/src/main/java/com/alttd/matrix/bridge/DefaultMatrixBridge.java
new file mode 100644
index 0000000..7f9e370
--- /dev/null
+++ b/matrix/src/main/java/com/alttd/matrix/bridge/DefaultMatrixBridge.java
@@ -0,0 +1,76 @@
+package com.alttd.matrix.bridge;
+
+import com.alttd.matrix.MatrixConfig;
+import com.alttd.matrix.interfaces.MatrixAppServiceServer;
+import com.alttd.matrix.interfaces.MatrixBridge;
+import com.alttd.matrix.interfaces.MatrixClient;
+
+import java.util.UUID;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+public final class DefaultMatrixBridge implements MatrixBridge {
+
+ private final MatrixClient client;
+ private final MatrixConfig cfg;
+ private final Executor io = Executors.newSingleThreadExecutor(r -> {
+ Thread t = new Thread(r, "matrix-io");
+ t.setDaemon(true);
+ return t;
+ });
+
+ private volatile InboundHandler inbound;
+ private volatile RoomResolver rooms;
+
+ public DefaultMatrixBridge(MatrixConfig cfg, MatrixAppServiceServer asServer, MatrixClient client) {
+ this.cfg = cfg;
+ this.client = client;
+
+ asServer.setTransactionListener(evt -> {
+ InboundHandler h = inbound;
+ RoomResolver rr = rooms;
+ if (h == null || rr == null) {
+ return;
+ }
+
+ // drop our own puppets to prevent loops
+ if (evt.senderMxid() != null && evt.senderMxid().startsWith("@" + cfg.userPrefix())) {
+ return;
+ }
+
+ h.onMatrixChat(evt.roomId(), evt.senderMxid(), evt.body());
+ });
+ }
+
+ @Override
+ public void setInboundHandler(InboundHandler handler) {
+ this.inbound = handler;
+ }
+
+ @Override
+ public void setRoomResolver(RoomResolver resolver) {
+ this.rooms = resolver;
+ }
+
+ @Override
+ public void sendChat(UUID playerUuid, String mcUsername, String serverName, String message) {
+ RoomResolver rr = rooms;
+ if (rr == null) {
+ return;
+ }
+
+ String roomId = rr.roomIdForServer(serverName);
+ if (roomId == null) {
+ return;
+ }
+
+ String mxid = "@" + cfg.userPrefix() + playerUuid + ":" + cfg.domain();
+
+ io.execute(() -> {
+ client.ensureUser(mxid);
+ client.setDisplayName(mxid, mcUsername);
+ client.ensureJoined(mxid, roomId);
+ client.sendText(mxid, roomId, message);
+ });
+ }
+}
diff --git a/matrix/src/main/java/com/alttd/matrix/client/HttpMatrixClient.java b/matrix/src/main/java/com/alttd/matrix/client/HttpMatrixClient.java
new file mode 100644
index 0000000..947ef54
--- /dev/null
+++ b/matrix/src/main/java/com/alttd/matrix/client/HttpMatrixClient.java
@@ -0,0 +1,155 @@
+package com.alttd.matrix.client;
+
+import com.alttd.matrix.interfaces.MatrixClient;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import java.net.URI;
+import java.net.URLEncoder;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.UUID;
+
+public final class HttpMatrixClient implements MatrixClient {
+
+ private final HttpClient http = HttpClient.newBuilder()
+ .connectTimeout(Duration.ofSeconds(5))
+ .build();
+
+ private final ObjectMapper om = new ObjectMapper();
+
+ private final String hs; // e.g. http://127.0.0.1:8008
+ private final String asToken; // AS access token
+
+ public HttpMatrixClient(String homeserver, String asToken) {
+ this.hs = homeserver.endsWith("/") ? homeserver.substring(0, homeserver.length() - 1) : homeserver;
+ this.asToken = asToken;
+ }
+
+ @Override
+ public void ensureUser(String mxid) {
+ String localpart = localpart(mxid);
+
+ String url = hs + "/_matrix/client/v3/register"
+ + "?access_token=" + enc(asToken);
+
+ String body = "{\"type\":\"m.login.application_service\",\"username\":\"" + json(localpart) + "\"}";
+
+ HttpRequest req = HttpRequest.newBuilder(URI.create(url))
+ .timeout(Duration.ofSeconds(10))
+ .header("Content-Type", "application/json")
+ .POST(HttpRequest.BodyPublishers.ofString(body))
+ .build();
+
+ sendIgnoreConflict(req);
+ }
+
+ @Override
+ public void setDisplayName(String mxid, String displayName) {
+ String url = hs + "/_matrix/client/v3/profile/" + encPath(mxid) + "/displayname"
+ + "?access_token=" + enc(asToken)
+ + "&user_id=" + enc(mxid);
+
+ String body = "{\"displayname\":\"" + json(displayName) + "\"}";
+
+ HttpRequest req = HttpRequest.newBuilder(URI.create(url))
+ .timeout(Duration.ofSeconds(10))
+ .header("Content-Type", "application/json")
+ .PUT(HttpRequest.BodyPublishers.ofString(body))
+ .build();
+
+ sendOkOrIgnore(req);
+ }
+
+ @Override
+ public void ensureJoined(String mxid, String roomId) {
+ String url = hs + "/_matrix/client/v3/rooms/" + encPath(roomId) + "/join"
+ + "?access_token=" + enc(asToken)
+ + "&user_id=" + enc(mxid);
+
+ HttpRequest req = HttpRequest.newBuilder(URI.create(url))
+ .timeout(Duration.ofSeconds(10))
+ .header("Content-Type", "application/json")
+ .POST(HttpRequest.BodyPublishers.ofString("{}"))
+ .build();
+
+ sendOkOrIgnore(req);
+ }
+
+ @Override
+ public void sendText(String mxid, String roomId, String bodyText) {
+ String txnId = UUID.randomUUID().toString();
+
+ String url = hs + "/_matrix/client/v3/rooms/" + encPath(roomId) + "/send/m.room.message/" + encPath(txnId)
+ + "?access_token=" + enc(asToken)
+ + "&user_id=" + enc(mxid);
+
+ String body = "{\"msgtype\":\"m.text\",\"body\":\"" + json(bodyText) + "\"}";
+
+ HttpRequest req = HttpRequest.newBuilder(URI.create(url))
+ .timeout(Duration.ofSeconds(10))
+ .header("Content-Type", "application/json")
+ .PUT(HttpRequest.BodyPublishers.ofString(body))
+ .build();
+
+ sendOkOrIgnore(req);
+ }
+
+ private void sendIgnoreConflict(HttpRequest req) {
+ try {
+ HttpResponse res = http.send(req, HttpResponse.BodyHandlers.ofString());
+ int c = res.statusCode();
+ if (c == 200) {
+ return;
+ }
+ if (c == 400 && res.body() != null && res.body().contains("M_USER_IN_USE")) {
+ return;
+ }
+ if (c == 401 || c == 403) {
+ throw new RuntimeException("Matrix auth failed: " + c + " " + res.body());
+ }
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void sendOkOrIgnore(HttpRequest req) {
+ try {
+ HttpResponse res = http.send(req, HttpResponse.BodyHandlers.ofString());
+ int c = res.statusCode();
+ if (c >= 200 && c < 300) {
+ return;
+ }
+ if (c == 401 || c == 403) {
+ throw new RuntimeException("Matrix auth failed: " + c + " " + res.body());
+ }
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static String localpart(String mxid) {
+ // "@localpart:domain"
+ int at = mxid.indexOf('@');
+ int colon = mxid.indexOf(':');
+ if (at != 0 || colon < 0) {
+ throw new IllegalArgumentException("bad mxid: " + mxid);
+ }
+ return mxid.substring(1, colon);
+ }
+
+ private static String enc(String s) {
+ return URLEncoder.encode(s, StandardCharsets.UTF_8);
+ }
+
+ private static String encPath(String s) {
+ // safe enough for matrix path segments
+ return URLEncoder.encode(s, StandardCharsets.UTF_8);
+ }
+
+ private static String json(String s) {
+ return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r");
+ }
+}
diff --git a/matrix/src/main/java/com/alttd/matrix/interfaces/MatrixAppServiceServer.java b/matrix/src/main/java/com/alttd/matrix/interfaces/MatrixAppServiceServer.java
new file mode 100644
index 0000000..7048b5f
--- /dev/null
+++ b/matrix/src/main/java/com/alttd/matrix/interfaces/MatrixAppServiceServer.java
@@ -0,0 +1,11 @@
+package com.alttd.matrix.interfaces;
+
+import java.io.Closeable;
+import java.util.function.Consumer;
+
+public interface MatrixAppServiceServer extends Closeable {
+ void setTransactionListener(Consumer listener);
+
+ record TransactionEvent(String roomId, String senderMxid, String body, String eventId) {
+ }
+}
diff --git a/matrix/src/main/java/com/alttd/matrix/interfaces/MatrixBridge.java b/matrix/src/main/java/com/alttd/matrix/interfaces/MatrixBridge.java
new file mode 100644
index 0000000..b73484d
--- /dev/null
+++ b/matrix/src/main/java/com/alttd/matrix/interfaces/MatrixBridge.java
@@ -0,0 +1,21 @@
+package com.alttd.matrix.interfaces;
+
+import java.util.UUID;
+
+public interface MatrixBridge {
+ void setInboundHandler(InboundHandler handler);
+
+ void setRoomResolver(RoomResolver resolver);
+
+ void sendChat(UUID playerUuid, String mcUsername, String serverName, String message);
+
+ interface InboundHandler {
+ void onMatrixChat(String roomId, String senderMxid, String body);
+ }
+
+ interface RoomResolver {
+ String roomIdForServer(String serverName);
+
+ String serverForRoomId(String roomId);
+ }
+}
diff --git a/matrix/src/main/java/com/alttd/matrix/interfaces/MatrixClient.java b/matrix/src/main/java/com/alttd/matrix/interfaces/MatrixClient.java
new file mode 100644
index 0000000..ed75a05
--- /dev/null
+++ b/matrix/src/main/java/com/alttd/matrix/interfaces/MatrixClient.java
@@ -0,0 +1,11 @@
+package com.alttd.matrix.interfaces;
+
+public interface MatrixClient {
+ void ensureUser(String mxid);
+
+ void setDisplayName(String mxid, String displayName);
+
+ void ensureJoined(String mxid, String roomId);
+
+ void sendText(String mxid, String roomId, String body);
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 943b3c5..69c2326 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -3,22 +3,13 @@ rootProject.name = "Chat"
include(":api")
include(":galaxy")
include(":velocity")
+include(":matrix")
val nexusUser = providers.gradleProperty("alttdSnapshotUsername").get()
val nexusPass = providers.gradleProperty("alttdSnapshotPassword").get()
dependencyResolutionManagement {
repositories {
-// mavenLocal()
- mavenCentral()
- maven("https://repo.alttd.com/snapshots") // Altitude - Galaxy
- maven("https://oss.sonatype.org/content/groups/public/") // Adventure
- maven("https://oss.sonatype.org/content/repositories/snapshots/") // Minimessage
- maven("https://nexus.velocitypowered.com/repository/") // Velocity
- maven("https://nexus.velocitypowered.com/repository/maven-public/") // Velocity
- maven("https://repo.spongepowered.org/maven") // Configurate
- maven("https://repo.extendedclip.com/content/repositories/placeholderapi/") // Papi
- maven("https://jitpack.io")
maven {
name = "nexus"
url = uri("https://repo.alttd.com/repository/alttd-snapshot/")
@@ -27,6 +18,13 @@ dependencyResolutionManagement {
password = nexusPass
}
}
+// mavenLocal()
+ mavenCentral()
+ maven("https://oss.sonatype.org/content/groups/public/") // Adventure
+ maven("https://oss.sonatype.org/content/repositories/snapshots/") // Minimessage
+ maven("https://repo.spongepowered.org/maven") // Configurate
+ maven("https://repo.extendedclip.com/content/repositories/placeholderapi/") // Papi
+ maven("https://jitpack.io")
}
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
}
@@ -36,3 +34,5 @@ pluginManagement {
gradlePluginPortal()
}
}
+
+include("matrix")
diff --git a/velocity/src/main/java/com/alttd/velocitychat/VelocityChat.java b/velocity/src/main/java/com/alttd/velocitychat/VelocityChat.java
index 9833870..c75e5da 100755
--- a/velocity/src/main/java/com/alttd/velocitychat/VelocityChat.java
+++ b/velocity/src/main/java/com/alttd/velocitychat/VelocityChat.java
@@ -2,23 +2,33 @@ package com.alttd.velocitychat;
import com.alttd.chat.ChatAPI;
import com.alttd.chat.ChatImplementation;
+import com.alttd.chat.config.Config;
+import com.alttd.chat.database.DatabaseConnection;
import com.alttd.chat.managers.ChatUserManager;
import com.alttd.chat.managers.PartyManager;
import com.alttd.chat.objects.ChatUser;
import com.alttd.chat.objects.chat_log.ChatLogHandler;
+import com.alttd.chat.util.ALogger;
+import com.alttd.matrix.MatrixConfig;
+import com.alttd.matrix.MatrixRuntime;
+import com.alttd.matrix.bridge.DefaultMatrixBridge;
+import com.alttd.matrix.interfaces.MatrixAppServiceServer;
+import com.alttd.matrix.interfaces.MatrixBridge;
+import com.alttd.matrix.interfaces.MatrixClient;
import com.alttd.velocitychat.commands.*;
-import com.alttd.chat.config.Config;
-import com.alttd.chat.database.DatabaseConnection;
import com.alttd.velocitychat.handlers.ChatHandler;
import com.alttd.velocitychat.handlers.ServerHandler;
import com.alttd.velocitychat.listeners.ChatListener;
import com.alttd.velocitychat.listeners.LiteBansListener;
-import com.alttd.velocitychat.listeners.ProxyPlayerListener;
import com.alttd.velocitychat.listeners.PluginMessageListener;
-import com.alttd.chat.util.ALogger;
+import com.alttd.velocitychat.listeners.ProxyPlayerListener;
+import com.alttd.velocitychat.matrix.MatrixRoomMap;
+import com.alttd.velocitychat.matrix.VelocityMatrixInbound;
+import com.alttd.velocitychat.matrix.VelocityMatrixOutbound;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import com.google.inject.Inject;
+import com.velocitypowered.api.event.EventManager;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
import com.velocitypowered.api.plugin.Dependency;
@@ -36,14 +46,16 @@ import java.nio.file.Path;
description = "A chat plugin for Altitude Minecraft Server",
authors = {"destro174", "teri"},
dependencies = {@Dependency(id = "luckperms"), @Dependency(id = "litebans"), @Dependency(id = "proxydiscordlink")}
- )
+)
public class VelocityChat {
private static VelocityChat plugin;
private final ProxyServer server;
private final Logger logger;
private final Path dataDirectory;
+ private final EventManager eventManager;
+ private MatrixAppServiceServer matrixAsServer;
private ChatAPI chatAPI;
private ChatHandler chatHandler;
private ServerHandler serverHandler;
@@ -51,11 +63,12 @@ public class VelocityChat {
private ChannelIdentifier channelIdentifier;
@Inject
- public VelocityChat(ProxyServer proxyServer, Logger proxyLogger, @DataDirectory Path proxydataDirectory) {
+ public VelocityChat(ProxyServer proxyServer, Logger proxyLogger, @DataDirectory Path proxydataDirectory, EventManager eventManager) {
plugin = this;
server = proxyServer;
logger = proxyLogger;
dataDirectory = proxydataDirectory;
+ this.eventManager = eventManager;
}
@Subscribe
@@ -80,6 +93,7 @@ public class VelocityChat {
ChatUser console = new ChatUser(Config.CONSOLEUUID, -1, null);
console.setDisplayName(Config.CONSOLENAME);
ChatUserManager.addUser(console);
+ initMatrix();
}
public void reloadConfig() {
@@ -92,6 +106,42 @@ public class VelocityChat {
getProxy().getAllServers().stream().forEach(registeredServer -> registeredServer.sendPluginMessage(getChannelIdentifier(), buf.toByteArray()));
}
+ //TODO call initMatrix
+ private void initMatrix() {
+ MatrixConfig cfg = new MatrixConfig(
+ Config.MATRIX_SERVER,
+ Config.MATRIX_AS_TOKEN,
+ Config.MATRIX_HS_TOKEN,
+ "matrix.alttd.com",
+ "mc_",
+ "127.0.0.1",
+ 9000
+ );
+
+ MatrixAppServiceServer asServer;
+ try {
+ asServer = MatrixRuntime.startAppService(cfg);
+ } catch (Exception e) {
+ logger.error("Failed to start Matrix AppService server", e);
+ return;
+ }
+
+ MatrixClient client = MatrixRuntime.createClient(cfg);
+
+ initMatrix(cfg, asServer, client);
+ }
+
+ public void initMatrix(MatrixConfig matrixConfig, MatrixAppServiceServer asServer, MatrixClient client) {
+ MatrixRoomMap roomMap = new MatrixRoomMap();
+ roomMap.put("bayou", "!bayou:matrix.alttd.com");
+
+ MatrixBridge matrix = new DefaultMatrixBridge(matrixConfig, asServer, client);
+ matrix.setRoomResolver(roomMap);
+ matrix.setInboundHandler(new VelocityMatrixInbound(server, roomMap));
+
+ eventManager.register(this, new VelocityMatrixOutbound(matrix));
+ }
+
public File getDataDirectory() {
return dataDirectory.toFile();
}
diff --git a/velocity/src/main/java/com/alttd/velocitychat/matrix/MatrixRoomMap.java b/velocity/src/main/java/com/alttd/velocitychat/matrix/MatrixRoomMap.java
new file mode 100644
index 0000000..fe85914
--- /dev/null
+++ b/velocity/src/main/java/com/alttd/velocitychat/matrix/MatrixRoomMap.java
@@ -0,0 +1,26 @@
+package com.alttd.velocitychat.matrix;
+
+import com.alttd.matrix.interfaces.MatrixBridge;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+public final class MatrixRoomMap implements MatrixBridge.RoomResolver {
+ private final Map serverToRoom = new ConcurrentHashMap<>();
+ private final Map roomToServer = new ConcurrentHashMap<>();
+
+ public void put(String serverName, String roomId) {
+ serverToRoom.put(serverName, roomId);
+ roomToServer.put(roomId, serverName);
+ }
+
+ @Override
+ public String roomIdForServer(String serverName) {
+ return serverToRoom.get(serverName);
+ }
+
+ @Override
+ public String serverForRoomId(String roomId) {
+ return roomToServer.get(roomId);
+ }
+}
diff --git a/velocity/src/main/java/com/alttd/velocitychat/matrix/VelocityMatrixInbound.java b/velocity/src/main/java/com/alttd/velocitychat/matrix/VelocityMatrixInbound.java
new file mode 100644
index 0000000..8c2a177
--- /dev/null
+++ b/velocity/src/main/java/com/alttd/velocitychat/matrix/VelocityMatrixInbound.java
@@ -0,0 +1,38 @@
+package com.alttd.velocitychat.matrix;
+
+import com.alttd.matrix.interfaces.MatrixBridge;
+import com.velocitypowered.api.proxy.ProxyServer;
+import com.velocitypowered.api.proxy.server.RegisteredServer;
+import net.kyori.adventure.text.Component;
+
+import java.util.Optional;
+
+public final class VelocityMatrixInbound implements MatrixBridge.InboundHandler {
+ private final ProxyServer proxy;
+ private final MatrixRoomMap rooms;
+
+ public VelocityMatrixInbound(ProxyServer proxy, MatrixRoomMap rooms) {
+ this.proxy = proxy;
+ this.rooms = rooms;
+ }
+
+ @Override
+ public void onMatrixChat(String roomId, String senderMxid, String body) {
+ final String serverName = rooms.serverForRoomId(roomId);
+ if (serverName == null) {
+ return;
+ }
+
+ // route to backend server players connected to that server
+ Optional rs = proxy.getServer(serverName);
+ if (rs.isEmpty()) {
+ return;
+ }
+
+ // minimal formatting; adjust to your chat format
+ String sender = senderMxid != null ? senderMxid : "matrix";
+ Component msg = Component.text("[M] " + sender + ": " + body);
+
+ rs.get().getPlayersConnected().forEach(p -> p.sendMessage(msg));
+ }
+}
diff --git a/velocity/src/main/java/com/alttd/velocitychat/matrix/VelocityMatrixOutbound.java b/velocity/src/main/java/com/alttd/velocitychat/matrix/VelocityMatrixOutbound.java
new file mode 100644
index 0000000..08c9b84
--- /dev/null
+++ b/velocity/src/main/java/com/alttd/velocitychat/matrix/VelocityMatrixOutbound.java
@@ -0,0 +1,35 @@
+package com.alttd.velocitychat.matrix;
+
+import com.alttd.matrix.interfaces.MatrixBridge;
+import com.velocitypowered.api.event.Subscribe;
+import com.velocitypowered.api.event.player.PlayerChatEvent;
+import com.velocitypowered.api.proxy.Player;
+import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
+
+public final class VelocityMatrixOutbound {
+ private final MatrixBridge matrix;
+
+ public VelocityMatrixOutbound(MatrixBridge matrix) {
+ this.matrix = matrix;
+ }
+
+ @Subscribe
+ public void onChat(PlayerChatEvent e) {
+ if (!e.getResult().isAllowed()) {
+ return;
+ }
+
+ Player p = e.getPlayer();
+ String serverName = p.getCurrentServer().map(s -> s.getServerInfo().getName()).orElse(null);
+ if (serverName == null) {
+ return;
+ }
+
+ String msg = PlainTextComponentSerializer.plainText().serialize(e.getMessage());
+ if (msg == null || msg.isBlank()) {
+ return;
+ }
+
+ matrix.sendChat(p.getUniqueId(), p.getUsername(), serverName, msg);
+ }
+}