From 0a4cf27342005e839ec66cff71a9b2e6e90e6ec9 Mon Sep 17 00:00:00 2001 From: Luke Chambers Date: Tue, 8 Aug 2023 16:10:26 -0400 Subject: [PATCH] Add ability to use texture url in /skull (#5120) Co-authored-by: MD <1917406+mdcfe@users.noreply.github.com> Co-authored-by: Josh Roy <10731363+JRoy@users.noreply.github.com> --- .../essentials/commands/Commandskull.java | 81 +++++++++++++++++-- .../src/main/resources/messages.properties | 3 + 2 files changed, 76 insertions(+), 8 deletions(-) diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandskull.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandskull.java index 4e1065548..b9b50b8d4 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandskull.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandskull.java @@ -5,13 +5,20 @@ import com.earth2me.essentials.craftbukkit.Inventories; import com.earth2me.essentials.utils.EnumUtil; import com.earth2me.essentials.utils.MaterialUtil; import com.google.common.collect.Lists; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import org.bukkit.Material; import org.bukkit.Server; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.SkullMeta; +import org.bukkit.profile.PlayerProfile; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Base64; import java.util.Collections; import java.util.List; +import java.util.UUID; import java.util.regex.Pattern; import static com.earth2me.essentials.I18n.tl; @@ -19,20 +26,53 @@ import static com.earth2me.essentials.I18n.tl; public class Commandskull extends EssentialsCommand { private static final Pattern NAME_PATTERN = Pattern.compile("^[A-Za-z0-9_]+$"); + private static final Pattern URL_VALUE_PATTERN = Pattern.compile("^[0-9a-fA-F]{64}$"); + private static final Pattern BASE_64_PATTERN = Pattern.compile("^[A-Za-z0-9+/=]{180}$"); + private static final Material SKULL_ITEM = EnumUtil.getMaterial("PLAYER_HEAD", "SKULL_ITEM"); + private final boolean playerProfileSupported; + public Commandskull() { super("skull"); + + // The player profile API is only available in newer versions of Spigot 1.18.1 and above + boolean playerProfileSupported = true; + try { + Class.forName("org.bukkit.profile.PlayerProfile"); + } catch (final ClassNotFoundException e) { + playerProfileSupported = false; + } + this.playerProfileSupported = playerProfileSupported; } @Override protected void run(final Server server, final User user, final String commandLabel, final String[] args) throws Exception { final String owner; if (args.length > 0 && user.isAuthorized("essentials.skull.others")) { - if (!NAME_PATTERN.matcher(args[0]).matches()) { + if (BASE_64_PATTERN.matcher(args[0]).matches()) { + try { + final String decoded = new String(Base64.getDecoder().decode(args[0])); + final JsonObject jsonObject = JsonParser.parseString(decoded).getAsJsonObject(); + final String url = jsonObject + .getAsJsonObject("textures") + .getAsJsonObject("SKIN") + .get("url") + .getAsString(); + owner = url.substring(url.lastIndexOf("/") + 1); + } catch (final Exception e) { + // Any exception that can realistically happen here is caused by an invalid texture value + throw new IllegalArgumentException(tl("skullInvalidBase64")); + } + + if (!URL_VALUE_PATTERN.matcher(owner).matches()) { + throw new IllegalArgumentException(tl("skullInvalidBase64")); + } + } else if (!NAME_PATTERN.matcher(args[0]).matches()) { throw new IllegalArgumentException(tl("alphaNames")); + } else { + owner = args[0]; } - owner = args[0]; } else { owner = user.getName(); } @@ -60,18 +100,43 @@ public class Commandskull extends EssentialsCommand { private void editSkull(final User user, final ItemStack stack, final SkullMeta skullMeta, final String owner, final boolean spawn) { ess.runTaskAsynchronously(() -> { - //Run this stuff async because SkullMeta#setOwner causes a http request. - skullMeta.setDisplayName("§fSkull of " + owner); - //noinspection deprecation - skullMeta.setOwner(owner); + // Run this stuff async because it causes an HTTP request + + final String shortOwnerName; + if (URL_VALUE_PATTERN.matcher(owner).matches()) { + if (!playerProfileSupported) { + user.sendMessage(tl("unsupportedFeature")); + return; + } + + final URL url; + try { + url = new URL("https://textures.minecraft.net/texture/" + owner); + } catch (final MalformedURLException e) { + // The URL should never be malformed + throw new RuntimeException(e); + } + + final PlayerProfile profile = ess.getServer().createPlayerProfile(UUID.randomUUID()); + profile.getTextures().setSkin(url); + skullMeta.setOwnerProfile(profile); + + shortOwnerName = owner.substring(0, 7); + } else { + //noinspection deprecation + skullMeta.setOwner(owner); + shortOwnerName = owner; + } + skullMeta.setDisplayName("§fSkull of " + shortOwnerName); + ess.scheduleSyncDelayedTask(() -> { stack.setItemMeta(skullMeta); if (spawn) { Inventories.addItem(user.getBase(), stack); - user.sendMessage(tl("givenSkull", owner)); + user.sendMessage(tl("givenSkull", shortOwnerName)); return; } - user.sendMessage(tl("skullChanged", owner)); + user.sendMessage(tl("skullChanged", shortOwnerName)); }); }); } diff --git a/Essentials/src/main/resources/messages.properties b/Essentials/src/main/resources/messages.properties index 1e4fc2dcf..84ef8c9b2 100644 --- a/Essentials/src/main/resources/messages.properties +++ b/Essentials/src/main/resources/messages.properties @@ -1208,6 +1208,9 @@ skullCommandUsage1=/ skullCommandUsage1Description=Gets your own skull skullCommandUsage2=/ skullCommandUsage2Description=Gets the skull of the specified player +skullCommandUsage3=/ +skullCommandUsage3Description=Gets a skull with the specified texture (either the hash from a texture URL or a Base64 texture value) +skullInvalidBase64=\u00a74The texture value is invalid. slimeMalformedSize=\u00a74Malformed size. smithingtableCommandDescription=Opens up a smithing table. smithingtableCommandUsage=/