From 069f4fb218e15122b2357efb36577809ac2ff761 Mon Sep 17 00:00:00 2001 From: Vankka Date: Tue, 26 Dec 2023 18:40:55 +0200 Subject: [PATCH] Add link create and linked commands --- ...erLocaleProvider.java => PaperPlayer.java} | 4 +- .../bukkit/player/SpigotPlayer.java | 45 +++++ .../bukkit/player/BukkitOfflinePlayer.java | 7 + .../bukkit/player/BukkitPlayer.java | 9 +- .../bukkit/player/BukkitPlayerProvider.java | 15 +- .../bungee/player/BungeePlayer.java | 6 + .../discordsrv/common/AbstractDiscordSRV.java | 3 +- .../common/channel/TimedUpdaterModule.java | 6 +- .../combined/commands/LinkInitCommand.java | 162 ++++++++++++------ .../combined/commands/LinkedCommand.java | 160 +++++++++++++++-- .../commands/subcommand/ExecuteCommand.java | 3 +- .../common/command/util/CommandUtil.java | 115 ++++++++----- .../config/messages/MessagesConfig.java | 89 +++++++++- .../common/console/SingleConsoleHandler.java | 3 +- .../connection/jda/JDAConnectionManager.java | 6 +- .../future/util/CompletableFutureUtil.java | 18 ++ .../common/groupsync/GroupSyncModule.java | 6 +- .../discordsrv/common/http/util/HttpUtil.java | 45 +++++ .../common/logging/impl/DiscordSRVLogger.java | 6 +- .../discord/DiscordChatMessageModule.java | 3 +- .../common/player/IOfflinePlayer.java | 18 ++ .../com/discordsrv/common/player/IPlayer.java | 7 +- .../common/player/OfflinePlayer.java | 44 +++++ .../common/player/ServerPlayerProvider.java | 12 +- .../provider/AbstractPlayerProvider.java | 57 ++++++ .../player/provider/PlayerProvider.java | 9 + .../provider/model/GameProfileResponse.java | 15 ++ .../player/provider/model/SkinInfo.java | 20 +++ .../player/provider/model/Textures.java | 46 +++++ .../player/provider/model/UUIDResponse.java | 7 + .../common/scheduler/Scheduler.java | 37 +--- .../common/scheduler/StandardScheduler.java | 9 +- .../common/update/UpdateChecker.java | 17 +- .../discordsrv/common/uuid/util/UUIDUtil.java | 37 ++++ .../MinecraftToDiscordChatMessageTest.java | 6 + .../sponge/player/SpongeOfflinePlayer.java | 17 ++ .../sponge/player/SpongePlayer.java | 9 + .../sponge/player/SpongePlayerProvider.java | 15 +- .../velocity/player/VelocityPlayer.java | 16 ++ 39 files changed, 920 insertions(+), 189 deletions(-) rename bukkit/paper/src/main/java/com/discordsrv/bukkit/player/{PlayerLocaleProvider.java => PaperPlayer.java} (92%) create mode 100644 bukkit/spigot/src/main/java/com/discordsrv/bukkit/player/SpigotPlayer.java create mode 100644 common/src/main/java/com/discordsrv/common/http/util/HttpUtil.java create mode 100644 common/src/main/java/com/discordsrv/common/player/OfflinePlayer.java create mode 100644 common/src/main/java/com/discordsrv/common/player/provider/model/GameProfileResponse.java create mode 100644 common/src/main/java/com/discordsrv/common/player/provider/model/SkinInfo.java create mode 100644 common/src/main/java/com/discordsrv/common/player/provider/model/Textures.java create mode 100644 common/src/main/java/com/discordsrv/common/player/provider/model/UUIDResponse.java create mode 100644 common/src/main/java/com/discordsrv/common/uuid/util/UUIDUtil.java diff --git a/bukkit/paper/src/main/java/com/discordsrv/bukkit/player/PlayerLocaleProvider.java b/bukkit/paper/src/main/java/com/discordsrv/bukkit/player/PaperPlayer.java similarity index 92% rename from bukkit/paper/src/main/java/com/discordsrv/bukkit/player/PlayerLocaleProvider.java rename to bukkit/paper/src/main/java/com/discordsrv/bukkit/player/PaperPlayer.java index 41d4aa32..dbe9f7f9 100644 --- a/bukkit/paper/src/main/java/com/discordsrv/bukkit/player/PlayerLocaleProvider.java +++ b/bukkit/paper/src/main/java/com/discordsrv/bukkit/player/PaperPlayer.java @@ -4,9 +4,9 @@ import org.bukkit.entity.Player; import java.util.Locale; -public final class PlayerLocaleProvider { +public final class PaperPlayer { - private PlayerLocaleProvider() {} + private PaperPlayer() {} private static final boolean localeMethodExists; private static final boolean getLocaleMethodExists; diff --git a/bukkit/spigot/src/main/java/com/discordsrv/bukkit/player/SpigotPlayer.java b/bukkit/spigot/src/main/java/com/discordsrv/bukkit/player/SpigotPlayer.java new file mode 100644 index 00000000..0e99e427 --- /dev/null +++ b/bukkit/spigot/src/main/java/com/discordsrv/bukkit/player/SpigotPlayer.java @@ -0,0 +1,45 @@ +package com.discordsrv.bukkit.player; + +import com.discordsrv.common.player.provider.model.SkinInfo; +import org.bukkit.entity.Player; +import org.bukkit.profile.PlayerTextures; + +import java.net.URL; +import java.util.Locale; + +public final class SpigotPlayer { + + private SpigotPlayer() {} + + private static final boolean playerProfileExists; + + static { + Class playerClass = Player.class; + + boolean playerProfile = false; + try { + playerClass.getMethod("getPlayerProfile"); + playerProfile = true; + } catch (ReflectiveOperationException ignored) {} + playerProfileExists = playerProfile; + } + + public static SkinInfo getSkinInfo(Player player) { + if (!playerProfileExists) { + return null; + } + + PlayerTextures textures = player.getPlayerProfile().getTextures(); + + URL skinUrl = textures.getSkin(); + if (skinUrl == null) { + return null; + } + + String skinUrlPlain = skinUrl.toString(); + return new SkinInfo( + skinUrlPlain.substring(skinUrlPlain.lastIndexOf('/') + 1), + textures.getSkinModel().toString().toLowerCase(Locale.ROOT) + ); + } +} diff --git a/bukkit/src/main/java/com/discordsrv/bukkit/player/BukkitOfflinePlayer.java b/bukkit/src/main/java/com/discordsrv/bukkit/player/BukkitOfflinePlayer.java index 4ddf8c4b..bb7c24c4 100644 --- a/bukkit/src/main/java/com/discordsrv/bukkit/player/BukkitOfflinePlayer.java +++ b/bukkit/src/main/java/com/discordsrv/bukkit/player/BukkitOfflinePlayer.java @@ -21,9 +21,11 @@ package com.discordsrv.bukkit.player; import com.discordsrv.bukkit.BukkitDiscordSRV; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.player.IOfflinePlayer; +import com.discordsrv.common.player.provider.model.SkinInfo; import net.kyori.adventure.identity.Identity; import org.bukkit.OfflinePlayer; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; public class BukkitOfflinePlayer implements IOfflinePlayer { @@ -47,6 +49,11 @@ public class BukkitOfflinePlayer implements IOfflinePlayer { return offlinePlayer.getName(); } + @Override + public @Nullable SkinInfo skinInfo() { + return null; + } + @Override public @NotNull Identity identity() { return identity; diff --git a/bukkit/src/main/java/com/discordsrv/bukkit/player/BukkitPlayer.java b/bukkit/src/main/java/com/discordsrv/bukkit/player/BukkitPlayer.java index dde659f2..2e013e57 100644 --- a/bukkit/src/main/java/com/discordsrv/bukkit/player/BukkitPlayer.java +++ b/bukkit/src/main/java/com/discordsrv/bukkit/player/BukkitPlayer.java @@ -24,10 +24,12 @@ import com.discordsrv.bukkit.component.PaperComponentHandle; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.component.util.ComponentUtil; import com.discordsrv.common.player.IPlayer; +import com.discordsrv.common.player.provider.model.SkinInfo; import net.kyori.adventure.identity.Identity; import net.kyori.adventure.text.Component; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.Locale; @@ -62,9 +64,14 @@ public class BukkitPlayer extends BukkitCommandSender implements IPlayer { return player.getName(); } + @Override + public @Nullable SkinInfo skinInfo() { + return SpigotPlayer.getSkinInfo(player); + } + @Override public Locale locale() { - return PlayerLocaleProvider.getLocale(player); + return PaperPlayer.getLocale(player); } @Override diff --git a/bukkit/src/main/java/com/discordsrv/bukkit/player/BukkitPlayerProvider.java b/bukkit/src/main/java/com/discordsrv/bukkit/player/BukkitPlayerProvider.java index 1ff97277..7b2c0be0 100644 --- a/bukkit/src/main/java/com/discordsrv/bukkit/player/BukkitPlayerProvider.java +++ b/bukkit/src/main/java/com/discordsrv/bukkit/player/BukkitPlayerProvider.java @@ -20,6 +20,7 @@ package com.discordsrv.bukkit.player; import com.discordsrv.bukkit.BukkitDiscordSRV; import com.discordsrv.common.player.IOfflinePlayer; +import com.discordsrv.common.player.IPlayer; import com.discordsrv.common.player.ServerPlayerProvider; import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; @@ -87,13 +88,23 @@ public class BukkitPlayerProvider extends ServerPlayerProvider offlinePlayer(UUID uuid) { + public CompletableFuture lookupOfflinePlayer(UUID uuid) { + IPlayer player = player(uuid); + if (player != null) { + return CompletableFuture.completedFuture(player); + } + return getFuture(() -> discordSRV.server().getOfflinePlayer(uuid)); } @SuppressWarnings("deprecation") // Shut up, I know @Override - public CompletableFuture offlinePlayer(String username) { + public CompletableFuture lookupOfflinePlayer(String username) { + IPlayer player = player(username); + if (player != null) { + return CompletableFuture.completedFuture(player); + } + return getFuture(() -> discordSRV.server().getOfflinePlayer(username)); } diff --git a/bungee/src/main/java/com/discordsrv/bungee/player/BungeePlayer.java b/bungee/src/main/java/com/discordsrv/bungee/player/BungeePlayer.java index c262f4f5..5e5b7f5e 100644 --- a/bungee/src/main/java/com/discordsrv/bungee/player/BungeePlayer.java +++ b/bungee/src/main/java/com/discordsrv/bungee/player/BungeePlayer.java @@ -23,6 +23,7 @@ import com.discordsrv.bungee.command.game.sender.BungeeCommandSender; import com.discordsrv.bungee.component.util.BungeeComponentUtil; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.player.IPlayer; +import com.discordsrv.common.player.provider.model.SkinInfo; import net.kyori.adventure.identity.Identity; import net.kyori.adventure.text.Component; import net.md_5.bungee.api.connection.ProxiedPlayer; @@ -52,6 +53,11 @@ public class BungeePlayer extends BungeeCommandSender implements IPlayer { return commandSender.getName(); } + @Override + public @Nullable SkinInfo skinInfo() { + return null; + } + @Override public @Nullable Locale locale() { return player.getLocale(); diff --git a/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java b/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java index 5525be06..2fd86583 100644 --- a/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java +++ b/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java @@ -101,6 +101,7 @@ import java.lang.reflect.Constructor; import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Path; +import java.time.Duration; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; @@ -671,7 +672,7 @@ public abstract class AbstractDiscordSRV< scheduler().run(() -> updateChecker.check(true)); } if (initial) { - scheduler().runAtFixedRate(() -> updateChecker.check(false), 6L, TimeUnit.HOURS); + scheduler().runAtFixedRate(() -> updateChecker.check(false), Duration.ofHours(6)); } if (flags.contains(ReloadFlag.LINKED_ACCOUNT_PROVIDER)) { diff --git a/common/src/main/java/com/discordsrv/common/channel/TimedUpdaterModule.java b/common/src/main/java/com/discordsrv/common/channel/TimedUpdaterModule.java index ff0fbc94..edbde099 100644 --- a/common/src/main/java/com/discordsrv/common/channel/TimedUpdaterModule.java +++ b/common/src/main/java/com/discordsrv/common/channel/TimedUpdaterModule.java @@ -31,6 +31,7 @@ import net.dv8tion.jda.api.managers.channel.ChannelManager; import net.dv8tion.jda.api.managers.channel.concrete.TextChannelManager; import org.apache.commons.lang3.StringUtils; +import java.time.Duration; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -76,9 +77,8 @@ public class TimedUpdaterModule extends AbstractModule { futures.add(discordSRV.scheduler().runAtFixedRate( () -> update(updaterConfig), - firstReload ? 0 : time, - time, - TimeUnit.SECONDS + firstReload ? Duration.ZERO : Duration.ofSeconds(time), + Duration.ofSeconds(time) )); } firstReload = false; diff --git a/common/src/main/java/com/discordsrv/common/command/combined/commands/LinkInitCommand.java b/common/src/main/java/com/discordsrv/common/command/combined/commands/LinkInitCommand.java index 7e4d5423..68d1f135 100644 --- a/common/src/main/java/com/discordsrv/common/command/combined/commands/LinkInitCommand.java +++ b/common/src/main/java/com/discordsrv/common/command/combined/commands/LinkInitCommand.java @@ -1,8 +1,10 @@ package com.discordsrv.common.command.combined.commands; +import com.discordsrv.api.discord.entity.DiscordUser; import com.discordsrv.api.discord.entity.interaction.command.CommandOption; import com.discordsrv.api.discord.entity.interaction.command.DiscordCommand; import com.discordsrv.api.discord.entity.interaction.component.ComponentIdentifier; +import com.discordsrv.api.placeholder.provider.SinglePlaceholder; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.command.combined.abstraction.CombinedCommand; import com.discordsrv.common.command.combined.abstraction.CommandExecution; @@ -12,15 +14,22 @@ import com.discordsrv.common.command.game.abstraction.GameCommand; import com.discordsrv.common.command.game.sender.ICommandSender; import com.discordsrv.common.command.util.CommandUtil; import com.discordsrv.common.component.util.ComponentUtil; +import com.discordsrv.common.config.messages.MessagesConfig; +import com.discordsrv.common.future.util.CompletableFutureUtil; import com.discordsrv.common.linking.LinkProvider; import com.discordsrv.common.linking.LinkStore; +import com.discordsrv.common.logging.Logger; +import com.discordsrv.common.logging.NamedLogger; import com.discordsrv.common.permission.Permission; +import com.discordsrv.common.player.IOfflinePlayer; import com.discordsrv.common.player.IPlayer; import com.github.benmanes.caffeine.cache.Cache; import net.kyori.adventure.text.format.NamedTextColor; import org.apache.commons.lang3.StringUtils; +import java.time.Duration; import java.util.UUID; +import java.util.concurrent.CompletableFuture; public class LinkInitCommand extends CombinedCommand { @@ -74,11 +83,13 @@ public class LinkInitCommand extends CombinedCommand { } private final DiscordSRV discordSRV; + private final Logger logger; private final Cache linkCheckRateLimit; public LinkInitCommand(DiscordSRV discordSRV) { super(discordSRV); this.discordSRV = discordSRV; + this.logger = new NamedLogger(discordSRV, "LINK_COMMAND"); this.linkCheckRateLimit = discordSRV.caffeineBuilder() .expireAfterWrite(LinkStore.LINKING_CODE_RATE_LIMIT) .build(); @@ -95,7 +106,7 @@ public class LinkInitCommand extends CombinedCommand { if (sender instanceof IPlayer) { startLinking((IPlayer) sender, ((GameCommandExecution) execution).getLabel()); } else { - sender.sendMessage(execution.messages().minecraft.pleaseSpecifyPlayerAndUserToLink.asComponent()); + sender.sendMessage(discordSRV.messagesConfig(sender).pleaseSpecifyPlayerAndUserToLink.asComponent()); } return; } @@ -112,84 +123,135 @@ public class LinkInitCommand extends CombinedCommand { return; } - UUID playerUUID = CommandUtil.lookupPlayer(discordSRV, execution, false, playerArgument, null); - if (playerUUID == null) { - execution.send( - execution.messages().minecraft.playerNotFound.asComponent(), - execution.messages().discord.playerNotFound - ); - return; - } + CompletableFuture playerUUIDFuture = CommandUtil.lookupPlayer(discordSRV, logger, execution, false, playerArgument, null); + CompletableFuture userIdFuture = CommandUtil.lookupUser(discordSRV, logger, execution, false, userArgument, null); - Long userId = CommandUtil.lookupUser(discordSRV, execution, false, userArgument, null); - if (userId == null) { - execution.send( - execution.messages().minecraft.userNotFound.asComponent(), - execution.messages().discord.userNotFound - ); - return; - } - - linkProvider.queryUserId(playerUUID).thenCompose(opt -> { - if (opt.isPresent()) { + playerUUIDFuture.whenComplete((playerUUID, __) -> userIdFuture.whenComplete((userId, ___) -> { + if (playerUUID == null) { execution.send( - execution.messages().minecraft.playerAlreadyLinked3rd.asComponent(), - execution.messages().discord.playerAlreadyLinked3rd + execution.messages().minecraft.playerNotFound.asComponent(), + execution.messages().discord.playerNotFound ); - return null; - } - - return linkProvider.queryPlayerUUID(userId); - }).thenCompose(opt -> { - if (opt.isPresent()) { - execution.send( - execution.messages().minecraft.userAlreadyLinked3rd.asComponent(), - execution.messages().discord.userAlreadyLinked3rd - ); - return null; - } - - return ((LinkStore) linkProvider).createLink(playerUUID, userId); - }).whenComplete((v, t) -> { - if (t != null) { - // TODO: it did not work return; } - execution.send( - execution.messages().minecraft.nowLinked3rd.asComponent(), - execution.messages().discord.nowLinked3rd + if (userId == null) { + execution.send( + execution.messages().minecraft.userNotFound.asComponent(), + execution.messages().discord.userNotFound + ); + return; + } + + CompletableFuture playerFuture = CompletableFutureUtil.timeout( + discordSRV, + discordSRV.playerProvider().lookupOfflinePlayer(playerUUID), + Duration.ofSeconds(5) ); - }); + CompletableFuture userFuture = CompletableFutureUtil.timeout( + discordSRV, + discordSRV.discordAPI().retrieveUserById(userId), + Duration.ofSeconds(5) + ); + + linkProvider.queryUserId(playerUUID).whenComplete((linkedUser, t) -> { + if (t != null) { + logger.error("Failed to check linking status", t); + execution.send( + execution.messages().minecraft.unableToCheckLinkingStatus.asComponent(), + execution.messages().discord.unableToCheckLinkingStatus + ); + return; + } + if (linkedUser.isPresent()) { + execution.send( + execution.messages().minecraft.playerAlreadyLinked3rd.asComponent(), + execution.messages().discord.playerAlreadyLinked3rd + ); + return; + } + + linkProvider.queryPlayerUUID(userId).whenComplete((linkedPlayer, t2) -> { + if (t2 != null) { + logger.error("Failed to check linking status", t2); + execution.send( + execution.messages().minecraft.unableToCheckLinkingStatus.asComponent(), + execution.messages().discord.unableToCheckLinkingStatus + ); + return; + } + if (linkedPlayer.isPresent()) { + execution.send( + execution.messages().minecraft.userAlreadyLinked3rd.asComponent(), + execution.messages().discord.userAlreadyLinked3rd + ); + return; + } + + ((LinkStore) linkProvider).createLink(playerUUID, userId).whenComplete((v, t3) -> { + if (t3 != null) { + logger.error("Failed to check linking status", t3); + execution.send( + execution.messages().minecraft.unableToLinkAtThisTime.asComponent(), + execution.messages().discord.unableToCheckLinkingStatus + ); + return; + } + + userFuture.whenComplete((user, ____) -> playerFuture.whenComplete((player, _____) -> execution.send( + ComponentUtil.fromAPI( + execution.messages().minecraft.nowLinked3rd.textBuilder() + .applyPlaceholderService() + .addContext(user, player) + .addPlaceholder("user_id", userId) + .addPlaceholder("player_uuid", playerUUID) + .build() + ), + discordSRV.placeholderService().replacePlaceholders( + execution.messages().discord.nowLinked3rd, + user, + player, + new SinglePlaceholder("user_id", userId), + new SinglePlaceholder("player_uuid", playerUUID) + ) + ))); + }); + }); + }); + })); } private void startLinking(IPlayer player, String label) { + MessagesConfig.Minecraft messages = discordSRV.messagesConfig(player); + LinkProvider linkProvider = discordSRV.linkProvider(); if (linkProvider.getCachedUserId(player.uniqueId()).isPresent()) { - player.sendMessage(discordSRV.messagesConfig(player).alreadyLinked1st.asComponent()); + player.sendMessage(messages.alreadyLinked1st.asComponent()); return; } if (linkCheckRateLimit.getIfPresent(player.uniqueId()) != null) { - player.sendMessage(discordSRV.messagesConfig(player).pleaseWaitBeforeRunningThatCommandAgain.asComponent()); + player.sendMessage(messages.pleaseWaitBeforeRunningThatCommandAgain.asComponent()); return; } linkCheckRateLimit.put(player.uniqueId(), true); player.sendMessage(discordSRV.messagesConfig(player).checkingLinkStatus.asComponent()); - linkProvider.queryUserId(player.uniqueId(), true).whenComplete((userId, t) -> { - if (t != null) { - player.sendMessage(discordSRV.messagesConfig(player).unableToLinkAtThisTime.asComponent()); + linkProvider.queryUserId(player.uniqueId(), true).whenComplete((userId, t1) -> { + if (t1 != null) { + logger.error("Failed to check linking status", t1); + player.sendMessage(messages.unableToLinkAtThisTime.asComponent()); return; } if (userId.isPresent()) { - player.sendMessage(discordSRV.messagesConfig(player).nowLinked1st.asComponent()); + player.sendMessage(messages.nowLinked1st.asComponent()); return; } linkProvider.getLinkingInstructions(player, label).whenComplete((comp, t2) -> { if (t2 != null) { - player.sendMessage(discordSRV.messagesConfig(player).unableToLinkAtThisTime.asComponent()); + logger.error("Failed to link account", t2); + player.sendMessage(messages.unableToLinkAtThisTime.asComponent()); return; } diff --git a/common/src/main/java/com/discordsrv/common/command/combined/commands/LinkedCommand.java b/common/src/main/java/com/discordsrv/common/command/combined/commands/LinkedCommand.java index 21992c74..88878abe 100644 --- a/common/src/main/java/com/discordsrv/common/command/combined/commands/LinkedCommand.java +++ b/common/src/main/java/com/discordsrv/common/command/combined/commands/LinkedCommand.java @@ -1,17 +1,25 @@ package com.discordsrv.common.command.combined.commands; +import com.discordsrv.api.discord.entity.DiscordUser; import com.discordsrv.api.discord.entity.interaction.command.CommandOption; import com.discordsrv.api.discord.entity.interaction.command.DiscordCommand; import com.discordsrv.api.discord.entity.interaction.component.ComponentIdentifier; +import com.discordsrv.api.placeholder.provider.SinglePlaceholder; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.command.combined.abstraction.CombinedCommand; import com.discordsrv.common.command.combined.abstraction.CommandExecution; -import com.discordsrv.common.command.combined.abstraction.Text; import com.discordsrv.common.command.game.abstraction.GameCommand; import com.discordsrv.common.command.util.CommandUtil; +import com.discordsrv.common.component.util.ComponentUtil; +import com.discordsrv.common.future.util.CompletableFutureUtil; +import com.discordsrv.common.logging.Logger; +import com.discordsrv.common.logging.NamedLogger; import com.discordsrv.common.permission.Permission; +import com.discordsrv.common.player.IOfflinePlayer; +import java.time.Duration; import java.util.UUID; +import java.util.concurrent.CompletableFuture; public class LinkedCommand extends CombinedCommand { @@ -28,7 +36,7 @@ public class LinkedCommand extends CombinedCommand { LinkedCommand command = getInstance(discordSRV); GAME = GameCommand.literal("linked") .then( - GameCommand.stringWord("target") + GameCommand.stringGreedy("target") .requiredPermission(Permission.COMMAND_LINKED_OTHER) .executor(command) ) @@ -60,30 +68,154 @@ public class LinkedCommand extends CombinedCommand { return DISCORD; } + private final Logger logger; + public LinkedCommand(DiscordSRV discordSRV) { super(discordSRV); + this.logger = new NamedLogger(discordSRV, "LINKED_COMMAND"); } @Override public void execute(CommandExecution execution) { execution.setEphemeral(true); - CommandUtil.TargetLookupResult result = CommandUtil.lookupTarget(discordSRV, execution, true, Permission.COMMAND_LINKED_OTHER); - if (!result.isValid()) { - return; - } + execution.runAsync(() -> CommandUtil.lookupTarget(discordSRV, logger, execution, true, Permission.COMMAND_LINKED_OTHER) + .whenComplete((result, t) -> { + if (t != null) { + logger.error("Failed to execute linked command", t); + return; + } + if (result.isValid()) { + processResult(result, execution); + } + }) + ); + } + private void processResult(CommandUtil.TargetLookupResult result, CommandExecution execution) { if (result.isPlayer()) { - execution.runAsync(() -> { - discordSRV.linkProvider().getUserId(result.getPlayerUUID()).whenComplete((userId, t) -> { - execution.send(new Text(userId.map(Long::toUnsignedString).orElse("Not linked"))); // TODO: username - }); + UUID playerUUID = result.getPlayerUUID(); + CompletableFuture playerFuture = CompletableFutureUtil.timeout( + discordSRV, + discordSRV.playerProvider().lookupOfflinePlayer(playerUUID), + Duration.ofSeconds(5) + ); + + discordSRV.linkProvider().getUserId(playerUUID).whenComplete((optUserId, t) -> { + if (t != null) { + logger.error("Failed to check linking status during linked command", t); + execution.send( + execution.messages().minecraft.unableToCheckLinkingStatus.asComponent(), + execution.messages().discord.unableToCheckLinkingStatus + ); + return; + } + if (!optUserId.isPresent()) { + playerFuture.whenComplete((player, ___) -> execution.send( + ComponentUtil.fromAPI( + execution.messages().minecraft.minecraftPlayerUnlinked + .textBuilder() + .applyPlaceholderService() + .addContext(player) + .addPlaceholder("player_uuid", playerUUID) + .build() + ), + discordSRV.placeholderService().replacePlaceholders( + execution.messages().discord.minecraftPlayerUnlinked, + player, + new SinglePlaceholder("player_uuid", playerUUID) + ) + )); + return; + } + + long userId = optUserId.get(); + CompletableFuture userFuture = CompletableFutureUtil.timeout( + discordSRV, + discordSRV.discordAPI().retrieveUserById(userId), + Duration.ofSeconds(5) + ); + + playerFuture.whenComplete((player, __) -> userFuture.whenComplete((user, ___) -> execution.send( + ComponentUtil.fromAPI( + execution.messages().minecraft.minecraftPlayerLinkedTo + .textBuilder() + .applyPlaceholderService() + .addContext(player, user) + .addPlaceholder("player_uuid", playerUUID) + .addPlaceholder("user_id", userId) + .build() + ), + discordSRV.placeholderService().replacePlaceholders( + execution.messages().discord.minecraftPlayerLinkedTo, + player, + user, + new SinglePlaceholder("player_uuid", playerUUID), + new SinglePlaceholder("user_id", userId) + ) + ))); }); } else { - execution.runAsync(() -> { - discordSRV.linkProvider().getPlayerUUID(result.getUserId()).whenComplete((playerUUID, t) -> { - execution.send(new Text(playerUUID.map(UUID::toString).orElse("Not linked"))); // TODO: player name - }); + long userId = result.getUserId(); + CompletableFuture userFuture = CompletableFutureUtil.timeout( + discordSRV, + discordSRV.discordAPI().retrieveUserById(userId), + Duration.ofSeconds(5) + ); + + discordSRV.linkProvider().getPlayerUUID(userId).whenComplete((optPlayerUUID, t) -> { + if (t != null) { + logger.error("Failed to check linking status during linked command", t); + execution.send( + execution.messages().minecraft.unableToCheckLinkingStatus.asComponent(), + execution.messages().discord.unableToCheckLinkingStatus + ); + return; + } + if (!optPlayerUUID.isPresent()) { + userFuture.whenComplete((user, ___) -> execution.send( + ComponentUtil.fromAPI( + execution.messages().minecraft.discordUserUnlinked + .textBuilder() + .applyPlaceholderService() + .addContext(user) + .addPlaceholder("user_id", userId) + .build() + ), + discordSRV.placeholderService().replacePlaceholders( + execution.messages().discord.discordUserUnlinked, + user, + new SinglePlaceholder("user_id", userId) + ) + )); + return; + } + + UUID playerUUID = optPlayerUUID.get(); + CompletableFuture playerFuture = CompletableFutureUtil.timeout( + discordSRV, + discordSRV.playerProvider().lookupOfflinePlayer(playerUUID), + Duration.ofSeconds(5) + ); + + userFuture.whenComplete((user, __) -> playerFuture.whenComplete((player, ___) -> execution.send( + ComponentUtil.fromAPI( + execution.messages().minecraft.discordUserLinkedTo + .textBuilder() + .applyPlaceholderService() + .addContext(user, player) + .addPlaceholder("user_id", userId) + .addPlaceholder("player_uuid", playerUUID) + .build() + ), + discordSRV.placeholderService().replacePlaceholders( + execution.messages().discord.discordUserLinkedTo, + user, + player, + new SinglePlaceholder("user_id", userId), + new SinglePlaceholder("player_uuid", playerUUID) + ) + ))); }); } } diff --git a/common/src/main/java/com/discordsrv/common/command/discord/commands/subcommand/ExecuteCommand.java b/common/src/main/java/com/discordsrv/common/command/discord/commands/subcommand/ExecuteCommand.java index 6803fc62..d03aec85 100644 --- a/common/src/main/java/com/discordsrv/common/command/discord/commands/subcommand/ExecuteCommand.java +++ b/common/src/main/java/com/discordsrv/common/command/discord/commands/subcommand/ExecuteCommand.java @@ -18,6 +18,7 @@ import net.dv8tion.jda.api.interactions.InteractionHook; import net.dv8tion.jda.api.interactions.commands.OptionMapping; import net.kyori.adventure.text.Component; +import java.time.Duration; import java.util.*; import java.util.concurrent.*; import java.util.function.Consumer; @@ -207,7 +208,7 @@ public class ExecuteCommand implements Consumer lookupPlayer( DiscordSRV discordSRV, + Logger logger, CommandExecution execution, boolean selfPermitted, String target, @Nullable Permission otherPermission ) { - TargetLookupResult result = lookupTarget(discordSRV, execution, target, selfPermitted, true, false, otherPermission); - if (result.isValid()) { - return result.getPlayerUUID(); - } - return null; + return lookupTarget(discordSRV, logger, execution, target, selfPermitted, true, false, otherPermission) + .thenApply((result) -> { + if (result != null && result.isValid()) { + return result.getPlayerUUID(); + } + return null; + }); } - @Nullable - public static Long lookupUser( + public static CompletableFuture lookupUser( DiscordSRV discordSRV, + Logger logger, CommandExecution execution, boolean selfPermitted, String target, @Nullable Permission otherPermission ) { - TargetLookupResult result = lookupTarget(discordSRV, execution, target, selfPermitted, false, true, otherPermission); - if (result.isValid()) { - return result.getUserId(); - } - return null; + return lookupTarget(discordSRV, logger, execution, target, selfPermitted, false, true, otherPermission) + .thenApply(result -> { + if (result != null && result.isValid()) { + return result.getUserId(); + } + return null; + }); } - public static TargetLookupResult lookupTarget( + public static CompletableFuture lookupTarget( DiscordSRV discordSRV, + Logger logger, CommandExecution execution, boolean selfPermitted, @Nullable Permission otherPermission @@ -63,11 +71,12 @@ public final class CommandUtil { if (target == null) { target = execution.getArgument("player"); } - return lookupTarget(discordSRV, execution, target, selfPermitted, true, true, otherPermission); + return lookupTarget(discordSRV, logger, execution, target, selfPermitted, true, true, otherPermission); } - private static TargetLookupResult lookupTarget( + private static CompletableFuture lookupTarget( DiscordSRV discordSRV, + Logger logger, CommandExecution execution, String target, boolean selfPermitted, @@ -82,16 +91,10 @@ public final class CommandUtil { if (target != null) { if (otherPermission != null && !sender.hasPermission(Permission.COMMAND_LINKED_OTHER)) { sender.sendMessage(discordSRV.messagesConfig(sender).noPermission.asComponent()); - return TargetLookupResult.INVALID; + return CompletableFuture.completedFuture(TargetLookupResult.INVALID); } } else if (sender instanceof IPlayer && selfPermitted && lookupPlayer) { target = ((IPlayer) sender).uniqueId().toString(); - } else { - execution.send( - messages.minecraft.pleaseSpecifyPlayer.asComponent(), - messages.discord.pleaseSpecifyPlayer - ); - return TargetLookupResult.INVALID; } } else if (execution instanceof DiscordCommandExecution) { if (target == null) { @@ -102,13 +105,17 @@ public final class CommandUtil { messages.minecraft.pleaseSpecifyUser.asComponent(), messages.discord.pleaseSpecifyUser ); - return TargetLookupResult.INVALID; + return CompletableFuture.completedFuture(TargetLookupResult.INVALID); } } } else { throw new IllegalStateException("Unexpected CommandExecution"); } + if (target == null) { + return CompletableFuture.completedFuture(requireTarget(execution, lookupUser, lookupPlayer, messages)); + } + if (lookupUser) { if (target.matches("\\d{17,22}")) { // Discord user id @@ -120,10 +127,10 @@ public final class CommandUtil { messages.minecraft.userNotFound.asComponent(), messages.discord.userNotFound ); - return TargetLookupResult.INVALID; + return CompletableFuture.completedFuture(TargetLookupResult.INVALID); } - return new TargetLookupResult(true, null, id); + return CompletableFuture.completedFuture(new TargetLookupResult(true, null, id)); } else if (target.startsWith("@")) { // Discord username String username = target.substring(1); @@ -132,7 +139,7 @@ public final class CommandUtil { List users = jda.getUsersByName(username, true); if (users.size() == 1) { - return new TargetLookupResult(true, null, users.get(0).getIdLong()); + return CompletableFuture.completedFuture(new TargetLookupResult(true, null, users.get(0).getIdLong())); } } } @@ -143,34 +150,62 @@ public final class CommandUtil { boolean shortUUID; if ((shortUUID = target.length() == 32) || target.length() == 36) { // Player UUID - if (shortUUID) { - target = target.substring(0, 8) + "-" + target.substring(8, 12) + "-" + target.substring(12, 16) - + "-" + target.substring(16, 20) + "-" + target.substring(20); - } - try { - uuid = UUID.fromString(target); + if (shortUUID) { + uuid = UUIDUtil.fromShort(target); + } else { + uuid = UUID.fromString(target); + } } catch (IllegalArgumentException ignored) { execution.send( messages.minecraft.playerNotFound.asComponent(), messages.discord.playerNotFound ); - return TargetLookupResult.INVALID; + return CompletableFuture.completedFuture(TargetLookupResult.INVALID); } - } else { + return CompletableFuture.completedFuture(new TargetLookupResult(true, uuid, 0L)); + } else if (target.matches("[a-zA-Z0-9_]{1,16}")) { // Player name IPlayer playerByName = discordSRV.playerProvider().player(target); if (playerByName != null) { uuid = playerByName.uniqueId(); } else { - throw new IllegalStateException("lookup offline"); // TODO: lookup offline player + return discordSRV.playerProvider().lookupOfflinePlayer(target) + .thenApply(offlinePlayer -> new TargetLookupResult(true, offlinePlayer.uniqueId(), 0L)) + .exceptionally(t -> { + logger.error("Failed to lookup offline player by username", t); + return TargetLookupResult.INVALID; + }); } + return CompletableFuture.completedFuture(new TargetLookupResult(true, uuid, 0L)); } - - return new TargetLookupResult(true, uuid, 0L); } - return TargetLookupResult.INVALID; + return CompletableFuture.completedFuture(requireTarget(execution, lookupUser, lookupPlayer, messages)); + } + + private static TargetLookupResult requireTarget(CommandExecution execution, boolean lookupUser, boolean lookupPlayer, MessagesConfig messages) { + if (lookupPlayer && lookupUser) { + execution.send( + messages.minecraft.pleaseSpecifyPlayerOrUser.asComponent(), + messages.discord.pleaseSpecifyPlayerOrUser + ); + return TargetLookupResult.INVALID; + } else if (lookupPlayer) { + execution.send( + messages.minecraft.pleaseSpecifyPlayer.asComponent(), + messages.discord.pleaseSpecifyPlayer + ); + return TargetLookupResult.INVALID; + } else if (lookupUser) { + execution.send( + messages.minecraft.pleaseSpecifyUser.asComponent(), + messages.discord.pleaseSpecifyUser + ); + return TargetLookupResult.INVALID; + } else { + throw new IllegalStateException("lookupPlayer & lookupUser are false"); + } } public static class TargetLookupResult { diff --git a/common/src/main/java/com/discordsrv/common/config/messages/MessagesConfig.java b/common/src/main/java/com/discordsrv/common/config/messages/MessagesConfig.java index 01eb74a7..37b25644 100644 --- a/common/src/main/java/com/discordsrv/common/config/messages/MessagesConfig.java +++ b/common/src/main/java/com/discordsrv/common/config/messages/MessagesConfig.java @@ -37,15 +37,17 @@ public class MessagesConfig implements Config { @Constants(ERROR_COLOR) public MinecraftMessage pleaseSpecifyUser = make("%1Please specify the Discord user"); @Constants(ERROR_COLOR) + public MinecraftMessage pleaseSpecifyPlayerOrUser = make("%1Please specify the Minecraft player or Discord user"); + @Constants(ERROR_COLOR) public MinecraftMessage playerNotFound = make("%1Minecraft player not found"); @Constants(ERROR_COLOR) public MinecraftMessage userNotFound = make("%1Discord user not found"); + @Constants(ERROR_COLOR) + public MinecraftMessage unableToCheckLinkingStatus = make("%1Unable to check linking status, please try again later"); @Untranslated(Untranslated.Type.COMMENT) @Comment("/discord link") @Constants(ERROR_COLOR) - public MinecraftMessage unableToCheckLinkingStatus = make("%1Unable to check linking status, please try again later"); - @Constants(ERROR_COLOR) public MinecraftMessage alreadyLinked1st = make("%1You are already linked"); @Constants(ERROR_COLOR) public MinecraftMessage pleaseSpecifyPlayerAndUserToLink = make("%1Please specify the Minecraft player and the Discord user to link"); @@ -61,8 +63,13 @@ public class MessagesConfig implements Config { public MinecraftMessage checkingLinkStatus = make("%1Checking linking status..."); @Constants(SUCCESS_COLOR) public MinecraftMessage nowLinked1st = make("%1You are now linked!"); - @Constants(SUCCESS_COLOR) - public MinecraftMessage nowLinked3rd = make("%1Link created successfully"); + @Constants({ + SUCCESS_COLOR, + NEUTRAL_COLOR, + SUCCESS_COLOR + "[hover:show_text:%player_uuid%][click:copy_to_clipboard:%player_uuid%]%player_name|text:''%[click][hover]" + NEUTRAL_COLOR, + SUCCESS_COLOR + "[hover:show_text:%user_id%][click:copy_to_clipboard:%user_id%]@%user_name%[click][hover]" + NEUTRAL_COLOR + }) + public MinecraftMessage nowLinked3rd = make("%1Link created successfully %2(%3 and %4)"); @Constants({ NEUTRAL_COLOR, "&f[click:open_url:%minecraftauth_link%][hover:show_text:Click to open]%minecraftauth_link_simple%[click]" + NEUTRAL_COLOR, @@ -70,6 +77,38 @@ public class MessagesConfig implements Config { }) public MinecraftMessage minecraftAuthLinking = make("%1Please visit %2 to link your account through %4"); + @Untranslated(Untranslated.Type.COMMENT) + @Comment("/discord linked") + @Constants({ + SUCCESS_COLOR + "[hover:show_text:%user_id%][click:copy_to_clipboard:%user_id%]@%user_name%[click][hover]", + NEUTRAL_COLOR, + SUCCESS_COLOR + "[hover:show_text:%player_uuid%][click:copy_to_clipboard:%player_uuid%]%player_name|text:''%[click][hover]" + }) + public MinecraftMessage discordUserLinkedTo = make("%1 %2is linked to %3"); + + @Untranslated(Untranslated.Type.COMMENT) + @Comment("/discord linked") + @Constants({ + SUCCESS_COLOR + "[hover:show_text:%user_id%][click:copy_to_clipboard:%user_id%]@%user_name%[click][hover]", + NEUTRAL_COLOR, + ERROR_COLOR + }) + public MinecraftMessage discordUserUnlinked = make("%1 %2is %3unlinked"); + + @Constants({ + SUCCESS_COLOR + "[hover:show_text:%player_uuid%][click:copy_to_clipboard:%player_uuid%]%player_name|text:''%[click][hover]", + NEUTRAL_COLOR, + SUCCESS_COLOR + "[hover:show_text:%user_id%][click:copy_to_clipboard:%user_id%]@%user_name%[click][hover]" + }) + public MinecraftMessage minecraftPlayerLinkedTo = make("%1 %2is linked to %3"); + + @Constants({ + SUCCESS_COLOR + "[hover:show_text:%player_uuid%][click:copy_to_clipboard:%player_uuid%]%player_name|text:''%[click][hover]", + NEUTRAL_COLOR, + ERROR_COLOR + }) + public MinecraftMessage minecraftPlayerUnlinked = make("%1 %2is %3unlinked"); + } public Discord discord = new Discord(); @@ -86,10 +125,14 @@ public class MessagesConfig implements Config { public String pleaseSpecifyPlayer = "%1Please specify the Minecraft player"; @Constants(INPUT_ERROR_PREFIX) public String pleaseSpecifyUser = "%1Please specify the Discord user"; + @Constants(INPUT_ERROR_PREFIX) + public String pleaseSpecifyPlayerOrUser = "%1Please specify the Minecraft player or Discord user"; @Constants(ERROR_PREFIX) public String playerNotFound = "%1Minecraft player not found"; @Constants(ERROR_PREFIX) public String userNotFound = "%1Discord user not found"; + @Constants(ERROR_PREFIX) + public String unableToCheckLinkingStatus = "%1Unable to check linking status, please try again later"; @Untranslated(Untranslated.Type.COMMENT) @Comment("/discord link") @@ -97,7 +140,41 @@ public class MessagesConfig implements Config { public String playerAlreadyLinked3rd = "%1That Minecraft player is already linked"; @Constants(ERROR_PREFIX) public String userAlreadyLinked3rd = "%1That Discord user is already linked"; - @Constants(SUCCESS_PREFIX) - public String nowLinked3rd = "%1Link created successfully"; + @Constants({ + SUCCESS_PREFIX, + "**%player_name%** (%player_uuid%)", + "**%user_name%** (%user_id%)" + }) + public String nowLinked3rd = "%1Link created successfully\n%2 and %3"; + + @Untranslated(Untranslated.Type.COMMENT) + @Comment("/discord linked") + @Constants({ + SUCCESS_PREFIX, + "**%user_name%** (<@%user_id%>)", + "**%player_name%** (%player_uuid%)" + }) + public String discordUserLinkedTo = "%1%2 is linked to %3"; + + @Untranslated(Untranslated.Type.COMMENT) + @Comment("/discord linked") + @Constants({ + ERROR_PREFIX, + "**%user_name%** (%user_id%)" + }) + public String discordUserUnlinked = "%1%2 is __unlinked__"; + + @Constants({ + SUCCESS_PREFIX, + "**%player_name%** (%player_uuid%)", + "**%user_name%** (<@%user_id%>)" + }) + public String minecraftPlayerLinkedTo = "%1%2 is linked to %3"; + + @Constants({ + ERROR_PREFIX, + "**%player_name%** (%player_uuid%)" + }) + public String minecraftPlayerUnlinked = "%1%2 is __unlinked__"; } } diff --git a/common/src/main/java/com/discordsrv/common/console/SingleConsoleHandler.java b/common/src/main/java/com/discordsrv/common/console/SingleConsoleHandler.java index a07575ca..63fb85e9 100644 --- a/common/src/main/java/com/discordsrv/common/console/SingleConsoleHandler.java +++ b/common/src/main/java/com/discordsrv/common/console/SingleConsoleHandler.java @@ -26,6 +26,7 @@ import net.dv8tion.jda.api.entities.Message; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; +import java.time.Duration; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; @@ -195,7 +196,7 @@ public class SingleConsoleHandler { if (config.appender.outputMode == ConsoleConfig.OutputMode.OFF) { return; } - this.queueProcessingFuture = discordSRV.scheduler().runLater(this::processQueue, 2, TimeUnit.SECONDS); + this.queueProcessingFuture = discordSRV.scheduler().runLater(this::processQueue, Duration.ofSeconds(2)); } private void processQueue() { diff --git a/common/src/main/java/com/discordsrv/common/discord/connection/jda/JDAConnectionManager.java b/common/src/main/java/com/discordsrv/common/discord/connection/jda/JDAConnectionManager.java index f65614c0..04fd98af 100644 --- a/common/src/main/java/com/discordsrv/common/discord/connection/jda/JDAConnectionManager.java +++ b/common/src/main/java/com/discordsrv/common/discord/connection/jda/JDAConnectionManager.java @@ -69,6 +69,7 @@ import org.apache.commons.lang3.exception.ExceptionUtils; import org.jetbrains.annotations.NotNull; import java.io.InterruptedIOException; +import java.time.Duration; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; @@ -303,9 +304,8 @@ public class JDAConnectionManager implements DiscordConnectionManager { ); this.failureCallbackFuture = discordSRV.scheduler().runAtFixedRate( this::checkDefaultFailureCallback, - 30, - 120, - TimeUnit.SECONDS + Duration.ofSeconds(30), + Duration.ofSeconds(120) ); MemberCachingConfig memberCachingConfig = discordSRV.config().memberCaching; diff --git a/common/src/main/java/com/discordsrv/common/future/util/CompletableFutureUtil.java b/common/src/main/java/com/discordsrv/common/future/util/CompletableFutureUtil.java index eaf580c3..a15f24e6 100644 --- a/common/src/main/java/com/discordsrv/common/future/util/CompletableFutureUtil.java +++ b/common/src/main/java/com/discordsrv/common/future/util/CompletableFutureUtil.java @@ -18,10 +18,15 @@ package com.discordsrv.common.future.util; +import com.discordsrv.common.DiscordSRV; + +import java.time.Duration; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeoutException; public final class CompletableFutureUtil { @@ -58,4 +63,17 @@ public final class CompletableFutureUtil { }); return future; } + + public static CompletableFuture timeout(DiscordSRV discordSRV, CompletableFuture future, Duration timeout) { + ScheduledFuture scheduledFuture = discordSRV.scheduler().runLater(() -> { + if (!future.isDone()) { + future.completeExceptionally(new TimeoutException()); + } + }, timeout); + return future.whenComplete((__, t) -> { + if (t == null) { + scheduledFuture.cancel(false); + } + }); + } } diff --git a/common/src/main/java/com/discordsrv/common/groupsync/GroupSyncModule.java b/common/src/main/java/com/discordsrv/common/groupsync/GroupSyncModule.java index 248cfbe5..7acfb081 100644 --- a/common/src/main/java/com/discordsrv/common/groupsync/GroupSyncModule.java +++ b/common/src/main/java/com/discordsrv/common/groupsync/GroupSyncModule.java @@ -42,6 +42,7 @@ import com.github.benmanes.caffeine.cache.Cache; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.Nullable; +import java.time.Duration; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; @@ -127,9 +128,8 @@ public class GroupSyncModule extends AbstractModule { int cycleTime = timer.cycleTime; future = discordSRV.scheduler().runAtFixedRate( () -> resyncPair(pair, GroupSyncCause.TIMER), - cycleTime, - cycleTime, - TimeUnit.MINUTES + Duration.ofMinutes(cycleTime), + Duration.ofMinutes(cycleTime) ); } diff --git a/common/src/main/java/com/discordsrv/common/http/util/HttpUtil.java b/common/src/main/java/com/discordsrv/common/http/util/HttpUtil.java new file mode 100644 index 00000000..1565abf0 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/http/util/HttpUtil.java @@ -0,0 +1,45 @@ +package com.discordsrv.common.http.util; + +import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.exception.MessageException; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.apache.commons.lang3.StringUtils; + +import java.io.IOException; +import java.util.concurrent.CompletableFuture; + +public final class HttpUtil { + + private HttpUtil() {} + + public static ResponseBody checkIfResponseSuccessful(Request request, Response response) throws IOException { + ResponseBody responseBody = response.body(); + if (responseBody == null || !response.isSuccessful()) { + String responseString = responseBody == null + ? "response body is null" + : StringUtils.substring(responseBody.string(), 0, 500); + throw new MessageException("Request to " + request.url().host() + " failed: " + response.code() + ": " + responseString); + } + return responseBody; + } + + public static CompletableFuture readJson(DiscordSRV discordSRV, Request request, Class type) { + CompletableFuture future = new CompletableFuture<>(); + discordSRV.scheduler().run(() -> { + try (Response response = discordSRV.httpClient().newCall(request).execute()) { + ResponseBody responseBody = checkIfResponseSuccessful(request, response); + + T result = discordSRV.json().readValue(responseBody.byteStream(), type); + if (result == null) { + throw new MessageException("Response json cannot be parsed"); + } + future.complete(result); + } catch (Throwable t) { + future.completeExceptionally(t); + } + }); + return future; + } +} diff --git a/common/src/main/java/com/discordsrv/common/logging/impl/DiscordSRVLogger.java b/common/src/main/java/com/discordsrv/common/logging/impl/DiscordSRVLogger.java index 644a0f11..011d5ee3 100644 --- a/common/src/main/java/com/discordsrv/common/logging/impl/DiscordSRVLogger.java +++ b/common/src/main/java/com/discordsrv/common/logging/impl/DiscordSRVLogger.java @@ -37,13 +37,13 @@ import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.text.DateFormat; import java.text.SimpleDateFormat; +import java.time.Duration; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; public class DiscordSRVLogger implements Logger { @@ -111,7 +111,7 @@ public class DiscordSRVLogger implements Logger { } return logs; } catch (IOException e) { - e.printStackTrace(); + doLog("LOGGING", LogLevel.ERROR, "Failed to rotate log", e); return null; } } @@ -180,7 +180,7 @@ public class DiscordSRVLogger implements Logger { linesToWrite.add(entry); synchronized (lineProcessingLock) { if (lineProcessingFuture == null || lineProcessingFuture.isDone()) { - lineProcessingFuture = discordSRV.scheduler().runLater(this::processLines, TimeUnit.SECONDS.toMillis(2)); + lineProcessingFuture = discordSRV.scheduler().runLater(this::processLines, Duration.ofSeconds(2)); } } } diff --git a/common/src/main/java/com/discordsrv/common/messageforwarding/discord/DiscordChatMessageModule.java b/common/src/main/java/com/discordsrv/common/messageforwarding/discord/DiscordChatMessageModule.java index 06eea706..b935fa22 100644 --- a/common/src/main/java/com/discordsrv/common/messageforwarding/discord/DiscordChatMessageModule.java +++ b/common/src/main/java/com/discordsrv/common/messageforwarding/discord/DiscordChatMessageModule.java @@ -47,6 +47,7 @@ import net.kyori.adventure.text.Component; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; +import java.time.Duration; import java.util.Arrays; import java.util.Collection; import java.util.Map; @@ -133,7 +134,7 @@ public class DiscordChatMessageModule extends AbstractModule { MessageSend send = new MessageSend(message, gameChannel, config); sends.put(key, send); - send.setFuture(discordSRV.scheduler().runLater(() -> processSend(key), delayMillis)); + send.setFuture(discordSRV.scheduler().runLater(() -> processSend(key), Duration.ofMillis(delayMillis))); } } diff --git a/common/src/main/java/com/discordsrv/common/player/IOfflinePlayer.java b/common/src/main/java/com/discordsrv/common/player/IOfflinePlayer.java index b3913deb..c04d4990 100644 --- a/common/src/main/java/com/discordsrv/common/player/IOfflinePlayer.java +++ b/common/src/main/java/com/discordsrv/common/player/IOfflinePlayer.java @@ -20,6 +20,7 @@ package com.discordsrv.common.player; import com.discordsrv.api.placeholder.annotation.Placeholder; import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.player.provider.model.SkinInfo; import com.discordsrv.common.profile.Profile; import net.kyori.adventure.identity.Identified; import org.jetbrains.annotations.ApiStatus; @@ -48,4 +49,21 @@ public interface IOfflinePlayer extends Identified { default UUID uniqueId() { return identity().uuid(); } + + @Nullable + SkinInfo skinInfo(); + + @Placeholder("player_skin_texture_id") + @Nullable + default String skinTextureId() { + SkinInfo info = skinInfo(); + return info != null ? info.textureId() : null; + } + + @Placeholder("player_skin_model") + @Nullable + default String skinModel() { + SkinInfo info = skinInfo(); + return info != null ? info.model() : null; + } } diff --git a/common/src/main/java/com/discordsrv/common/player/IPlayer.java b/common/src/main/java/com/discordsrv/common/player/IPlayer.java index ba0c94cf..efc86b43 100644 --- a/common/src/main/java/com/discordsrv/common/player/IPlayer.java +++ b/common/src/main/java/com/discordsrv/common/player/IPlayer.java @@ -63,11 +63,6 @@ public interface IPlayer extends DiscordSRVPlayer, IOfflinePlayer, ICommandSende return uniqueId().toString().replace("-", ""); } - @Placeholder("player_texture") - default @Nullable String textureId() { - return null; // TODO: implement - } - @NotNull @Placeholder("player_display_name") Component displayName(); @@ -81,7 +76,7 @@ public interface IPlayer extends DiscordSRVPlayer, IOfflinePlayer, ICommandSende if (avatarConfig.autoDecideAvatarUrl) { // Offline mode - if (uniqueId().version() == 3) avatarUrlTemplate = "https://cravatar.eu/helmavatar/%player_name%/128.png#%texture%"; + if (uniqueId().version() == 3) avatarUrlTemplate = "https://cravatar.eu/helmavatar/%player_name%/128.png#%player_skin_texture_id%"; // Bedrock else if (uniqueId().getLeastSignificantBits() == 0) avatarUrlTemplate = "https://api.tydiumcraft.net/skin?uuid=%player_uuid_nodashes%&type=avatar&size=128"; } diff --git a/common/src/main/java/com/discordsrv/common/player/OfflinePlayer.java b/common/src/main/java/com/discordsrv/common/player/OfflinePlayer.java new file mode 100644 index 00000000..1e99ce30 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/player/OfflinePlayer.java @@ -0,0 +1,44 @@ +package com.discordsrv.common.player; + +import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.player.provider.model.SkinInfo; +import net.kyori.adventure.identity.Identity; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.UUID; + +public class OfflinePlayer implements IOfflinePlayer { + + private final DiscordSRV discordSRV; + private final String username; + private final Identity identity; + private final SkinInfo skinInfo; + + public OfflinePlayer(DiscordSRV discordSRV, String username, UUID uuid, SkinInfo skinInfo) { + this.discordSRV = discordSRV; + this.username = username; + this.identity = Identity.identity(uuid); + this.skinInfo = skinInfo; + } + + @Override + public DiscordSRV discordSRV() { + return discordSRV; + } + + @Override + public @Nullable String username() { + return username; + } + + @Override + public @NotNull Identity identity() { + return identity; + } + + @Override + public @Nullable SkinInfo skinInfo() { + return skinInfo; + } +} diff --git a/common/src/main/java/com/discordsrv/common/player/ServerPlayerProvider.java b/common/src/main/java/com/discordsrv/common/player/ServerPlayerProvider.java index 6a99062d..6bd51ab7 100644 --- a/common/src/main/java/com/discordsrv/common/player/ServerPlayerProvider.java +++ b/common/src/main/java/com/discordsrv/common/player/ServerPlayerProvider.java @@ -30,6 +30,14 @@ public abstract class ServerPlayerProvider offlinePlayer(UUID uuid); - public abstract CompletableFuture offlinePlayer(String username); + @Override + public CompletableFuture lookupUUIDForUsername(String username) { + return lookupOfflinePlayer(username).thenApply(IOfflinePlayer::uniqueId); + } + + @Override + public abstract CompletableFuture lookupOfflinePlayer(String username); + + @Override + public abstract CompletableFuture lookupOfflinePlayer(UUID uuid); } diff --git a/common/src/main/java/com/discordsrv/common/player/provider/AbstractPlayerProvider.java b/common/src/main/java/com/discordsrv/common/player/provider/AbstractPlayerProvider.java index 630af56d..43538d06 100644 --- a/common/src/main/java/com/discordsrv/common/player/provider/AbstractPlayerProvider.java +++ b/common/src/main/java/com/discordsrv/common/player/provider/AbstractPlayerProvider.java @@ -19,9 +19,18 @@ package com.discordsrv.common.player.provider; import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.http.util.HttpUtil; +import com.discordsrv.common.player.IOfflinePlayer; import com.discordsrv.common.player.IPlayer; +import com.discordsrv.common.player.OfflinePlayer; import com.discordsrv.common.player.event.PlayerConnectedEvent; import com.discordsrv.common.player.event.PlayerDisconnectedEvent; +import com.discordsrv.common.player.provider.model.GameProfileResponse; +import com.discordsrv.common.player.provider.model.SkinInfo; +import com.discordsrv.common.player.provider.model.Textures; +import com.discordsrv.common.player.provider.model.UUIDResponse; +import com.discordsrv.common.uuid.util.UUIDUtil; +import okhttp3.Request; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -29,12 +38,17 @@ import java.util.Collection; import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicBoolean; public abstract class AbstractPlayerProvider implements PlayerProvider { + private static final String MOJANG_API_URL = "https://api.mojang.com"; + private static final String USERNAME_TO_UUID_URL = MOJANG_API_URL + "/users/profiles/minecraft/%s"; + private static final String UUID_TO_PROFILE_URL = MOJANG_API_URL + "/session/minecraft/profile/%s"; + private final Map players = new ConcurrentHashMap<>(); private final List allPlayers = new CopyOnWriteArrayList<>(); protected final DT discordSRV; @@ -88,4 +102,47 @@ public abstract class AbstractPlayerProvider allPlayers() { return allPlayers; } + + @Override + public CompletableFuture lookupUUIDForUsername(String username) { + IPlayer player = player(username); + if (player != null) { + return CompletableFuture.completedFuture(player.uniqueId()); + } + + Request request = new Request.Builder() + .url(String.format(USERNAME_TO_UUID_URL, username)) + .get() + .build(); + + return HttpUtil.readJson(discordSRV, request, UUIDResponse.class) + .thenApply(response -> UUIDUtil.fromShort(response.id)); + } + + @Override + public CompletableFuture lookupOfflinePlayer(UUID uuid) { + IPlayer player = player(uuid); + if (player != null) { + return CompletableFuture.completedFuture(player); + } + + Request request = new Request.Builder() + .url(String.format(UUID_TO_PROFILE_URL, uuid)) + .get() + .build(); + + return HttpUtil.readJson(discordSRV, request, GameProfileResponse.class).thenApply(response -> { + SkinInfo skinInfo = null; + for (GameProfileResponse.Property property : response.properties) { + if (!Textures.KEY.equals(property.name)) { + continue; + } + + Textures textures = Textures.getFromBase64(discordSRV, property.value); + skinInfo = textures.getSkinInfo(); + } + + return new OfflinePlayer(discordSRV, response.name, uuid, skinInfo); + }); + } } diff --git a/common/src/main/java/com/discordsrv/common/player/provider/PlayerProvider.java b/common/src/main/java/com/discordsrv/common/player/provider/PlayerProvider.java index d35d3351..22851f92 100644 --- a/common/src/main/java/com/discordsrv/common/player/provider/PlayerProvider.java +++ b/common/src/main/java/com/discordsrv/common/player/provider/PlayerProvider.java @@ -20,12 +20,14 @@ package com.discordsrv.common.player.provider; import com.discordsrv.api.player.DiscordSRVPlayer; import com.discordsrv.api.player.IPlayerProvider; +import com.discordsrv.common.player.IOfflinePlayer; import com.discordsrv.common.player.IPlayer; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Collection; import java.util.UUID; +import java.util.concurrent.CompletableFuture; public interface PlayerProvider extends IPlayerProvider { @@ -51,4 +53,11 @@ public interface PlayerProvider extends IPlayerProvider { */ @NotNull Collection allPlayers(); + + CompletableFuture lookupUUIDForUsername(String username); + + default CompletableFuture lookupOfflinePlayer(String username) { + return lookupUUIDForUsername(username).thenCompose(this::lookupOfflinePlayer); + } + CompletableFuture lookupOfflinePlayer(UUID uuid); } diff --git a/common/src/main/java/com/discordsrv/common/player/provider/model/GameProfileResponse.java b/common/src/main/java/com/discordsrv/common/player/provider/model/GameProfileResponse.java new file mode 100644 index 00000000..e21da4f8 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/player/provider/model/GameProfileResponse.java @@ -0,0 +1,15 @@ +package com.discordsrv.common.player.provider.model; + +import java.util.List; + +public class GameProfileResponse { + + public String id; + public String name; + public List properties; + + public static class Property { + public String name; + public String value; + } +} diff --git a/common/src/main/java/com/discordsrv/common/player/provider/model/SkinInfo.java b/common/src/main/java/com/discordsrv/common/player/provider/model/SkinInfo.java new file mode 100644 index 00000000..9c0967bc --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/player/provider/model/SkinInfo.java @@ -0,0 +1,20 @@ +package com.discordsrv.common.player.provider.model; + +public class SkinInfo { + + private final String textureId; + private final String model; + + public SkinInfo(String textureId, String model) { + this.textureId = textureId; + this.model = model; + } + + public String textureId() { + return textureId; + } + + public String model() { + return model; + } +} diff --git a/common/src/main/java/com/discordsrv/common/player/provider/model/Textures.java b/common/src/main/java/com/discordsrv/common/player/provider/model/Textures.java new file mode 100644 index 00000000..ec84a012 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/player/provider/model/Textures.java @@ -0,0 +1,46 @@ +package com.discordsrv.common.player.provider.model; + +import com.discordsrv.common.DiscordSRV; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.Base64; +import java.util.Map; + +public class Textures { + + public static String KEY = "textures"; + + public String profileId; + public String profileName; + public boolean signatureRequired; + public Map textures; + + public static class Texture { + public String url; + public Map metadata; + } + public SkinInfo getSkinInfo() { + Textures.Texture texture = textures.get("SKIN"); + if (texture == null) { + return null; + } + + String url = texture.url; + Map metadata = texture.metadata; + + String textureId = url.substring(url.lastIndexOf("/") + 1); + return new SkinInfo(textureId, metadata != null ? (String) metadata.get("model") : null); + } + + public static Textures getFromBase64(DiscordSRV discordSRV, String base64) { + byte[] bytes = Base64.getDecoder().decode(base64); + Textures textures; + try { + textures = discordSRV.json().readValue(bytes, Textures.class); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return textures; + } +} diff --git a/common/src/main/java/com/discordsrv/common/player/provider/model/UUIDResponse.java b/common/src/main/java/com/discordsrv/common/player/provider/model/UUIDResponse.java new file mode 100644 index 00000000..ad3d3417 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/player/provider/model/UUIDResponse.java @@ -0,0 +1,7 @@ +package com.discordsrv.common.player.provider.model; + +public class UUIDResponse { + + public String name; + public String id; +} diff --git a/common/src/main/java/com/discordsrv/common/scheduler/Scheduler.java b/common/src/main/java/com/discordsrv/common/scheduler/Scheduler.java index 40cc17d9..244e7f1b 100644 --- a/common/src/main/java/com/discordsrv/common/scheduler/Scheduler.java +++ b/common/src/main/java/com/discordsrv/common/scheduler/Scheduler.java @@ -21,6 +21,7 @@ package com.discordsrv.common.scheduler; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; +import java.time.Duration; import java.util.concurrent.*; @SuppressWarnings({"UnusedReturnValue", "unused"}) // API @@ -62,36 +63,23 @@ public interface Scheduler { */ Future run(@NotNull Runnable task); - /** - * Schedules the given task to run after the provided time in the provided {@link TimeUnit}. - * - * @param task the task - * @param time the amount of time in the provided unit - * @param unit the unit for the time - */ - @ApiStatus.NonExtendable - default ScheduledFuture runLater(@NotNull Runnable task, long time, @NotNull TimeUnit unit) { - return runLater(task, unit.toMillis(time)); - } - /** * Schedules the given task after the provided amount of milliseconds. * * @param task the task - * @param timeMillis the delay before executing the task + * @param delay the delay before executing the task */ - ScheduledFuture runLater(Runnable task, long timeMillis); + ScheduledFuture runLater(Runnable task, Duration delay); /** * Schedules the given task at the given rate. * * @param task the task * @param rate the rate in the given unit - * @param unit the unit for the rate */ @ApiStatus.NonExtendable - default ScheduledFuture runAtFixedRate(@NotNull Runnable task, long rate, @NotNull TimeUnit unit) { - return runAtFixedRate(task, rate, rate, unit); + default ScheduledFuture runAtFixedRate(@NotNull Runnable task, Duration rate) { + return runAtFixedRate(task, rate, rate); } /** @@ -100,21 +88,8 @@ public interface Scheduler { * @param task the task * @param initialDelay the initial delay in the provided unit * @param rate the rate to run the task at in the given unit - * @param unit the unit for the initial delay and rate */ @ApiStatus.NonExtendable - default ScheduledFuture runAtFixedRate(@NotNull Runnable task, long initialDelay, long rate, @NotNull TimeUnit unit) { - return runAtFixedRate(task, unit.toMillis(initialDelay), unit.toMillis(rate)); - } - - /** - * Schedules a task to run at the given rate after the initial delay. - * - * @param task the task - * @param initialDelayMillis the initial delay in milliseconds - * @param rateMillis the rate in milliseconds - */ - ScheduledFuture runAtFixedRate(@NotNull Runnable task, long initialDelayMillis, long rateMillis); - + ScheduledFuture runAtFixedRate(@NotNull Runnable task, Duration initialDelay, Duration rate); } diff --git a/common/src/main/java/com/discordsrv/common/scheduler/StandardScheduler.java b/common/src/main/java/com/discordsrv/common/scheduler/StandardScheduler.java index 4c5fa3a4..d8c0c0ee 100644 --- a/common/src/main/java/com/discordsrv/common/scheduler/StandardScheduler.java +++ b/common/src/main/java/com/discordsrv/common/scheduler/StandardScheduler.java @@ -27,6 +27,7 @@ import com.discordsrv.common.scheduler.threadfactory.CountingForkJoinWorkerThrea import com.discordsrv.common.scheduler.threadfactory.CountingThreadFactory; import org.jetbrains.annotations.NotNull; +import java.time.Duration; import java.util.concurrent.*; public class StandardScheduler implements Scheduler { @@ -124,13 +125,13 @@ public class StandardScheduler implements Scheduler { } @Override - public ScheduledFuture runLater(Runnable task, long timeMillis) { - return scheduledExecutorService.schedule(wrap(task), timeMillis, TimeUnit.MILLISECONDS); + public ScheduledFuture runLater(Runnable task, Duration delay) { + return scheduledExecutorService.schedule(wrap(task), delay.toMillis(), TimeUnit.MILLISECONDS); } @Override - public ScheduledFuture runAtFixedRate(@NotNull Runnable task, long initialDelayMillis, long rateMillis) { - return scheduledExecutorService.scheduleAtFixedRate(wrap(task), initialDelayMillis, rateMillis, TimeUnit.MILLISECONDS); + public ScheduledFuture runAtFixedRate(@NotNull Runnable task, Duration initialDelay, Duration rate) { + return scheduledExecutorService.scheduleAtFixedRate(wrap(task), initialDelay.toMillis(), rate.toMillis(), TimeUnit.MILLISECONDS); } public class ExceptionHandlingExecutor implements Executor { diff --git a/common/src/main/java/com/discordsrv/common/update/UpdateChecker.java b/common/src/main/java/com/discordsrv/common/update/UpdateChecker.java index f69ab2d6..3cc5008e 100644 --- a/common/src/main/java/com/discordsrv/common/update/UpdateChecker.java +++ b/common/src/main/java/com/discordsrv/common/update/UpdateChecker.java @@ -24,6 +24,7 @@ import com.discordsrv.common.config.connection.ConnectionConfig; import com.discordsrv.common.config.connection.UpdateConfig; import com.discordsrv.common.debug.data.VersionInfo; import com.discordsrv.common.exception.MessageException; +import com.discordsrv.common.http.util.HttpUtil; import com.discordsrv.common.logging.NamedLogger; import com.discordsrv.common.permission.Permission; import com.discordsrv.common.player.IPlayer; @@ -144,16 +145,6 @@ public class UpdateChecker { return true; } - private ResponseBody checkResponse(Request request, Response response, ResponseBody responseBody) throws IOException { - if (responseBody == null || !response.isSuccessful()) { - String responseString = responseBody == null - ? "response body is null" - : StringUtils.substring(responseBody.string(), 0, 500); - throw new MessageException("Request to " + request.url().host() + " failed: " + response.code() + ": " + responseString); - } - return responseBody; - } - /** * @return {@code null} for preventing shutdown */ @@ -167,7 +158,7 @@ public class UpdateChecker { String responseString; try (Response response = discordSRV.httpClient().newCall(request).execute()) { - ResponseBody responseBody = checkResponse(request, response, response.body()); + ResponseBody responseBody = HttpUtil.checkIfResponseSuccessful(request, response); responseString = responseBody.string(); } @@ -205,7 +196,7 @@ public class UpdateChecker { .get().build(); try (Response response = discordSRV.httpClient().newCall(request).execute()) { - ResponseBody responseBody = checkResponse(request, response, response.body()); + ResponseBody responseBody = HttpUtil.checkIfResponseSuccessful(request, response); GitHubCompareResponse compare = discordSRV.json().readValue(responseBody.byteStream(), GitHubCompareResponse.class); VersionCheck versionCheck = new VersionCheck(); @@ -237,7 +228,7 @@ public class UpdateChecker { .get().build(); try (Response response = discordSRV.httpClient().newCall(request).execute()) { - ResponseBody responseBody = checkResponse(request, response, response.body()); + ResponseBody responseBody = HttpUtil.checkIfResponseSuccessful(request, response); List releases = discordSRV.json().readValue(responseBody.byteStream(), new TypeReference>() {}); for (GithubRelease release : releases) { diff --git a/common/src/main/java/com/discordsrv/common/uuid/util/UUIDUtil.java b/common/src/main/java/com/discordsrv/common/uuid/util/UUIDUtil.java new file mode 100644 index 00000000..234458c5 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/uuid/util/UUIDUtil.java @@ -0,0 +1,37 @@ +package com.discordsrv.common.uuid.util; + +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +public final class UUIDUtil { + + private UUIDUtil() {} + + public static UUID fromShortOrFull(@NotNull String uuidString) { + int length = uuidString.length(); + if (length == 32) { + return fromShort(uuidString); + } else if (length == 36) { + return UUID.fromString(uuidString); + } + throw new IllegalArgumentException("Not a valid 36 or 32 character long UUID"); + } + + public static UUID fromShort(@NotNull String shortUUID) { + if (shortUUID.length() != 32) { + throw new IllegalArgumentException("Short uuids are 32 characters long"); + } + + String fullLengthUUID = shortUUID.substring(0, 8) + + "-" + shortUUID.substring(8, 12) + + "-" + shortUUID.substring(12, 16) + + "-" + shortUUID.substring(16, 20) + + "-" + shortUUID.substring(20); + return UUID.fromString(fullLengthUUID); + } + + public static String toShort(@NotNull UUID uuid) { + return uuid.toString().replace("-", ""); + } +} diff --git a/common/src/test/java/com/discordsrv/common/messageforwarding/game/MinecraftToDiscordChatMessageTest.java b/common/src/test/java/com/discordsrv/common/messageforwarding/game/MinecraftToDiscordChatMessageTest.java index 7db13409..7133ba4c 100644 --- a/common/src/test/java/com/discordsrv/common/messageforwarding/game/MinecraftToDiscordChatMessageTest.java +++ b/common/src/test/java/com/discordsrv/common/messageforwarding/game/MinecraftToDiscordChatMessageTest.java @@ -14,6 +14,7 @@ import com.discordsrv.common.MockDiscordSRV; import com.discordsrv.common.channel.GlobalChannel; import com.discordsrv.common.component.util.ComponentUtil; import com.discordsrv.common.player.IPlayer; +import com.discordsrv.common.player.provider.model.SkinInfo; import com.discordsrv.common.testing.TestHelper; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.identity.Identity; @@ -73,6 +74,11 @@ public class MinecraftToDiscordChatMessageTest { return "Vankka"; } + @Override + public @Nullable SkinInfo skinInfo() { + return null; + } + @Override public @Nullable Locale locale() { return Locale.getDefault(); diff --git a/sponge/src/main/java/com/discordsrv/sponge/player/SpongeOfflinePlayer.java b/sponge/src/main/java/com/discordsrv/sponge/player/SpongeOfflinePlayer.java index d099a908..e93196f7 100644 --- a/sponge/src/main/java/com/discordsrv/sponge/player/SpongeOfflinePlayer.java +++ b/sponge/src/main/java/com/discordsrv/sponge/player/SpongeOfflinePlayer.java @@ -20,10 +20,14 @@ package com.discordsrv.sponge.player; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.player.IOfflinePlayer; +import com.discordsrv.common.player.provider.model.SkinInfo; +import com.discordsrv.common.player.provider.model.Textures; import com.discordsrv.sponge.SpongeDiscordSRV; import net.kyori.adventure.identity.Identity; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.spongepowered.api.entity.living.player.User; +import org.spongepowered.api.profile.property.ProfileProperty; public class SpongeOfflinePlayer implements IOfflinePlayer { @@ -45,6 +49,19 @@ public class SpongeOfflinePlayer implements IOfflinePlayer { return user.name(); } + @Override + public @Nullable SkinInfo skinInfo() { + for (ProfileProperty property : user.profile().properties()) { + if (!Textures.KEY.equals(property.name())) { + continue; + } + + Textures textures = Textures.getFromBase64(discordSRV, property.value()); + return textures.getSkinInfo(); + } + return null; + } + @Override public @NotNull Identity identity() { return user.profile(); diff --git a/sponge/src/main/java/com/discordsrv/sponge/player/SpongePlayer.java b/sponge/src/main/java/com/discordsrv/sponge/player/SpongePlayer.java index 6f7cb10b..788f9335 100644 --- a/sponge/src/main/java/com/discordsrv/sponge/player/SpongePlayer.java +++ b/sponge/src/main/java/com/discordsrv/sponge/player/SpongePlayer.java @@ -20,6 +20,8 @@ package com.discordsrv.sponge.player; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.player.IPlayer; +import com.discordsrv.common.player.provider.model.SkinInfo; +import com.discordsrv.common.player.provider.model.Textures; import com.discordsrv.sponge.SpongeDiscordSRV; import com.discordsrv.sponge.command.game.sender.SpongeCommandSender; import net.kyori.adventure.identity.Identity; @@ -51,6 +53,13 @@ public class SpongePlayer extends SpongeCommandSender implements IPlayer { return player.name(); } + @Override + public @Nullable SkinInfo skinInfo() { + String texturesRaw = player.skinProfile().get().value(); + Textures textures = Textures.getFromBase64(discordSRV, texturesRaw); + return textures.getSkinInfo(); + } + @Override public @Nullable Locale locale() { return player.locale(); diff --git a/sponge/src/main/java/com/discordsrv/sponge/player/SpongePlayerProvider.java b/sponge/src/main/java/com/discordsrv/sponge/player/SpongePlayerProvider.java index fff141cf..42b3ebad 100644 --- a/sponge/src/main/java/com/discordsrv/sponge/player/SpongePlayerProvider.java +++ b/sponge/src/main/java/com/discordsrv/sponge/player/SpongePlayerProvider.java @@ -19,6 +19,7 @@ package com.discordsrv.sponge.player; import com.discordsrv.common.player.IOfflinePlayer; +import com.discordsrv.common.player.IPlayer; import com.discordsrv.common.player.ServerPlayerProvider; import com.discordsrv.sponge.SpongeDiscordSRV; import org.spongepowered.api.entity.living.player.User; @@ -77,14 +78,24 @@ public class SpongePlayerProvider extends ServerPlayerProvider offlinePlayer(UUID uuid) { + public CompletableFuture lookupOfflinePlayer(UUID uuid) { + IPlayer player = player(uuid); + if (player != null) { + return CompletableFuture.completedFuture(player); + } + return discordSRV.game().server().userManager() .load(uuid) .thenApply(optional -> optional.map(this::convert).orElse(null)); } @Override - public CompletableFuture offlinePlayer(String username) { + public CompletableFuture lookupOfflinePlayer(String username) { + IPlayer player = player(username); + if (player != null) { + return CompletableFuture.completedFuture(player); + } + return discordSRV.game().server().userManager() .load(username) .thenApply(optional -> optional.map(this::convert).orElse(null)); diff --git a/velocity/src/main/java/com/discordsrv/velocity/player/VelocityPlayer.java b/velocity/src/main/java/com/discordsrv/velocity/player/VelocityPlayer.java index 2baee745..b82cb497 100644 --- a/velocity/src/main/java/com/discordsrv/velocity/player/VelocityPlayer.java +++ b/velocity/src/main/java/com/discordsrv/velocity/player/VelocityPlayer.java @@ -20,9 +20,12 @@ package com.discordsrv.velocity.player; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.player.IPlayer; +import com.discordsrv.common.player.provider.model.SkinInfo; +import com.discordsrv.common.player.provider.model.Textures; import com.discordsrv.velocity.VelocityDiscordSRV; import com.discordsrv.velocity.command.game.sender.VelocityCommandSender; import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.util.GameProfile; import net.kyori.adventure.identity.Identity; import net.kyori.adventure.text.Component; import org.jetbrains.annotations.NotNull; @@ -49,6 +52,19 @@ public class VelocityPlayer extends VelocityCommandSender implements IPlayer { return player.getUsername(); } + @Override + public @Nullable SkinInfo skinInfo() { + for (GameProfile.Property property : player.getGameProfile().getProperties()) { + if (!Textures.KEY.equals(property.getName())) { + continue; + } + + Textures textures = Textures.getFromBase64(discordSRV, property.getValue()); + return textures.getSkinInfo(); + } + return null; + } + @Override public @Nullable Locale locale() { return player.getPlayerSettings().getLocale();