1
0
mirror of https://github.com/GeyserMC/Floodgate.git synced 2025-12-19 14:59:20 +00:00

Initial version of local link management commands

Fixes #41, fixes #64
This commit is contained in:
Tim203
2024-02-18 15:19:42 +01:00
parent 18989cd068
commit c4cc692f6f
22 changed files with 566 additions and 144 deletions

View File

@@ -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<PlayerAudience> 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);
} else {
sender.sendMessage(CommonCommandMessage.GLOBAL_LINKING_NOTICE,
Constants.LINK_INFO_URL);
return;
}
}
if (!link.isActive()) {
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;
} else if (linkState.globalLinkingEnabled()) {
sender.sendMessage(CommonCommandMessage.LOCAL_LINKING_NOTICE, Constants.LINK_INFO_URL);
}
ProfileAudience targetUser = context.get("player");

View File

@@ -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<PlayerAudience> 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);
} else {
sender.sendMessage(CommonCommandMessage.GLOBAL_LINKING_NOTICE,
Constants.LINK_INFO_URL);
return;
}
}
if (!link.isActive()) {
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;
} else if (linkState.globalLinkingEnabled()) {
sender.sendMessage(CommonCommandMessage.LOCAL_LINKING_NOTICE, Constants.LINK_INFO_URL);
}
link.isLinked(sender.uuid())

View File

@@ -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<LocalPlayerLinking> 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<UserAudience> onBuild(Command.Builder<UserAudience> commandBuilder) {
return super.onBuild(commandBuilder)
.argument(ProfileAudience.ofAnyIdentifierBedrock("bedrock"))
.argument(ProfileAudience.ofAnyIdentifierJava("java"));
}
@Override
public void execute(CommandContext<UserAudience> 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<ProfileAudience> bedrockRef = new AtomicReference<>(bedrockInput);
AtomicReference<ProfileAudience> javaRef = new AtomicReference<>(javaInput);
var futures = new ArrayList<CompletableFuture<?>>();
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));
});
});
}
}

View File

@@ -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<LocalPlayerLinking> 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<UserAudience> onBuild(Command.Builder<UserAudience> commandBuilder) {
return super.onBuild(commandBuilder).argument(ProfileAudience.ofAnyIdentifierBoth("player"));
}
@Override
public void execute(CommandContext<UserAudience> 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)));
});
});
}
}

View File

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

View File

@@ -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<LocalPlayerLinking> 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<UserAudience> onBuild(Command.Builder<UserAudience> commandBuilder) {
return super.onBuild(commandBuilder).argument(ProfileAudience.ofAnyIdentifierBoth("player"));
}
@Override
public void execute(CommandContext<UserAudience> 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()));
});
});
}
}

View File

@@ -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

View File

@@ -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<UserAudience> buildCommand(CommandManager<UserAudience> commandManager) {
Builder<UserAudience> 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<UserAudience> 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);
}
}

View File

@@ -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

View File

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

View File

@@ -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<UserAudience, ProfileAudience> ofAnyIdentifierJava(String name) {
return of(name, true, true, PlayerType.ONLY_JAVA);
}
public static CommandComponent.Builder<UserAudience, ProfileAudience> ofAnyIdentifierBoth(String name) {
return of(name, true, true, PlayerType.ALL_PLAYERS);
}
public static CommandComponent.Builder<UserAudience, ProfileAudience> ofAnyUsernameBoth(String name) {
return of(name, false, true, PlayerType.ALL_PLAYERS);
}
private static CommandComponent.Builder<UserAudience, ProfileAudience> of(String name, boolean allowUuid, boolean allowOffline, PlayerType limitTo) {
private static CommandComponent.Builder<UserAudience, ProfileAudience> of(
String name,
boolean allowUuid,
boolean allowOffline,
PlayerType limitTo) {
return CommandComponent.<UserAudience, ProfileAudience>builder()
.name(name)
.parser(quotedStringParser().flatMapSuccess(ProfileAudience.class, (context, input) -> {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -97,7 +97,13 @@ public abstract class CommonPlayerLink {
public abstract CompletableFuture<Void> 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);
}
}
}

View File

@@ -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 <U> CompletableFuture<U> failedFuture() {

View File

@@ -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<Object> getOnlinePlayersWithPermission(String permission) {
List<Object> 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.

View File

@@ -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<UserAudience> onBuild(Command.Builder<UserAudience> 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<UserAudience> context);
public Class<?> parent() {
return parent;
}
public String name() {
return name;
}
public String description() {
return description;
}
public @Nullable Permission permission() {
return permission;
}
}

View File

@@ -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<FloodgateSubCommand> subCommands;
protected SubCommands(String name, String description, Permission permission) {
this.name = name;
this.description = description;
this.permission = permission;
}
@Override
public Command<UserAudience> buildCommand(CommandManager<UserAudience> 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<UserAudience> 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<FloodgateSubCommand> subCommands() {
return subCommands;
}
}

View File

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