trying to set up a matrix bridge

This commit is contained in:
akastijn 2026-01-17 04:12:36 +01:00
parent 7cc7cca129
commit 636ee3c57e
18 changed files with 684 additions and 38 deletions

2
.gitignore vendored
View File

@ -40,3 +40,5 @@ target/
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid hs_err_pid
!gradle/wrapper/gradle-wrapper.jar !gradle/wrapper/gradle-wrapper.jar
*.bat

View File

@ -4,7 +4,7 @@ plugins {
dependencies { dependencies {
// Cosmos // 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 isChanging = true
} }
compileOnly("org.spongepowered:configurate-yaml:4.2.0") // Configurate compileOnly("org.spongepowered:configurate-yaml:4.2.0") // Configurate

View File

@ -38,9 +38,9 @@ public final class Config {
CONFIGPATH = new File(File.separator + "mnt" + File.separator + "configs" + File.separator + "ChatPlugin"); CONFIGPATH = new File(File.separator + "mnt" + File.separator + "configs" + File.separator + "ChatPlugin");
CONFIG_FILE = new File(CONFIGPATH, "config.yml"); CONFIG_FILE = new File(CONFIGPATH, "config.yml");
configLoader = YamlConfigurationLoader.builder() configLoader = YamlConfigurationLoader.builder()
.file(CONFIG_FILE) .file(CONFIG_FILE)
.nodeStyle(NodeStyle.BLOCK) .nodeStyle(NodeStyle.BLOCK)
.build(); .build();
if (!CONFIG_FILE.getParentFile().exists()) { if (!CONFIG_FILE.getParentFile().exists()) {
if (!CONFIG_FILE.getParentFile().mkdirs()) { if (!CONFIG_FILE.getParentFile().mkdirs()) {
return; return;
@ -182,12 +182,12 @@ public final class Config {
private static void settings() { private static void settings() {
PREFIXGROUPS = getList("settings.prefix-groups", PREFIXGROUPS = getList("settings.prefix-groups",
Lists.newArrayList("discord", "socialmedia", "eventteam", "eventleader", "youtube", "twitch", Lists.newArrayList("discord", "socialmedia", "eventteam", "eventleader", "youtube", "twitch",
"developer")); "developer"));
CONFLICTINGPREFIXGROUPS = getList("settings.prefix-conflicts-groups", CONFLICTINGPREFIXGROUPS = getList("settings.prefix-conflicts-groups",
Lists.newArrayList("eventteam", "eventleader")); Lists.newArrayList("eventteam", "eventleader"));
STAFFGROUPS = getList("settings.staff-groups", 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); CONSOLENAME = getString("settings.console-name", CONSOLENAME);
CONSOLEUUID = UUID.fromString(getString("settings.console-uuid", CONSOLEUUID.toString())); CONSOLEUUID = UUID.fromString(getString("settings.console-uuid", CONSOLEUUID.toString()));
MINIMIUMSTAFFRANK = getString("settings.minimum-staff-rank", MINIMIUMSTAFFRANK); MINIMIUMSTAFFRANK = getString("settings.minimum-staff-rank", MINIMIUMSTAFFRANK);
@ -282,7 +282,7 @@ public final class Config {
public static String NO_PERMISSION = "<red>You don't have permission to use this command.</red>"; public static String NO_PERMISSION = "<red>You don't have permission to use this command.</red>";
public static String NO_CONSOLE = "<red>This command can not be used by console</red>"; public static String NO_CONSOLE = "<red>This command can not be used by console</red>";
public static String CREATED_PARTY = "<green>You created a chat party called: " + public static String CREATED_PARTY = "<green>You created a chat party called: " +
"'<gold><party_name></gold>' with the password: '<gold><party_password></gold>'</green>"; "'<gold><party_name></gold>' with the password: '<gold><party_password></gold>'</green>";
public static String NOT_IN_A_PARTY = "<red>You're not in a chat party.</red>"; public static String NOT_IN_A_PARTY = "<red>You're not in a chat party.</red>";
public static String NOT_YOUR_PARTY = "<red>You don't own this chat party.</red>"; public static String NOT_YOUR_PARTY = "<red>You don't own this chat party.</red>";
public static String NOT_A_PARTY = "<red>This chat party does not exist.</red>"; public static String NOT_A_PARTY = "<red>This chat party does not exist.</red>";
@ -307,14 +307,14 @@ public final class Config {
public static String ALREADY_IN_THIS_PARTY = "<red>You're already in <party>!</red>"; public static String ALREADY_IN_THIS_PARTY = "<red>You're already in <party>!</red>";
public static String SENT_PARTY_INV = "<green>You send a chat party invite to <player>!</green>"; public static String SENT_PARTY_INV = "<green>You send a chat party invite to <player>!</green>";
public static String JOIN_PARTY_CLICK_MESSAGE = "<click:run_command:'/party join <party> <party_password>'>" + public static String JOIN_PARTY_CLICK_MESSAGE = "<click:run_command:'/party join <party> <party_password>'>" +
"<dark_aqua>You received an invite to join <party>, click this message to accept.</dark_aqua></click>"; "<dark_aqua>You received an invite to join <party>, click this message to accept.</dark_aqua></click>";
public static String PARTY_MEMBER_LOGGED_ON = "<dark_aqua>[ChatParty] <player> joined Altitude...</dark_aqua>"; public static String PARTY_MEMBER_LOGGED_ON = "<dark_aqua>[ChatParty] <player> joined Altitude...</dark_aqua>";
public static String PARTY_MEMBER_LOGGED_OFF = "<dark_aqua>[ChatParty] <player> left Altitude...</dark_aqua>"; public static String PARTY_MEMBER_LOGGED_OFF = "<dark_aqua>[ChatParty] <player> left Altitude...</dark_aqua>";
public static String RENAMED_PARTY = public static String RENAMED_PARTY =
"<dark_aqua>[ChatParty] <owner> changed the party name from <old_name> to <new_name>!</dark_aqua>"; "<dark_aqua>[ChatParty] <owner> changed the party name from <old_name> to <new_name>!</dark_aqua>";
public static String CHANGED_PASSWORD = "<green>Password was set to <password></green>"; public static String CHANGED_PASSWORD = "<green>Password was set to <password></green>";
public static String DISBAND_PARTY_CONFIRM = "<green><bold>Are you sure you want to disband your party?</bold> " + public static String DISBAND_PARTY_CONFIRM = "<green><bold>Are you sure you want to disband your party?</bold> " +
"Type <gold>/party disband confirm <party></gold> to confirm."; "Type <gold>/party disband confirm <party></gold> to confirm.";
public static String DISBANDED_PARTY = public static String DISBANDED_PARTY =
"<dark_aqua>[ChatParty] <owner> has disbanded <party>, everyone has been removed.</dark_aqua>"; "<dark_aqua>[ChatParty] <owner> has disbanded <party>, everyone has been removed.</dark_aqua>";
public static String PARTY_INFO = """ public static String PARTY_INFO = """
@ -413,7 +413,7 @@ public final class Config {
ConfigurationNode node = getNode("chat-channels"); ConfigurationNode node = getNode("chat-channels");
if (node.empty()) { if (node.empty()) {
getString("chat-channels.ac.format", getString("chat-channels.ac.format",
"<white><gray><sender></gray> <hover:show_text:on <server>><yellow>to <channel></yellow></hover><gray>: <message>"); "<white><gray><sender></gray> <hover:show_text:on <server>><yellow>to <channel></yellow></hover><gray>: <message>");
getList("chat-channels.ac.servers", List.of("lobby")); getList("chat-channels.ac.servers", List.of("lobby"));
getBoolean("chat-channels.ac.proxy", false); getBoolean("chat-channels.ac.proxy", false);
node = getNode("chat-channels"); node = getNode("chat-channels");
@ -423,11 +423,11 @@ public final class Config {
String channelName = Objects.requireNonNull(configurationNode.key()).toString(); String channelName = Objects.requireNonNull(configurationNode.key()).toString();
String key = "chat-channels." + channelName + "."; String key = "chat-channels." + channelName + ".";
new CustomChannel(channelName, new CustomChannel(channelName,
getString(key + "format", ""), getString(key + "format", ""),
getList(key + "servers", Collections.EMPTY_LIST), getList(key + "servers", Collections.EMPTY_LIST),
getList(key + "alias", Collections.EMPTY_LIST), getList(key + "alias", Collections.EMPTY_LIST),
getBoolean(key + "proxy", false), getBoolean(key + "proxy", false),
getBoolean(key + "local", 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_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_WAIT_TIME = getLong("nicknames.wait-time", NICK_WAIT_TIME);
NICK_ITEM_LORE = getList("nicknames.item-lore", NICK_ITEM_LORE = getList("nicknames.item-lore",
List.of("<aqua>New nick: <newnick>", "<aqua>Old nick: <oldnick>", "<aqua>Last changed: <lastchanged>", List.of("<aqua>New nick: <newnick>", "<aqua>Old nick: <oldnick>", "<aqua>Last changed: <lastchanged>",
"<green>Left click to Accept <light_purple>| <red>Right click to Deny")); "<green>Left click to Accept <light_purple>| <red>Right click to Deny"));
NICK_BLOCKED_COLOR_CODESLIST = getList("nicknames.blocked-color-codes", List.of("&k", "&l", "&n", "&m", "&o")); 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", 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", List.of("&0", "&1", "&2", "&3", "&4", "&5", "&6", "&7", "&8", "&9", "&a", "&b", "&c", "&d", "&e", "&f",
"&r")); "&r"));
NICK_CURRENT = getString("nicknames.messages.nick-current", NICK_CURRENT); 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_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); 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 = "<token>";
public static String MATRIX_HS_TOKEN = "<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);
}
} }

View File

@ -5,7 +5,7 @@ plugins {
dependencies { dependencies {
implementation(project(":api")) // API 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 isChanging = true
} }
compileOnly("com.gitlab.ruany:LiteBansAPI:0.6.1") // move to proxy compileOnly("com.gitlab.ruany:LiteBansAPI:0.6.1") // move to proxy

17
matrix/build.gradle.kts Normal file
View File

@ -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<JavaCompile>().configureEach {
options.encoding = "UTF-8"
options.release.set(21)
}

View File

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

View File

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

View File

@ -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<TransactionEvent> 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<TransactionEvent> 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<TransactionEvent> listener = txListener;
if (listener != null) {
JsonNode events = root.path("events");
if (events.isArray()) {
for (Iterator<JsonNode> 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<String, String> params = parseQuery(q);
String token = params.get("access_token");
return token != null && token.equals(hsToken);
}
private static Map<String, String> parseQuery(String raw) {
var map = new java.util.HashMap<String, String>();
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();
}
}

View File

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

View File

@ -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<String> 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<String> 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");
}
}

View File

@ -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<TransactionEvent> listener);
record TransactionEvent(String roomId, String senderMxid, String body, String eventId) {
}
}

View File

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

View File

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

View File

@ -3,22 +3,13 @@ rootProject.name = "Chat"
include(":api") include(":api")
include(":galaxy") include(":galaxy")
include(":velocity") include(":velocity")
include(":matrix")
val nexusUser = providers.gradleProperty("alttdSnapshotUsername").get() val nexusUser = providers.gradleProperty("alttdSnapshotUsername").get()
val nexusPass = providers.gradleProperty("alttdSnapshotPassword").get() val nexusPass = providers.gradleProperty("alttdSnapshotPassword").get()
dependencyResolutionManagement { dependencyResolutionManagement {
repositories { 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 { maven {
name = "nexus" name = "nexus"
url = uri("https://repo.alttd.com/repository/alttd-snapshot/") url = uri("https://repo.alttd.com/repository/alttd-snapshot/")
@ -27,6 +18,13 @@ dependencyResolutionManagement {
password = nexusPass 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) repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
} }
@ -36,3 +34,5 @@ pluginManagement {
gradlePluginPortal() gradlePluginPortal()
} }
} }
include("matrix")

View File

@ -2,23 +2,33 @@ package com.alttd.velocitychat;
import com.alttd.chat.ChatAPI; import com.alttd.chat.ChatAPI;
import com.alttd.chat.ChatImplementation; 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.ChatUserManager;
import com.alttd.chat.managers.PartyManager; import com.alttd.chat.managers.PartyManager;
import com.alttd.chat.objects.ChatUser; import com.alttd.chat.objects.ChatUser;
import com.alttd.chat.objects.chat_log.ChatLogHandler; 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.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.ChatHandler;
import com.alttd.velocitychat.handlers.ServerHandler; import com.alttd.velocitychat.handlers.ServerHandler;
import com.alttd.velocitychat.listeners.ChatListener; import com.alttd.velocitychat.listeners.ChatListener;
import com.alttd.velocitychat.listeners.LiteBansListener; import com.alttd.velocitychat.listeners.LiteBansListener;
import com.alttd.velocitychat.listeners.ProxyPlayerListener;
import com.alttd.velocitychat.listeners.PluginMessageListener; 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.ByteArrayDataOutput;
import com.google.common.io.ByteStreams; import com.google.common.io.ByteStreams;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.velocitypowered.api.event.EventManager;
import com.velocitypowered.api.event.Subscribe; import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
import com.velocitypowered.api.plugin.Dependency; import com.velocitypowered.api.plugin.Dependency;
@ -36,14 +46,16 @@ import java.nio.file.Path;
description = "A chat plugin for Altitude Minecraft Server", description = "A chat plugin for Altitude Minecraft Server",
authors = {"destro174", "teri"}, authors = {"destro174", "teri"},
dependencies = {@Dependency(id = "luckperms"), @Dependency(id = "litebans"), @Dependency(id = "proxydiscordlink")} dependencies = {@Dependency(id = "luckperms"), @Dependency(id = "litebans"), @Dependency(id = "proxydiscordlink")}
) )
public class VelocityChat { public class VelocityChat {
private static VelocityChat plugin; private static VelocityChat plugin;
private final ProxyServer server; private final ProxyServer server;
private final Logger logger; private final Logger logger;
private final Path dataDirectory; private final Path dataDirectory;
private final EventManager eventManager;
private MatrixAppServiceServer matrixAsServer;
private ChatAPI chatAPI; private ChatAPI chatAPI;
private ChatHandler chatHandler; private ChatHandler chatHandler;
private ServerHandler serverHandler; private ServerHandler serverHandler;
@ -51,11 +63,12 @@ public class VelocityChat {
private ChannelIdentifier channelIdentifier; private ChannelIdentifier channelIdentifier;
@Inject @Inject
public VelocityChat(ProxyServer proxyServer, Logger proxyLogger, @DataDirectory Path proxydataDirectory) { public VelocityChat(ProxyServer proxyServer, Logger proxyLogger, @DataDirectory Path proxydataDirectory, EventManager eventManager) {
plugin = this; plugin = this;
server = proxyServer; server = proxyServer;
logger = proxyLogger; logger = proxyLogger;
dataDirectory = proxydataDirectory; dataDirectory = proxydataDirectory;
this.eventManager = eventManager;
} }
@Subscribe @Subscribe
@ -80,6 +93,7 @@ public class VelocityChat {
ChatUser console = new ChatUser(Config.CONSOLEUUID, -1, null); ChatUser console = new ChatUser(Config.CONSOLEUUID, -1, null);
console.setDisplayName(Config.CONSOLENAME); console.setDisplayName(Config.CONSOLENAME);
ChatUserManager.addUser(console); ChatUserManager.addUser(console);
initMatrix();
} }
public void reloadConfig() { public void reloadConfig() {
@ -92,6 +106,42 @@ public class VelocityChat {
getProxy().getAllServers().stream().forEach(registeredServer -> registeredServer.sendPluginMessage(getChannelIdentifier(), buf.toByteArray())); 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() { public File getDataDirectory() {
return dataDirectory.toFile(); return dataDirectory.toFile();
} }

View File

@ -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<String, String> serverToRoom = new ConcurrentHashMap<>();
private final Map<String, String> 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);
}
}

View File

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

View File

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