diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 4fa7ae5..89fc5a8 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,7 +8,7 @@ updates: - package-ecosystem: "maven" # See documentation for possible values directory: "/" # Location of package manifests schedule: - interval: "daily" + interval: "weekly" commit-message: # Prefix all commit messages with "npm: " prefix: "maven" @@ -17,7 +17,7 @@ updates: # default location of `.github/workflows` directory: "/" schedule: - interval: "daily" + interval: "weekly" commit-message: # Prefix all commit messages with "npm: " prefix: "gh-actions" diff --git a/.github/workflows/autobuild.yml b/.github/workflows/autobuild.yml index 524e85a..2f39634 100644 --- a/.github/workflows/autobuild.yml +++ b/.github/workflows/autobuild.yml @@ -12,10 +12,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v7.0.1 - name: Set up JDK 17 - uses: actions/setup-java@v4.2.1 + uses: actions/setup-java@v6.0.0 with: distribution: 'zulu' java-version: '17' diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 033612f..d25c8bc 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -12,11 +12,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v7.0.1 with: fetch-depth: 0 - name: Set up JDK - uses: actions/setup-java@v4.2.1 + uses: actions/setup-java@v6.0.0 with: distribution: 'zulu' java-version: '17' diff --git a/ensure-java-16 b/ensure-java-16 deleted file mode 100644 index b7d264f..0000000 --- a/ensure-java-16 +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash - -JV=`java -version 2>&1 >/dev/null | head -1` -echo $JV | sed -E 's/^.*version "([^".]*)\.[^"]*".*$/\1/' - -if [ "$JV" != 16 ]; then - case "$1" in - install) - echo "Installing SDKMAN..." - curl -s "https://get.sdkman.io" | bash - source ~/.sdkman/bin/sdkman-init.sh - sdk version - sdk install java 16.0.2-open - ;; - use) - echo "must source ~/.sdkman/bin/sdkman-init.sh" - exit 1 - ;; - esac -fi \ No newline at end of file diff --git a/jitpack.yml b/jitpack.yml index 8b41472..33c3123 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -1,10 +1,9 @@ jdk: - - openjdk16 + - openjdk17 before_install: - - echo "Before Install" - - bash ensure-java-16 install + - sdk install java 17.0.1-open + - sdk use java 17.0.1-open install: - echo "Install" - - if ! bash ensure-java-16 use; then source ~/.sdkman/bin/sdkman-init.sh; fi - java -version - mvn install \ No newline at end of file diff --git a/pom.xml b/pom.xml index 96f8954..05c9b39 100644 --- a/pom.xml +++ b/pom.xml @@ -4,9 +4,9 @@ de.jeter ChatEx ChatEx - 2024 + 2026 - 3.2.2 + 3.3.0 ChatManagement plugin for Bukkit https://www.spigotmc.org/resources/chatex-continued.71041/ @@ -32,6 +32,31 @@ + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.6.0 + + google_checks.xml + warning + false + false + true + + + + verify + + check + + + + org.apache.maven.plugins maven-shade-plugin @@ -91,7 +116,7 @@ org.spigotmc spigot-api - 1.20.1-R0.1-SNAPSHOT + 1.18.2-R0.1-SNAPSHOT provided @@ -109,7 +134,7 @@ net.luckperms api - 5.4 + 5.5 provided @@ -121,13 +146,13 @@ org.purpurmc.purpur purpur-api - 1.20.1-R0.1-SNAPSHOT + 1.18.2-R0.1-SNAPSHOT provided org.bstats bstats-bukkit - 3.0.2 + 3.2.1 compile @@ -136,12 +161,18 @@ 2.0.6 compile + + org.junit.jupiter + junit-jupiter + 5.10.3 + test + - 16 - 16 - 16 + 17 + 17 + 17 UTF-8 diff --git a/src/main/java/de/jeter/chatex/ChannelHandler.java b/src/main/java/de/jeter/chatex/ChannelHandler.java index d47ef8e..d98dc1e 100644 --- a/src/main/java/de/jeter/chatex/ChannelHandler.java +++ b/src/main/java/de/jeter/chatex/ChannelHandler.java @@ -27,6 +27,7 @@ import java.io.*; import java.util.concurrent.TimeUnit; +import java.util.logging.Level; public class ChannelHandler implements PluginMessageListener { @@ -66,7 +67,7 @@ public void onPluginMessageReceived(String channel, Player player, byte[] messag millis = msgin.readLong(); msg = msgin.readUTF(); } catch (IOException ex) { - ex.printStackTrace(); + ChatEx.getInstance().getLogger().log(Level.SEVERE, "Could not read cross-server chat message", ex); msg = "null"; } @@ -90,7 +91,7 @@ public void sendMessage(Player p, String message) { msgout.writeLong(System.currentTimeMillis()); msgout.writeUTF(message); } catch (IOException exception) { - exception.printStackTrace(); + ChatEx.getInstance().getLogger().log(Level.SEVERE, "Could not write cross-server chat message", exception); } out.writeShort(msgbytes.toByteArray().length); diff --git a/src/main/java/de/jeter/chatex/ChatEx.java b/src/main/java/de/jeter/chatex/ChatEx.java index bebcfb0..0060101 100644 --- a/src/main/java/de/jeter/chatex/ChatEx.java +++ b/src/main/java/de/jeter/chatex/ChatEx.java @@ -29,6 +29,7 @@ public class ChatEx extends JavaPlugin { private static ChatEx INSTANCE; private UpdateChecker updatechecker = null; + private ChatListener chatListener; public static ChatEx getInstance() { return INSTANCE; @@ -43,8 +44,10 @@ public void onEnable() { PluginManager.load(); ChatLogger.load(); RGBColors.load(); + DomainDictionary.load(); - getServer().getPluginManager().registerEvents(new ChatListener(), this); + chatListener = new ChatListener(); + chatListener.register(this); getServer().getPluginManager().registerEvents(new PlayerListener(), this); getCommand("chatex").setExecutor(new CommandHandler()); @@ -75,4 +78,8 @@ public void onDisable() { public UpdateChecker getUpdateChecker() { return this.updatechecker; } + + public ChatListener getChatListener() { + return this.chatListener; + } } diff --git a/src/main/java/de/jeter/chatex/ChatListener.java b/src/main/java/de/jeter/chatex/ChatListener.java index 279c689..f14f8bb 100644 --- a/src/main/java/de/jeter/chatex/ChatListener.java +++ b/src/main/java/de/jeter/chatex/ChatListener.java @@ -26,10 +26,10 @@ import de.jeter.chatex.utils.adManager.SmartAdManager; import org.bukkit.Bukkit; import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.player.AsyncPlayerChatEvent; +import org.bukkit.plugin.Plugin; import java.util.UnknownFormatConversionException; import java.util.regex.Matcher; @@ -38,173 +38,220 @@ public class ChatListener implements Listener { private final AdManager adManager = Config.ADS_SMART_MANAGER.getBoolean() ? new SmartAdManager() : new SimpleAdManager(); + private EventPriority registeredPriority; - @EventHandler(priority = EventPriority.LOWEST) - public void onLowest(final AsyncPlayerChatEvent event) { - if (Config.PRIORITY.getString().equalsIgnoreCase("LOWEST")) { - executeChatEvent(event); - } - } - - @EventHandler(priority = EventPriority.LOW) - public void onLow(final AsyncPlayerChatEvent event) { - if (Config.PRIORITY.getString().equalsIgnoreCase("LOW")) { - executeChatEvent(event); - } - } - - @EventHandler(priority = EventPriority.NORMAL) - public void onNormal(final AsyncPlayerChatEvent event) { - if (Config.PRIORITY.getString().equalsIgnoreCase("NORMAL")) { - executeChatEvent(event); - } + /** + * Registers this listener for {@link AsyncPlayerChatEvent} at the priority configured + * in {@link Config#PRIORITY}, instead of relying on one fixed {@code @EventHandler} per priority. + */ + public void register(Plugin plugin) { + registeredPriority = resolvePriority(); + Bukkit.getPluginManager().registerEvent( + AsyncPlayerChatEvent.class, + this, + registeredPriority, + (listener, event) -> executeChatEvent((AsyncPlayerChatEvent) event), + plugin + ); } - @EventHandler(priority = EventPriority.HIGH) - public void onHigh(final AsyncPlayerChatEvent event) { - if (Config.PRIORITY.getString().equalsIgnoreCase("HIGH")) { - executeChatEvent(event); + /** + * Re-registers this listener if the configured {@link Config#PRIORITY} changed, e.g. after {@code /chatex reload}. + */ + public void reregister(Plugin plugin) { + if (resolvePriority() == registeredPriority) { + return; } + AsyncPlayerChatEvent.getHandlerList().unregister(this); + register(plugin); } - @EventHandler(priority = EventPriority.HIGHEST) - public void onHighest(final AsyncPlayerChatEvent event) { - if (Config.PRIORITY.getString().equalsIgnoreCase("HIGHEST")) { - executeChatEvent(event); + private EventPriority resolvePriority() { + try { + return EventPriority.valueOf(Config.PRIORITY.getString().toUpperCase()); + } catch (IllegalArgumentException ex) { + ChatEx.getInstance().getLogger().warning("Unknown EventPriority '" + Config.PRIORITY.getString() + "' configured, falling back to LOWEST."); + return EventPriority.LOWEST; } } - @EventHandler(priority = EventPriority.MONITOR) - public void onMonitor(final AsyncPlayerChatEvent event) { - if (Config.PRIORITY.getString().equalsIgnoreCase("MONITOR")) { - executeChatEvent(event); - } + /** + * Carries the message-format pipeline state between the individual processing steps below. + */ + private record RoutingResult(String format, String chatMessage, boolean global) { } private void executeChatEvent(AsyncPlayerChatEvent event) { LogHelper.debug("ChatEvent fired with priority: " + Config.PRIORITY.getString().toUpperCase() + ", ChatEx reacting to it..."); Player player = event.getPlayer(); - if (!player.hasPermission("chatex.allowchat")) { - String msg = Locales.COMMAND_RESULT_NO_PERM.getString(player).replaceAll("%perm", "chatex.allowchat"); - player.sendMessage(msg); - event.setCancelled(true); + if (!hasChatPermission(event, player)) { return; } - String format = PluginManager.getInstance().getMessageFormat(event.getPlayer()); - LogHelper.debug("Format: " + format); - LogHelper.debug("Prefix: " + PluginManager.getInstance().getPrefix(event.getPlayer())); - LogHelper.debug("Suffix: " + PluginManager.getInstance().getSuffix(event.getPlayer())); + String format = PluginManager.getMessageFormat(player); + logFormatDebug(player, format); String chatMessage = event.getMessage(); - if (!AntiSpamManager.getInstance().isAllowed(event.getPlayer())) { - long remainingTime = AntiSpamManager.getInstance().getRemainingSeconds(event.getPlayer()); - String message = Locales.ANTI_SPAM_DENIED.getString(event.getPlayer()).replaceAll("%time%", remainingTime + ""); - MessageBlockedBySpamManagerEvent messageBlockedBySpamManagerEvent = new MessageBlockedBySpamManagerEvent(event.getPlayer(), chatMessage, message, remainingTime); - Bukkit.getPluginManager().callEvent(messageBlockedBySpamManagerEvent); - event.setCancelled(!messageBlockedBySpamManagerEvent.isCancelled()); - if (!messageBlockedBySpamManagerEvent.isCancelled()) { - event.getPlayer().sendMessage(messageBlockedBySpamManagerEvent.getPluginMessage()); - return; + // Every apply*-step below returns null if it already cancelled/handled the event itself. + chatMessage = applyAntiSpam(event, player, chatMessage); + if (chatMessage == null) { + return; + } + + chatMessage = applyAdBlocker(event, player, chatMessage); + if (chatMessage == null) { + return; + } + + chatMessage = applyWordBlocker(event, player, chatMessage); + if (chatMessage == null) { + return; + } + + RoutingResult routing = applyRouting(event, player, format, chatMessage); + if (routing == null) { + return; + } + + forwardCrossServerMessage(player, routing); + finalizeMessage(event, player, routing); + } + + private boolean hasChatPermission(AsyncPlayerChatEvent event, Player player) { + if (player.hasPermission("chatex.allowchat")) { + return true; + } + String msg = Locales.COMMAND_RESULT_NO_PERM.getString(player).replaceAll("%perm", "chatex.allowchat"); + player.sendMessage(msg); + event.setCancelled(true); + return false; + } + + private void logFormatDebug(Player player, String format) { + LogHelper.debug("Format: " + format); + LogHelper.debug("Prefix: " + PluginManager.getPrefix(player)); + LogHelper.debug("Suffix: " + PluginManager.getSuffix(player)); + } + + private String applyAntiSpam(AsyncPlayerChatEvent event, Player player, String chatMessage) { + if (!AntiSpamManager.getInstance().isAllowed(player)) { + long remainingTime = AntiSpamManager.getInstance().getRemainingSeconds(player); + String message = Locales.ANTI_SPAM_DENIED.getString(player).replaceAll("%time%", remainingTime + ""); + MessageBlockedBySpamManagerEvent blockedEvent = new MessageBlockedBySpamManagerEvent(player, chatMessage, message, remainingTime); + Bukkit.getPluginManager().callEvent(blockedEvent); + event.setCancelled(!blockedEvent.isCancelled()); + if (!blockedEvent.isCancelled()) { + player.sendMessage(blockedEvent.getPluginMessage()); + return null; } - chatMessage = messageBlockedBySpamManagerEvent.getMessage(); + chatMessage = blockedEvent.getMessage(); } AntiSpamManager.getInstance().put(player); - LogHelper.debug("Player did not activate the AntiSpam. Continuing..."); + return chatMessage; + } + private String applyAdBlocker(AsyncPlayerChatEvent event, Player player, String chatMessage) { if (adManager.checkForAds(chatMessage, player)) { String message = Locales.MESSAGES_AD.getString(null).replaceAll("%perm", "chatex.bypassads"); - MessageBlockedByAdManagerEvent messageBlockedByAdManagerEvent = new MessageBlockedByAdManagerEvent(player, chatMessage, message); - Bukkit.getPluginManager().callEvent(messageBlockedByAdManagerEvent); - chatMessage = messageBlockedByAdManagerEvent.getMessage(); - event.setCancelled(!messageBlockedByAdManagerEvent.isCancelled()); - if (!messageBlockedByAdManagerEvent.isCancelled()) { - event.getPlayer().sendMessage(messageBlockedByAdManagerEvent.getPluginMessage()); - return; + MessageBlockedByAdManagerEvent blockedEvent = new MessageBlockedByAdManagerEvent(player, chatMessage, message); + Bukkit.getPluginManager().callEvent(blockedEvent); + chatMessage = blockedEvent.getMessage(); + event.setCancelled(!blockedEvent.isCancelled()); + if (!blockedEvent.isCancelled()) { + player.sendMessage(blockedEvent.getPluginMessage()); + return null; } } - LogHelper.debug("Player did not activate the AdBlocker. Continuing..."); + return chatMessage; + } - for(String block : Config.BLOCKED_WORDS.getStringList()) { - if(chatMessage.toLowerCase().contains(block.toLowerCase())) { + private String applyWordBlocker(AsyncPlayerChatEvent event, Player player, String chatMessage) { + for (String block : Config.BLOCKED_WORDS.getStringList()) { + if (chatMessage.toLowerCase().contains(block.toLowerCase())) { LogHelper.debug("Player activated wordblocker! ChatMessage: " + chatMessage + " contains blockedWord: " + block); String message = Locales.MESSAGES_BLOCKED.getString(null); - MessageContainsBlockedWordEvent messageContainsBlockedWordEvent = new MessageContainsBlockedWordEvent(player, chatMessage, message); - Bukkit.getPluginManager().callEvent(messageContainsBlockedWordEvent); - event.setCancelled(!messageContainsBlockedWordEvent.isCancelled()); - chatMessage = messageContainsBlockedWordEvent.getMessage(); - if (!messageContainsBlockedWordEvent.isCancelled()) { - event.getPlayer().sendMessage(messageContainsBlockedWordEvent.getPluginMessage()); - return; + MessageContainsBlockedWordEvent blockedEvent = new MessageContainsBlockedWordEvent(player, chatMessage, message); + Bukkit.getPluginManager().callEvent(blockedEvent); + event.setCancelled(!blockedEvent.isCancelled()); + chatMessage = blockedEvent.getMessage(); + if (!blockedEvent.isCancelled()) { + player.sendMessage(blockedEvent.getPluginMessage()); + return null; } } } - LogHelper.debug("Player did not use a blocked word. Continuing..."); LogHelper.debug("ChatMessage: " + chatMessage); + return chatMessage; + } + + private RoutingResult applyRouting(AsyncPlayerChatEvent event, Player player, String format, String chatMessage) { boolean global = false; if (Config.RANGEMODE.getBoolean() || Config.BUNGEECORD.getBoolean()) { LogHelper.debug("Message starts with prefix (" + Config.RANGEPREFIX.getString() + "): " + chatMessage.startsWith(Config.RANGEPREFIX.getString())); if ((Config.RANGEMODE.getBoolean() && chatMessage.startsWith(Config.RANGEPREFIX.getString())) || Config.BUNGEECORD.getBoolean()) { LogHelper.debug("Global mode enabled!"); - if (player.hasPermission("chatex.chat.global")) { - chatMessage = chatMessage.replaceFirst(Pattern.quote(Config.RANGEPREFIX.getString()), ""); - format = PluginManager.getInstance().getGlobalMessageFormat(player); - global = true; - - PlayerUsesGlobalChatEvent playerUsesGlobalChatEvent = new PlayerUsesGlobalChatEvent(player, chatMessage); - Bukkit.getPluginManager().callEvent(playerUsesGlobalChatEvent); - chatMessage = playerUsesGlobalChatEvent.getMessage(); - if (playerUsesGlobalChatEvent.isCancelled()) { - event.setCancelled(true); - return; - } - } else { + if (!player.hasPermission("chatex.chat.global")) { player.sendMessage(Locales.COMMAND_RESULT_NO_PERM.getString(player).replaceAll("%perm", "chatex.chat.global")); event.setCancelled(true); - return; + return null; } - } else { - if (Config.RANGEMODE.getBoolean()) { - LogHelper.debug("Range mode enabled!"); - event.getRecipients().clear(); - if (Utils.getLocalRecipients(player).size() == 1 && Config.SHOW_NO_RECEIVER_MSG.getBoolean()) { - player.sendMessage(Locales.NO_LISTENING_PLAYERS.getString(player)); - event.setCancelled(true); - return; - } else { - event.getRecipients().addAll(Utils.getLocalRecipients(player)); - - PlayerUsesRangeModeEvent playerUsesRangeModeEvent = new PlayerUsesRangeModeEvent(player, chatMessage); - Bukkit.getPluginManager().callEvent(playerUsesRangeModeEvent); - chatMessage = playerUsesRangeModeEvent.getMessage(); - if (playerUsesRangeModeEvent.isCancelled()) { - event.setCancelled(true); - return; - } - } + + chatMessage = chatMessage.replaceFirst(Pattern.quote(Config.RANGEPREFIX.getString()), ""); + format = PluginManager.getGlobalMessageFormat(player); + global = true; + + PlayerUsesGlobalChatEvent globalChatEvent = new PlayerUsesGlobalChatEvent(player, chatMessage); + Bukkit.getPluginManager().callEvent(globalChatEvent); + chatMessage = globalChatEvent.getMessage(); + if (globalChatEvent.isCancelled()) { + event.setCancelled(true); + return null; + } + } else if (Config.RANGEMODE.getBoolean()) { + LogHelper.debug("Range mode enabled!"); + event.getRecipients().clear(); + if (Utils.getLocalRecipients(player).size() == 1 && Config.SHOW_NO_RECEIVER_MSG.getBoolean()) { + player.sendMessage(Locales.NO_LISTENING_PLAYERS.getString(player)); + event.setCancelled(true); + return null; + } + + event.getRecipients().addAll(Utils.getLocalRecipients(player)); + + PlayerUsesRangeModeEvent rangeModeEvent = new PlayerUsesRangeModeEvent(player, chatMessage); + Bukkit.getPluginManager().callEvent(rangeModeEvent); + chatMessage = rangeModeEvent.getMessage(); + if (rangeModeEvent.isCancelled()) { + event.setCancelled(true); + return null; } } } - if (global && Config.BUNGEECORD.getBoolean()) { + return new RoutingResult(format, chatMessage, global); + } + + private void forwardCrossServerMessage(Player player, RoutingResult routing) { + if (routing.global() && Config.BUNGEECORD.getBoolean()) { LogHelper.debug("Local mode & Bungeecord mode enabled! Spreading Cross server message..."); - String msgToSend = Utils.replacePlayerPlaceholders(player, format.replaceAll("%message", Matcher.quoteReplacement(chatMessage))); + String msgToSend = Utils.replacePlayerPlaceholders(player, routing.format().replaceAll("%message", Matcher.quoteReplacement(routing.chatMessage()))); ChannelHandler.getInstance().sendMessage(player, msgToSend); } + } + private void finalizeMessage(AsyncPlayerChatEvent event, Player player, RoutingResult routing) { LogHelper.debug("Replacing Placeholder in format..."); - format = Utils.replacePlayerPlaceholders(player, format); + String format = Utils.replacePlayerPlaceholders(player, routing.format()); format = Utils.escape(format); format = format.replace("%%message", "%2$s"); LogHelper.debug("Format after replacing: " + format); - try { event.setFormat(format); } catch (UnknownFormatConversionException ex) { @@ -214,8 +261,8 @@ private void executeChatEvent(AsyncPlayerChatEvent event) { event.setFormat(format); } - event.setMessage(Utils.translateColorCodes(chatMessage, player)); - ChatLogger.writeToFile(player, chatMessage); + event.setMessage(Utils.translateColorCodes(routing.chatMessage(), player)); + ChatLogger.writeToFile(player, routing.chatMessage()); LogHelper.debug("Everything done! Method end."); } diff --git a/src/main/java/de/jeter/chatex/CommandHandler.java b/src/main/java/de/jeter/chatex/CommandHandler.java index fdf82c8..0708e9a 100644 --- a/src/main/java/de/jeter/chatex/CommandHandler.java +++ b/src/main/java/de/jeter/chatex/CommandHandler.java @@ -19,6 +19,7 @@ package de.jeter.chatex; import de.jeter.chatex.utils.Config; +import de.jeter.chatex.utils.DomainDictionary; import de.jeter.chatex.utils.Locales; import de.jeter.chatex.utils.Utils; import org.bukkit.Bukkit; @@ -27,13 +28,16 @@ import java.util.ArrayList; import java.util.List; +import java.util.regex.Matcher; public class CommandHandler implements CommandExecutor, TabCompleter { @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { if (args.length == 0) { - sender.sendMessage("§aChatEx plugin by " + ChatEx.getInstance().getDescription().getAuthors() + " (" + ChatEx.getInstance().getDescription().getVersion() + ")"); + sender.sendMessage(Locales.COMMAND_INFO.getString(null) + .replaceAll("%authors", Matcher.quoteReplacement(String.valueOf(ChatEx.getInstance().getDescription().getAuthors()))) + .replaceAll("%version", Matcher.quoteReplacement(ChatEx.getInstance().getDescription().getVersion()))); return true; } else if (args.length > 1) { sender.sendMessage(Locales.COMMAND_RESULT_WRONG_USAGE.getString(null).replaceAll("%cmd", command.getName())); @@ -42,6 +46,9 @@ public boolean onCommand(CommandSender sender, Command command, String label, St if (args[0].equalsIgnoreCase("reload")) { if (sender.hasPermission("chatex.reload")) { Config.reload(true); + Locales.reload(true); + DomainDictionary.load(); + ChatEx.getInstance().getChatListener().reregister(ChatEx.getInstance()); sender.sendMessage(Locales.MESSAGES_RELOAD.getString(null)); if (Config.CHANGE_TABLIST_NAME.getBoolean()) { @@ -61,23 +68,42 @@ public boolean onCommand(CommandSender sender, Command command, String label, St Bukkit.broadcastMessage("\n"); } - Player clearer = null; + Player clearer = sender instanceof Player ? (Player) sender : null; + String who = null; + if (clearer == null) { + who = (sender instanceof ConsoleCommandSender || sender instanceof BlockCommandSender) + ? Locales.COMMAND_CLEAR_CONSOLE.getString(null) + : Locales.COMMAND_CLEAR_UNKNOWN.getString(null); + } - String who = Locales.COMMAND_CLEAR_UNKNOWN.getString(null); - if ((sender instanceof ConsoleCommandSender) || (sender instanceof BlockCommandSender)) { - who = Locales.COMMAND_CLEAR_CONSOLE.getString(null); - } else if (sender instanceof Player) { - who = sender.getName(); - clearer = (Player) sender; + // The locale text is the source of truth for where "who cleared the chat" is + // placed (via %prefix%displayname%suffix, same as every bundled translation). + // Appending it in code below is only a fallback for locale files saved before + // this was configurable, i.e. that don't contain %displayname at all yet. + String rawMsg = Locales.MESSAGES_CLEAR.getString(null); + String msg; + if (rawMsg.contains("%displayname")) { + msg = clearer != null + ? Locales.MESSAGES_CLEAR.getString(clearer) + : rawMsg.replace("%prefix", "").replace("%displayname", who).replace("%suffix", ""); + } else { + msg = Utils.replacePlayerPlaceholders(clearer, rawMsg + (clearer != null ? "%prefix%displayname%suffix" : who)); } - Bukkit.broadcastMessage(Locales.MESSAGES_CLEAR.getString(clearer) + who); + + Bukkit.broadcastMessage(msg); } else { sender.sendMessage(Locales.COMMAND_RESULT_NO_PERM.getString(null).replaceAll("%perm", "chatex.clear")); } return true; } else if (args[0].equalsIgnoreCase("help") || args[0].equalsIgnoreCase("?")) { - sender.sendMessage("§a/" + command.getName() + " reload - " + Locales.COMMAND_RELOAD_DESCRIPTION.getString(null)); - sender.sendMessage("§a/" + command.getName() + " clear - " + Locales.COMMAND_CLEAR_DESCRIPTION.getString(null)); + sender.sendMessage(Locales.COMMAND_HELP_LINE.getString(null) + .replaceAll("%cmd", Matcher.quoteReplacement(command.getName())) + .replaceAll("%subcommand", "reload") + .replaceAll("%description", Matcher.quoteReplacement(Locales.COMMAND_RELOAD_DESCRIPTION.getString(null)))); + sender.sendMessage(Locales.COMMAND_HELP_LINE.getString(null) + .replaceAll("%cmd", Matcher.quoteReplacement(command.getName())) + .replaceAll("%subcommand", "clear") + .replaceAll("%description", Matcher.quoteReplacement(Locales.COMMAND_CLEAR_DESCRIPTION.getString(null)))); return true; } else { sender.sendMessage(Locales.COMMAND_RESULT_WRONG_USAGE.getString(null).replaceAll("%cmd", "/chatex")); diff --git a/src/main/java/de/jeter/chatex/PlayerListener.java b/src/main/java/de/jeter/chatex/PlayerListener.java index 4288205..b32824b 100644 --- a/src/main/java/de/jeter/chatex/PlayerListener.java +++ b/src/main/java/de/jeter/chatex/PlayerListener.java @@ -18,9 +18,11 @@ */ package de.jeter.chatex; +import de.jeter.chatex.utils.AntiSpamManager; import de.jeter.chatex.utils.Config; import de.jeter.chatex.utils.Locales; import de.jeter.chatex.utils.Utils; +import de.jeter.chatex.utils.adManager.SmartAdManager; import de.jeter.updatechecker.Result; import de.jeter.updatechecker.UpdateChecker; import net.md_5.bungee.api.chat.ClickEvent; @@ -61,7 +63,7 @@ public void onJoin(PlayerJoinEvent e) { if (checker.getResult() == Result.UPDATE_FOUND) { try { TextComponent msg = new TextComponent(Locales.UPDATE_FOUND.getString(null).replaceAll("%oldversion", ChatEx.getInstance().getDescription().getVersion()).replaceAll("%newversion", ChatEx.getInstance().getUpdateChecker().getLatestRemoteVersion())); - msg.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("§aClick to download"))); + msg.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(Locales.UPDATE_FOUND_HOVER.getString(null)))); msg.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, checker.getDownloadLink())); e.getPlayer().spigot().sendMessage(msg); } catch (NoClassDefFoundError ex) { @@ -75,6 +77,9 @@ public void onJoin(PlayerJoinEvent e) { @EventHandler(priority = EventPriority.LOWEST) public void onQuit(final PlayerQuitEvent e) { + AntiSpamManager.getInstance().remove(e.getPlayer().getUniqueId()); + SmartAdManager.clearPlayer(e.getPlayer().getUniqueId()); + if (!Config.CHANGE_JOIN_AND_QUIT.getBoolean()) { return; } diff --git a/src/main/java/de/jeter/chatex/api/ChatExAPI.java b/src/main/java/de/jeter/chatex/api/ChatExAPI.java index 9fcae53..ed5dd6a 100644 --- a/src/main/java/de/jeter/chatex/api/ChatExAPI.java +++ b/src/main/java/de/jeter/chatex/api/ChatExAPI.java @@ -25,7 +25,7 @@ public class ChatExAPI { public String getPermissionHandlerName() { - return PluginManager.getInstance().getName(); + return PluginManager.getName(); } public AntiSpamManager getAntiSpamManager() { @@ -33,22 +33,22 @@ public AntiSpamManager getAntiSpamManager() { } public String getPrefix(Player p) { - return PluginManager.getInstance().getPrefix(p); + return PluginManager.getPrefix(p); } public String getSuffix(Player p) { - return PluginManager.getInstance().getSuffix(p); + return PluginManager.getSuffix(p); } public String[] getGroupNames(Player p) { - return PluginManager.getInstance().getGroupNames(p); + return PluginManager.getGroupNames(p); } public String getMessageFormat(Player p) { - return PluginManager.getInstance().getMessageFormat(p); + return PluginManager.getMessageFormat(p); } public String getGlobalMessageFormat(Player p) { - return PluginManager.getInstance().getGlobalMessageFormat(p); + return PluginManager.getGlobalMessageFormat(p); } } \ No newline at end of file diff --git a/src/main/java/de/jeter/chatex/plugins/PluginManager.java b/src/main/java/de/jeter/chatex/plugins/PluginManager.java index f932180..606453b 100644 --- a/src/main/java/de/jeter/chatex/plugins/PluginManager.java +++ b/src/main/java/de/jeter/chatex/plugins/PluginManager.java @@ -23,17 +23,16 @@ import de.jeter.chatex.utils.Utils; import org.bukkit.entity.Player; -public class PluginManager implements PermissionsPlugin { +/** + * Picks and delegates to whichever {@link PermissionsPlugin} handler is available on this + * server (LuckPerms, Vault or a no-op fallback). Purely static: there is no per-instance state, + * so unlike an earlier version of this class it no longer wraps that in a redundant singleton. + */ +public class PluginManager { private static PermissionsPlugin handler; - private static PluginManager INSTANCE; - - public static PermissionsPlugin getInstance() { - return INSTANCE; - } public static void load() { - INSTANCE = new PluginManager(); if (HookManager.checkLuckperms()) { handler = new LuckPerms(); } else if (HookManager.checkVault() && Vault.setupChat()) { @@ -60,33 +59,27 @@ public static void load() { } } - @Override - public String getName() { + public static String getName() { return handler.getName(); } - @Override - public String getPrefix(Player p) { + public static String getPrefix(Player p) { return handler.getPrefix(p); } - @Override - public String getSuffix(Player p) { + public static String getSuffix(Player p) { return handler.getSuffix(p); } - @Override - public String[] getGroupNames(Player p) { + public static String[] getGroupNames(Player p) { return handler.getGroupNames(p); } - @Override - public String getMessageFormat(Player p) { + public static String getMessageFormat(Player p) { return Utils.replaceColors(handler.getMessageFormat(p)); } - @Override - public String getGlobalMessageFormat(Player p) { + public static String getGlobalMessageFormat(Player p) { return Utils.replaceColors(handler.getGlobalMessageFormat(p)); } -} \ No newline at end of file +} diff --git a/src/main/java/de/jeter/chatex/utils/AntiSpamManager.java b/src/main/java/de/jeter/chatex/utils/AntiSpamManager.java index 5296600..01910ad 100644 --- a/src/main/java/de/jeter/chatex/utils/AntiSpamManager.java +++ b/src/main/java/de/jeter/chatex/utils/AntiSpamManager.java @@ -20,14 +20,15 @@ import org.bukkit.entity.Player; -import java.util.HashMap; import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; public class AntiSpamManager { private static final AntiSpamManager instance = new AntiSpamManager(); - private final Map map = new HashMap<>(); + private final Map map = new ConcurrentHashMap<>(); private AntiSpamManager() { @@ -38,15 +39,16 @@ public static AntiSpamManager getInstance() { } public void put(Player chatter) { - map.put(chatter, System.currentTimeMillis()); + map.put(chatter.getUniqueId(), System.currentTimeMillis()); } public boolean isAllowed(Player chatter) { - if (!map.containsKey(chatter) || !Config.ANTISPAM_ENABLED.getBoolean() || chatter.hasPermission("chatex.antispam.bypass")) { + Long lastMessage = map.get(chatter.getUniqueId()); + if (lastMessage == null || !Config.ANTISPAM_ENABLED.getBoolean() || chatter.hasPermission("chatex.antispam.bypass")) { return true; } - long lastChat = map.get(chatter) + (Config.ANTISPAM_SECONDS.getInt() * 1000L); + long lastChat = lastMessage + (Config.ANTISPAM_SECONDS.getInt() * 1000L); long current = System.currentTimeMillis(); return current > lastChat; @@ -57,13 +59,17 @@ public long getRemainingSeconds(Player chatter) { return 0; } - long lastChat = map.get(chatter) + (Config.ANTISPAM_SECONDS.getInt() * 1000L); + long lastChat = map.get(chatter.getUniqueId()) + (Config.ANTISPAM_SECONDS.getInt() * 1000L); long current = System.currentTimeMillis(); long diff = lastChat - current; return TimeUnit.MILLISECONDS.toSeconds(diff); } + public void remove(UUID uuid) { + map.remove(uuid); + } + public void clear() { map.clear(); } diff --git a/src/main/java/de/jeter/chatex/utils/ChatLogger.java b/src/main/java/de/jeter/chatex/utils/ChatLogger.java index cec8439..fba0f77 100644 --- a/src/main/java/de/jeter/chatex/utils/ChatLogger.java +++ b/src/main/java/de/jeter/chatex/utils/ChatLogger.java @@ -25,64 +25,76 @@ import java.io.File; import java.io.FileWriter; import java.io.IOException; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.Calendar; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.logging.Level; public class ChatLogger { + private static final DateTimeFormatter FILE_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + private static final DateTimeFormatter TIME_ONLY_FORMAT = DateTimeFormatter.ofPattern("[HH:mm:ss] "); + private static final DateTimeFormatter DATE_TIME_FORMAT = DateTimeFormatter.ofPattern("[yyyy-MM-dd HH:mm:ss] "); + private static BufferedWriter chatWriter = null; private static BufferedWriter adWriter = null; + private static LocalDate chatWriterDate = null; - public static void load() { + public static synchronized void load() { try { File logFolder = new File(ChatEx.getInstance().getDataFolder(), "logs"); if (Config.LOGCHAT.getBoolean() || Config.ADS_LOG.getBoolean()) { logFolder.mkdirs(); } if (Config.LOGCHAT.getBoolean()) { - File chatLog = new File(logFolder, fileName()); - chatLog.createNewFile(); - chatWriter = new BufferedWriter(new FileWriter(chatLog, true)); + chatWriterDate = LocalDate.now(); + chatWriter = openWriter(logFolder, fileName(chatWriterDate)); } if (Config.ADS_LOG.getBoolean()) { - File adLog = new File(logFolder, "ads.log"); - adLog.createNewFile(); - adWriter = new BufferedWriter(new FileWriter(adLog, true)); + adWriter = openWriter(logFolder, "ads.log"); } } catch (IOException ex) { - ex.printStackTrace(); + ChatEx.getInstance().getLogger().log(Level.SEVERE, "Could not open chat log files", ex); } } - public static void close() { + private static BufferedWriter openWriter(File logFolder, String name) throws IOException { + File logFile = new File(logFolder, name); + logFile.createNewFile(); + return new BufferedWriter(new FileWriter(logFile, true)); + } + + public static synchronized void close() { try { if (chatWriter != null) { chatWriter.close(); + chatWriter = null; } if (adWriter != null) { adWriter.close(); + adWriter = null; } } catch (IOException ex) { - ex.printStackTrace(); + ChatEx.getInstance().getLogger().log(Level.SEVERE, "Could not close chat log files", ex); } } - public static void writeToFile(Player player, String message) { + public static synchronized void writeToFile(Player player, String message) { if (!Config.LOGCHAT.getBoolean() || chatWriter == null) { return; } try { + rotateIfNeeded(); chatWriter.write(prefix(false) + player.getName() + " (uuid: " + player.getUniqueId() + "): " + message); chatWriter.newLine(); chatWriter.flush(); } catch (IOException ex) { - ex.printStackTrace(); + ChatEx.getInstance().getLogger().log(Level.SEVERE, "Could not write to chat log", ex); } } - public static void writeToAdFile(Player player, String message) { + public static synchronized void writeToAdFile(Player player, String message) { if (!Config.ADS_LOG.getBoolean() || adWriter == null) { return; } @@ -91,20 +103,29 @@ public static void writeToAdFile(Player player, String message) { adWriter.newLine(); adWriter.flush(); } catch (IOException ex) { - ex.printStackTrace(); + ChatEx.getInstance().getLogger().log(Level.SEVERE, "Could not write to ad log", ex); + } + } + + private static void rotateIfNeeded() throws IOException { + LocalDate today = LocalDate.now(); + if (today.equals(chatWriterDate)) { + return; } + + chatWriter.close(); + File logFolder = new File(ChatEx.getInstance().getDataFolder(), "logs"); + chatWriter = openWriter(logFolder, fileName(today)); + chatWriterDate = today; } - private static String fileName() { - DateFormat date = new SimpleDateFormat("yyyy-MM-dd"); - Calendar cal = Calendar.getInstance(); - return date.format(cal.getTime()) + ".log"; + private static String fileName(LocalDate date) { + return FILE_DATE_FORMAT.format(date) + ".log"; } - private static String prefix(boolean day) { - DateFormat date = day ? new SimpleDateFormat("[yyyy-MM-dd HH:mm:ss] ") : new SimpleDateFormat("[HH:mm:ss] "); - Calendar cal = Calendar.getInstance(); - return date.format(cal.getTime()); + private static String prefix(boolean withDate) { + LocalDateTime now = LocalDateTime.now(); + return (withDate ? DATE_TIME_FORMAT : TIME_ONLY_FORMAT).format(now); } -} \ No newline at end of file +} diff --git a/src/main/java/de/jeter/chatex/utils/Config.java b/src/main/java/de/jeter/chatex/utils/Config.java index 8b34d97..39a0296 100644 --- a/src/main/java/de/jeter/chatex/utils/Config.java +++ b/src/main/java/de/jeter/chatex/utils/Config.java @@ -28,6 +28,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.logging.Level; public enum Config { @@ -110,7 +111,7 @@ public static void load() { try { cfg.save(f); } catch (IOException ex) { - ex.printStackTrace(); + ChatEx.getInstance().getLogger().log(Level.SEVERE, "Could not save config.yml", ex); } } @@ -164,7 +165,7 @@ public void set(Object value, boolean save) { try { cfg.save(f); } catch (IOException ex) { - ex.printStackTrace(); + ChatEx.getInstance().getLogger().log(Level.SEVERE, "Could not save config.yml", ex); } reload(false); } diff --git a/src/main/java/de/jeter/chatex/utils/CustomCharts.java b/src/main/java/de/jeter/chatex/utils/CustomCharts.java index a6abe18..41b3680 100644 --- a/src/main/java/de/jeter/chatex/utils/CustomCharts.java +++ b/src/main/java/de/jeter/chatex/utils/CustomCharts.java @@ -11,7 +11,7 @@ public static void addUpdateCheckerChart(Metrics metrics) { } public static void addPermissionsPluginChart(Metrics metrics) { - metrics.addCustomChart(new SimplePie("used_permissions_plugin", () -> PluginManager.getInstance().getName())); + metrics.addCustomChart(new SimplePie("used_permissions_plugin", () -> PluginManager.getName())); } } diff --git a/src/main/java/de/jeter/chatex/utils/DomainDictionary.java b/src/main/java/de/jeter/chatex/utils/DomainDictionary.java index 7cfdfcc..d4fb579 100644 --- a/src/main/java/de/jeter/chatex/utils/DomainDictionary.java +++ b/src/main/java/de/jeter/chatex/utils/DomainDictionary.java @@ -19,18 +19,27 @@ package de.jeter.chatex.utils; import java.util.HashSet; +import java.util.Set; public class DomainDictionary { - private static final HashSet endingSet = new HashSet<>(Config.ADS_SMART_DOMAIN_ENDINGS.getStringList()); + private static Set endingSet = new HashSet<>(); + + public static void load() { + endingSet = new HashSet<>(Config.ADS_SMART_DOMAIN_ENDINGS.getStringList()); + } public static boolean containsTopLevelEnding(String checkString) { + return containsTopLevelEnding(checkString, endingSet); + } + + static boolean containsTopLevelEnding(String checkString, Set endings) { String[] parts = checkString.split("\\."); String ending = parts[parts.length - 1]; StringBuilder stringBuilder = new StringBuilder(); - for (char Character : ending.toCharArray()) { - stringBuilder.append(Character); - if (endingSet.contains(stringBuilder.toString())) { + for (char c : ending.toCharArray()) { + stringBuilder.append(c); + if (endings.contains(stringBuilder.toString())) { return true; } } diff --git a/src/main/java/de/jeter/chatex/utils/Locales.java b/src/main/java/de/jeter/chatex/utils/Locales.java index fa36077..78e7dda 100644 --- a/src/main/java/de/jeter/chatex/utils/Locales.java +++ b/src/main/java/de/jeter/chatex/utils/Locales.java @@ -24,6 +24,7 @@ import java.io.File; import java.io.IOException; +import java.util.logging.Level; public enum Locales { @@ -32,7 +33,9 @@ public enum Locales { COMMAND_CLEAR_CONSOLE("Commands.Clear.Console", "CONSOLE"), COMMAND_CLEAR_UNKNOWN("Commands.Clear.Unknown", "UNKNOWN"), MESSAGES_RELOAD("Messages.Commands.Reload.Success", "&aConfig was reloaded."), - MESSAGES_CLEAR("Messages.Commands.Clear.Success", "&aThe chat has been cleared by "), + MESSAGES_CLEAR("Messages.Commands.Clear.Success", "&aThe chat has been cleared by %prefix%displayname%suffix"), + COMMAND_INFO("Messages.Info", "&aChatEx plugin by %authors (%version)"), + COMMAND_HELP_LINE("Messages.Commands.Help.Line", "&a/%cmd %subcommand - %description"), MESSAGES_AD("Messages.Chat.AdDetected", "&4[ERROR] &7Advertising is not allowed! &c(%perm)"), MESSAGES_BLOCKED("Messages.Chat.BlockedWord", "&4[ERROR] &7You tried to write a word that is blocked!"), MESSAGES_AD_NOTIFY("Messages.Chat.AdNotify", "&c%player tried to write an ad in chat. He wrote: \n&a %message"), @@ -45,6 +48,7 @@ public enum Locales { PLAYER_QUIT("Messages.Player.Quit", "%prefix%displayname%suffix &eleft the game!"), NO_LISTENING_PLAYERS("Messages.Chat.NoOneListens", "&cNo players are near you to hear you talking! Try to use the global mode to chat globally."), UPDATE_FOUND("Messages.UpdateFound", "&a[ChatEx]&7 A new update has been found on SpigotMC. Current version: %oldversion New version: %newversion. Click this message to download it!"), + UPDATE_FOUND_HOVER("Messages.UpdateFoundHover", "&aClick to download"), ; private static final File localeFolder = new File(ChatEx.getInstance().getDataFolder(), "locales"); @@ -79,7 +83,7 @@ public static void load() { } cfg.save(f); } catch (IOException ioex) { - ioex.printStackTrace(); + ChatEx.getInstance().getLogger().log(Level.SEVERE, "Could not save locale file", ioex); } } } else { @@ -92,7 +96,7 @@ public static void load() { } cfg.save(f); } catch (IOException ex) { - ex.printStackTrace(); + ChatEx.getInstance().getLogger().log(Level.SEVERE, "Could not save locale file", ex); } } } @@ -125,7 +129,7 @@ public void set(Object value, boolean save) { try { cfg.save(f); } catch (IOException ex) { - ex.printStackTrace(); + ChatEx.getInstance().getLogger().log(Level.SEVERE, "Could not save locale file", ex); } reload(false); } diff --git a/src/main/java/de/jeter/chatex/utils/RGBColors.java b/src/main/java/de/jeter/chatex/utils/RGBColors.java index a3b436c..924272d 100644 --- a/src/main/java/de/jeter/chatex/utils/RGBColors.java +++ b/src/main/java/de/jeter/chatex/utils/RGBColors.java @@ -1,7 +1,6 @@ package de.jeter.chatex.utils; import de.jeter.chatex.ChatEx; -import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.configuration.ConfigurationSection; @@ -14,15 +13,8 @@ public class RGBColors { private static final HashMap placeHolderColorMap = new HashMap<>(); - private static Boolean supported = null; - public static void load() { - ChatEx.getInstance().getLogger().info("Server version:" + Bukkit.getVersion()); - if (isNotSupported()) { - ChatEx.getInstance().getLogger().info("This server version doesn't support custom color codes!"); - return; - } - ChatEx.getInstance().getLogger().info("Version is later than 1.16. Loading RGB ColorCodes!"); + ChatEx.getInstance().getLogger().info("Loading RGB ColorCodes!"); ConfigurationSection configurationSection = Config.RGB_COLORS.getConfigurationSection(); if (configurationSection == null) { @@ -47,12 +39,12 @@ public static void load() { } public static String translateCustomColorCodes(String s) { - if (isNotSupported()) { - return s; - } s = translateSingleMessageColorCodes(s); for (Map.Entry stringColorEntry : placeHolderColorMap.entrySet()) { - s = s.replace(stringColorEntry.getKey(), stringColorEntry.getValue()); + // Custom color placeholders are typed as "&" (e.g. "&$g"); the leading "&" + // must be consumed here too, otherwise it survives as a stray character in the + // output since it's no longer followed by a valid legacy color code. + s = s.replace("&" + stringColorEntry.getKey(), stringColorEntry.getValue()); } return s; } @@ -75,27 +67,10 @@ public static String translateSingleMessageColorCodes(String s) { return s; } - private static boolean isNotSupported() { - if (supported == null) { - try { - final String version = Bukkit.getVersion(); - String ver = version.split("\\(MC: ")[1]; - String[] numbers = ver.replaceAll("\\)", "").split("\\."); - ver = numbers[0] + numbers[1]; - int toCheck = Integer.valueOf(ver); - LogHelper.debug(ver + " INT: " + toCheck); - supported = toCheck >= 116; - } catch (Exception ex) { - ex.printStackTrace(); - } - } - return !supported; - } - public static String translateGradientCodes(String message) { final Pattern hexPattern = Pattern.compile("#([A-Fa-f0-9]{6})"); Matcher matcher = hexPattern.matcher(message); - StringBuffer buffer = new StringBuffer(message.length() + 4 * 8); + StringBuilder buffer = new StringBuilder(message.length() + 4 * 8); while (matcher.find()) { String group = matcher.group(1); diff --git a/src/main/java/de/jeter/chatex/utils/Utils.java b/src/main/java/de/jeter/chatex/utils/Utils.java index 1d8af0a..6f514fd 100644 --- a/src/main/java/de/jeter/chatex/utils/Utils.java +++ b/src/main/java/de/jeter/chatex/utils/Utils.java @@ -27,16 +27,28 @@ import java.util.ArrayList; import java.util.List; +import java.util.regex.Pattern; public class Utils { + private static final Pattern COLOR_CODE_PATTERN = Pattern.compile("(?i)&[0-9a-fk-or]|#[0-9a-f]{6}"); + public static String translateColorCodes(String string, Player p) { - return p.hasPermission("chatex.chat.color") ? replaceColors(string) : string; + if (p.hasPermission("chatex.chat.color")) { + return replaceColors(string); + } + if (COLOR_CODE_PATTERN.matcher(string).find()) { + p.sendMessage(Locales.COMMAND_RESULT_NO_PERM.getString(p).replaceAll("%perm", "chatex.chat.color")); + } + return string; } public static String replaceColors(String message) { - message = RGBColors.translateGradientCodes(message); + // Must run before translateGradientCodes: it matches "&#RRGGBB" (consuming the "&"), + // whereas translateGradientCodes matches bare "#RRGGBB" regardless of a leading "&" and + // would otherwise steal the hex code first, leaving a stray unconverted "&" behind. message = RGBColors.translateCustomColorCodes(message); + message = RGBColors.translateGradientCodes(message); return ChatColor.translateAlternateColorCodes('&', message); } @@ -62,11 +74,11 @@ public static String replacePlayerPlaceholders(Player player, String format) { String result = format; result = result.replace("%displayname", player.getDisplayName()); - result = result.replace("%prefix", PluginManager.getInstance().getPrefix(player)); - result = result.replace("%suffix", PluginManager.getInstance().getSuffix(player)); + result = result.replace("%prefix", PluginManager.getPrefix(player)); + result = result.replace("%suffix", PluginManager.getSuffix(player)); result = result.replace("%player", player.getName()); result = result.replace("%world", player.getWorld().getName()); - result = result.replace("%group", PluginManager.getInstance().getGroupNames(player).length > 0 ? PluginManager.getInstance().getGroupNames(player)[0] : "none"); + result = result.replace("%group", PluginManager.getGroupNames(player).length > 0 ? PluginManager.getGroupNames(player)[0] : "none"); if (HookManager.checkPlaceholderAPI()) { LogHelper.debug("PlaceholderAPI is installed! Replacing..."); diff --git a/src/main/java/de/jeter/chatex/utils/adManager/SmartAdManager.java b/src/main/java/de/jeter/chatex/utils/adManager/SmartAdManager.java index acdfc51..78042dc 100644 --- a/src/main/java/de/jeter/chatex/utils/adManager/SmartAdManager.java +++ b/src/main/java/de/jeter/chatex/utils/adManager/SmartAdManager.java @@ -21,19 +21,21 @@ import de.jeter.chatex.utils.*; import org.bukkit.entity.Player; -import java.util.HashMap; import java.util.Map; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; public class SmartAdManager implements AdManager { - private static final Map uuidErrorMap = new HashMap<>(); + private static final Map uuidErrorMap = new ConcurrentHashMap<>(); private static final Pattern ipPattern = Pattern.compile("((? ENDINGS = Set.of("com", "net", "de", "co.uk"); + + @Test + void matchesKnownTopLevelEnding() { + assertTrue(DomainDictionary.containsTopLevelEnding("example.com", ENDINGS)); + assertTrue(DomainDictionary.containsTopLevelEnding("example.de", ENDINGS)); + } + + @Test + void matchesOnLeftAnchoredPrefixOfTheEnding() { + // The scan builds the ending left-to-right and checks each prefix against + // the dictionary, so "coma" already matches on its "com" prefix. + assertTrue(DomainDictionary.containsTopLevelEnding("example.coma", ENDINGS)); + } + + @Test + void doesNotMatchUnknownEnding() { + assertFalse(DomainDictionary.containsTopLevelEnding("example.xyz", ENDINGS)); + } + + @Test + void emptyEndingSetNeverMatches() { + assertFalse(DomainDictionary.containsTopLevelEnding("example.com", Set.of())); + } + + @Test + void usesLastDotSeparatedPartOnly() { + assertFalse(DomainDictionary.containsTopLevelEnding("com.example.xyz", ENDINGS)); + } +}