From 730325c51024a30de2f255032c82d6ec6d74da2d Mon Sep 17 00:00:00 2001 From: Tim203 Date: Tue, 7 Oct 2025 00:07:56 +0200 Subject: [PATCH 1/3] Prepare for the new Global API, which will use PostgreSQL --- build.gradle.kts | 6 +- gradle/libs.versions.toml | 12 ++- gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 3 +- settings.gradle.kts | 1 + .../globallinkserver/GlobalLinkServer.java | 4 +- .../handler/CommandHandler.java | 3 +- .../geysermc/globallinkserver/link/Link.java | 12 +-- .../globallinkserver/link/LinkManager.java | 50 +++++++--- .../globallinkserver/link/LinkRequest.java | 65 ++++++++++++- .../manager/DatabaseManager.java | 65 ++++++------- .../manager/PlayerManager.java | 51 +++++++++- .../service/LinkLookupService.java | 92 ++++++++++--------- src/main/resources/config.yml | 6 +- 14 files changed, 250 insertions(+), 122 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index e2a33ae..20cdadf 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,10 +8,12 @@ plugins { group = "org.geysermc.globallinkserver" dependencies { - paperweight.paperDevBundle("1.21.4-R0.1-SNAPSHOT") + paperweight.paperDevBundle("1.21.8-R0.1-SNAPSHOT") + compileOnly(libs.geyser.core) compileOnly(libs.floodgate.api) - implementation(libs.mariadb.client) + implementation(libs.hikaricp) + implementation(libs.postgresql) implementation(libs.bundles.fastutil) compileOnly(libs.checker.qual) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 56cb898..6166298 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,16 +1,20 @@ [versions] +geyser = "2.8.4-SNAPSHOT" floodgate = "2.2.3-SNAPSHOT" -mariadb-client = "2.7.3" +hikari = "7.0.2" +postgresql = "42.7.8" fastutil = "8.5.2" checker-qual = "3.21.1" indra = "3.1.2" -paperweight = "2.0.0-beta.12" -runpaper = "2.3.1" +paperweight = "2.0.0-beta.19" +runpaper = "3.0.1" [libraries] +geyser-core = { module = "org.geysermc.geyser:core", version.ref = "geyser" } floodgate-api = { group = "org.geysermc.floodgate", name = "api", version.ref = "floodgate" } -mariadb-client = { module = "org.mariadb.jdbc:mariadb-java-client", version.ref = "mariadb-client" } +hikaricp = { module = "com.zaxxer:HikariCP", version.ref = "hikari" } +postgresql = { module = "org.postgresql:postgresql", version.ref = "postgresql" } fastutil-int-int-maps = { group = "com.nukkitx.fastutil", name = "fastutil-int-int-maps", version.ref = "fastutil" } fastutil-int-object-maps = { group = "com.nukkitx.fastutil", name = "fastutil-int-object-maps", version.ref = "fastutil" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index cea7a79..2e11132 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index f5feea6..f3b75f3 100755 --- a/gradlew +++ b/gradlew @@ -86,8 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s -' "$PWD" ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/settings.gradle.kts b/settings.gradle.kts index 3f8d8b3..2145181 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -4,6 +4,7 @@ pluginManagement { repositories { gradlePluginPortal() maven("https://repo.papermc.io/repository/maven-public/") + mavenCentral() } } diff --git a/src/main/java/org/geysermc/globallinkserver/GlobalLinkServer.java b/src/main/java/org/geysermc/globallinkserver/GlobalLinkServer.java index 6cb017a..a27ee65 100644 --- a/src/main/java/org/geysermc/globallinkserver/GlobalLinkServer.java +++ b/src/main/java/org/geysermc/globallinkserver/GlobalLinkServer.java @@ -22,7 +22,6 @@ import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; -import org.bukkit.event.block.Action; import org.bukkit.event.entity.FoodLevelChangeEvent; import org.bukkit.event.entity.PlayerDeathEvent; import org.bukkit.event.player.PlayerCommandPreprocessEvent; @@ -33,6 +32,7 @@ import org.bukkit.plugin.java.JavaPlugin; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.floodgate.api.FloodgateApi; +import org.geysermc.geyser.GeyserImpl; import org.geysermc.globallinkserver.config.ConfigReader; import org.geysermc.globallinkserver.handler.CommandHandler; import org.geysermc.globallinkserver.handler.JoinHandler; @@ -68,7 +68,7 @@ public class GlobalLinkServer extends JavaPlugin implements Listener { public void onEnable() { var config = ConfigReader.readConfig(this); - var playerManager = new PlayerManager(FloodgateApi.getInstance()); + var playerManager = new PlayerManager(FloodgateApi.getInstance(), GeyserImpl.getInstance()); var databaseManager = new DatabaseManager(config); var linkManager = new LinkManager(playerManager, databaseManager); linkLookupService = new LinkLookupService(playerManager, databaseManager); diff --git a/src/main/java/org/geysermc/globallinkserver/handler/CommandHandler.java b/src/main/java/org/geysermc/globallinkserver/handler/CommandHandler.java index 3bf9317..cf13f63 100644 --- a/src/main/java/org/geysermc/globallinkserver/handler/CommandHandler.java +++ b/src/main/java/org/geysermc/globallinkserver/handler/CommandHandler.java @@ -83,7 +83,8 @@ public int linkWithCode(CommandContext ctx) { return Command.SINGLE_SUCCESS; } - var completedLink = Link.fromRequest(linkRequest, player.getUniqueId(), player.getName(), isRequesterBedrock); + String correctUsername = playerManager.correctUsername(player); + Link completedLink = Link.fromRequest(linkRequest, player.getUniqueId(), correctUsername, playerManager.nameTimestampMillis(player), isRequesterBedrock); linkManager.finaliseLink(completedLink).whenComplete((result, error) -> { if (error != null || !result) { diff --git a/src/main/java/org/geysermc/globallinkserver/link/Link.java b/src/main/java/org/geysermc/globallinkserver/link/Link.java index d1ca5bd..2a8ec47 100644 --- a/src/main/java/org/geysermc/globallinkserver/link/Link.java +++ b/src/main/java/org/geysermc/globallinkserver/link/Link.java @@ -9,15 +9,15 @@ import org.jspecify.annotations.NullMarked; @NullMarked -public record Link(UUID javaId, String javaUsername, long bedrockId) { - public Link(UUID javaId, String javaUsername, UUID bedrockId) { - this(javaId, javaUsername, bedrockId.getLeastSignificantBits()); +public record Link(UUID javaId, String javaUsername, long javaNameTimestamp, long bedrockId, String bedrockName, long bedrockNameTimestamp) { + public Link(UUID javaId, String javaUsername, long javaNameTimestamp, UUID bedrockId, String bedrockName, long bedrockNameTimestamp) { + this(javaId, javaUsername, javaNameTimestamp, bedrockId.getLeastSignificantBits(), bedrockName, bedrockNameTimestamp); } - public static Link fromRequest(LinkRequest left, UUID rightId, String rightName, boolean isLeftBedrock) { + public static Link fromRequest(LinkRequest left, UUID rightUuid, String rightUsername, long rightNameTimestamp, boolean isLeftBedrock) { if (isLeftBedrock) { - return new Link(rightId, rightName, left.requesterUuid()); + return new Link(rightUuid, rightUsername, rightNameTimestamp, left.requesterUuid(), left.requesterUsername(), left.nameTimestamp()); } - return new Link(left.requesterUuid(), left.requesterUsername(), rightId); + return new Link(left.requesterUuid(), left.requesterUsername(), left.nameTimestamp(), rightUuid, rightUsername, rightNameTimestamp); } } diff --git a/src/main/java/org/geysermc/globallinkserver/link/LinkManager.java b/src/main/java/org/geysermc/globallinkserver/link/LinkManager.java index 42fb70a..7afd20f 100644 --- a/src/main/java/org/geysermc/globallinkserver/link/LinkManager.java +++ b/src/main/java/org/geysermc/globallinkserver/link/LinkManager.java @@ -12,6 +12,8 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; +import java.sql.Timestamp; +import java.time.Instant; import java.util.Iterator; import java.util.Random; import java.util.UUID; @@ -46,7 +48,9 @@ public LinkManager(PlayerManager playerManager, DatabaseManager database) { } public int createTempLink(Player player) { - var linkRequest = new LinkRequest(createCode(), PENDING_LINK_TTL_MILLIS, player); + long nameTimestamp = playerManager.nameTimestampMillis(player); + String correctUsername = playerManager.correctUsername(player); + var linkRequest = new LinkRequest(createCode(), PENDING_LINK_TTL_MILLIS, player.getUniqueId(), correctUsername, nameTimestamp); linkRequests.put(linkRequest.code(), linkRequest); linkRequestForPlayer.put(player.getUniqueId(), linkRequest.code()); @@ -98,15 +102,39 @@ public CompletableFuture finaliseLink(Link linkRequest) { return CompletableFuture.supplyAsync( () -> { try (Connection connection = database.connection()) { - try (PreparedStatement query = connection.prepareStatement( - "INSERT INTO `links` (`java_id`, `bedrock_id`, `java_name`) VALUES (?, ?, ?) " - + "ON DUPLICATE KEY UPDATE " - + "`java_id` = VALUES(`java_id`)," - + "`bedrock_id` = VALUES(`bedrock_id`)," - + "`java_name` = VALUES(`java_name`);")) { + try (PreparedStatement query = connection.prepareStatement(""" + INSERT INTO java_identity_current AS c(id, username, detected_at) + VALUES (?::uuid, ?, ?) + ON CONFLICT (id) DO + UPDATE SET username = EXCLUDED.username, detected_at = EXCLUDED.detected_at + WHERE c.detected_at <= EXCLUDED.detected_at AND c.username != EXCLUDED.username + """)) { query.setString(1, linkRequest.javaId().toString()); - query.setLong(2, linkRequest.bedrockId()); - query.setString(3, linkRequest.javaUsername()); + query.setString(2, linkRequest.javaUsername()); + query.setTimestamp(3, Timestamp.from(Instant.EPOCH.plusMillis(linkRequest.javaNameTimestamp()))); + query.executeUpdate(); + } + + try (PreparedStatement query = connection.prepareStatement(""" + INSERT INTO xbox_identity_current AS c(xuid, gamertag, detected_at) + VALUES (?::xuid, ?, ?) + ON CONFLICT (xuid) DO + UPDATE SET gamertag = EXCLUDED.gamertag, detected_at = EXCLUDED.detected_at + WHERE c.detected_at <= EXCLUDED.detected_at AND c.gamertag != EXCLUDED.gamertag + """)) { + query.setLong(1, linkRequest.bedrockId()); + query.setString(2, linkRequest.bedrockName()); + query.setTimestamp(3, Timestamp.from(Instant.EPOCH.plusMillis(linkRequest.bedrockNameTimestamp()))); + query.executeUpdate(); + } + + try (PreparedStatement query = connection.prepareStatement(""" + INSERT INTO links (xuid, java_id) + VALUES (?::xuid, ?::uuid) + ON CONFLICT (xuid) DO + UPDATE SET java_id = EXCLUDED.java_id, inserted_at = EXCLUDED.inserted_at""")) { + query.setLong(1, linkRequest.bedrockId()); + query.setString(2, linkRequest.javaId().toString()); return query.executeUpdate() != 0; } } catch (SQLException exception) { @@ -122,10 +150,10 @@ public CompletableFuture unlinkAccount(Player player) { try (Connection connection = database.connection()) { PreparedStatement query; if (playerManager.isBedrockPlayer(player)) { - query = connection.prepareStatement("DELETE FROM `links` WHERE `bedrock_id` = ?;"); + query = connection.prepareStatement("DELETE FROM links WHERE xuid = ?::xuid"); query.setLong(1, player.getUniqueId().getLeastSignificantBits()); } else { - query = connection.prepareStatement("DELETE FROM `links` WHERE `java_id` = ?;"); + query = connection.prepareStatement("DELETE FROM links WHERE java_id = ?::uuid"); query.setString(1, player.getUniqueId().toString()); } boolean affected = query.executeUpdate() != 0; diff --git a/src/main/java/org/geysermc/globallinkserver/link/LinkRequest.java b/src/main/java/org/geysermc/globallinkserver/link/LinkRequest.java index ea31be5..036d2e5 100644 --- a/src/main/java/org/geysermc/globallinkserver/link/LinkRequest.java +++ b/src/main/java/org/geysermc/globallinkserver/link/LinkRequest.java @@ -5,6 +5,7 @@ */ package org.geysermc.globallinkserver.link; +import java.util.Objects; import java.util.UUID; import org.bukkit.Bukkit; import org.bukkit.entity.Player; @@ -12,12 +13,70 @@ import org.jspecify.annotations.Nullable; @NullMarked -public record LinkRequest(int code, long expiryTime, UUID requesterUuid, String requesterUsername) { - public LinkRequest(int code, long ttl, Player requester) { - this(code, System.currentTimeMillis() + ttl, requester.getUniqueId(), requester.getName()); +public final class LinkRequest { + private final int code; + private final long expiryTime; + private final UUID requesterUuid; + private final String requesterUsername; + private final long nameTimestamp; + + public LinkRequest(int code, long ttl, UUID requesterUuid, String requesterUsername, long nameTimestamp) { + this.code = code; + this.expiryTime = System.nanoTime() + ttl; + this.requesterUuid = requesterUuid; + this.requesterUsername = requesterUsername; + this.nameTimestamp = nameTimestamp; } public @Nullable Player requester() { return Bukkit.getPlayer(requesterUuid); } + + public int code() { + return code; + } + + public long expiryTime() { + return expiryTime; + } + + public UUID requesterUuid() { + return requesterUuid; + } + + public String requesterUsername() { + return requesterUsername; + } + + public long nameTimestamp() { + return nameTimestamp; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (LinkRequest) obj; + return this.code == that.code && + this.expiryTime == that.expiryTime && + Objects.equals(this.requesterUuid, that.requesterUuid) && + Objects.equals(this.requesterUsername, that.requesterUsername) && + this.nameTimestamp == that.nameTimestamp; + } + + @Override + public int hashCode() { + return Objects.hash(code, expiryTime, requesterUuid, requesterUsername, nameTimestamp); + } + + @Override + public String toString() { + return "LinkRequest[" + + "code=" + code + ", " + + "expiryTime=" + expiryTime + ", " + + "requesterUuid=" + requesterUuid + ", " + + "requesterUsername=" + requesterUsername + ", " + + "nameTimestamp=" + nameTimestamp + ']'; + } + } diff --git a/src/main/java/org/geysermc/globallinkserver/manager/DatabaseManager.java b/src/main/java/org/geysermc/globallinkserver/manager/DatabaseManager.java index 70720f2..35504c5 100644 --- a/src/main/java/org/geysermc/globallinkserver/manager/DatabaseManager.java +++ b/src/main/java/org/geysermc/globallinkserver/manager/DatabaseManager.java @@ -5,61 +5,50 @@ */ package org.geysermc.globallinkserver.manager; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; import java.sql.Connection; import java.sql.SQLException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import javax.sql.DataSource; import org.geysermc.globallinkserver.config.Config; import org.jspecify.annotations.NullMarked; -import org.mariadb.jdbc.MariaDbPoolDataSource; +import org.jspecify.annotations.Nullable; @NullMarked public final class DatabaseManager { - private final MariaDbPoolDataSource dataSource; + private final DataSource dataSource; private final ExecutorService executorService = Executors.newCachedThreadPool(); public DatabaseManager(Config config) { - try { - Class.forName("org.mariadb.jdbc.Driver"); - - var hostname = config.database().hostname(); - String serverName; - int port = 3306; - - var hostnameSplit = hostname.split(":"); - if (hostnameSplit.length > 1) { - serverName = hostnameSplit[0]; - port = Integer.parseInt(hostnameSplit[1]); - } else { - serverName = hostname; - } - - dataSource = new MariaDbPoolDataSource(); - dataSource.setServerName(serverName); - dataSource.setPort(port); - dataSource.setDatabaseName(config.database().database()); - dataSource.setUser(config.database().username()); - dataSource.setPassword(config.database().password()); - dataSource.setMinPoolSize(1); - dataSource.setMaxPoolSize(config.database().maxPoolSize()); - - } catch (ClassNotFoundException exception) { - throw new RuntimeException("Cannot find required class to load the MariaDB database"); - } catch (SQLException exception) { - throw new RuntimeException("Unable to set the datasource connection fields", exception); + var hostname = config.database().hostname(); + String serverName; + Integer port = null; + + var hostnameSplit = hostname.split(":"); + if (hostnameSplit.length > 1) { + serverName = hostnameSplit[0]; + port = Integer.parseInt(hostnameSplit[1]); + } else { + serverName = hostname; } - connectionCheck(); + dataSource = new HikariDataSource(hikariConfigFor(config, serverName, port)); } - private void connectionCheck() { - try (var connection = connection(); var statement = connection.createStatement()) { - try (var resultSet = statement.executeQuery("SELECT 1")) { - resultSet.next(); - } - } catch (SQLException exception) { - throw new RuntimeException("Could not connect to database!", exception); + private static HikariConfig hikariConfigFor(Config config, String serverName, @Nullable Integer port) { + HikariConfig hikari = new HikariConfig(); + hikari.setDataSourceClassName("org.postgresql.ds.PGSimpleDataSource"); + hikari.addDataSourceProperty("serverName", serverName); + if (port != null) { + hikari.addDataSourceProperty("portNumber", port); } + hikari.addDataSourceProperty("user", config.database().username()); + hikari.addDataSourceProperty("password", config.database().password()); + hikari.addDataSourceProperty("databaseName", config.database().database()); + hikari.setMaximumPoolSize(config.database().maxPoolSize()); + return hikari; } public Connection connection() throws SQLException { diff --git a/src/main/java/org/geysermc/globallinkserver/manager/PlayerManager.java b/src/main/java/org/geysermc/globallinkserver/manager/PlayerManager.java index 6ab40f4..d8abeb0 100644 --- a/src/main/java/org/geysermc/globallinkserver/manager/PlayerManager.java +++ b/src/main/java/org/geysermc/globallinkserver/manager/PlayerManager.java @@ -6,19 +6,22 @@ package org.geysermc.globallinkserver.manager; import java.util.UUID; -import java.util.concurrent.CompletableFuture; import org.bukkit.entity.Player; import org.geysermc.floodgate.api.FloodgateApi; import org.geysermc.floodgate.api.player.FloodgatePlayer; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.session.GeyserSession; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @NullMarked public final class PlayerManager { private final FloodgateApi api; + private final GeyserImpl geyserImpl; - public PlayerManager(FloodgateApi api) { + public PlayerManager(FloodgateApi api, GeyserImpl geyserImpl) { this.api = api; + this.geyserImpl = geyserImpl; } public boolean isBedrockPlayer(Player player) { @@ -33,7 +36,47 @@ public boolean isBedrockId(UUID id) { return api.getPlayer(uuid); } - public CompletableFuture<@Nullable String> fetchGamertagFor(long xuid) { - return api.getGamertagFor(xuid); + private @Nullable GeyserSession bedrockSession(Player player) { + // There is no linking in the linking server itself, only link management + if (player.getUniqueId().getMostSignificantBits() != 0L) { + return null; + } + + String xuid = String.valueOf(player.getUniqueId().getLeastSignificantBits()); + for (GeyserSession session : geyserImpl.getSessionManager().getAllSessions()) { + if (session.xuid().equals(xuid)) { + return session; + } + } + return null; + } + + public String correctUsername(Player player) { + FloodgatePlayer floodgatePlayer = bedrockPlayer(player.getUniqueId()); + if (floodgatePlayer == null) { + return player.getName(); + } + return floodgatePlayer.getUsername(); + } + + /** + * Returns the earliest known time that the profile has this name. + * Works for both Java and Bedrock accounts. + */ + public long nameTimestampMillis(Player player) { + GeyserSession session = bedrockSession(player); + if (session == null) { + long timestamp = player.getPlayerProfile().getTextures().getTimestamp(); + if (timestamp == 0L) { + return System.currentTimeMillis(); + } + return timestamp; + } + + long issuedAt = session.getAuthData().issuedAt(); + if (issuedAt == -1) { + return System.currentTimeMillis(); + } + return issuedAt * 1000L; } } diff --git a/src/main/java/org/geysermc/globallinkserver/service/LinkLookupService.java b/src/main/java/org/geysermc/globallinkserver/service/LinkLookupService.java index f4a65af..09e68b1 100644 --- a/src/main/java/org/geysermc/globallinkserver/service/LinkLookupService.java +++ b/src/main/java/org/geysermc/globallinkserver/service/LinkLookupService.java @@ -20,8 +20,6 @@ import org.geysermc.globallinkserver.link.FullLink; import org.geysermc.globallinkserver.manager.DatabaseManager; import org.geysermc.globallinkserver.manager.PlayerManager; -import org.geysermc.globallinkserver.util.ThrowingConsumer; -import org.geysermc.globallinkserver.util.ThrowingFunction; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -77,54 +75,58 @@ public void invalidate(Player player) { } public CompletableFuture<@Nullable FullLink> findJavaLink(UUID javaId, String javaName) { - return attemptFindLink( - "SELECT `bedrock_id` FROM `links` WHERE `java_id` = ?", - stmt -> stmt.setString(1, javaId.toString()), - resultSet -> resultSet.getLong("bedrock_id")) - .thenCompose(xuid -> { - if (xuid == null) { - return CompletableFuture.completedFuture(null); + return CompletableFuture.supplyAsync(() -> { + try (Connection connection = database.connection(); + PreparedStatement statement = connection.prepareStatement(""" + SELECT xuid, gamertag + FROM links + LEFT JOIN xbox_identity_current USING (xuid) + WHERE java_id = ?::uuid""")) { + + statement.setString(1, javaId.toString()); + + try (ResultSet result = statement.executeQuery()) { + if (!result.next()) { + return null; } - - return playerManager.fetchGamertagFor(xuid).thenApply(gamertag -> { - return new FullLink(new UUID(0, xuid), gamertag, javaId, javaName); - }); - }); + return new FullLink( + new UUID(0, result.getLong("xuid")), + result.getString("gamertag"), + javaId, + javaName); + } + } catch (SQLException exception) { + throw new CompletionException("Error while finding link! ", exception); + } + }, database.executor()); } public CompletableFuture<@Nullable FullLink> findBedrockLink(UUID bedrockId, String gamertag) { - return attemptFindLink( - "SELECT `java_id`, `java_name` FROM `links` WHERE `bedrock_id` = ?", - stmt -> stmt.setLong(1, bedrockId.getLeastSignificantBits()), - resultSet -> { - UUID javaId = UUID.fromString(resultSet.getString("java_id")); - String javaName = resultSet.getString("java_name"); - return new FullLink(bedrockId, gamertag, javaId, javaName); - }); - } + long xuid = bedrockId.getLeastSignificantBits(); - private CompletableFuture attemptFindLink( - String query, - ThrowingConsumer parameterSetter, - ThrowingFunction resultProcessor) { - return CompletableFuture.supplyAsync( - () -> { - try (Connection connection = database.connection(); - PreparedStatement queryStmt = connection.prepareStatement(query)) { - parameterSetter.accept(queryStmt); - - try (ResultSet resultSet = queryStmt.executeQuery()) { - if (resultSet.next()) { - return resultProcessor.apply(resultSet); - } else { - return null; - } - } - - } catch (SQLException exception) { - throw new CompletionException("Error while finding link! ", exception); + return CompletableFuture.supplyAsync(() -> { + try (Connection connection = database.connection(); + PreparedStatement statement = connection.prepareStatement(""" + SELECT java_id, username + FROM links + LEFT JOIN java_identity_current ON java_id = id + WHERE xuid = ?::xuid""")) { + + statement.setLong(1, xuid); + + try (ResultSet result = statement.executeQuery()) { + if (!result.next()) { + return null; } - }, - database.executor()); + return new FullLink( + new UUID(0, xuid), + gamertag, + UUID.fromString(result.getString("java_id")), + result.getString("username")); + } + } catch (SQLException exception) { + throw new CompletionException("Error while finding link! ", exception); + } + }, database.executor()); } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index a438d3b..b3622d8 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,8 +1,8 @@ database: hostname: 127.0.0.1 - username: global_link - password: some_pass - database: global_link_dev + username: global_api + password: global_api_dev + database: global_api_dev max-pool-size: 3 spawn: From beb732409d365639c6b623886887795cf0f4e18d Mon Sep 17 00:00:00 2001 From: Tim203 Date: Sun, 1 Mar 2026 01:29:18 +0100 Subject: [PATCH 2/3] Store playfab ID as well, and store it on join instead of much later --- build.gradle.kts | 2 +- gradle/libs.versions.toml | 2 +- .../globallinkserver/GlobalLinkServer.java | 6 +- .../handler/CommandHandler.java | 4 +- .../globallinkserver/handler/JoinHandler.java | 35 ++++++++- .../geysermc/globallinkserver/link/Link.java | 14 ++-- .../globallinkserver/link/LinkManager.java | 33 +------- .../globallinkserver/link/LinkRequest.java | 18 ++--- .../manager/DatabaseManager.java | 2 +- .../manager/PlayerManager.java | 25 +----- .../service/LinkLookupService.java | 4 +- .../service/MappingService.java | 76 +++++++++++++++++++ 12 files changed, 136 insertions(+), 85 deletions(-) create mode 100644 src/main/java/org/geysermc/globallinkserver/service/MappingService.java diff --git a/build.gradle.kts b/build.gradle.kts index 20cdadf..4c0802c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,7 +8,7 @@ plugins { group = "org.geysermc.globallinkserver" dependencies { - paperweight.paperDevBundle("1.21.8-R0.1-SNAPSHOT") + paperweight.paperDevBundle("1.21.11-R0.1-SNAPSHOT") compileOnly(libs.geyser.core) compileOnly(libs.floodgate.api) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6166298..899ae70 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -geyser = "2.8.4-SNAPSHOT" +geyser = "2.9.4-SNAPSHOT" floodgate = "2.2.3-SNAPSHOT" hikari = "7.0.2" postgresql = "42.7.8" diff --git a/src/main/java/org/geysermc/globallinkserver/GlobalLinkServer.java b/src/main/java/org/geysermc/globallinkserver/GlobalLinkServer.java index a27ee65..b518156 100644 --- a/src/main/java/org/geysermc/globallinkserver/GlobalLinkServer.java +++ b/src/main/java/org/geysermc/globallinkserver/GlobalLinkServer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2025 GeyserMC + * Copyright (c) 2021-2026 GeyserMC * Licensed under the MIT license * @link https://github.com/GeyserMC/GlobalLinkServer */ @@ -43,6 +43,7 @@ import org.geysermc.globallinkserver.manager.PlayerManager; import org.geysermc.globallinkserver.service.LinkInfoService; import org.geysermc.globallinkserver.service.LinkLookupService; +import org.geysermc.globallinkserver.service.MappingService; import org.geysermc.globallinkserver.util.MultiConditionSet; import org.geysermc.globallinkserver.util.Utils; @@ -73,6 +74,7 @@ public void onEnable() { var linkManager = new LinkManager(playerManager, databaseManager); linkLookupService = new LinkLookupService(playerManager, databaseManager); linkInfoService = new LinkInfoService(linkLookupService, playerManager); + var mappingService = new MappingService(databaseManager); var commandUtils = new CommandHandler(linkLookupService, linkInfoService, linkManager, playerManager, this); @@ -83,7 +85,7 @@ public void onEnable() { var pluginManager = getServer().getPluginManager(); pluginManager.registerEvents(this, this); - pluginManager.registerEvents(new JoinHandler(linkLookupService, playerIdleTracker, this), this); + pluginManager.registerEvents(new JoinHandler(playerManager, linkLookupService, mappingService, playerIdleTracker, this), this); pluginManager.registerEvents(new MoveInactivityHandler(playerIdleTracker), this); pluginManager.registerEvents(new TeleportToSpawnHandler(config.spawn()), this); diff --git a/src/main/java/org/geysermc/globallinkserver/handler/CommandHandler.java b/src/main/java/org/geysermc/globallinkserver/handler/CommandHandler.java index cf13f63..727e9b6 100644 --- a/src/main/java/org/geysermc/globallinkserver/handler/CommandHandler.java +++ b/src/main/java/org/geysermc/globallinkserver/handler/CommandHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2025 GeyserMC + * Copyright (c) 2021-2026 GeyserMC * Licensed under the MIT license * @link https://github.com/GeyserMC/GlobalLinkServer */ @@ -84,7 +84,7 @@ public int linkWithCode(CommandContext ctx) { } String correctUsername = playerManager.correctUsername(player); - Link completedLink = Link.fromRequest(linkRequest, player.getUniqueId(), correctUsername, playerManager.nameTimestampMillis(player), isRequesterBedrock); + Link completedLink = Link.fromRequest(linkRequest, player.getUniqueId(), correctUsername, isRequesterBedrock); linkManager.finaliseLink(completedLink).whenComplete((result, error) -> { if (error != null || !result) { diff --git a/src/main/java/org/geysermc/globallinkserver/handler/JoinHandler.java b/src/main/java/org/geysermc/globallinkserver/handler/JoinHandler.java index 553c241..f3b39f6 100644 --- a/src/main/java/org/geysermc/globallinkserver/handler/JoinHandler.java +++ b/src/main/java/org/geysermc/globallinkserver/handler/JoinHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 GeyserMC + * Copyright (c) 2025-2026 GeyserMC * Licensed under the MIT license * @link https://github.com/GeyserMC/GlobalLinkServer */ @@ -11,19 +11,33 @@ import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.plugin.Plugin; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.auth.AuthData; import org.geysermc.globallinkserver.Components; +import org.geysermc.globallinkserver.manager.PlayerManager; import org.geysermc.globallinkserver.service.LinkLookupService; +import org.geysermc.globallinkserver.service.MappingService; import org.geysermc.globallinkserver.util.MultiConditionSet; import org.jspecify.annotations.NullMarked; @NullMarked public final class JoinHandler implements Listener { + private final PlayerManager playerManager; private final LinkLookupService linkLookupService; + private final MappingService mappingService; private final MultiConditionSet playerIdleTracker; private final Plugin plugin; - public JoinHandler(LinkLookupService linkLookupService, MultiConditionSet playerIdleTracker, Plugin plugin) { + public JoinHandler( + PlayerManager playerManager, + LinkLookupService linkLookupService, + MappingService mappingService, + MultiConditionSet playerIdleTracker, + Plugin plugin + ) { + this.playerManager = playerManager; this.linkLookupService = linkLookupService; + this.mappingService = mappingService; this.playerIdleTracker = playerIdleTracker; this.plugin = plugin; } @@ -42,6 +56,23 @@ public void onPlayerJoin(PlayerJoinEvent event) { otherPlayer.hidePlayer(plugin, player); }); + GeyserSession bedrockSession = playerManager.bedrockSession(player); + if (bedrockSession != null) { + AuthData authData = bedrockSession.getAuthData(); + mappingService.insertBedrockProfile( + player.getUniqueId().getLeastSignificantBits(), + authData.name(), + authData.playFabId(), + authData.issuedAt() + ); + } else { + long earliestKnownAt = player.getPlayerProfile().getTextures().getTimestamp(); + if (earliestKnownAt == 0) { + earliestKnownAt = System.currentTimeMillis(); + } + mappingService.insertJavaProfile(player.getUniqueId(), player.getName(), earliestKnownAt); + } + playerIdleTracker.add(player.getUniqueId()); linkLookupService.lookup(player).whenComplete(($, throwable) -> { diff --git a/src/main/java/org/geysermc/globallinkserver/link/Link.java b/src/main/java/org/geysermc/globallinkserver/link/Link.java index 2a8ec47..107d18e 100644 --- a/src/main/java/org/geysermc/globallinkserver/link/Link.java +++ b/src/main/java/org/geysermc/globallinkserver/link/Link.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 GeyserMC + * Copyright (c) 2025-2026 GeyserMC * Licensed under the MIT license * @link https://github.com/GeyserMC/GlobalLinkServer */ @@ -9,15 +9,15 @@ import org.jspecify.annotations.NullMarked; @NullMarked -public record Link(UUID javaId, String javaUsername, long javaNameTimestamp, long bedrockId, String bedrockName, long bedrockNameTimestamp) { - public Link(UUID javaId, String javaUsername, long javaNameTimestamp, UUID bedrockId, String bedrockName, long bedrockNameTimestamp) { - this(javaId, javaUsername, javaNameTimestamp, bedrockId.getLeastSignificantBits(), bedrockName, bedrockNameTimestamp); +public record Link(UUID javaId, String javaUsername, long bedrockId, String bedrockName) { + public Link(UUID javaId, String javaUsername, UUID bedrockId, String bedrockName) { + this(javaId, javaUsername, bedrockId.getLeastSignificantBits(), bedrockName); } - public static Link fromRequest(LinkRequest left, UUID rightUuid, String rightUsername, long rightNameTimestamp, boolean isLeftBedrock) { + public static Link fromRequest(LinkRequest left, UUID rightUuid, String rightUsername, boolean isLeftBedrock) { if (isLeftBedrock) { - return new Link(rightUuid, rightUsername, rightNameTimestamp, left.requesterUuid(), left.requesterUsername(), left.nameTimestamp()); + return new Link(rightUuid, rightUsername, left.requesterUuid(), left.requesterUsername()); } - return new Link(left.requesterUuid(), left.requesterUsername(), left.nameTimestamp(), rightUuid, rightUsername, rightNameTimestamp); + return new Link(left.requesterUuid(), left.requesterUsername(), rightUuid, rightUsername); } } diff --git a/src/main/java/org/geysermc/globallinkserver/link/LinkManager.java b/src/main/java/org/geysermc/globallinkserver/link/LinkManager.java index 7afd20f..a800a15 100644 --- a/src/main/java/org/geysermc/globallinkserver/link/LinkManager.java +++ b/src/main/java/org/geysermc/globallinkserver/link/LinkManager.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2025 GeyserMC + * Copyright (c) 2021-2026 GeyserMC * Licensed under the MIT license * @link https://github.com/GeyserMC/GlobalLinkServer */ @@ -12,8 +12,6 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; -import java.sql.Timestamp; -import java.time.Instant; import java.util.Iterator; import java.util.Random; import java.util.UUID; @@ -48,9 +46,8 @@ public LinkManager(PlayerManager playerManager, DatabaseManager database) { } public int createTempLink(Player player) { - long nameTimestamp = playerManager.nameTimestampMillis(player); String correctUsername = playerManager.correctUsername(player); - var linkRequest = new LinkRequest(createCode(), PENDING_LINK_TTL_MILLIS, player.getUniqueId(), correctUsername, nameTimestamp); + var linkRequest = new LinkRequest(createCode(), PENDING_LINK_TTL_MILLIS, player.getUniqueId(), correctUsername); linkRequests.put(linkRequest.code(), linkRequest); linkRequestForPlayer.put(player.getUniqueId(), linkRequest.code()); @@ -102,32 +99,6 @@ public CompletableFuture finaliseLink(Link linkRequest) { return CompletableFuture.supplyAsync( () -> { try (Connection connection = database.connection()) { - try (PreparedStatement query = connection.prepareStatement(""" - INSERT INTO java_identity_current AS c(id, username, detected_at) - VALUES (?::uuid, ?, ?) - ON CONFLICT (id) DO - UPDATE SET username = EXCLUDED.username, detected_at = EXCLUDED.detected_at - WHERE c.detected_at <= EXCLUDED.detected_at AND c.username != EXCLUDED.username - """)) { - query.setString(1, linkRequest.javaId().toString()); - query.setString(2, linkRequest.javaUsername()); - query.setTimestamp(3, Timestamp.from(Instant.EPOCH.plusMillis(linkRequest.javaNameTimestamp()))); - query.executeUpdate(); - } - - try (PreparedStatement query = connection.prepareStatement(""" - INSERT INTO xbox_identity_current AS c(xuid, gamertag, detected_at) - VALUES (?::xuid, ?, ?) - ON CONFLICT (xuid) DO - UPDATE SET gamertag = EXCLUDED.gamertag, detected_at = EXCLUDED.detected_at - WHERE c.detected_at <= EXCLUDED.detected_at AND c.gamertag != EXCLUDED.gamertag - """)) { - query.setLong(1, linkRequest.bedrockId()); - query.setString(2, linkRequest.bedrockName()); - query.setTimestamp(3, Timestamp.from(Instant.EPOCH.plusMillis(linkRequest.bedrockNameTimestamp()))); - query.executeUpdate(); - } - try (PreparedStatement query = connection.prepareStatement(""" INSERT INTO links (xuid, java_id) VALUES (?::xuid, ?::uuid) diff --git a/src/main/java/org/geysermc/globallinkserver/link/LinkRequest.java b/src/main/java/org/geysermc/globallinkserver/link/LinkRequest.java index 036d2e5..c487392 100644 --- a/src/main/java/org/geysermc/globallinkserver/link/LinkRequest.java +++ b/src/main/java/org/geysermc/globallinkserver/link/LinkRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2025 GeyserMC + * Copyright (c) 2021-2026 GeyserMC * Licensed under the MIT license * @link https://github.com/GeyserMC/GlobalLinkServer */ @@ -18,14 +18,12 @@ public final class LinkRequest { private final long expiryTime; private final UUID requesterUuid; private final String requesterUsername; - private final long nameTimestamp; - public LinkRequest(int code, long ttl, UUID requesterUuid, String requesterUsername, long nameTimestamp) { + public LinkRequest(int code, long ttl, UUID requesterUuid, String requesterUsername) { this.code = code; this.expiryTime = System.nanoTime() + ttl; this.requesterUuid = requesterUuid; this.requesterUsername = requesterUsername; - this.nameTimestamp = nameTimestamp; } public @Nullable Player requester() { @@ -48,10 +46,6 @@ public String requesterUsername() { return requesterUsername; } - public long nameTimestamp() { - return nameTimestamp; - } - @Override public boolean equals(Object obj) { if (obj == this) return true; @@ -60,13 +54,12 @@ public boolean equals(Object obj) { return this.code == that.code && this.expiryTime == that.expiryTime && Objects.equals(this.requesterUuid, that.requesterUuid) && - Objects.equals(this.requesterUsername, that.requesterUsername) && - this.nameTimestamp == that.nameTimestamp; + Objects.equals(this.requesterUsername, that.requesterUsername); } @Override public int hashCode() { - return Objects.hash(code, expiryTime, requesterUuid, requesterUsername, nameTimestamp); + return Objects.hash(code, expiryTime, requesterUuid, requesterUsername); } @Override @@ -75,8 +68,7 @@ public String toString() { "code=" + code + ", " + "expiryTime=" + expiryTime + ", " + "requesterUuid=" + requesterUuid + ", " + - "requesterUsername=" + requesterUsername + ", " + - "nameTimestamp=" + nameTimestamp + ']'; + "requesterUsername=" + requesterUsername + ']'; } } diff --git a/src/main/java/org/geysermc/globallinkserver/manager/DatabaseManager.java b/src/main/java/org/geysermc/globallinkserver/manager/DatabaseManager.java index 35504c5..8e6d564 100644 --- a/src/main/java/org/geysermc/globallinkserver/manager/DatabaseManager.java +++ b/src/main/java/org/geysermc/globallinkserver/manager/DatabaseManager.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 GeyserMC + * Copyright (c) 2025-2026 GeyserMC * Licensed under the MIT license * @link https://github.com/GeyserMC/GlobalLinkServer */ diff --git a/src/main/java/org/geysermc/globallinkserver/manager/PlayerManager.java b/src/main/java/org/geysermc/globallinkserver/manager/PlayerManager.java index d8abeb0..6446bf4 100644 --- a/src/main/java/org/geysermc/globallinkserver/manager/PlayerManager.java +++ b/src/main/java/org/geysermc/globallinkserver/manager/PlayerManager.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 GeyserMC + * Copyright (c) 2025-2026 GeyserMC * Licensed under the MIT license * @link https://github.com/GeyserMC/GlobalLinkServer */ @@ -36,7 +36,7 @@ public boolean isBedrockId(UUID id) { return api.getPlayer(uuid); } - private @Nullable GeyserSession bedrockSession(Player player) { + public @Nullable GeyserSession bedrockSession(Player player) { // There is no linking in the linking server itself, only link management if (player.getUniqueId().getMostSignificantBits() != 0L) { return null; @@ -58,25 +58,4 @@ public String correctUsername(Player player) { } return floodgatePlayer.getUsername(); } - - /** - * Returns the earliest known time that the profile has this name. - * Works for both Java and Bedrock accounts. - */ - public long nameTimestampMillis(Player player) { - GeyserSession session = bedrockSession(player); - if (session == null) { - long timestamp = player.getPlayerProfile().getTextures().getTimestamp(); - if (timestamp == 0L) { - return System.currentTimeMillis(); - } - return timestamp; - } - - long issuedAt = session.getAuthData().issuedAt(); - if (issuedAt == -1) { - return System.currentTimeMillis(); - } - return issuedAt * 1000L; - } } diff --git a/src/main/java/org/geysermc/globallinkserver/service/LinkLookupService.java b/src/main/java/org/geysermc/globallinkserver/service/LinkLookupService.java index 09e68b1..cbd3036 100644 --- a/src/main/java/org/geysermc/globallinkserver/service/LinkLookupService.java +++ b/src/main/java/org/geysermc/globallinkserver/service/LinkLookupService.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 GeyserMC + * Copyright (c) 2025-2026 GeyserMC * Licensed under the MIT license * @link https://github.com/GeyserMC/GlobalLinkServer */ @@ -96,7 +96,7 @@ LEFT JOIN xbox_identity_current USING (xuid) javaName); } } catch (SQLException exception) { - throw new CompletionException("Error while finding link! ", exception); + throw new CompletionException("Error while finding link!", exception); } }, database.executor()); } diff --git a/src/main/java/org/geysermc/globallinkserver/service/MappingService.java b/src/main/java/org/geysermc/globallinkserver/service/MappingService.java new file mode 100644 index 0000000..619a7e9 --- /dev/null +++ b/src/main/java/org/geysermc/globallinkserver/service/MappingService.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2026 GeyserMC + * Licensed under the MIT license + * @link https://github.com/GeyserMC/GlobalLinkServer + */ +package org.geysermc.globallinkserver.service; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.time.Instant; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import org.geysermc.globallinkserver.manager.DatabaseManager; +import org.jspecify.annotations.NullMarked; + +@NullMarked +public final class MappingService { + private final DatabaseManager database; + + public MappingService(DatabaseManager database) { + this.database = database; + } + + public CompletableFuture insertJavaProfile(UUID uuid, String username, long retrievedAtMillis) { + return CompletableFuture.runAsync(() -> { + try (Connection connection = database.connection()) { + try (PreparedStatement query = connection.prepareStatement(""" + INSERT INTO java_identity_current AS c(id, username, detected_at) + VALUES (?::uuid, ?, ?) + ON CONFLICT (id) DO + UPDATE SET username = EXCLUDED.username, detected_at = EXCLUDED.detected_at + WHERE c.detected_at <= EXCLUDED.detected_at AND c.username != EXCLUDED.username""")) { + query.setString(1, uuid.toString()); + query.setString(2, username); + query.setTimestamp(3, Timestamp.from(Instant.ofEpochMilli(retrievedAtMillis))); + query.executeUpdate(); + } + } catch (SQLException exception) { + throw new CompletionException("Error while inserting Java profile", exception); + } + }, database.executor()); + } + public CompletableFuture insertBedrockProfile(long xuid, String gamertag, String playfabId, long issuedAt) { + Instant issuedAtInstant = Instant.ofEpochSecond(issuedAt); + + return CompletableFuture.runAsync(() -> { + try (Connection connection = database.connection()) { + try (PreparedStatement query = connection.prepareStatement(""" + INSERT INTO xbox_identity_current AS c(xuid, gamertag, detected_at) + VALUES (?::xuid, ?, ?) + ON CONFLICT (xuid) DO + UPDATE SET gamertag = EXCLUDED.gamertag, detected_at = EXCLUDED.detected_at + WHERE c.detected_at <= EXCLUDED.detected_at AND c.gamertag != EXCLUDED.gamertag""")) { + query.setLong(1, xuid); + query.setString(2, gamertag); + query.setTimestamp(3, Timestamp.from(issuedAtInstant)); + query.executeUpdate(); + } + try (PreparedStatement query = connection.prepareStatement(""" + INSERT INTO playfab_identity (id, xuid, detected_at) + VALUES (?, ?, ?) + ON CONFLICT (id) DO NOTHING""")) { + query.setString(1, playfabId); + query.setLong(2, xuid); + query.setTimestamp(3, Timestamp.from(issuedAtInstant)); + query.executeUpdate(); + } + } catch (SQLException exception) { + throw new CompletionException("Error while inserting Java profile", exception); + } + }, database.executor()); + } +} From 5c90b3f06be26acf911b819412498ee6e1dd9b56 Mon Sep 17 00:00:00 2001 From: Tim203 Date: Sun, 13 Sep 2026 11:50:18 +0200 Subject: [PATCH 3/3] Comment that storing the mappings is required for finaliseLink to work --- .../geysermc/globallinkserver/link/LinkManager.java | 1 + .../globallinkserver/service/MappingService.java | 11 +++++------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/geysermc/globallinkserver/link/LinkManager.java b/src/main/java/org/geysermc/globallinkserver/link/LinkManager.java index a800a15..d38d0d6 100644 --- a/src/main/java/org/geysermc/globallinkserver/link/LinkManager.java +++ b/src/main/java/org/geysermc/globallinkserver/link/LinkManager.java @@ -96,6 +96,7 @@ public boolean hasActiveLinkRequest(UUID uuid) { } public CompletableFuture finaliseLink(Link linkRequest) { + // We already store the required mappings on join, otherwise we'd fail here return CompletableFuture.supplyAsync( () -> { try (Connection connection = database.connection()) { diff --git a/src/main/java/org/geysermc/globallinkserver/service/MappingService.java b/src/main/java/org/geysermc/globallinkserver/service/MappingService.java index 619a7e9..089841a 100644 --- a/src/main/java/org/geysermc/globallinkserver/service/MappingService.java +++ b/src/main/java/org/geysermc/globallinkserver/service/MappingService.java @@ -8,7 +8,6 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; -import java.sql.Timestamp; import java.time.Instant; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -29,13 +28,13 @@ public CompletableFuture insertJavaProfile(UUID uuid, String username, lon try (Connection connection = database.connection()) { try (PreparedStatement query = connection.prepareStatement(""" INSERT INTO java_identity_current AS c(id, username, detected_at) - VALUES (?::uuid, ?, ?) + VALUES (?::uuid, ?, ?::timestamptz) ON CONFLICT (id) DO UPDATE SET username = EXCLUDED.username, detected_at = EXCLUDED.detected_at WHERE c.detected_at <= EXCLUDED.detected_at AND c.username != EXCLUDED.username""")) { query.setString(1, uuid.toString()); query.setString(2, username); - query.setTimestamp(3, Timestamp.from(Instant.ofEpochMilli(retrievedAtMillis))); + query.setString(3, Instant.ofEpochMilli(retrievedAtMillis).toString()); query.executeUpdate(); } } catch (SQLException exception) { @@ -50,13 +49,13 @@ public CompletableFuture insertBedrockProfile(long xuid, String gamertag, try (Connection connection = database.connection()) { try (PreparedStatement query = connection.prepareStatement(""" INSERT INTO xbox_identity_current AS c(xuid, gamertag, detected_at) - VALUES (?::xuid, ?, ?) + VALUES (?::xuid, ?, ?::timestamptz) ON CONFLICT (xuid) DO UPDATE SET gamertag = EXCLUDED.gamertag, detected_at = EXCLUDED.detected_at WHERE c.detected_at <= EXCLUDED.detected_at AND c.gamertag != EXCLUDED.gamertag""")) { query.setLong(1, xuid); query.setString(2, gamertag); - query.setTimestamp(3, Timestamp.from(issuedAtInstant)); + query.setString(3, issuedAtInstant.toString()); query.executeUpdate(); } try (PreparedStatement query = connection.prepareStatement(""" @@ -65,7 +64,7 @@ INSERT INTO playfab_identity (id, xuid, detected_at) ON CONFLICT (id) DO NOTHING""")) { query.setString(1, playfabId); query.setLong(2, xuid); - query.setTimestamp(3, Timestamp.from(issuedAtInstant)); + query.setString(3, issuedAtInstant.toString()); query.executeUpdate(); } } catch (SQLException exception) {