diff --git a/core/src/main/java/org/geysermc/floodgate/core/command/LinkAccountCommand.java b/core/src/main/java/org/geysermc/floodgate/core/command/LinkAccountCommand.java index 599b0b1f..7b0c3d56 100644 --- a/core/src/main/java/org/geysermc/floodgate/core/command/LinkAccountCommand.java +++ b/core/src/main/java/org/geysermc/floodgate/core/command/LinkAccountCommand.java @@ -27,7 +27,6 @@ package org.geysermc.floodgate.core.command; import static org.incendo.cloud.parser.standard.StringParser.stringParser; -import io.micronaut.context.annotation.Secondary; import jakarta.inject.Inject; import jakarta.inject.Singleton; import java.util.concurrent.CompletableFuture; @@ -40,7 +39,6 @@ import org.geysermc.floodgate.core.connection.audience.ProfileAudience; import org.geysermc.floodgate.core.connection.audience.UserAudience; import org.geysermc.floodgate.core.connection.audience.UserAudience.PlayerAudience; import org.geysermc.floodgate.core.link.CommonPlayerLink; -import org.geysermc.floodgate.core.link.GlobalPlayerLinking; import org.geysermc.floodgate.core.link.LinkVerificationException; import org.geysermc.floodgate.core.platform.command.FloodgateCommand; import org.geysermc.floodgate.core.platform.command.TranslatableMessage; @@ -52,7 +50,6 @@ import org.incendo.cloud.context.CommandContext; import org.incendo.cloud.description.Description; @Singleton -@Secondary public final class LinkAccountCommand implements FloodgateCommand { @Inject SimpleFloodgateApi api; @Inject CommonPlayerLink link; @@ -73,21 +70,16 @@ public final class LinkAccountCommand implements FloodgateCommand { public void execute(CommandContext context) { UserAudience sender = context.sender(); - //todo make this less hacky - if (link instanceof GlobalPlayerLinking) { - if (((GlobalPlayerLinking) link).getDatabase() != null) { - sender.sendMessage(CommonCommandMessage.LOCAL_LINKING_NOTICE, - Constants.LINK_INFO_URL); + var linkState = link.state(); + if (!linkState.localLinkingActive()) { + if (!linkState.globalLinkingEnabled()) { + sender.sendMessage(CommonCommandMessage.LINKING_DISABLED); } else { - sender.sendMessage(CommonCommandMessage.GLOBAL_LINKING_NOTICE, - Constants.LINK_INFO_URL); - return; + sender.sendMessage(CommonCommandMessage.GLOBAL_LINKING_NOTICE, Constants.LINK_INFO_URL); } - } - - if (!link.isActive()) { - sender.sendMessage(CommonCommandMessage.LINKING_DISABLED); return; + } else if (linkState.globalLinkingEnabled()) { + sender.sendMessage(CommonCommandMessage.LOCAL_LINKING_NOTICE, Constants.LINK_INFO_URL); } ProfileAudience targetUser = context.get("player"); diff --git a/core/src/main/java/org/geysermc/floodgate/core/command/UnlinkAccountCommand.java b/core/src/main/java/org/geysermc/floodgate/core/command/UnlinkAccountCommand.java index 509e4116..5e4fcd50 100644 --- a/core/src/main/java/org/geysermc/floodgate/core/command/UnlinkAccountCommand.java +++ b/core/src/main/java/org/geysermc/floodgate/core/command/UnlinkAccountCommand.java @@ -33,7 +33,6 @@ import org.geysermc.floodgate.core.config.FloodgateConfig; import org.geysermc.floodgate.core.connection.audience.UserAudience; import org.geysermc.floodgate.core.connection.audience.UserAudience.PlayerAudience; import org.geysermc.floodgate.core.link.CommonPlayerLink; -import org.geysermc.floodgate.core.link.GlobalPlayerLinking; import org.geysermc.floodgate.core.platform.command.FloodgateCommand; import org.geysermc.floodgate.core.platform.command.TranslatableMessage; import org.geysermc.floodgate.core.util.Constants; @@ -59,21 +58,16 @@ public final class UnlinkAccountCommand implements FloodgateCommand { public void execute(CommandContext context) { UserAudience sender = context.sender(); - //todo make this less hacky - if (link instanceof GlobalPlayerLinking) { - if (((GlobalPlayerLinking) link).getDatabase() != null) { - sender.sendMessage(CommonCommandMessage.LOCAL_LINKING_NOTICE, - Constants.LINK_INFO_URL); + var linkState = link.state(); + if (!linkState.localLinkingActive()) { + if (!linkState.globalLinkingEnabled()) { + sender.sendMessage(CommonCommandMessage.LINKING_DISABLED); } else { - sender.sendMessage(CommonCommandMessage.GLOBAL_LINKING_NOTICE, - Constants.LINK_INFO_URL); - return; + sender.sendMessage(CommonCommandMessage.GLOBAL_LINKING_NOTICE, Constants.LINK_INFO_URL); } - } - - if (!link.isActive()) { - sender.sendMessage(CommonCommandMessage.LINKING_DISABLED); return; + } else if (linkState.globalLinkingEnabled()) { + sender.sendMessage(CommonCommandMessage.LOCAL_LINKING_NOTICE, Constants.LINK_INFO_URL); } link.isLinked(sender.uuid()) diff --git a/core/src/main/java/org/geysermc/floodgate/core/command/linkedaccounts/AddLinkedAccountCommand.java b/core/src/main/java/org/geysermc/floodgate/core/command/linkedaccounts/AddLinkedAccountCommand.java new file mode 100644 index 00000000..18868012 --- /dev/null +++ b/core/src/main/java/org/geysermc/floodgate/core/command/linkedaccounts/AddLinkedAccountCommand.java @@ -0,0 +1,91 @@ +package org.geysermc.floodgate.core.command.linkedaccounts; + +import static org.geysermc.floodgate.core.command.linkedaccounts.LinkedAccountsCommand.linkInfoMessage; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import java.util.ArrayList; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; +import org.geysermc.floodgate.api.logger.FloodgateLogger; +import org.geysermc.floodgate.core.command.CommonCommandMessage; +import org.geysermc.floodgate.core.command.LinkAccountCommand; +import org.geysermc.floodgate.core.command.util.Permission; +import org.geysermc.floodgate.core.connection.audience.ProfileAudience; +import org.geysermc.floodgate.core.connection.audience.UserAudience; +import org.geysermc.floodgate.core.http.ProfileFetcher; +import org.geysermc.floodgate.core.link.LocalPlayerLinking; +import org.geysermc.floodgate.core.platform.command.FloodgateSubCommand; +import org.geysermc.floodgate.core.util.Constants; +import org.incendo.cloud.Command; +import org.incendo.cloud.context.CommandContext; + +@Singleton +final class AddLinkedAccountCommand extends FloodgateSubCommand { + @Inject Optional optionalLinking; + @Inject ProfileFetcher fetcher; + @Inject FloodgateLogger logger; + + AddLinkedAccountCommand() { + super(LinkedAccountsCommand.class, "add", "Manually add a locally linked account", Permission.COMMAND_LINKED_MANAGE, "a"); + } + + @Override + public Command.Builder onBuild(Command.Builder commandBuilder) { + return super.onBuild(commandBuilder) + .argument(ProfileAudience.ofAnyIdentifierBedrock("bedrock")) + .argument(ProfileAudience.ofAnyIdentifierJava("java")); + } + + @Override + public void execute(CommandContext context) { + UserAudience sender = context.sender(); + + if (optionalLinking.isEmpty()) { + sender.sendMessage(CommonCommandMessage.LINKING_DISABLED); + return; + } + + var linking = optionalLinking.get(); + if (linking.state().globalLinkingEnabled()) { + sender.sendMessage(CommonCommandMessage.LOCAL_LINKING_NOTICE, Constants.LINK_INFO_URL); + } + + ProfileAudience bedrockInput = context.get("bedrock"); + ProfileAudience javaInput = context.get("java"); + AtomicReference bedrockRef = new AtomicReference<>(bedrockInput); + AtomicReference javaRef = new AtomicReference<>(javaInput); + + var futures = new ArrayList>(); + + if (bedrockRef.get().uuid() == null) { + futures.add(fetcher.fetchXuidFor(bedrockRef.get().username()).thenAccept(bedrockRef::set)); + } + if (javaRef.get().uuid() == null) { + futures.add(fetcher.fetchUniqueIdFor(javaRef.get().username()).thenAccept(javaRef::set)); + } + + CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new)) + .thenAccept($ -> { + var bedrock = bedrockRef.get(); + var java = javaRef.get(); + + if (bedrock == null) { + sender.sendMessage("Could not find Bedrock account with username " + bedrockInput.username()); + } + if (java == null) { + sender.sendMessage("Could not find Java account with username " + javaInput.username()); + } + + linking.addLink(java.uuid(), java.username(), bedrock.uuid()).whenComplete((player, throwable) -> { + if (throwable != null) { + sender.sendMessage(LinkAccountCommand.Message.LINK_REQUEST_ERROR); + logger.error("Exception while manually linking accounts", throwable); + return; + } + sender.sendMessage("You've successfully linked:\n" + linkInfoMessage(player)); + }); + }); + } +} diff --git a/core/src/main/java/org/geysermc/floodgate/core/command/linkedaccounts/InfoLinkedAccountCommand.java b/core/src/main/java/org/geysermc/floodgate/core/command/linkedaccounts/InfoLinkedAccountCommand.java new file mode 100644 index 00000000..55247596 --- /dev/null +++ b/core/src/main/java/org/geysermc/floodgate/core/command/linkedaccounts/InfoLinkedAccountCommand.java @@ -0,0 +1,97 @@ +package org.geysermc.floodgate.core.command.linkedaccounts; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import org.geysermc.floodgate.api.logger.FloodgateLogger; +import org.geysermc.floodgate.core.command.CommonCommandMessage; +import org.geysermc.floodgate.core.config.FloodgateConfig; +import org.geysermc.floodgate.core.connection.audience.ProfileAudience; +import org.geysermc.floodgate.core.connection.audience.UserAudience; +import org.geysermc.floodgate.core.http.ProfileFetcher; +import org.geysermc.floodgate.core.link.LocalPlayerLinking; +import org.geysermc.floodgate.core.platform.command.FloodgateSubCommand; +import org.geysermc.floodgate.core.util.Constants; +import org.incendo.cloud.Command; +import org.incendo.cloud.context.CommandContext; + +@Singleton +final class InfoLinkedAccountCommand extends FloodgateSubCommand { + @Inject Optional optionalLinking; + @Inject FloodgateConfig config; + @Inject ProfileFetcher fetcher; + @Inject FloodgateLogger logger; + + InfoLinkedAccountCommand() { + super(LinkedAccountsCommand.class, "info", "Gets info about the link status of an user", "i"); + } + + @Override + public Command.Builder onBuild(Command.Builder commandBuilder) { + return super.onBuild(commandBuilder).argument(ProfileAudience.ofAnyIdentifierBoth("player")); + } + + @Override + public void execute(CommandContext context) { + UserAudience sender = context.sender(); + + if (optionalLinking.isEmpty()) { + sender.sendMessage(CommonCommandMessage.LINKING_DISABLED); + return; + } + + var linking = optionalLinking.get(); + if (linking.state().globalLinkingEnabled()) { + sender.sendMessage(CommonCommandMessage.LOCAL_LINKING_NOTICE, Constants.LINK_INFO_URL); + } + + ProfileAudience playerInput = context.get("player"); + final boolean bedrock; + + var future = CompletableFuture.completedFuture(playerInput); + if (playerInput.uuid() == null) { + if (playerInput.username().startsWith(config.usernamePrefix())) { + future = fetcher.fetchXuidFor(playerInput.username().substring(config.usernamePrefix().length())); + bedrock = true; + } else { + bedrock = false; + future = fetcher.fetchUniqueIdFor(playerInput.username()); + } + } else { + bedrock = playerInput.uuid().getMostSignificantBits() == 0; + } + + String platform = bedrock ? "Bedrock" : "Java"; + + future.whenComplete((result, throwable) -> { + if (throwable != null) { + logger.error("Error while fetching player", throwable); + return; + } + + if (result == null) { + sender.sendMessage("Could not find %s user with username %s".formatted(platform, playerInput.username())); + return; + } + + linking.fetchLink(result.uuid()).whenComplete((link, error) -> { + if (error != null) { + sender.sendMessage("Error while looking up player link, see console"); + logger.error("Exception while fetching link status", error); + return; + } + + var usernameOrUniqueId = playerInput.username() != null ? playerInput.username() : playerInput.uuid(); + + if (link == null) { + sender.sendMessage("%s user %s is not linked!".formatted(platform, usernameOrUniqueId)); + return; + } + + sender.sendMessage("Link info for %s user %s:\n%s" + .formatted(platform, usernameOrUniqueId, LinkedAccountsCommand.linkInfoMessage(link))); + }); + }); + } +} diff --git a/core/src/main/java/org/geysermc/floodgate/core/command/linkedaccounts/LinkedAccountsCommand.java b/core/src/main/java/org/geysermc/floodgate/core/command/linkedaccounts/LinkedAccountsCommand.java new file mode 100644 index 00000000..7cb251b4 --- /dev/null +++ b/core/src/main/java/org/geysermc/floodgate/core/command/linkedaccounts/LinkedAccountsCommand.java @@ -0,0 +1,19 @@ +package org.geysermc.floodgate.core.command.linkedaccounts; + +import jakarta.inject.Singleton; +import org.geysermc.floodgate.core.command.util.Permission; +import org.geysermc.floodgate.core.database.entity.LinkedPlayer; +import org.geysermc.floodgate.core.platform.command.SubCommands; + +@Singleton +final class LinkedAccountsCommand extends SubCommands { + LinkedAccountsCommand() { + super("linkedaccounts", "Manage locally linked accounts", Permission.COMMAND_LINKED); + } + + @Singleton + static String linkInfoMessage(LinkedPlayer player) { + return "Java UUID: %s\nJava username: %s\nBedrock UUID: %s" + .formatted(player.javaUniqueId(), player.javaUsername(), player.bedrockId()); + } +} diff --git a/core/src/main/java/org/geysermc/floodgate/core/command/linkedaccounts/RemoveLinkedAccountCommand.java b/core/src/main/java/org/geysermc/floodgate/core/command/linkedaccounts/RemoveLinkedAccountCommand.java new file mode 100644 index 00000000..9c757c49 --- /dev/null +++ b/core/src/main/java/org/geysermc/floodgate/core/command/linkedaccounts/RemoveLinkedAccountCommand.java @@ -0,0 +1,92 @@ +package org.geysermc.floodgate.core.command.linkedaccounts; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import org.geysermc.floodgate.api.logger.FloodgateLogger; +import org.geysermc.floodgate.core.command.CommonCommandMessage; +import org.geysermc.floodgate.core.command.LinkAccountCommand; +import org.geysermc.floodgate.core.command.util.Permission; +import org.geysermc.floodgate.core.config.FloodgateConfig; +import org.geysermc.floodgate.core.connection.audience.ProfileAudience; +import org.geysermc.floodgate.core.connection.audience.UserAudience; +import org.geysermc.floodgate.core.http.ProfileFetcher; +import org.geysermc.floodgate.core.link.LocalPlayerLinking; +import org.geysermc.floodgate.core.platform.command.FloodgateSubCommand; +import org.geysermc.floodgate.core.util.Constants; +import org.incendo.cloud.Command; +import org.incendo.cloud.context.CommandContext; + +@Singleton +final class RemoveLinkedAccountCommand extends FloodgateSubCommand { + @Inject Optional optionalLinking; + @Inject FloodgateConfig config; + @Inject ProfileFetcher fetcher; + @Inject FloodgateLogger logger; + + RemoveLinkedAccountCommand() { + super(LinkedAccountsCommand.class, "remove", "Manually remove a locally linked account", Permission.COMMAND_LINKED_MANAGE, "r"); + } + + @Override + public Command.Builder onBuild(Command.Builder commandBuilder) { + return super.onBuild(commandBuilder).argument(ProfileAudience.ofAnyIdentifierBoth("player")); + } + + @Override + public void execute(CommandContext context) { + UserAudience sender = context.sender(); + + if (optionalLinking.isEmpty()) { + sender.sendMessage(CommonCommandMessage.LINKING_DISABLED); + return; + } + + var linking = optionalLinking.get(); + if (linking.state().globalLinkingEnabled()) { + sender.sendMessage(CommonCommandMessage.LOCAL_LINKING_NOTICE, Constants.LINK_INFO_URL); + } + + ProfileAudience playerInput = context.get("player"); + final boolean bedrock; + + var future = CompletableFuture.completedFuture(playerInput); + if (playerInput.uuid() == null) { + if (playerInput.username().startsWith(config.usernamePrefix())) { + future = fetcher.fetchXuidFor(playerInput.username().substring(config.usernamePrefix().length())); + bedrock = true; + } else { + bedrock = false; + future = fetcher.fetchUniqueIdFor(playerInput.username()); + } + } else { + bedrock = playerInput.uuid().getMostSignificantBits() == 0; + } + + String platform = bedrock ? "Bedrock" : "Java"; + + future.whenComplete((result, throwable) -> { + if (throwable != null) { + logger.error("Error while fetching player", throwable); + return; + } + + if (result == null) { + sender.sendMessage("Could not find %s user with username %s" + .formatted(platform, playerInput.username())); + return; + } + + linking.unlink(result.uuid()).whenComplete(($, error) -> { + if (error != null) { + sender.sendMessage(LinkAccountCommand.Message.LINK_REQUEST_ERROR); + logger.error("Exception while manually linking accounts", error); + return; + } + sender.sendMessage("You've successfully unlinked %s user %s" + .formatted(platform, playerInput.username())); + }); + }); + } +} diff --git a/core/src/main/java/org/geysermc/floodgate/core/command/main/FirewallCheckSubcommand.java b/core/src/main/java/org/geysermc/floodgate/core/command/main/FirewallCheckSubcommand.java index 342f10d1..87f82d63 100644 --- a/core/src/main/java/org/geysermc/floodgate/core/command/main/FirewallCheckSubcommand.java +++ b/core/src/main/java/org/geysermc/floodgate/core/command/main/FirewallCheckSubcommand.java @@ -30,6 +30,7 @@ import static org.geysermc.floodgate.core.util.Constants.COLOR_CHAR; import com.google.gson.JsonElement; import it.unimi.dsi.fastutil.Pair; import jakarta.inject.Inject; +import jakarta.inject.Singleton; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BooleanSupplier; @@ -42,27 +43,17 @@ import org.geysermc.floodgate.core.util.HttpClient.HttpResponse; import org.geysermc.floodgate.core.util.Utils; import org.incendo.cloud.context.CommandContext; +@Singleton final class FirewallCheckSubcommand extends FloodgateSubCommand { @Inject HttpClient httpProvider; - @Override - public Class parent() { - return FirewallCheckSubcommand.class; - } - - @Override - public String name() { - return "firewall"; - } - - @Override - public String description() { - return "Check if your outgoing firewall allows Floodgate to work properly"; - } - - @Override - public Permission permission() { - return Permission.COMMAND_MAIN_FIREWALL; + FirewallCheckSubcommand() { + super( + MainCommand.class, + "firewall", + "Check if your outgoing firewall allows Floodgate to work properly", + Permission.COMMAND_MAIN_FIREWALL + ); } @Override diff --git a/core/src/main/java/org/geysermc/floodgate/core/command/main/MainCommand.java b/core/src/main/java/org/geysermc/floodgate/core/command/main/MainCommand.java index 7ccc1348..00fda53b 100644 --- a/core/src/main/java/org/geysermc/floodgate/core/command/main/MainCommand.java +++ b/core/src/main/java/org/geysermc/floodgate/core/command/main/MainCommand.java @@ -25,57 +25,14 @@ package org.geysermc.floodgate.core.command.main; -import static org.geysermc.floodgate.core.util.Constants.COLOR_CHAR; -import static org.incendo.cloud.description.Description.description; - import jakarta.inject.Singleton; -import java.util.Locale; import org.geysermc.floodgate.core.command.util.Permission; -import org.geysermc.floodgate.core.connection.audience.UserAudience; import org.geysermc.floodgate.core.platform.command.FloodgateCommand; -import org.geysermc.floodgate.core.platform.command.FloodgateSubCommand; import org.geysermc.floodgate.core.platform.command.SubCommands; -import org.incendo.cloud.Command; -import org.incendo.cloud.Command.Builder; -import org.incendo.cloud.CommandManager; -import org.incendo.cloud.context.CommandContext; -import org.incendo.cloud.description.Description; @Singleton public final class MainCommand extends SubCommands implements FloodgateCommand { - @Override - public Command buildCommand(CommandManager commandManager) { - Builder builder = commandManager.commandBuilder( - "floodgate", - Description.of("A set of Floodgate related actions in one command")) - .senderType(UserAudience.class) - .permission(Permission.COMMAND_MAIN.get()) - .handler(this::execute); - - for (FloodgateSubCommand subCommand : subCommands()) { - commandManager.command(builder - .literal(subCommand.name().toLowerCase(Locale.ROOT), description(subCommand.description())) - .permission(subCommand.permission().get()) - .handler(subCommand::execute) - ); - } - - // also register /floodgate itself - return builder.build(); - } - - public void execute(CommandContext context) { - StringBuilder helpMessage = new StringBuilder("Available subcommands are:\n"); - - for (FloodgateSubCommand subCommand : subCommands()) { - if (context.sender().hasPermission(subCommand.permission().get())) { - helpMessage.append('\n').append(COLOR_CHAR).append('b') - .append(subCommand.name().toLowerCase(Locale.ROOT)) - .append(COLOR_CHAR).append("f - ").append(COLOR_CHAR).append('7') - .append(subCommand.description()); - } - } - - context.sender().sendMessage(helpMessage.toString()); + MainCommand() { + super("floodgate", "A set of Floodgate related actions in one command", Permission.COMMAND_MAIN); } } diff --git a/core/src/main/java/org/geysermc/floodgate/core/command/main/VersionSubcommand.java b/core/src/main/java/org/geysermc/floodgate/core/command/main/VersionSubcommand.java index 39ffd289..cde47006 100644 --- a/core/src/main/java/org/geysermc/floodgate/core/command/main/VersionSubcommand.java +++ b/core/src/main/java/org/geysermc/floodgate/core/command/main/VersionSubcommand.java @@ -29,6 +29,7 @@ import static org.geysermc.floodgate.core.util.Constants.COLOR_CHAR; import com.google.gson.JsonElement; import jakarta.inject.Inject; +import jakarta.inject.Singleton; import org.geysermc.floodgate.api.logger.FloodgateLogger; import org.geysermc.floodgate.core.command.WhitelistCommand.Message; import org.geysermc.floodgate.core.command.util.Permission; @@ -38,28 +39,18 @@ import org.geysermc.floodgate.core.util.Constants; import org.geysermc.floodgate.core.util.HttpClient; import org.incendo.cloud.context.CommandContext; +@Singleton public class VersionSubcommand extends FloodgateSubCommand { @Inject HttpClient httpClient; @Inject FloodgateLogger logger; - @Override - public Class parent() { - return MainCommand.class; - } - - @Override - public String name() { - return "version"; - } - - @Override - public String description() { - return "Displays version information about Floodgate"; - } - - @Override - public Permission permission() { - return Permission.COMMAND_MAIN_VERSION; + VersionSubcommand() { + super( + MainCommand.class, + "version", + "Displays version information about Floodgate", + Permission.COMMAND_MAIN_VERSION + ); } @Override diff --git a/core/src/main/java/org/geysermc/floodgate/core/command/util/Permission.java b/core/src/main/java/org/geysermc/floodgate/core/command/util/Permission.java index 69710bff..f9ec6af4 100644 --- a/core/src/main/java/org/geysermc/floodgate/core/command/util/Permission.java +++ b/core/src/main/java/org/geysermc/floodgate/core/command/util/Permission.java @@ -32,6 +32,8 @@ public enum Permission { COMMAND_LINK("floodgate.command.linkaccount", PermissionDefault.TRUE), COMMAND_UNLINK("floodgate.command.unlinkaccount", PermissionDefault.TRUE), COMMAND_WHITELIST("floodgate.command.fwhitelist", PermissionDefault.OP), + COMMAND_LINKED("floodgate.command.linkedaccounts", PermissionDefault.OP), + COMMAND_LINKED_MANAGE(COMMAND_LINKED, "manage", PermissionDefault.OP), NEWS_RECEIVE("floodgate.news.receive", PermissionDefault.OP); diff --git a/core/src/main/java/org/geysermc/floodgate/core/connection/audience/ProfileAudience.java b/core/src/main/java/org/geysermc/floodgate/core/connection/audience/ProfileAudience.java index b06607f9..6b935db9 100644 --- a/core/src/main/java/org/geysermc/floodgate/core/connection/audience/ProfileAudience.java +++ b/core/src/main/java/org/geysermc/floodgate/core/connection/audience/ProfileAudience.java @@ -43,11 +43,23 @@ public record ProfileAudience(@Nullable UUID uuid, @Nullable String username) { return of(name, true, true, PlayerType.ONLY_BEDROCK); } + public static CommandComponent.Builder ofAnyIdentifierJava(String name) { + return of(name, true, true, PlayerType.ONLY_JAVA); + } + + public static CommandComponent.Builder ofAnyIdentifierBoth(String name) { + return of(name, true, true, PlayerType.ALL_PLAYERS); + } + public static CommandComponent.Builder ofAnyUsernameBoth(String name) { return of(name, false, true, PlayerType.ALL_PLAYERS); } - private static CommandComponent.Builder of(String name, boolean allowUuid, boolean allowOffline, PlayerType limitTo) { + private static CommandComponent.Builder of( + String name, + boolean allowUuid, + boolean allowOffline, + PlayerType limitTo) { return CommandComponent.builder() .name(name) .parser(quotedStringParser().flatMapSuccess(ProfileAudience.class, (context, input) -> { diff --git a/core/src/main/java/org/geysermc/floodgate/core/http/ProfileFetcher.java b/core/src/main/java/org/geysermc/floodgate/core/http/ProfileFetcher.java new file mode 100644 index 00000000..9c70bdde --- /dev/null +++ b/core/src/main/java/org/geysermc/floodgate/core/http/ProfileFetcher.java @@ -0,0 +1,57 @@ +package org.geysermc.floodgate.core.http; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.floodgate.core.connection.audience.ProfileAudience; +import org.geysermc.floodgate.core.http.minecraft.MinecraftClient; +import org.geysermc.floodgate.core.http.minecraft.ProfileResult; +import org.geysermc.floodgate.core.http.xbox.XboxClient; +import org.geysermc.floodgate.core.util.Utils; + +@Singleton +public final class ProfileFetcher { + @Inject MinecraftClient minecraftClient; + @Inject XboxClient xboxClient; + + public CompletableFuture<@Nullable ProfileAudience> fetchUniqueIdFor(String username) { + return minecraftClient.profileByName(username).thenApply(this::convert); + } + + public CompletableFuture<@Nullable ProfileAudience> fetchUsernameFor(UUID uniqueId) { + return minecraftClient.profileByUniqueId(uniqueId).thenApply(this::convert); + } + + public CompletableFuture<@Nullable ProfileAudience> fetchXuidFor(String gamertag) { + return xboxClient.xuidByGamertag(gamertag).thenApply(result -> { + var xuid = result.xuid(); + if (xuid == null) { + return null; + } + return new ProfileAudience(Utils.getJavaUuid(xuid), gamertag); + }); + } + + public CompletableFuture<@Nullable ProfileAudience> fetchGamertagFor(long xuid) { + return xboxClient.gamertagByXuid(xuid).thenApply(result -> { + var gamertag = result.gamertag(); + if (gamertag == null) { + return null; + } + return new ProfileAudience(Utils.getJavaUuid(xuid), gamertag); + }); + } + + public CompletableFuture<@Nullable ProfileAudience> fetchGamertagFor(UUID xuid) { + return fetchGamertagFor(xuid.getLeastSignificantBits()); + } + + private @Nullable ProfileAudience convert(@Nullable ProfileResult result) { + if (result == null) { + return null; + } + return new ProfileAudience(Utils.fromShortUniqueId(result.id()), result.name()); + } +} diff --git a/core/src/main/java/org/geysermc/floodgate/core/http/minecraft/MinecraftClient.java b/core/src/main/java/org/geysermc/floodgate/core/http/minecraft/MinecraftClient.java new file mode 100644 index 00000000..10688e5d --- /dev/null +++ b/core/src/main/java/org/geysermc/floodgate/core/http/minecraft/MinecraftClient.java @@ -0,0 +1,23 @@ +package org.geysermc.floodgate.core.http.minecraft; + +import static io.micronaut.http.HttpHeaders.ACCEPT; +import static io.micronaut.http.HttpHeaders.USER_AGENT; + +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Header; +import io.micronaut.http.client.annotation.Client; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +@Client("https://api.minecraftservices.com/minecraft") +@Header(name = USER_AGENT, value = "${http.userAgent}") +@Header(name = ACCEPT, value = "application/json") +public interface MinecraftClient { + @Get("/profile/lookup/name/{name}") + CompletableFuture<@Nullable ProfileResult> profileByName(@NonNull String name); + + @Get("/profile/lookup/{uuid}") + CompletableFuture<@Nullable ProfileResult> profileByUniqueId(@NonNull UUID uuid); +} diff --git a/core/src/main/java/org/geysermc/floodgate/core/http/minecraft/ProfileResult.java b/core/src/main/java/org/geysermc/floodgate/core/http/minecraft/ProfileResult.java new file mode 100644 index 00000000..46110dfd --- /dev/null +++ b/core/src/main/java/org/geysermc/floodgate/core/http/minecraft/ProfileResult.java @@ -0,0 +1,8 @@ +package org.geysermc.floodgate.core.http.minecraft; + +import io.micronaut.serde.annotation.Serdeable; +import org.checkerframework.checker.nullness.qual.NonNull; + +@Serdeable +public record ProfileResult(@NonNull String id, @NonNull String name) { +} diff --git a/core/src/main/java/org/geysermc/floodgate/core/http/xbox/GetGamertagResult.java b/core/src/main/java/org/geysermc/floodgate/core/http/xbox/GetGamertagResult.java index 7023e96e..32bdff1f 100644 --- a/core/src/main/java/org/geysermc/floodgate/core/http/xbox/GetGamertagResult.java +++ b/core/src/main/java/org/geysermc/floodgate/core/http/xbox/GetGamertagResult.java @@ -25,7 +25,9 @@ package org.geysermc.floodgate.core.http.xbox; +import io.micronaut.serde.annotation.Serdeable; import jakarta.annotation.Nullable; +@Serdeable public record GetGamertagResult(@Nullable String gamertag) { } diff --git a/core/src/main/java/org/geysermc/floodgate/core/http/xbox/GetXuidResult.java b/core/src/main/java/org/geysermc/floodgate/core/http/xbox/GetXuidResult.java index b86ee02c..7c321592 100644 --- a/core/src/main/java/org/geysermc/floodgate/core/http/xbox/GetXuidResult.java +++ b/core/src/main/java/org/geysermc/floodgate/core/http/xbox/GetXuidResult.java @@ -25,7 +25,9 @@ package org.geysermc.floodgate.core.http.xbox; +import io.micronaut.serde.annotation.Serdeable; import jakarta.annotation.Nullable; +@Serdeable public record GetXuidResult(@Nullable Long xuid) { } diff --git a/core/src/main/java/org/geysermc/floodgate/core/link/CommonPlayerLink.java b/core/src/main/java/org/geysermc/floodgate/core/link/CommonPlayerLink.java index dfca8ec0..dc309af0 100644 --- a/core/src/main/java/org/geysermc/floodgate/core/link/CommonPlayerLink.java +++ b/core/src/main/java/org/geysermc/floodgate/core/link/CommonPlayerLink.java @@ -97,7 +97,13 @@ public abstract class CommonPlayerLink { public abstract CompletableFuture invalidateLinkRequest(@NonNull LinkRequest request); - public boolean isActive() { - return enabled && allowLinking; + public PlayerLinkState state() { + return new PlayerLinkState(enabled && allowLinking); + } + + public record PlayerLinkState(boolean localLinkingActive, boolean globalLinkingEnabled) { + public PlayerLinkState(boolean localLinkingActive) { + this(localLinkingActive, false); + } } } diff --git a/core/src/main/java/org/geysermc/floodgate/core/link/GlobalPlayerLinking.java b/core/src/main/java/org/geysermc/floodgate/core/link/GlobalPlayerLinking.java index ec4b6b9a..c35150fe 100644 --- a/core/src/main/java/org/geysermc/floodgate/core/link/GlobalPlayerLinking.java +++ b/core/src/main/java/org/geysermc/floodgate/core/link/GlobalPlayerLinking.java @@ -40,6 +40,7 @@ import org.geysermc.floodgate.core.database.entity.LinkRequest; import org.geysermc.floodgate.core.database.entity.LinkedPlayer; import org.geysermc.floodgate.core.http.link.GlobalLinkClient; +@Requires(property = "config.playerLink.enabled", value = "true") @Requires(property = "config.playerLink.enableGlobalLinking", value = "true") @Primary @Singleton @@ -151,8 +152,8 @@ public class GlobalPlayerLinking extends CommonPlayerLink { } @Override - public boolean isActive() { - return database != null && database.isActive(); + public PlayerLinkState state() { + return new PlayerLinkState(database != null && database.state().localLinkingActive(), true); } private CompletableFuture failedFuture() { diff --git a/core/src/main/java/org/geysermc/floodgate/core/platform/command/CommandUtil.java b/core/src/main/java/org/geysermc/floodgate/core/platform/command/CommandUtil.java index c8903dbc..4dd43045 100644 --- a/core/src/main/java/org/geysermc/floodgate/core/platform/command/CommandUtil.java +++ b/core/src/main/java/org/geysermc/floodgate/core/platform/command/CommandUtil.java @@ -27,6 +27,7 @@ package org.geysermc.floodgate.core.platform.command; import static org.geysermc.floodgate.core.platform.util.PlayerType.ALL_PLAYERS; import static org.geysermc.floodgate.core.platform.util.PlayerType.ONLY_BEDROCK; +import static org.geysermc.floodgate.core.platform.util.PlayerType.ONLY_JAVA; import java.util.ArrayList; import java.util.Collection; @@ -130,12 +131,18 @@ public abstract class CommandUtil { } protected Object applyPlayerTypeFilter(Object player, PlayerType filter, Object fallback) { + if (player == null) { + return fallback; + } if (filter == ALL_PLAYERS || player instanceof String || player instanceof UUID) { return player; } - return (filter == ONLY_BEDROCK) == api.isBedrockPlayer(getUuidFromSource(player)) - ? player - : fallback; + if (filter == ONLY_BEDROCK || filter == ONLY_JAVA) { + if (api.isBedrockPlayer(getUuidFromSource(player)) == (filter == ONLY_BEDROCK)) { + return player; + } + } + return fallback; } /** @@ -147,22 +154,6 @@ public abstract class CommandUtil { */ public abstract boolean hasPermission(Object player, String permission); - /** - * Get all online players with the given permission. - * - * @param permission the permission to check - * @return a list of online players that have the given permission - */ - public Collection getOnlinePlayersWithPermission(String permission) { - List players = new ArrayList<>(); - for (Object player : getOnlinePlayers()) { - if (hasPermission(player, permission)) { - players.add(player); - } - } - return players; - } - /** * Sends a raw message to the specified target, no matter what platform Floodgate is running * on. diff --git a/core/src/main/java/org/geysermc/floodgate/core/platform/command/FloodgateSubCommand.java b/core/src/main/java/org/geysermc/floodgate/core/platform/command/FloodgateSubCommand.java index 7a920a98..5cad3b70 100644 --- a/core/src/main/java/org/geysermc/floodgate/core/platform/command/FloodgateSubCommand.java +++ b/core/src/main/java/org/geysermc/floodgate/core/platform/command/FloodgateSubCommand.java @@ -25,18 +25,58 @@ package org.geysermc.floodgate.core.platform.command; +import java.util.Locale; +import java.util.Objects; +import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.floodgate.core.command.util.Permission; import org.geysermc.floodgate.core.connection.audience.UserAudience; +import org.incendo.cloud.Command; import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.description.Description; public abstract class FloodgateSubCommand { - public abstract Class parent(); + private final Class parent; + private final String name; + private final String description; + private final Permission permission; + private final String[] aliases; - public abstract String name(); + protected FloodgateSubCommand(Class parent, String name, String description, Permission permission, String... aliases) { + this.parent = Objects.requireNonNull(parent); + this.name = Objects.requireNonNull(name); + this.description = Objects.requireNonNull(description); + this.permission = permission; + this.aliases = Objects.requireNonNull(aliases); + } - public abstract String description(); + protected FloodgateSubCommand(Class parent, String name, String description, String... aliases) { + this(parent, name, description, null, aliases); + } - public abstract Permission permission(); + public Command.Builder onBuild(Command.Builder commandBuilder) { + var builder = commandBuilder; + if (permission != null) { + builder = builder.permission(permission.get()); + } + return builder.literal(name.toLowerCase(Locale.ROOT), Description.of(description), aliases) + .handler(this::execute); + } public abstract void execute(CommandContext context); + + public Class parent() { + return parent; + } + + public String name() { + return name; + } + + public String description() { + return description; + } + + public @Nullable Permission permission() { + return permission; + } } diff --git a/core/src/main/java/org/geysermc/floodgate/core/platform/command/SubCommands.java b/core/src/main/java/org/geysermc/floodgate/core/platform/command/SubCommands.java index a1ff5582..c7aadf95 100644 --- a/core/src/main/java/org/geysermc/floodgate/core/platform/command/SubCommands.java +++ b/core/src/main/java/org/geysermc/floodgate/core/platform/command/SubCommands.java @@ -25,19 +25,66 @@ package org.geysermc.floodgate.core.platform.command; +import static org.geysermc.floodgate.core.util.Constants.COLOR_CHAR; +import static org.incendo.cloud.description.Description.description; + import jakarta.annotation.PostConstruct; import jakarta.inject.Inject; +import java.util.Locale; import java.util.Set; +import org.geysermc.floodgate.core.command.util.Permission; +import org.geysermc.floodgate.core.connection.audience.UserAudience; +import org.incendo.cloud.Command; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.context.CommandContext; + +public abstract class SubCommands implements FloodgateCommand { + private final String name; + private final String description; + private final Permission permission; -public abstract class SubCommands { @Inject Set subCommands; + protected SubCommands(String name, String description, Permission permission) { + this.name = name; + this.description = description; + this.permission = permission; + } + + @Override + public Command buildCommand(CommandManager commandManager) { + var builder = commandManager + .commandBuilder(name, description(description)) + .senderType(UserAudience.class) + .permission(permission.get()) + .handler(this::execute); + + for (FloodgateSubCommand command : subCommands) { + commandManager.command(command.onBuild(builder)); + } + + // also register /floodgate itself + return builder.build(); + } + + public void execute(CommandContext context) { + StringBuilder helpMessage = new StringBuilder("Available subcommands are:\n"); + + for (FloodgateSubCommand subCommand : subCommands) { + var permission = subCommand.permission(); + if (permission == null || context.sender().hasPermission(permission.get())) { + helpMessage.append('\n').append(COLOR_CHAR).append('b') + .append(subCommand.name().toLowerCase(Locale.ROOT)) + .append(COLOR_CHAR).append("f - ").append(COLOR_CHAR).append('7') + .append(subCommand.description()); + } + } + + context.sender().sendMessage(helpMessage.toString()); + } + @PostConstruct public void setup() { subCommands.removeIf(subCommand -> !subCommand.parent().isAssignableFrom(this.getClass())); } - - protected Set subCommands() { - return subCommands; - } } diff --git a/core/src/main/java/org/geysermc/floodgate/core/util/Utils.java b/core/src/main/java/org/geysermc/floodgate/core/util/Utils.java index 72f63904..ac8eeb04 100644 --- a/core/src/main/java/org/geysermc/floodgate/core/util/Utils.java +++ b/core/src/main/java/org/geysermc/floodgate/core/util/Utils.java @@ -33,6 +33,7 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintWriter; import java.io.StringWriter; +import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.util.Locale; import java.util.Properties; @@ -43,7 +44,8 @@ import org.geysermc.floodgate.core.crypto.RandomUtils; public class Utils { private static final Pattern NON_UNIQUE_PREFIX = Pattern.compile("^\\w{0,16}$"); - private static final Random random = RandomUtils.secureRandom(); + private static final Random RANDOM = RandomUtils.secureRandom(); + private static final BigInteger MAX_LONG_VALUE = BigInteger.ONE.shiftLeft(64); /** * This method is used in Addons.
Most addons can be removed once the player associated to @@ -92,6 +94,11 @@ public class Utils { return getJavaUuid(Long.parseLong(xuid)); } + public static UUID fromShortUniqueId(String uuid) { + var bigInt = new BigInteger(uuid, 16); + return new UUID(bigInt.shiftRight(64).longValue(), bigInt.xor(MAX_LONG_VALUE).longValue()); + } + public static boolean isUniquePrefix(String prefix) { return !NON_UNIQUE_PREFIX.matcher(prefix).matches(); } @@ -105,7 +112,7 @@ public class Utils { } public static char generateCodeChar() { - var codeChar = random.nextInt() % (10 + 26); + var codeChar = RANDOM.nextInt() % (10 + 26); if (codeChar < 10) { return (char) ('0' + codeChar); }