From 636ee3c57e456173cb522b83bf4dedd8fab58a5f Mon Sep 17 00:00:00 2001 From: akastijn Date: Sat, 17 Jan 2026 04:12:36 +0100 Subject: [PATCH] trying to set up a matrix bridge --- .gitignore | 2 + api/build.gradle.kts | 2 +- .../java/com/alttd/chat/config/Config.java | 50 +++--- galaxy/build.gradle.kts | 2 +- matrix/build.gradle.kts | 17 ++ .../java/com/alttd/matrix/MatrixConfig.java | 12 ++ .../java/com/alttd/matrix/MatrixRuntime.java | 29 ++++ .../MatrixAppServiceHttpServer.java | 153 +++++++++++++++++ .../matrix/bridge/DefaultMatrixBridge.java | 76 +++++++++ .../alttd/matrix/client/HttpMatrixClient.java | 155 ++++++++++++++++++ .../interfaces/MatrixAppServiceServer.java | 11 ++ .../alttd/matrix/interfaces/MatrixBridge.java | 21 +++ .../alttd/matrix/interfaces/MatrixClient.java | 11 ++ settings.gradle.kts | 20 +-- .../com/alttd/velocitychat/VelocityChat.java | 62 ++++++- .../velocitychat/matrix/MatrixRoomMap.java | 26 +++ .../matrix/VelocityMatrixInbound.java | 38 +++++ .../matrix/VelocityMatrixOutbound.java | 35 ++++ 18 files changed, 684 insertions(+), 38 deletions(-) create mode 100644 matrix/build.gradle.kts create mode 100644 matrix/src/main/java/com/alttd/matrix/MatrixConfig.java create mode 100644 matrix/src/main/java/com/alttd/matrix/MatrixRuntime.java create mode 100644 matrix/src/main/java/com/alttd/matrix/app_service/MatrixAppServiceHttpServer.java create mode 100644 matrix/src/main/java/com/alttd/matrix/bridge/DefaultMatrixBridge.java create mode 100644 matrix/src/main/java/com/alttd/matrix/client/HttpMatrixClient.java create mode 100644 matrix/src/main/java/com/alttd/matrix/interfaces/MatrixAppServiceServer.java create mode 100644 matrix/src/main/java/com/alttd/matrix/interfaces/MatrixBridge.java create mode 100644 matrix/src/main/java/com/alttd/matrix/interfaces/MatrixClient.java create mode 100644 velocity/src/main/java/com/alttd/velocitychat/matrix/MatrixRoomMap.java create mode 100644 velocity/src/main/java/com/alttd/velocitychat/matrix/VelocityMatrixInbound.java create mode 100644 velocity/src/main/java/com/alttd/velocitychat/matrix/VelocityMatrixOutbound.java 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); + } +}