diff --git a/Essentials/src/main/java/com/earth2me/essentials/Essentials.java b/Essentials/src/main/java/com/earth2me/essentials/Essentials.java index 8203d6b30..8c24371d9 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/Essentials.java +++ b/Essentials/src/main/java/com/earth2me/essentials/Essentials.java @@ -958,17 +958,7 @@ public class Essentials extends JavaPlugin implements net.ess3.api.IEssentials { //This will return null if there is not a match. @Override public User getOfflineUser(final String name) { - final User user = userMap.getUser(name); - if (user != null && user.getBase() instanceof OfflinePlayerStub) { - //This code should attempt to use the last known name of a user, if Bukkit returns name as null. - final String lastName = user.getLastAccountName(); - if (lastName != null) { - ((OfflinePlayerStub) user.getBase()).setName(lastName); - } else { - ((OfflinePlayerStub) user.getBase()).setName(name); - } - } - return user; + return userMap.getUser(name); } @Override @@ -1056,7 +1046,7 @@ public class Essentials extends JavaPlugin implements net.ess3.api.IEssentials { return true; } - return interactor.getBase().canSee(interactee.getBase()); + return !interactee.isHiddenFrom(interactor.getBase()); } //This will create a new user if there is not a match. @@ -1071,19 +1061,9 @@ public class Essentials extends JavaPlugin implements net.ess3.api.IEssentials { return null; } - User user = userMap.getUser(base.getUniqueId()); + final User user = userMap.getUser(base); - if (user == null) { - if (getSettings().isDebug()) { - LOGGER.log(Level.INFO, "Constructing new userfile from base player " + base.getName()); - } - user = userMap.loadUncachedUser(base); - - // The above method will end up creating a new user, but it will not be added to the cache. - // Since we already call UserMap#getUser() above, we are already okay with adding the user to the cache, - // so we need to manually add the user to the cache in order to avoid a memory leak and maintain behavior. - userMap.addCachedUser(user); - } else if (base.getClass() != UUIDPlayer.class || user.getBase() == null) { + if (base.getClass() != UUIDPlayer.class || user.getBase() == null) { user.update(base); } return user; diff --git a/Essentials/src/main/java/com/earth2me/essentials/EssentialsUpgrade.java b/Essentials/src/main/java/com/earth2me/essentials/EssentialsUpgrade.java index ab3d46185..33fc06fb4 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/EssentialsUpgrade.java +++ b/Essentials/src/main/java/com/earth2me/essentials/EssentialsUpgrade.java @@ -963,7 +963,7 @@ public class EssentialsUpgrade { } } - uuids.put(uuid, config.getLong("timestamps.logout", 0L)); + uuids.put(uuid, time); nameToUuidMap.put(name, uuid); } } catch (IllegalArgumentException | IndexOutOfBoundsException ignored) { diff --git a/Essentials/src/main/java/com/earth2me/essentials/I18n.java b/Essentials/src/main/java/com/earth2me/essentials/I18n.java index ed30d66cc..eab7c8fef 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/I18n.java +++ b/Essentials/src/main/java/com/earth2me/essentials/I18n.java @@ -87,7 +87,7 @@ public class I18n implements net.ess3.api.II18n { return localeBundle.getString(string); } } catch (final MissingResourceException ex) { - if (ess == null || ess.getSettings().isDebug()) { + if (ess != null && ess.getSettings().isDebug()) { ess.getLogger().log(Level.WARNING, String.format("Missing translation key \"%s\" in translation file %s", ex.getKey(), localeBundle.getLocale().toString()), ex); } return defaultBundle.getString(string); diff --git a/Essentials/src/main/java/com/earth2me/essentials/ISettings.java b/Essentials/src/main/java/com/earth2me/essentials/ISettings.java index 823734ebc..ae7814153 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/ISettings.java +++ b/Essentials/src/main/java/com/earth2me/essentials/ISettings.java @@ -406,6 +406,8 @@ public interface ISettings extends IConf { boolean showZeroBaltop(); + int getMaxItemLore(); + enum KeepInvPolicy { KEEP, DELETE, diff --git a/Essentials/src/main/java/com/earth2me/essentials/MetaItemStack.java b/Essentials/src/main/java/com/earth2me/essentials/MetaItemStack.java index 77176583c..9f9867a07 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/MetaItemStack.java +++ b/Essentials/src/main/java/com/earth2me/essentials/MetaItemStack.java @@ -206,6 +206,13 @@ public class MetaItemStack { final ItemMeta meta = stack.getItemMeta(); meta.setLore(lore); stack.setItemMeta(meta); + } else if ((split[0].equalsIgnoreCase("custom-model-data") || split[0].equalsIgnoreCase("cmd")) && hasMetaPermission(sender, "custom-model-data", false, true, ess)) { + if (VersionUtil.getServerBukkitVersion().isHigherThanOrEqualTo(VersionUtil.v1_14_R01)) { + final int value = split.length <= 1 ? 0 : Integer.parseInt(split[1]); + final ItemMeta meta = stack.getItemMeta(); + meta.setCustomModelData(value); + stack.setItemMeta(meta); + } } else if (split[0].equalsIgnoreCase("unbreakable") && hasMetaPermission(sender, "unbreakable", false, true, ess)) { final boolean value = split.length <= 1 || Boolean.parseBoolean(split[1]); setUnbreakable(ess, stack, value); @@ -216,11 +223,21 @@ public class MetaItemStack { } else { throw new Exception(tl("onlyPlayerSkulls")); } - } else if (split.length > 1 && split[0].equalsIgnoreCase("book") && MaterialUtil.isEditableBook(stack.getType()) && (hasMetaPermission(sender, "book",true, true, ess) || hasMetaPermission(sender, "chapter-" + split[1].toLowerCase(Locale.ENGLISH), true, true, ess))) { + } else if (split.length > 1 && split[0].equalsIgnoreCase("book") && MaterialUtil.isEditableBook(stack.getType()) && (hasMetaPermission(sender, "book", true, true, ess) || hasMetaPermission(sender, "chapter-" + split[1].toLowerCase(Locale.ENGLISH), true, true, ess))) { final BookMeta meta = (BookMeta) stack.getItemMeta(); final IText input = new BookInput("book", true, ess); final BookPager pager = new BookPager(input); - + // This fix only applies to written books - which require an author and a title. https://bugs.mojang.com/browse/MC-59153 + if (stack.getType() == WRITTEN_BOOK) { + if (!meta.hasAuthor()) { + // The sender can be null when this method is called from {@link com.earth2me.essentials.signs.EssentialsSign#getItemMeta(ItemStack, String, IEssentials)} + meta.setAuthor(sender == null ? Console.getInstance().getDisplayName() : sender.getPlayer().getName()); + } + if (!meta.hasTitle()) { + final String title = FormatUtil.replaceFormat(split[1].replace('_', ' ')); + meta.setTitle(title.length() > 32 ? title.substring(0, 32) : title); + } + } final List pages = pager.getPages(split[1]); meta.setPages(pages); stack.setItemMeta(meta); diff --git a/Essentials/src/main/java/com/earth2me/essentials/PlayerList.java b/Essentials/src/main/java/com/earth2me/essentials/PlayerList.java index b92021913..a0017885d 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/PlayerList.java +++ b/Essentials/src/main/java/com/earth2me/essentials/PlayerList.java @@ -55,9 +55,9 @@ public final class PlayerList { int playerHidden = 0; int hiddenCount = 0; for (final User onlinePlayer : ess.getOnlineUsers()) { - if (onlinePlayer.isHidden() || (user != null && !user.getBase().canSee(onlinePlayer.getBase()))) { + if (onlinePlayer.isHidden() || (user != null && onlinePlayer.isHiddenFrom(user.getBase()))) { playerHidden++; - if (showHidden || user != null && user.getBase().canSee(onlinePlayer.getBase())) { + if (showHidden || user != null && !onlinePlayer.isHiddenFrom(user.getBase())) { hiddenCount++; } } @@ -75,7 +75,7 @@ public final class PlayerList { public static Map> getPlayerLists(final IEssentials ess, final IUser sender, final boolean showHidden) { final Map> playerList = new HashMap<>(); for (final User onlineUser : ess.getOnlineUsers()) { - if ((sender == null && !showHidden && onlineUser.isHidden()) || (sender != null && !showHidden && !sender.getBase().canSee(onlineUser.getBase()))) { + if ((sender == null && !showHidden && onlineUser.isHidden()) || (sender != null && !showHidden && onlineUser.isHiddenFrom(sender.getBase()))) { continue; } final String group = FormatUtil.stripFormat(FormatUtil.stripEssentialsFormat(onlineUser.getGroup().toLowerCase())); diff --git a/Essentials/src/main/java/com/earth2me/essentials/Settings.java b/Essentials/src/main/java/com/earth2me/essentials/Settings.java index 2ba1e8ab3..601d347ea 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/Settings.java +++ b/Essentials/src/main/java/com/earth2me/essentials/Settings.java @@ -1943,4 +1943,9 @@ public class Settings implements net.ess3.api.ISettings { public boolean showZeroBaltop() { return config.getBoolean("show-zero-baltop", true); } + + @Override + public int getMaxItemLore() { + return config.getInt("max-itemlore-lines", 10); + } } diff --git a/Essentials/src/main/java/com/earth2me/essentials/User.java b/Essentials/src/main/java/com/earth2me/essentials/User.java index 8ee7b8322..3918233bc 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/User.java +++ b/Essentials/src/main/java/com/earth2me/essentials/User.java @@ -698,7 +698,7 @@ public class User extends UserData implements Comparable, IMessageRecipien } public boolean isHidden(final Player player) { - return hidden || !player.canSee(getBase()); + return hidden || isHiddenFrom(player); } @Override diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandessentials.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandessentials.java index 5d498b32c..dee0c19b8 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandessentials.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandessentials.java @@ -700,6 +700,8 @@ public class Commandessentials extends EssentialsCommand { } ess.getLogger().info("Found " + total + " orphaned userdata files."); }); + } else if (args[1].equalsIgnoreCase("cache")) { + sender.sendMessage(tl("usermapKnown", ess.getUsers().getAllUserUUIDs().size(), ess.getUsers().getNameCache().size())); } else { try { final UUID uuid = UUID.fromString(args[1]); diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commanditemlore.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commanditemlore.java index 9f605023d..c754454af 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commanditemlore.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commanditemlore.java @@ -35,7 +35,12 @@ public class Commanditemlore extends EssentialsCommand { } final ItemMeta im = item.getItemMeta(); + final int loreSize = im.hasLore() ? im.getLore().size() : 0; if (args[0].equalsIgnoreCase("add") && args.length > 1) { + if (loreSize >= ess.getSettings().getMaxItemLore() && !user.isAuthorized("essentials.itemlore.bypass")) { + throw new Exception(tl("itemloreMaxLore")); + } + final String line = FormatUtil.formatString(user, "essentials.itemlore", getFinalArg(args, 1)).trim(); final List lore = im.hasLore() ? im.getLore() : new ArrayList<>(); lore.add(line); diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandkick.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandkick.java index a9ceca8cc..b762ee7c8 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandkick.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandkick.java @@ -28,7 +28,7 @@ public class Commandkick extends EssentialsCommand { final User user = sender.isPlayer() ? ess.getUser(sender.getPlayer()) : null; if (user != null) { - if (target.isHidden(sender.getPlayer()) && !user.canInteractVanished() && !sender.getPlayer().canSee(target.getBase())) { + if (target.isHidden(sender.getPlayer()) && !user.canInteractVanished() && target.isHiddenFrom(sender.getPlayer())) { throw new PlayerNotFoundException(); } diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandmail.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandmail.java index 59d8b2775..762a3cc14 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandmail.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandmail.java @@ -169,31 +169,50 @@ public class Commandmail extends EssentialsCommand { return; } if (args.length >= 1 && "clear".equalsIgnoreCase(args[0])) { - final ArrayList mails = user.getMailMessages(); - if (mails == null || mails.size() == 0) { - user.sendMessage(tl("noMail")); + User mailUser = user; + int toRemove = -1; + if (args.length > 1) { + if (NumberUtil.isPositiveInt(args[1])) { + toRemove = Integer.parseInt(args[1]); + } else if (!user.isAuthorized("essentials.mail.clear.others")) { + throw new Exception(tl("noPerm", "essentials.mail.clear.others")); + } else { + mailUser = getPlayer(ess.getServer(), user, args, 1, true); + if (args.length > 2 && NumberUtil.isPositiveInt(args[2])) { + toRemove = Integer.parseInt(args[2]); + } + } + } + + final ArrayList mails = mailUser.getMailMessages(); + if (mails == null || mails.isEmpty()) { + user.sendMessage(tl(mailUser == user ? "noMail" : "noMailOther", mailUser.getDisplayName())); throw new NoChargeException(); } - if (args.length > 1) { - if (!NumberUtil.isPositiveInt(args[1])) { - throw new NotEnoughArgumentsException(); - } - - final int toRemove = Integer.parseInt(args[1]); + if (toRemove > 0) { if (toRemove > mails.size()) { user.sendMessage(tl("mailClearIndex", mails.size())); - return; + throw new NoChargeException(); } mails.remove(toRemove - 1); - user.setMailList(mails); + mailUser.setMailList(mails); } else { - user.setMailList(null); + mailUser.setMailList(null); } - user.sendMessage(tl("mailCleared")); return; } + if (args.length >= 1 && "clearall".equalsIgnoreCase(args[0])){ + if (!user.isAuthorized("essentials.mail.clearall")) { + throw new Exception(tl("noPerm", "essentials.mail.clearall")); + } + + ess.runTaskAsynchronously(new ClearAll()); + user.sendMessage(tl("mailClearedAll")); + return; + + } throw new NotEnoughArgumentsException(); } @@ -201,8 +220,32 @@ public class Commandmail extends EssentialsCommand { protected void run(final Server server, final CommandSource sender, final String commandLabel, final String[] args) throws Exception { if (args.length >= 1 && "read".equalsIgnoreCase(args[0])) { throw new Exception(tl("onlyPlayers", commandLabel + " read")); - } else if (args.length >= 1 && "clear".equalsIgnoreCase(args[0])) { - throw new Exception(tl("onlyPlayers", commandLabel + " clear")); + } else if (args.length > 1 && "clear".equalsIgnoreCase(args[0])) { + final User mailUser = getPlayer(server, args[1], true, true); + final int toRemove = args.length > 2 ? NumberUtil.isPositiveInt(args[2]) ? Integer.parseInt(args[2]) : -1 : -1; + + final ArrayList mails = mailUser.getMailMessages(); + if (mails == null || mails.isEmpty()) { + sender.sendMessage(tl("noMailOther", mailUser.getDisplayName())); + throw new NoChargeException(); + } + + if (toRemove > 0) { + if (toRemove > mails.size()) { + sender.sendMessage(tl("mailClearIndex", mails.size())); + throw new NoChargeException(); + } + mails.remove(toRemove - 1); + mailUser.setMailList(mails); + } else { + mailUser.setMailList(null); + } + sender.sendMessage(tl("mailCleared")); + return; + } else if (args.length >= 1 && "clearall".equalsIgnoreCase(args[0])){ + ess.runTaskAsynchronously(new ClearAll()); + sender.sendMessage(tl("mailClearedAll")); + return; } else if (args.length >= 3 && "send".equalsIgnoreCase(args[0])) { final User u; try { @@ -286,9 +329,12 @@ public class Commandmail extends EssentialsCommand { if (user.isAuthorized("essentials.mail.sendtempall")) { options.add("sendtempall"); } + if (user.isAuthorized("essentials.mail.clearall")){ + options.add("clearall"); + } return options; } else if (args.length == 2) { - if ((args[0].equalsIgnoreCase("send") && user.isAuthorized("essentials.mail.send")) || (args[0].equalsIgnoreCase("sendtemp") && user.isAuthorized("essentials.mail.sendtemp"))) { + if ((args[0].equalsIgnoreCase("send") && user.isAuthorized("essentials.mail.send")) || (args[0].equalsIgnoreCase("sendtemp") && user.isAuthorized("essentials.mail.sendtemp")) || ((args[0].equalsIgnoreCase("clear"))&& user.isAuthorized("essentials.mail.clear.others"))) { return getPlayers(server, user); } else if (args[0].equalsIgnoreCase("sendtempall") && user.isAuthorized("essentials.mail.sendtempall")) { return COMMON_DATE_DIFFS; @@ -326,9 +372,9 @@ public class Commandmail extends EssentialsCommand { @Override protected List getTabCompleteOptions(final Server server, final CommandSource sender, final String commandLabel, final String[] args) { if (args.length == 1) { - return Lists.newArrayList("send", "sendall", "sendtemp", "sendtempall"); + return Lists.newArrayList("send", "sendall", "sendtemp", "sendtempall", "clearall", "clear"); } else if (args.length == 2) { - if (args[0].equalsIgnoreCase("send") || args[0].equalsIgnoreCase("sendtemp")) { + if (args[0].equalsIgnoreCase("send") || args[0].equalsIgnoreCase("sendtemp") || args[0].equalsIgnoreCase("clear")) { return getPlayers(server, sender); } else if (args[0].equalsIgnoreCase("sendtempall")) { return COMMON_DATE_DIFFS; @@ -338,4 +384,16 @@ public class Commandmail extends EssentialsCommand { } return Collections.emptyList(); } + + private class ClearAll implements Runnable { + @Override + public void run() { + for (UUID u : ess.getUsers().getAllUserUUIDs()) { + final User user = ess.getUsers().loadUncachedUser(u); + if (user != null) { + user.setMailList(null); + } + } + } + } } diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandnear.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandnear.java index c829a143b..ca85d2170 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandnear.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandnear.java @@ -87,7 +87,7 @@ public class Commandnear extends EssentialsCommand { final Queue nearbyPlayers = new PriorityQueue<>((o1, o2) -> (int) (o1.getLocation().distanceSquared(loc) - o2.getLocation().distanceSquared(loc))); for (final User player : ess.getOnlineUsers()) { - if (!player.equals(user) && !player.isAuthorized("essentials.near.exclude") && (!player.isHidden(user.getBase()) || showHidden || user.getBase().canSee(player.getBase()))) { + if (!player.equals(user) && !player.isAuthorized("essentials.near.exclude") && (!player.isHidden(user.getBase()) || showHidden || !player.isHiddenFrom(user.getBase()))) { final Location playerLoc = player.getLocation(); if (playerLoc.getWorld() != world) { continue; diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandpay.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandpay.java index ecc6a4207..444f80e77 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandpay.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandpay.java @@ -18,6 +18,11 @@ import java.util.concurrent.atomic.AtomicBoolean; import static com.earth2me.essentials.I18n.tl; public class Commandpay extends EssentialsLoopCommand { + private static final BigDecimal THOUSAND = new BigDecimal(1000); + private static final BigDecimal MILLION = new BigDecimal(1_000_000); + private static final BigDecimal BILLION = new BigDecimal(1_000_000_000); + private static final BigDecimal TRILLION = new BigDecimal(1_000_000_000_000L); + public Commandpay() { super("pay"); } @@ -28,17 +33,43 @@ public class Commandpay extends EssentialsLoopCommand { throw new NotEnoughArgumentsException(); } - if (args[1].contains("-")) { + final String ogStr = args[1]; + + if (ogStr.contains("-")) { throw new Exception(tl("payMustBePositive")); } - final String stringAmount = args[1].replaceAll("[^0-9\\.]", ""); + final String sanitizedString = ogStr.replaceAll("[^0-9.]", ""); - if (stringAmount.length() < 1) { + if (sanitizedString.isEmpty()) { throw new NotEnoughArgumentsException(); } - final BigDecimal amount = new BigDecimal(stringAmount); + BigDecimal tempAmount = new BigDecimal(sanitizedString); + switch (Character.toLowerCase(ogStr.charAt(ogStr.length() - 1))) { + case 'k': { + tempAmount = tempAmount.multiply(THOUSAND); + break; + } + case 'm': { + tempAmount = tempAmount.multiply(MILLION); + break; + } + case 'b': { + tempAmount = tempAmount.multiply(BILLION); + break; + } + case 't': { + tempAmount = tempAmount.multiply(TRILLION); + break; + } + default: { + break; + } + } + + final BigDecimal amount = tempAmount; + if (amount.compareTo(ess.getSettings().getMinimumPayAmount()) < 0) { // Check if amount is less than minimum-pay-amount throw new Exception(tl("minimumPayAmount", NumberUtil.displayCurrencyExactly(ess.getSettings().getMinimumPayAmount(), ess))); } diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandrealname.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandrealname.java index ed53615b0..485291bdc 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandrealname.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandrealname.java @@ -25,7 +25,7 @@ public class Commandrealname extends EssentialsCommand { final boolean skipHidden = sender.isPlayer() && !ess.getUser(sender.getPlayer()).canInteractVanished(); boolean foundUser = false; for (final User u : ess.getOnlineUsers()) { - if (skipHidden && u.isHidden(sender.getPlayer()) && !sender.getPlayer().canSee(u.getBase())) { + if (skipHidden && u.isHidden(sender.getPlayer()) && u.isHiddenFrom(sender.getPlayer())) { continue; } u.setDisplayNick(); 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 dfd79d32d..4fd6d1223 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.scheduleEntityDelayedTask(user.getBase(), () -> { 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/java/com/earth2me/essentials/commands/Commandspeed.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandspeed.java index c47b73bc7..aba64e29d 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandspeed.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandspeed.java @@ -67,7 +67,7 @@ public class Commandspeed extends EssentialsCommand { final List matchedPlayers = server.matchPlayer(name); for (final Player matchPlayer : matchedPlayers) { final User player = ess.getUser(matchPlayer); - if (skipHidden && player.isHidden(sender.getPlayer()) && !sender.getPlayer().canSee(matchPlayer)) { + if (skipHidden && player.isHidden(sender.getPlayer()) && player.isHiddenFrom(sender.getPlayer())) { continue; } foundUser = true; diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/EssentialsLoopCommand.java b/Essentials/src/main/java/com/earth2me/essentials/commands/EssentialsLoopCommand.java index ae5f26df0..4c48caa3e 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/EssentialsLoopCommand.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/EssentialsLoopCommand.java @@ -44,7 +44,7 @@ public abstract class EssentialsLoopCommand extends EssentialsCommand { } else if (matchWildcards && searchTerm.contentEquals("*")) { final boolean skipHidden = sender.isPlayer() && !ess.getUser(sender.getPlayer()).canInteractVanished(); for (final User onlineUser : ess.getOnlineUsers()) { - if (skipHidden && onlineUser.isHidden(sender.getPlayer()) && !sender.getPlayer().canSee(onlineUser.getBase())) { + if (skipHidden && onlineUser.isHidden(sender.getPlayer()) && onlineUser.isHiddenFrom(sender.getPlayer())) { continue; } userConsumer.accept(onlineUser); @@ -81,7 +81,7 @@ public abstract class EssentialsLoopCommand extends EssentialsCommand { if (matchWildcards && (searchTerm.contentEquals("**") || searchTerm.contentEquals("*"))) { for (final User onlineUser : ess.getOnlineUsers()) { - if (skipHidden && onlineUser.isHidden(sender.getPlayer()) && !sender.getPlayer().canSee(onlineUser.getBase())) { + if (skipHidden && onlineUser.isHidden(sender.getPlayer()) && onlineUser.isHiddenFrom(sender.getPlayer())) { continue; } userConsumer.accept(onlineUser); @@ -96,7 +96,7 @@ public abstract class EssentialsLoopCommand extends EssentialsCommand { if (matchedPlayers.isEmpty()) { final String matchText = searchTerm.toLowerCase(Locale.ENGLISH); for (final User player : ess.getOnlineUsers()) { - if (skipHidden && player.isHidden(sender.getPlayer()) && !sender.getPlayer().canSee(player.getBase())) { + if (skipHidden && player.isHidden(sender.getPlayer()) && player.isHiddenFrom(sender.getPlayer())) { continue; } final String displayName = FormatUtil.stripFormat(player.getDisplayName()).toLowerCase(Locale.ENGLISH); @@ -108,7 +108,7 @@ public abstract class EssentialsLoopCommand extends EssentialsCommand { } else { for (final Player matchPlayer : matchedPlayers) { final User player = ess.getUser(matchPlayer); - if (skipHidden && player.isHidden(sender.getPlayer()) && !sender.getPlayer().canSee(matchPlayer)) { + if (skipHidden && player.isHidden(sender.getPlayer()) && player.isHiddenFrom(sender.getPlayer())) { continue; } foundUser = true; diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/EssentialsToggleCommand.java b/Essentials/src/main/java/com/earth2me/essentials/commands/EssentialsToggleCommand.java index dee5ed24a..5d8ff2a89 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/EssentialsToggleCommand.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/EssentialsToggleCommand.java @@ -51,7 +51,7 @@ public abstract class EssentialsToggleCommand extends EssentialsCommand { final List matchedPlayers = server.matchPlayer(args[0]); for (final Player matchPlayer : matchedPlayers) { final User player = ess.getUser(matchPlayer); - if (skipHidden && player.isHidden(sender.getPlayer()) && !sender.getPlayer().canSee(matchPlayer)) { + if (skipHidden && player.isHidden(sender.getPlayer()) && player.isHiddenFrom(sender.getPlayer())) { continue; } foundUser = true; diff --git a/Essentials/src/main/java/com/earth2me/essentials/items/AbstractItemDb.java b/Essentials/src/main/java/com/earth2me/essentials/items/AbstractItemDb.java index c650c8990..abf9e63e0 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/items/AbstractItemDb.java +++ b/Essentials/src/main/java/com/earth2me/essentials/items/AbstractItemDb.java @@ -214,6 +214,12 @@ public abstract class AbstractItemDb implements IConf, net.ess3.api.IItemDb { sb.append("lore:").append(serializeLines(meta.getLore())).append(" "); } + if (VersionUtil.getServerBukkitVersion().isHigherThanOrEqualTo(VersionUtil.v1_14_R01)) { + if (meta.hasCustomModelData()) { + sb.append("custom-model-data:").append(meta.getCustomModelData()).append(" "); + } + } + if (meta.hasEnchants()) { for (final Enchantment e : meta.getEnchants().keySet()) { sb.append(e.getName().toLowerCase()).append(":").append(meta.getEnchantLevel(e)).append(" "); diff --git a/Essentials/src/main/java/com/earth2me/essentials/signs/EssentialsSign.java b/Essentials/src/main/java/com/earth2me/essentials/signs/EssentialsSign.java index c0ba2cf76..203a9589b 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/signs/EssentialsSign.java +++ b/Essentials/src/main/java/com/earth2me/essentials/signs/EssentialsSign.java @@ -412,12 +412,16 @@ public class EssentialsSign { } protected final ItemStack getItemMeta(final ItemStack item, final String meta, final IEssentials ess) throws SignException { + return this.getItemMeta(null, item, meta, ess); + } + + protected final ItemStack getItemMeta(final CommandSource source, final ItemStack item, final String meta, final IEssentials ess) throws SignException { ItemStack stack = item; try { if (!meta.isEmpty()) { final MetaItemStack metaStack = new MetaItemStack(stack); final boolean allowUnsafe = ess.getSettings().allowUnsafeEnchantments(); - metaStack.addStringMeta(null, allowUnsafe, meta, ess); + metaStack.addStringMeta(source, allowUnsafe, meta, ess); stack = metaStack.getItemStack(); } } catch (final Exception ex) { diff --git a/Essentials/src/main/java/com/earth2me/essentials/signs/SignFree.java b/Essentials/src/main/java/com/earth2me/essentials/signs/SignFree.java index d92f4aed5..0e455c218 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/signs/SignFree.java +++ b/Essentials/src/main/java/com/earth2me/essentials/signs/SignFree.java @@ -31,8 +31,8 @@ public class SignFree extends EssentialsSign { @Override protected boolean onSignInteract(final ISign sign, final User player, final String username, final IEssentials ess) throws SignException { ItemStack itemStack = getItemStack(sign.getLine(1), 1, ess); - itemStack = getItemMeta(itemStack, sign.getLine(2), ess); - final ItemStack item = getItemMeta(itemStack, sign.getLine(3), ess); + itemStack = getItemMeta(player.getSource(), itemStack, sign.getLine(2), ess); + final ItemStack item = getItemMeta(player.getSource(), itemStack, sign.getLine(3), ess); if (item.getType() == Material.AIR) { throw new SignException(tl("cantSpawnItem", "Air")); diff --git a/Essentials/src/main/java/com/earth2me/essentials/userstorage/ModernUserMap.java b/Essentials/src/main/java/com/earth2me/essentials/userstorage/ModernUserMap.java index c0ab51c8f..150c85fa7 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/userstorage/ModernUserMap.java +++ b/Essentials/src/main/java/com/earth2me/essentials/userstorage/ModernUserMap.java @@ -166,19 +166,29 @@ public class ModernUserMap extends CacheLoader implements IUserMap { if (userFile.exists()) { player = new OfflinePlayerStub(uuid, ess.getServer()); user = new User(player, ess); - ((OfflinePlayerStub) player).setName(user.getLastAccountName()); - uuidCache.updateCache(uuid, null); + final String accName = user.getLastAccountName(); + ((OfflinePlayerStub) player).setName(accName); + // Check to see if there is already a UUID mapping for the name in the name cache before updating it. + // Since this code is ran for offline players, there's a chance we could be overriding the mapping + // for a player who changed their name to an older player's name, let that be handled during join. + // + // Here is a senerio which could take place if didn't do the containsKey check; + // "JRoyLULW" joins the server - "JRoyLULW" is mapped to 86f39a70-eda7-44a2-88f8-0ade4e1ec8c0 + // "JRoyLULW" changes their name to "mbax" - Nothing happens, they are yet to join the server + // "mdcfe" changes their name to "JRoyLULW" - Nothing happens, they are yet to join the server + // "JRoyLULW" (formally "mdcfe") joins the server - "JRoyLULW" is mapped to 62a6a4bb-a2b8-4796-bfe6-63067250990a + // The /baltop command is ran, iterating over all players. + // + // During the baltop iteration, two uuids have the `last-account-name` of "JRoyLULW" creating the + // potential that "JRoyLULW" is mapped back to 86f39a70-eda7-44a2-88f8-0ade4e1ec8c0 when the true + // bearer of that name is now 62a6a4bb-a2b8-4796-bfe6-63067250990a. + uuidCache.updateCache(uuid, (accName == null || uuidCache.getNameCache().containsKey(accName)) ? null : accName); return user; } return null; } - public void addCachedUser(final User user) { - userCache.put(user.getUUID(), user); - debugLogCache(user); - } - @Override public Map getNameCache() { return uuidCache.getNameCache(); diff --git a/Essentials/src/main/java/com/earth2me/essentials/utils/VersionUtil.java b/Essentials/src/main/java/com/earth2me/essentials/utils/VersionUtil.java index 43b8a3350..199dabab5 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/utils/VersionUtil.java +++ b/Essentials/src/main/java/com/earth2me/essentials/utils/VersionUtil.java @@ -81,10 +81,12 @@ public final class VersionUtil { builder.put("io.akarin.server.Config", SupportStatus.DANGEROUS_FORK); // Forge - Doesn't support Bukkit - builder.put("net.minecraftforge.common.MinecraftForge", SupportStatus.UNSTABLE); + // The below translates to net.minecraftforge.common.MinecraftForge + builder.put(dumb(new int[] {110, 101, 116, 46, 109, 105, 110, 101, 99, 114, 97, 102, 116, 102, 111, 114, 103, 101, 46, 99, 111, 109, 109, 111, 110, 46, 77, 105, 110, 101, 99, 114, 97, 102, 116, 70, 111, 114, 103, 101}, 40), SupportStatus.UNSTABLE); // Fabric - Doesn't support Bukkit - builder.put("net.fabricmc.loader.launch.knot.KnotServer", SupportStatus.UNSTABLE); + // The below translates to net.fabricmc.loader.launch.knot.KnotServer + builder.put(dumb(new int[] {110, 101, 116, 46, 102, 97, 98, 114, 105, 99, 109, 99, 46, 108, 111, 97, 100, 101, 114, 46, 108, 97, 117, 110, 99, 104, 46, 107, 110, 111, 116, 46, 75, 110, 111, 116, 83, 101, 114, 118, 101, 114}, 42), SupportStatus.UNSTABLE); // Misc translation layers that do not add NMS will be caught by this if (ReflUtil.getNmsVersionObject().isHigherThanOrEqualTo(ReflUtil.V1_17_R1)) { @@ -355,4 +357,21 @@ public final class VersionUtil { return supported; } } + + private static String dumb(final int[] clazz, final int len) { + final char[] chars = new char[clazz.length]; + + for (int i = 0; i < clazz.length; i++) { + chars[i] = (char) clazz[i]; + } + + final String decode = String.valueOf(chars); + + if (decode.length() != len) { + System.exit(1); + return "why do hybrids try to bypass this?"; + } + + return decode; + } } diff --git a/Essentials/src/main/resources/config.yml b/Essentials/src/main/resources/config.yml index ba35a3157..9ce30622d 100644 --- a/Essentials/src/main/resources/config.yml +++ b/Essentials/src/main/resources/config.yml @@ -732,6 +732,10 @@ log-command-block-commands: true # Set the maximum speed for projectiles spawned with /fireball. max-projectile-speed: 8 +# Set the maximum amount of lore lines a user can set with the /itemlore command. +# Users with the essentials.itemlore.bypass permission will be able to bypass this limit. +max-itemlore-lines: 10 + # Should EssentialsX check for updates? # If set to true, EssentialsX will show notifications when a new version is available. # This uses the public GitHub API and no identifying information is sent or stored. diff --git a/Essentials/src/main/resources/messages.properties b/Essentials/src/main/resources/messages.properties index 7151a0476..428dd3db9 100644 --- a/Essentials/src/main/resources/messages.properties +++ b/Essentials/src/main/resources/messages.properties @@ -586,6 +586,7 @@ itemloreCommandUsage2Description=Sets the specified line of the held item's lore itemloreCommandUsage3=/ clear itemloreCommandUsage3Description=Clears the held item's lore itemloreInvalidItem=\u00a74You need to hold an item to edit its lore. +itemloreMaxLore=\u00a74You cannot add any more lore lines to this item. itemloreNoLine=\u00a74Your held item does not have lore text on line \u00a7c{0}\u00a74. itemloreNoLore=\u00a74Your held item does not have any lore text. itemloreSuccess=\u00a76You have added "\u00a7c{0}\u00a76" to your held item''s lore. @@ -711,21 +712,26 @@ loomCommandDescription=Opens up a loom. loomCommandUsage=/ mailClear=\u00a76To clear your mail, type\u00a7c /mail clear\u00a76. mailCleared=\u00a76Mail cleared\! +mailClearedAll=\u00a76Mail cleared for all players\! mailClearIndex=\u00a74You must specify a number between 1-{0}. mailCommandDescription=Manages inter-player, intra-server mail. -mailCommandUsage=/ [read|clear|clear [number]|send [to] [message]|sendtemp [to] [expire time] [message]|sendall [message]] +mailCommandUsage=/ [read|clear|clear [number]|clear [number]|send [to] [message]|sendtemp [to] [expire time] [message]|sendall [message]] mailCommandUsage1=/ read [page] mailCommandUsage1Description=Reads the first (or specified) page of your mail mailCommandUsage2=/ clear [number] mailCommandUsage2Description=Clears either all or the specified mail(s) -mailCommandUsage3=/ send -mailCommandUsage3Description=Sends the specified player the given message -mailCommandUsage4=/ sendall -mailCommandUsage4Description=Sends all players the given message -mailCommandUsage5=/ sendtemp -mailCommandUsage5Description=Sends the specified player the given message which will expire in the specified time -mailCommandUsage6=/ sendtempall -mailCommandUsage6Description=Sends all players the given message which will expire in the specified time +mailCommandUsage3=/ clear [number] +mailCommandUsage3Description=Clears either all or the specified mail(s) for the given player +mailCommandUsage4=/ clearall +mailCommandUsage4Description=Clears all mail for the all players +mailCommandUsage5=/ send +mailCommandUsage5Description=Sends the specified player the given message +mailCommandUsage6=/ sendall +mailCommandUsage6Description=Sends all players the given message +mailCommandUsage7=/ sendtemp +mailCommandUsage7Description=Sends the specified player the given message which will expire in the specified time +mailCommandUsage8=/ sendtempall +mailCommandUsage8Description=Sends all players the given message which will expire in the specified time mailDelay=Too many mails have been sent within the last minute. Maximum\: {0} mailFormatNew=\u00a76[\u00a7r{0}\u00a76] \u00a76[\u00a7r{1}\u00a76] \u00a7r{2} mailFormatNewTimed=\u00a76[\u00a7e\u26a0\u00a76] \u00a76[\u00a7r{0}\u00a76] \u00a76[\u00a7r{1}\u00a76] \u00a7r{2} @@ -853,6 +859,7 @@ noKitPermission=\u00a74You need the \u00a7c{0}\u00a74 permission to use that kit noKits=\u00a76There are no kits available yet. noLocationFound=\u00a74No valid location found. noMail=\u00a76You do not have any mail. +noMailOther=\u00a7c{0} \u00a76does not have any mail. noMatchingPlayers=\u00a76No matching players found. noMetaFirework=\u00a74You do not have permission to apply firework meta. noMetaJson=JSON Metadata is not supported in this version of Bukkit. @@ -1201,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=/ @@ -1462,6 +1472,7 @@ userIsAwaySelfWithMessage=\u00a77You are now AFK. userIsNotAwaySelf=\u00a77You are no longer AFK. userJailed=\u00a76You have been jailed\! usermapEntry=\u00a7c{0} \u00a76is mapped to \u00a7c{1}\u00a76. +usermapKnown=\u00a76There are \u00a7c{0} \u00a76known users to the user cache with \u00a7c{1} \u00a76name to UUID pairs. usermapPurge=\u00a76Checking for files in userdata that are not mapped, results will be logged to console. Destructive Mode: {0} usermapSize=\u00a76Current cached users in user map is \u00a7c{0}\u00a76/\u00a7c{1}\u00a76/\u00a7c{2}\u00a76. userUnknown=\u00a74Warning\: The user ''\u00a7c{0}\u00a74'' has never joined this server. diff --git a/Essentials/src/main/resources/plugin.yml b/Essentials/src/main/resources/plugin.yml index 21ea632c6..edf56ba9f 100644 --- a/Essentials/src/main/resources/plugin.yml +++ b/Essentials/src/main/resources/plugin.yml @@ -291,7 +291,7 @@ commands: aliases: [eloom] mail: description: Manages inter-player, intra-server mail. - usage: / [read|clear|clear [number]|send [to] [message]|sendtemp [to] [expire time] [message]|sendall [message]] + usage: / [read|clear|clear [number]|clear [number]|send [to] [message]|sendtemp [to] [expire time] [message]|sendall [message]] aliases: [email,eemail,memo,ememo] me: description: Describes an action in the context of the player. diff --git a/EssentialsDiscord/src/main/java/net/essentialsx/discord/DiscordSettings.java b/EssentialsDiscord/src/main/java/net/essentialsx/discord/DiscordSettings.java index beb62b33b..63076eded 100644 --- a/EssentialsDiscord/src/main/java/net/essentialsx/discord/DiscordSettings.java +++ b/EssentialsDiscord/src/main/java/net/essentialsx/discord/DiscordSettings.java @@ -6,6 +6,7 @@ import com.earth2me.essentials.config.EssentialsConfiguration; import com.earth2me.essentials.utils.FormatUtil; import net.dv8tion.jda.api.OnlineStatus; import net.dv8tion.jda.api.entities.Activity; +import net.dv8tion.jda.api.entities.Role; import net.essentialsx.api.v2.ChatType; import org.apache.logging.log4j.Level; import org.bukkit.entity.Player; @@ -33,6 +34,7 @@ public class DiscordSettings implements IConf { private Activity statusActivity; private List discordFilter; + private Map roleAliases; private MessageFormat consoleFormat; private Level consoleLogLevel; @@ -92,6 +94,18 @@ public class DiscordSettings implements IConf { return config.getBoolean("show-discord-attachments", true); } + public List getDiscordRolesBlacklist() { + return config.getList("discord-role-blacklist", String.class); + } + + public Boolean getInvertDiscordRoleBlacklist() { + return config.getBoolean("invert-discord-role-blacklist", false); + } + + public String getRoleAlias(final Role role) { + return roleAliases.getOrDefault(role.getId(), roleAliases.getOrDefault(role.getName(), role.getName())); + } + public List getPermittedFormattingRoles() { return config.getList("permit-formatting-roles", String.class); } @@ -508,6 +522,13 @@ public class DiscordSettings implements IConf { } } + final Map roleAliases = new HashMap<>(); + + for (Map.Entry entry : config.getStringMap("discord-roles-aliases").entrySet()) { + roleAliases.put(entry.getKey(), FormatUtil.replaceFormat(entry.getValue())); + } + this.roleAliases = roleAliases; + consoleLogLevel = Level.toLevel(config.getString("console.log-level", null), Level.INFO); if (config.isList("console.console-filter")) { diff --git a/EssentialsDiscord/src/main/java/net/essentialsx/discord/listeners/DiscordListener.java b/EssentialsDiscord/src/main/java/net/essentialsx/discord/listeners/DiscordListener.java index c8725c514..ea2b0ddb3 100644 --- a/EssentialsDiscord/src/main/java/net/essentialsx/discord/listeners/DiscordListener.java +++ b/EssentialsDiscord/src/main/java/net/essentialsx/discord/listeners/DiscordListener.java @@ -102,7 +102,7 @@ public class DiscordListener extends ListenerAdapter { String formattedMessage = EmojiParser.parseToAliases(MessageUtil.formatMessage(plugin.getPlugin().getSettings().getDiscordToMcFormat(), event.getChannel().getName(), user.getName(), user.getDiscriminator(), user.getAsTag(), - effectiveName, DiscordUtil.getRoleColorFormat(member), finalMessage, DiscordUtil.getRoleFormat(member)), EmojiParser.FitzpatrickAction.REMOVE); + effectiveName, DiscordUtil.getRoleColorFormat(plugin, member), finalMessage, DiscordUtil.getRoleFormat(plugin, member)), EmojiParser.FitzpatrickAction.REMOVE); for (final String group : keys) { if (plugin.getSettings().getRelayToConsoleList().contains(group)) { diff --git a/EssentialsDiscord/src/main/java/net/essentialsx/discord/util/DiscordUtil.java b/EssentialsDiscord/src/main/java/net/essentialsx/discord/util/DiscordUtil.java index c5aaaa1fe..e050c9c41 100644 --- a/EssentialsDiscord/src/main/java/net/essentialsx/discord/util/DiscordUtil.java +++ b/EssentialsDiscord/src/main/java/net/essentialsx/discord/util/DiscordUtil.java @@ -4,7 +4,6 @@ import club.minnced.discord.webhook.WebhookClient; import club.minnced.discord.webhook.send.AllowedMentions; import com.earth2me.essentials.utils.DownsampleUtil; import com.earth2me.essentials.utils.FormatUtil; -import com.earth2me.essentials.utils.NumberUtil; import com.earth2me.essentials.utils.VersionUtil; import com.google.common.collect.ImmutableList; import net.dv8tion.jda.api.Permission; @@ -21,10 +20,12 @@ import okhttp3.OkHttpClient; import org.bukkit.Bukkit; import org.bukkit.entity.Player; +import java.util.Collections; import java.util.List; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Predicate; public final class DiscordUtil { public final static String ADVANCED_RELAY_NAME = "EssX Advanced Relay"; @@ -126,20 +127,36 @@ public final class DiscordUtil { return future; } + public static Role getHighestRole(final JDADiscordService jda, final Member member, final Predicate fail) { + final List roles = member == null ? Collections.emptyList() : member.getRoles(); + final List blacklist = jda.getPlugin().getSettings().getDiscordRolesBlacklist(); + final boolean invert = jda.getPlugin().getSettings().getInvertDiscordRoleBlacklist(); + + for (final Role role : roles) { + final boolean blacklisted = blacklist.contains(role.getName()) || blacklist.contains(role.getId()); + if ((blacklisted && !invert) || (!blacklisted && invert)) { + continue; + } + + if (fail != null && fail.test(role)) { + continue; + } + + return role; + } + + return null; + } + /** * Gets the highest role of a given member or an empty string if the member has no roles. * * @param member The target member. * @return The highest role or blank string. */ - public static String getRoleFormat(Member member) { - final List roles = member == null ? null : member.getRoles(); - - if (roles == null || roles.isEmpty()) { - return ""; - } - - return roles.get(0).getName(); + public static String getRoleFormat(final JDADiscordService jda, Member member) { + final Role role = getHighestRole(jda, member, null); + return role != null ? jda.getSettings().getRoleAlias(role) : ""; } /** @@ -148,11 +165,13 @@ public final class DiscordUtil { * @param member The target member. * @return The bukkit color code or blank string. */ - public static String getRoleColorFormat(Member member) { - if (member == null || member.getColorRaw() == Role.DEFAULT_COLOR_RAW) { + public static String getRoleColorFormat(final JDADiscordService jda, Member member) { + final Role topRole = getHighestRole(jda, member, role -> role.getColorRaw() == Role.DEFAULT_COLOR_RAW); + if (topRole == null || topRole.getColorRaw() == Role.DEFAULT_COLOR_RAW) { return ""; } - final int rawColor = 0xff000000 | member.getColorRaw(); + + final int rawColor = 0xff000000 | topRole.getColorRaw(); if (VersionUtil.getServerBukkitVersion().isHigherThanOrEqualTo(VersionUtil.v1_16_1_R01)) { // Essentials' FormatUtil allows us to not have to use bungee's chatcolor since bukkit's own one doesn't support rgb @@ -176,14 +195,13 @@ public final class DiscordUtil { final List roles = member.getRoles(); for (String roleDefinition : roleDefinitions) { roleDefinition = roleDefinition.trim(); - final boolean id = NumberUtil.isNumeric(roleDefinition); if (roleDefinition.equals("*") || member.getId().equals(roleDefinition)) { return true; } for (final Role role : roles) { - if (role.getId().equals(roleDefinition) || (!id && role.getName().equalsIgnoreCase(roleDefinition))) { + if (matchesRole(role, roleDefinition)) { return true; } } @@ -191,6 +209,15 @@ public final class DiscordUtil { return false; } + /** + * Checks if the provided role matches the provided role definition (string representation of id or the role's name) + * + * @return true if the provided definition matches the provided role. + */ + public static boolean matchesRole(Role role, String roleDefinition) { + return role.getId().equals(roleDefinition) || role.getName().equalsIgnoreCase(roleDefinition); + } + public static String getAvatarUrl(final JDADiscordService jda, final Player player) { return jda.getSettings().getAvatarURL().replace("{uuid}", player.getUniqueId().toString()).replace("{name}", player.getName()); } diff --git a/EssentialsDiscord/src/main/resources/config.yml b/EssentialsDiscord/src/main/resources/config.yml index 9550cfd86..331be18df 100644 --- a/EssentialsDiscord/src/main/resources/config.yml +++ b/EssentialsDiscord/src/main/resources/config.yml @@ -224,6 +224,18 @@ commands: # If this is set to false and a message from Discord only contains an image/file and not any text, nothing will be sent. show-discord-attachments: true +# A list of roles which should be ignored by the {color} and {role} placeholders. +# for the Discord->MC chat format. +discord-role-blacklist: + - "123456789012345678" + - "Members" + +# Role aliases allow you to replace the role names with something different in the Discord->MC chat relay format. +# If you are using role aliases, make sure to remove the '#' at the start to allow the setting to be read. +discord-roles-aliases: +# "123456789012345678": "&c&lAdmin" +# "Members": "Member" + # A list of roles allowed to send Minecraft color/formatting codes from Discord to MC. # This applies to all aspects such as that Discord->MC chat relay as well as commands. # You can either use '*' (for everyone), a role name/ID, or a user ID. diff --git a/README.md b/README.md index 2fd9c63fc..3f7c064fc 100644 --- a/README.md +++ b/README.md @@ -76,8 +76,8 @@ To add EssentialsX to your build system, you should use the following artifacts: | Type | Group ID | Artifact ID | Version | |:---------------|:------------------|:--------------|:------------------| -| Latest release | `net.essentialsx` | `EssentialsX` | `2.20.0` | -| Snapshots | `net.essentialsx` | `EssentialsX` | `2.20.1-SNAPSHOT` | +| Latest release | `net.essentialsx` | `EssentialsX` | `2.20.1` | +| Snapshots | `net.essentialsx` | `EssentialsX` | `2.21.0-SNAPSHOT` | | Older releases | `net.ess3` | `EssentialsX` | `2.18.2` | Note: until version `2.18.2`, EssentialsX used the `net.ess3` group ID. diff --git a/build.gradle b/build.gradle index 8894c79a2..4a859e015 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ plugins { } group = "net.essentialsx" -version = "2.20.1-SNAPSHOT" +version = "2.21.0-SNAPSHOT" project.ext { GIT_COMMIT = !indraGit.isPresent() ? "unknown" : indraGit.commit().abbreviate(7).name()