&7- Captcha verification.",
- "&bChatGuard staff commands:",
- "&e/cg reload &7- Reload ChatGuard config.",
- "&e/cg strike &7- View strike of player.",
- "&e/cg strike [0-5] &7- Set strike of player."
- };
-
- for (String message : messages) {
- if (sender instanceof Player) sender.sendMessage(translate(message));
- else logInfo(message.replaceAll("&.", ""));
- }
- }
-
- private static void playerNotFound(CommandSender sender, String playerName) {
- sender.sendMessage(translate(String.format("&c[ChatGuard] &e%s &cdoes not exist in the configuration.", playerName)));
- }
-
- private static void reloadCommand(CommandSender sender) {
- if (!hasPermission(sender, "chatguard.config", "[ChatGuard] You don't have permission to reload the config.")) return;
-
- if (sender instanceof Player) sender.sendMessage(translate("&a[ChatGuard] Configurations reloaded."));
- logInfo("[ChatGuard] Configurations reloaded.");
- getConfig().loadConfig();
- getStrikes().loadConfig();
- }
-
- private static void setPlayerStrikes(CommandSender sender, String playerName, int oldStrike, String setStrike) {
- int newStrike;
-
- try {
- newStrike = Integer.parseInt(setStrike);
- } catch (NumberFormatException e) {
- sender.sendMessage(translate("&c[ChatGuard] Invalid input. Please enter a number from 0 to 5."));
- return;
- }
-
- if (newStrike < 0 || newStrike > 5) {
- sender.sendMessage(translate("&c[ChatGuard] Invalid range. Please choose from &e0&c to &e5."));
- return;
- }
-
- getStrikes().setProperty(playerName, newStrike);
- getStrikes().save();
-
- if (sender instanceof Player)
- sender.sendMessage(translate(String.format("&c[ChatGuard] &e%s &cset from strike &e%d &cto &e%d&c.", playerName, oldStrike, newStrike)));
-
- logInfo(String.format("[ChatGuard] Player '%s' set from strike %d to %d.", playerName, oldStrike, newStrike));
- }
}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/command/subcommand/AboutCommand.java b/src/main/java/io/github/aleksandarharalanov/chatguard/command/subcommand/AboutCommand.java
new file mode 100644
index 0000000..2e5b34e
--- /dev/null
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/command/subcommand/AboutCommand.java
@@ -0,0 +1,29 @@
+package io.github.aleksandarharalanov.chatguard.command.subcommand;
+
+import io.github.aleksandarharalanov.chatguard.ChatGuard;
+import io.github.aleksandarharalanov.chatguard.util.misc.AboutUtil;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandExecutor;
+import org.bukkit.command.CommandSender;
+
+import java.util.Arrays;
+import java.util.List;
+
+public final class AboutCommand implements CommandExecutor {
+
+ private final ChatGuard plugin;
+
+ public AboutCommand(ChatGuard plugin) {
+ this.plugin = plugin;
+ }
+
+ @Override
+ public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
+ // Contributors: If you've contributed code, you can add your name here to be credited
+ List contributors = Arrays.asList(
+ "moderator_man"
+ );
+ AboutUtil.aboutPlugin(sender, plugin, contributors);
+ return true;
+ }
+}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/command/subcommand/CaptchaCommand.java b/src/main/java/io/github/aleksandarharalanov/chatguard/command/subcommand/CaptchaCommand.java
new file mode 100644
index 0000000..d30cae7
--- /dev/null
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/command/subcommand/CaptchaCommand.java
@@ -0,0 +1,54 @@
+package io.github.aleksandarharalanov.chatguard.command.subcommand;
+
+import io.github.aleksandarharalanov.chatguard.ChatGuard;
+import io.github.aleksandarharalanov.chatguard.core.data.CaptchaData;
+import io.github.aleksandarharalanov.chatguard.core.log.LogType;
+import io.github.aleksandarharalanov.chatguard.util.auth.AccessUtil;
+import io.github.aleksandarharalanov.chatguard.core.misc.AudioCuePlayer;
+import io.github.aleksandarharalanov.chatguard.util.misc.ColorUtil;
+import io.github.aleksandarharalanov.chatguard.util.log.LogUtil;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandExecutor;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+
+public final class CaptchaCommand implements CommandExecutor {
+
+ private final ChatGuard plugin;
+
+ public CaptchaCommand(ChatGuard plugin) {
+ this.plugin = plugin;
+ }
+
+ @Override
+ public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
+ if (AccessUtil.denyIfNotPlayer(sender, plugin)) return true;
+
+ if (args.length < 2) {
+ sender.sendMessage(ColorUtil.translateColorCodes("&cUsage: /cg captcha "));
+ return true;
+ }
+
+ Player player = (Player) sender;
+ String captchaCode = CaptchaData.getPlayerCaptcha(player.getName());
+
+ if (captchaCode == null) {
+ player.sendMessage(ColorUtil.translateColorCodes("&c[ChatGuard] No active captcha verification."));
+ return true;
+ }
+
+ if (!args[1].equals(captchaCode)) {
+ player.sendMessage(ColorUtil.translateColorCodes("&c[ChatGuard] Incorrect captcha code."));
+ AudioCuePlayer.play(LogType.CAPTCHA, player, false);
+ return true;
+ }
+
+ player.sendMessage(ColorUtil.translateColorCodes("&a[ChatGuard] Captcha verification passed."));
+ CaptchaData.removePlayerCaptcha(player.getName());
+
+ AudioCuePlayer.play(LogType.CAPTCHA, player, true);
+ LogUtil.logConsoleInfo(String.format("[ChatGuard] Player '%s' passed captcha verification.", player.getName()));
+
+ return true;
+ }
+}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/command/subcommand/HelpCommand.java b/src/main/java/io/github/aleksandarharalanov/chatguard/command/subcommand/HelpCommand.java
new file mode 100644
index 0000000..ee2c5c4
--- /dev/null
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/command/subcommand/HelpCommand.java
@@ -0,0 +1,30 @@
+package io.github.aleksandarharalanov.chatguard.command.subcommand;
+
+import io.github.aleksandarharalanov.chatguard.util.log.LogUtil;
+import io.github.aleksandarharalanov.chatguard.util.misc.ColorUtil;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+
+public final class HelpCommand {
+
+ public static void sendHelp(CommandSender sender) {
+ String[] messages = {
+ "&bChatGuard commands:",
+ "&e/cg &7- Displays this content.",
+ "&e/cg about &7- About ChatGuard.",
+ "&e/cg captcha &7- Captcha verification.",
+ "&bChatGuard staff commands:",
+ "&e/cg reload &7- Reload ChatGuard config.",
+ "&e/cg strike &7- View strike of player.",
+ "&e/cg strike [0-5] &7- Set strike of player."
+ };
+
+ for (String message : messages) {
+ if (sender instanceof Player) {
+ sender.sendMessage(ColorUtil.translateColorCodes(message));
+ } else {
+ LogUtil.logConsoleInfo(message.replaceAll("&.", ""));
+ }
+ }
+ }
+}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/command/subcommand/ReloadCommand.java b/src/main/java/io/github/aleksandarharalanov/chatguard/command/subcommand/ReloadCommand.java
new file mode 100644
index 0000000..d412403
--- /dev/null
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/command/subcommand/ReloadCommand.java
@@ -0,0 +1,32 @@
+package io.github.aleksandarharalanov.chatguard.command.subcommand;
+
+import io.github.aleksandarharalanov.chatguard.ChatGuard;
+import io.github.aleksandarharalanov.chatguard.util.auth.AccessUtil;
+import io.github.aleksandarharalanov.chatguard.util.log.LogUtil;
+import io.github.aleksandarharalanov.chatguard.util.misc.ColorUtil;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandExecutor;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+
+public final class ReloadCommand implements CommandExecutor {
+
+ @Override
+ public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
+ if (!AccessUtil.senderHasPermission(sender, "chatguard.config", "[ChatGuard] You don't have permission to reload the config.")) {
+ return true;
+ }
+
+ if (sender instanceof Player) {
+ sender.sendMessage(ColorUtil.translateColorCodes("&a[ChatGuard] Configurations reloaded."));
+ }
+ LogUtil.logConsoleInfo("[ChatGuard] Configurations reloaded.");
+
+ ChatGuard.getConfig().loadAndLog();
+ ChatGuard.getDiscord().loadAndLog();
+ ChatGuard.getStrikes().loadAndLog();
+ ChatGuard.getCaptchas().loadAndLog();
+
+ return true;
+ }
+}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/command/subcommand/StrikeCommand.java b/src/main/java/io/github/aleksandarharalanov/chatguard/command/subcommand/StrikeCommand.java
new file mode 100644
index 0000000..d2f2c2d
--- /dev/null
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/command/subcommand/StrikeCommand.java
@@ -0,0 +1,79 @@
+package io.github.aleksandarharalanov.chatguard.command.subcommand;
+
+import io.github.aleksandarharalanov.chatguard.ChatGuard;
+import io.github.aleksandarharalanov.chatguard.core.data.PenaltyData;
+import io.github.aleksandarharalanov.chatguard.util.auth.AccessUtil;
+import io.github.aleksandarharalanov.chatguard.util.misc.ColorUtil;
+import io.github.aleksandarharalanov.chatguard.util.log.LogUtil;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandExecutor;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+
+import java.util.List;
+
+public final class StrikeCommand implements CommandExecutor {
+
+ @Override
+ public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
+ if (!AccessUtil.senderHasPermission(sender, "chatguard.config", "[ChatGuard] You don't have permission to modify the config.")) {
+ return true;
+ }
+
+ if (args.length < 2) {
+ sender.sendMessage(ColorUtil.translateColorCodes("&cUsage: /cg strike [0-5]"));
+ return true;
+ }
+
+ String playerName = args[1];
+ List keys = ChatGuard.getStrikes().getKeys();
+ String foundKey = keys.stream()
+ .filter(key -> key.equalsIgnoreCase(playerName))
+ .findFirst()
+ .orElse(null);
+ int playerStrikeTier = foundKey != null ? PenaltyData.getStrike(foundKey) : -1;
+
+ if (foundKey == null) {
+ sender.sendMessage(ColorUtil.translateColorCodes(String.format(
+ "&c[ChatGuard] Player &e%s &cnot found.",
+ playerName
+ )));
+ return true;
+ }
+
+ if (args.length == 2) {
+ sender.sendMessage(ColorUtil.translateColorCodes(String.format(
+ "&a[ChatGuard] &e%s &ais on strike &e%d&a.",
+ foundKey, playerStrikeTier
+ )));
+ return true;
+ }
+
+ try {
+ int newStrike = Integer.parseInt(args[2]);
+ if (newStrike < 0 || newStrike > 5) {
+ sender.sendMessage(ColorUtil.translateColorCodes("&c[ChatGuard] Invalid range. Choose from &e0 &cto &e5&c."));
+ return true;
+ }
+
+ ChatGuard.getStrikes().setProperty(playerName, newStrike);
+ ChatGuard.getStrikes().save();
+
+ if (sender instanceof Player) {
+ sender.sendMessage(ColorUtil.translateColorCodes(String.format(
+ "&a[ChatGuard] &e%s &astrike set from &e%d &ato &e%d&a.",
+ foundKey, playerStrikeTier, newStrike
+ )));
+ }
+
+ LogUtil.logConsoleInfo(String.format(
+ "[ChatGuard] Player '%s' set from strike %d to %d.",
+ foundKey, playerStrikeTier, newStrike
+ ));
+ } catch (NumberFormatException e) {
+ sender.sendMessage(ColorUtil.translateColorCodes("&c[ChatGuard] Invalid input. Enter a number from &e0 &cto &e5&c."));
+ }
+
+ return true;
+ }
+}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/core/data/CaptchaData.java b/src/main/java/io/github/aleksandarharalanov/chatguard/core/data/CaptchaData.java
new file mode 100644
index 0000000..7f3bc81
--- /dev/null
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/core/data/CaptchaData.java
@@ -0,0 +1,48 @@
+package io.github.aleksandarharalanov.chatguard.core.data;
+
+import io.github.aleksandarharalanov.chatguard.ChatGuard;
+import org.bukkit.entity.Player;
+
+import java.util.HashMap;
+import java.util.LinkedList;
+
+public final class CaptchaData {
+
+ private static final HashMap> playerMessages = new HashMap<>();
+
+ private CaptchaData() {}
+
+ public static HashMap> getPlayerMessages() {
+ return playerMessages;
+ }
+
+ public static LinkedList getMessageHistory(String playerName) {
+ return playerMessages.computeIfAbsent(playerName, k -> new LinkedList<>());
+ }
+
+ public static void removePlayerCaptcha(String playerName) {
+ ChatGuard.getCaptchas().removeProperty(playerName);
+ ChatGuard.getCaptchas().save();
+ }
+
+ public static void setPlayerCaptcha(String playerName, String captchaCode) {
+ ChatGuard.getCaptchas().setProperty(playerName, captchaCode);
+ ChatGuard.getCaptchas().save();
+ }
+
+ public static String getPlayerCaptcha(String playerName) {
+ return ChatGuard.getCaptchas().getString(playerName);
+ }
+
+ public static int getThreshold() {
+ return ChatGuard.getConfig().getInt("captcha.threshold", 5);
+ }
+
+ public static int getCodeLength() {
+ return ChatGuard.getConfig().getInt("captcha.code.length", 5);
+ }
+
+ public static String getCodeCharacters() {
+ return ChatGuard.getConfig().getString("captcha.code.characters", "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789");
+ }
+}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/core/data/LoginData.java b/src/main/java/io/github/aleksandarharalanov/chatguard/core/data/LoginData.java
new file mode 100644
index 0000000..a0d3dd9
--- /dev/null
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/core/data/LoginData.java
@@ -0,0 +1,19 @@
+package io.github.aleksandarharalanov.chatguard.core.data;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public final class LoginData {
+
+ private static final Map playerPreLoginIPs = new HashMap<>();
+
+ private LoginData() {}
+
+ public static void storePlayerIP(String playerName, String playerIp) {
+ playerPreLoginIPs.put(playerName.toLowerCase(), playerIp);
+ }
+
+ public static String popPlayerIP(String playerName) {
+ return playerPreLoginIPs.remove(playerName.toLowerCase());
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/core/data/PenaltyData.java b/src/main/java/io/github/aleksandarharalanov/chatguard/core/data/PenaltyData.java
new file mode 100644
index 0000000..6f5f729
--- /dev/null
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/core/data/PenaltyData.java
@@ -0,0 +1,46 @@
+package io.github.aleksandarharalanov.chatguard.core.data;
+
+import io.github.aleksandarharalanov.chatguard.ChatGuard;
+import org.bukkit.entity.Player;
+
+public final class PenaltyData {
+
+ private PenaltyData() {}
+
+ public static int getStrike(String playerName) {
+ return ChatGuard.getStrikes().getInt(playerName, 0);
+ }
+
+ public static int getStrike(Player player) {
+ return ChatGuard.getStrikes().getInt(player.getName(), 0);
+ }
+
+ public static String getMuteDuration(String playerName) {
+ return ChatGuard.getConfig().getString(String.format(
+ "filter.essentials-mute.duration.s%d",
+ ChatGuard.getStrikes().getInt(playerName, 0)
+ ));
+ }
+
+ public static String getMuteDuration(Player player) {
+ return ChatGuard.getConfig().getString(String.format(
+ "filter.essentials-mute.duration.s%d",
+ ChatGuard.getStrikes().getInt(player.getName(), 0)
+ ));
+ }
+
+ public static boolean isPlayerOnFinalStrike(String playerName) {
+ return ChatGuard.getStrikes().getInt(playerName, 0) == 5;
+ }
+
+ public static boolean isPlayerOnFinalStrike(Player player) {
+ return ChatGuard.getStrikes().getInt(player.getName(), 0) == 5;
+ }
+
+ public static void setDefaultStrikeTier(Player player) {
+ if (ChatGuard.getStrikes().getInt(player.getName(), -1) == -1) {
+ ChatGuard.getStrikes().setProperty(player.getName(), 0);
+ ChatGuard.getStrikes().save();
+ }
+ }
+}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/core/data/TimestampData.java b/src/main/java/io/github/aleksandarharalanov/chatguard/core/data/TimestampData.java
new file mode 100644
index 0000000..8d06fa1
--- /dev/null
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/core/data/TimestampData.java
@@ -0,0 +1,27 @@
+package io.github.aleksandarharalanov.chatguard.core.data;
+
+import java.util.HashMap;
+
+public final class TimestampData {
+
+ private static final HashMap playerCommandTimestamps = new HashMap<>();
+ private static final HashMap playerMessageTimestamps = new HashMap<>();
+
+ private TimestampData() {}
+
+ public static long getCommandTimestamp(String playerName) {
+ return playerCommandTimestamps.getOrDefault(playerName, 0L);
+ }
+
+ public static void setCommandTimestamp(String playerName, long timestamp) {
+ playerCommandTimestamps.put(playerName, timestamp);
+ }
+
+ public static long getMessageTimestamp(String playerName) {
+ return playerMessageTimestamps.getOrDefault(playerName, 0L);
+ }
+
+ public static void setMessageTimestamp(String playerName, long timestamp) {
+ playerMessageTimestamps.put(playerName, timestamp);
+ }
+}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/core/log/LogAttribute.java b/src/main/java/io/github/aleksandarharalanov/chatguard/core/log/LogAttribute.java
new file mode 100644
index 0000000..1dc993e
--- /dev/null
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/core/log/LogAttribute.java
@@ -0,0 +1,9 @@
+package io.github.aleksandarharalanov.chatguard.core.log;
+
+public enum LogAttribute {
+
+ FILTER,
+ STRIKE,
+ MUTE,
+ AUDIO
+}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/core/log/LogType.java b/src/main/java/io/github/aleksandarharalanov/chatguard/core/log/LogType.java
new file mode 100644
index 0000000..ec978ba
--- /dev/null
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/core/log/LogType.java
@@ -0,0 +1,21 @@
+package io.github.aleksandarharalanov.chatguard.core.log;
+
+import java.util.EnumSet;
+
+public enum LogType {
+
+ CHAT(EnumSet.of(LogAttribute.FILTER, LogAttribute.STRIKE, LogAttribute.MUTE, LogAttribute.AUDIO)),
+ SIGN(EnumSet.of(LogAttribute.FILTER, LogAttribute.STRIKE, LogAttribute.AUDIO)),
+ NAME(EnumSet.of(LogAttribute.FILTER)),
+ CAPTCHA(EnumSet.of(LogAttribute.AUDIO));
+
+ private final EnumSet attributes;
+
+ LogType(EnumSet attributes) {
+ this.attributes = attributes;
+ }
+
+ public boolean hasAttribute(LogAttribute attribute) {
+ return attributes.contains(attribute);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/core/log/embed/CaptchaEmbed.java b/src/main/java/io/github/aleksandarharalanov/chatguard/core/log/embed/CaptchaEmbed.java
new file mode 100644
index 0000000..85e207f
--- /dev/null
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/core/log/embed/CaptchaEmbed.java
@@ -0,0 +1,25 @@
+package io.github.aleksandarharalanov.chatguard.core.log.embed;
+
+import io.github.aleksandarharalanov.chatguard.ChatGuard;
+import io.github.aleksandarharalanov.chatguard.core.data.CaptchaData;
+import org.bukkit.entity.Player;
+import org.bukkit.plugin.java.JavaPlugin;
+
+import java.awt.*;
+
+public final class CaptchaEmbed extends DiscordEmbed {
+
+ public CaptchaEmbed(JavaPlugin plugin, Player player, String content) {
+ super(plugin, player, content);
+ setupBaseEmbed();
+ }
+
+ @Override
+ protected void setupEmbedDetails() {
+ String hex = ChatGuard.getDiscord().getString("customize.type.captcha.color");
+ embed.setTitle("Captcha Trigger")
+ .setDescription(String.format("Repeated Content ・ %dx", CaptchaData.getThreshold()))
+ .addField("Content:", content, false)
+ .setColor(Color.decode(hex));
+ }
+}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/core/log/embed/ChatEmbed.java b/src/main/java/io/github/aleksandarharalanov/chatguard/core/log/embed/ChatEmbed.java
new file mode 100644
index 0000000..6ee9542
--- /dev/null
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/core/log/embed/ChatEmbed.java
@@ -0,0 +1,42 @@
+package io.github.aleksandarharalanov.chatguard.core.log.embed;
+
+import io.github.aleksandarharalanov.chatguard.ChatGuard;
+import io.github.aleksandarharalanov.chatguard.core.data.PenaltyData;
+import org.bukkit.entity.Player;
+import org.bukkit.plugin.java.JavaPlugin;
+
+import java.awt.*;
+
+public final class ChatEmbed extends DiscordEmbed {
+
+ private final String trigger;
+
+ public ChatEmbed(JavaPlugin plugin, Player player, String content, String trigger) {
+ super(plugin, player, content);
+ this.trigger = trigger;
+ setupBaseEmbed();
+ }
+
+ @Override
+ protected void setupEmbedDetails() {
+ String hex = ChatGuard.getDiscord().getString("customize.type.chat.color");
+ boolean censorData = ChatGuard.getDiscord().getBoolean("embed-log.optional.censor", true);
+
+ if (!PenaltyData.isPlayerOnFinalStrike(player)) {
+ embed.setDescription(String.format(
+ "S%d ► S%d ・ Mute Duration: %s",
+ PenaltyData.getStrike(player), PenaltyData.getStrike(player) + 1, PenaltyData.getMuteDuration(player)
+ ));
+ } else {
+ embed.setDescription(String.format(
+ "S%d (Max) ・ Mute Duration: %s",
+ PenaltyData.getStrike(player), PenaltyData.getMuteDuration(player)
+ ));
+ }
+
+ embed.setTitle("Chat Filter")
+ .addField("Content:", content, false)
+ .addField("Trigger:", String.format(censorData ? "||`%s`||" : "`%s`", trigger), true)
+ .setColor(Color.decode(hex));
+ }
+}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/core/log/embed/DiscordEmbed.java b/src/main/java/io/github/aleksandarharalanov/chatguard/core/log/embed/DiscordEmbed.java
new file mode 100644
index 0000000..33077a3
--- /dev/null
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/core/log/embed/DiscordEmbed.java
@@ -0,0 +1,63 @@
+package io.github.aleksandarharalanov.chatguard.core.log.embed;
+
+import io.github.aleksandarharalanov.chatguard.ChatGuard;
+import io.github.aleksandarharalanov.chatguard.util.log.DiscordUtil;
+import org.bukkit.craftbukkit.entity.CraftPlayer;
+import org.bukkit.entity.Player;
+import org.bukkit.plugin.java.JavaPlugin;
+
+public abstract class DiscordEmbed {
+
+ protected final String pluginVersion;
+ protected final Player player;
+ protected final String content;
+ protected final long timestamp;
+ protected final DiscordUtil.EmbedObject embed;
+
+ public DiscordEmbed(JavaPlugin plugin, Player player, String content) {
+ this.pluginVersion = plugin.getDescription().getVersion();
+ this.player = player;
+ this.content = content;
+ this.timestamp = System.currentTimeMillis() / 1000;
+ this.embed = new DiscordUtil.EmbedObject();
+ }
+
+ protected void setupBaseEmbed() {
+ String avatar = ChatGuard.getDiscord()
+ .getString("customize.player-avatar")
+ .replace("%player%", player.getName());
+ boolean censorData = ChatGuard.getDiscord().getBoolean("embed-log.optional.censor", true);
+ boolean logIPAddress = ChatGuard.getDiscord().getBoolean("embed-log.optional.data.ip-address", true);
+ boolean logTimestamp = ChatGuard.getDiscord().getBoolean("embed-log.optional.data.timestamp", true);
+
+ embed.setAuthor(player.getName(), null, avatar);
+
+ setupEmbedDetails();
+
+ if (logIPAddress) {
+ embed.addField("IP:", String.format(censorData ? "||%s||" : "%s", getPlayerIP()), true);
+ }
+
+ if (logTimestamp) {
+ embed.addField("Timestamp:", String.format("", timestamp), true);
+ }
+
+ embed.setFooter(
+ String.format("ChatGuard v%s ・ Logger", pluginVersion),
+ "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png"
+ );
+ }
+
+ protected String getPlayerIP() {
+ if (((CraftPlayer) player).getHandle().netServerHandler == null) {
+ return "N/A";
+ }
+ return player.getAddress().getAddress().getHostAddress();
+ }
+
+ protected abstract void setupEmbedDetails();
+
+ public DiscordUtil.EmbedObject getEmbed() {
+ return embed;
+ }
+}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/core/log/embed/NameEmbed.java b/src/main/java/io/github/aleksandarharalanov/chatguard/core/log/embed/NameEmbed.java
new file mode 100644
index 0000000..6884de4
--- /dev/null
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/core/log/embed/NameEmbed.java
@@ -0,0 +1,34 @@
+package io.github.aleksandarharalanov.chatguard.core.log.embed;
+
+import io.github.aleksandarharalanov.chatguard.ChatGuard;
+import io.github.aleksandarharalanov.chatguard.core.data.LoginData;
+import org.bukkit.entity.Player;
+import org.bukkit.plugin.java.JavaPlugin;
+
+import java.awt.*;
+
+public final class NameEmbed extends DiscordEmbed {
+
+ private final String trigger;
+
+ public NameEmbed(JavaPlugin plugin, Player player, String content, String trigger) {
+ super(plugin, player, content);
+ this.trigger = trigger;
+ setupBaseEmbed();
+ }
+
+ @Override
+ protected void setupEmbedDetails() {
+ String hex = ChatGuard.getDiscord().getString("customize.type.name.color");
+ boolean censorData = ChatGuard.getDiscord().getBoolean("embed-log.optional.censor", true);
+
+ embed.setTitle("Name Filter")
+ .addField("Trigger:", String.format(censorData ? "||`%s`||" : "`%s`", trigger), true)
+ .setColor(Color.decode(hex));
+ }
+
+ @Override
+ protected String getPlayerIP() {
+ return LoginData.popPlayerIP(player.getName());
+ }
+}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/core/log/embed/SignEmbed.java b/src/main/java/io/github/aleksandarharalanov/chatguard/core/log/embed/SignEmbed.java
new file mode 100644
index 0000000..d1fdb4b
--- /dev/null
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/core/log/embed/SignEmbed.java
@@ -0,0 +1,40 @@
+package io.github.aleksandarharalanov.chatguard.core.log.embed;
+
+import io.github.aleksandarharalanov.chatguard.ChatGuard;
+import io.github.aleksandarharalanov.chatguard.core.data.PenaltyData;
+import org.bukkit.entity.Player;
+import org.bukkit.plugin.java.JavaPlugin;
+
+import java.awt.*;
+
+public final class SignEmbed extends DiscordEmbed {
+
+ private final String trigger;
+
+ public SignEmbed(JavaPlugin plugin, Player player, String content, String trigger) {
+ super(plugin, player, content);
+ this.trigger = trigger;
+ setupBaseEmbed();
+ }
+
+ @Override
+ protected void setupEmbedDetails() {
+ String hex = ChatGuard.getDiscord().getString("customize.type.sign.color");
+ boolean censorData = ChatGuard.getDiscord().getBoolean("embed-log.optional.censor", true);
+
+ if (!PenaltyData.isPlayerOnFinalStrike(player)) {
+ embed.setDescription(String.format(
+ "S%d ► S%d", PenaltyData.getStrike(player), PenaltyData.getStrike(player) + 1
+ ));
+ } else {
+ embed.setDescription(String.format(
+ "S%d (Max)", PenaltyData.getStrike(player)
+ ));
+ }
+
+ embed.setTitle("Sign Filter")
+ .addField("Content:", content, false)
+ .addField("Trigger:", String.format(censorData ? "||`%s`||" : "`%s`", trigger), true)
+ .setColor(Color.decode(hex));
+ }
+}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/core/log/logger/ConsoleLogger.java b/src/main/java/io/github/aleksandarharalanov/chatguard/core/log/logger/ConsoleLogger.java
new file mode 100644
index 0000000..3afeae0
--- /dev/null
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/core/log/logger/ConsoleLogger.java
@@ -0,0 +1,43 @@
+package io.github.aleksandarharalanov.chatguard.core.log.logger;
+
+import io.github.aleksandarharalanov.chatguard.ChatGuard;
+import io.github.aleksandarharalanov.chatguard.core.log.LogAttribute;
+import io.github.aleksandarharalanov.chatguard.core.log.LogType;
+import io.github.aleksandarharalanov.chatguard.util.log.LogUtil;
+import org.bukkit.entity.Player;
+
+public final class ConsoleLogger {
+
+ private ConsoleLogger() {}
+
+ public static void log(LogType logType, Player player, String content) {
+ if (!isConsoleLogEnabled(logType)) return;
+
+ String logMessage = String.format("[ChatGuard] [%s]", logType.name());
+ switch (logType) {
+ case CHAT:
+ logMessage = String.format("%s Stopped player '%s'; Bad content: '%s'", logMessage, player.getName(), content);
+ break;
+ case SIGN:
+ logMessage = String.format("%s Stopped player '%s'; Bad sign: '%s'", logMessage, player.getName(), content);
+ break;
+ case NAME:
+ logMessage = String.format("%s Stopped player '%s'; Bad name.", logMessage, content);
+ break;
+ case CAPTCHA:
+ logMessage = String.format("%s Stopped player '%s'; Triggered captcha: '%s'", logMessage, player.getName(), content);
+ break;
+ default:
+ return;
+ }
+
+ LogUtil.logConsoleInfo(logMessage);
+ }
+
+ private static boolean isConsoleLogEnabled(LogType logType) {
+ if (logType.hasAttribute(LogAttribute.FILTER)) {
+ return ChatGuard.getConfig().getBoolean("filter.log.console", true);
+ }
+ return ChatGuard.getConfig().getBoolean("captcha.log-console", true);
+ }
+}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/core/log/logger/DiscordLogger.java b/src/main/java/io/github/aleksandarharalanov/chatguard/core/log/logger/DiscordLogger.java
new file mode 100644
index 0000000..1aeb6fb
--- /dev/null
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/core/log/logger/DiscordLogger.java
@@ -0,0 +1,76 @@
+package io.github.aleksandarharalanov.chatguard.core.log.logger;
+
+import io.github.aleksandarharalanov.chatguard.ChatGuard;
+import io.github.aleksandarharalanov.chatguard.core.log.LogType;
+import io.github.aleksandarharalanov.chatguard.core.log.embed.*;
+import io.github.aleksandarharalanov.chatguard.util.log.DiscordUtil;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+
+import java.io.IOException;
+
+public final class DiscordLogger {
+
+ private DiscordLogger() {}
+
+ public static void log(LogType logType, Player player, String content, String trigger) {
+ if (!isDiscordLogTypeEnabled(logType)) return;
+
+ String webhookUrl = ChatGuard.getDiscord().getString("webhook-url");
+ String webhookName = ChatGuard.getDiscord().getString(String.format(
+ "customize.type.%s.webhook.name",
+ logType.name().toLowerCase()
+ ));
+ String webhookIcon = ChatGuard.getDiscord().getString(String.format(
+ "customize.type.%s.webhook.icon",
+ logType.name().toLowerCase()
+ ));
+
+ DiscordUtil webhook = new DiscordUtil(webhookUrl);
+ webhook.setUsername(webhookName);
+ webhook.setAvatarUrl(webhookIcon);
+
+ DiscordEmbed embed;
+ switch (logType) {
+ case CHAT:
+ embed = new ChatEmbed(ChatGuard.getInstance(), player, content, trigger);
+ break;
+ case SIGN:
+ embed = new SignEmbed(ChatGuard.getInstance(), player, content, trigger);
+ break;
+ case NAME:
+ embed = new NameEmbed(ChatGuard.getInstance(), player, content, trigger);
+ break;
+ case CAPTCHA:
+ embed = new CaptchaEmbed(ChatGuard.getInstance(), player, content);
+ break;
+ default:
+ return;
+ }
+
+ webhook.addEmbed(embed.getEmbed());
+
+ Bukkit.getServer().getScheduler().scheduleAsyncDelayedTask(ChatGuard.getInstance(), () -> {
+ try {
+ webhook.execute();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }, 0L);
+ }
+
+ private static boolean isDiscordLogTypeEnabled(LogType logType) {
+ switch (logType) {
+ case CHAT:
+ return ChatGuard.getDiscord().getBoolean("embed-log.type.chat", true);
+ case SIGN:
+ return ChatGuard.getDiscord().getBoolean("embed-log.type.sign", true);
+ case NAME:
+ return ChatGuard.getDiscord().getBoolean("embed-log.type.name", true);
+ case CAPTCHA:
+ return ChatGuard.getDiscord().getBoolean("embed-log.type.captcha", true);
+ default:
+ return false;
+ }
+ }
+}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/core/log/logger/FileLogger.java b/src/main/java/io/github/aleksandarharalanov/chatguard/core/log/logger/FileLogger.java
new file mode 100644
index 0000000..2ba0b7c
--- /dev/null
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/core/log/logger/FileLogger.java
@@ -0,0 +1,29 @@
+package io.github.aleksandarharalanov.chatguard.core.log.logger;
+
+import io.github.aleksandarharalanov.chatguard.ChatGuard;
+import io.github.aleksandarharalanov.chatguard.core.log.LogAttribute;
+import io.github.aleksandarharalanov.chatguard.core.log.LogType;
+import io.github.aleksandarharalanov.chatguard.util.log.LogUtil;
+import org.bukkit.entity.Player;
+
+public final class FileLogger {
+
+ private FileLogger() {}
+
+ public static void log(LogType logType, Player player, String content) {
+ if (!shouldLocalFileLog(logType)) return;
+
+ LogUtil.writeToLogFile(String.format(
+ "[%s] [%s] <%s> %s",
+ logType.name(),
+ player.getAddress().getAddress().getHostAddress(),
+ player.getName(),
+ content
+ ), true);
+ }
+
+ private static boolean shouldLocalFileLog(LogType logType) {
+ boolean isLocalFileLogEnabled = ChatGuard.getConfig().getBoolean("filter.log.console", true);
+ return isLocalFileLogEnabled && logType.hasAttribute(LogAttribute.FILTER) && logType != LogType.NAME;
+ }
+}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/core/misc/AudioCuePlayer.java b/src/main/java/io/github/aleksandarharalanov/chatguard/core/misc/AudioCuePlayer.java
new file mode 100644
index 0000000..051e5f2
--- /dev/null
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/core/misc/AudioCuePlayer.java
@@ -0,0 +1,28 @@
+package io.github.aleksandarharalanov.chatguard.core.misc;
+
+import io.github.aleksandarharalanov.chatguard.ChatGuard;
+import io.github.aleksandarharalanov.chatguard.core.log.LogAttribute;
+import io.github.aleksandarharalanov.chatguard.core.log.LogType;
+import org.bukkit.Effect;
+import org.bukkit.Location;
+import org.bukkit.entity.Player;
+
+public final class AudioCuePlayer {
+
+ private AudioCuePlayer() {}
+
+ public static void play(LogType logType, Player player, boolean highPitch) {
+ if (!shouldAudioCuePlay(logType)) return;
+
+ Location location = player.getLocation();
+ location.setY(location.getY() + player.getEyeHeight());
+
+ if (highPitch) player.playEffect(location, Effect.CLICK1, 0);
+ else player.playEffect(location, Effect.CLICK2, 0);
+ }
+
+ private static boolean shouldAudioCuePlay(LogType logType) {
+ boolean isAudioCuesEnabled = ChatGuard.getConfig().getBoolean("miscellaneous.audio-cues", true);
+ return isAudioCuesEnabled && logType.hasAttribute(LogAttribute.AUDIO) && logType != LogType.NAME;
+ }
+}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/core/misc/TimeFormatter.java b/src/main/java/io/github/aleksandarharalanov/chatguard/core/misc/TimeFormatter.java
new file mode 100644
index 0000000..93f7616
--- /dev/null
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/core/misc/TimeFormatter.java
@@ -0,0 +1,32 @@
+package io.github.aleksandarharalanov.chatguard.core.misc;
+
+import com.earth2me.essentials.User;
+import io.github.aleksandarharalanov.chatguard.util.misc.ColorUtil;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public final class TimeFormatter {
+
+ private TimeFormatter() {}
+
+ public static void printFormattedMuteDuration(User user) {
+ long remainingMillis = user.getMuteTimeout() - System.currentTimeMillis();
+
+ Map timeUnits = new LinkedHashMap<>();
+ timeUnits.put("d.", (int) (remainingMillis / (1000 * 60 * 60 * 24)));
+ timeUnits.put("h.", (int) ((remainingMillis / (1000 * 60 * 60)) % 24));
+ timeUnits.put("m.", (int) ((remainingMillis / (1000 * 60)) % 60));
+ timeUnits.put("s.", (int) ((remainingMillis / 1000) % 60));
+
+ String formattedTime = timeUnits.entrySet().stream()
+ .filter(entry -> entry.getValue() > 0)
+ .map(entry -> entry.getValue() + entry.getKey())
+ .collect(Collectors.joining(" ", "", ""));
+
+ user.sendMessage(ColorUtil.translateColorCodes(String.format(
+ "&7Expires in %s", formattedTime
+ )));
+ }
+}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/core/security/captcha/CaptchaDetector.java b/src/main/java/io/github/aleksandarharalanov/chatguard/core/security/captcha/CaptchaDetector.java
new file mode 100644
index 0000000..67e6737
--- /dev/null
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/core/security/captcha/CaptchaDetector.java
@@ -0,0 +1,35 @@
+package io.github.aleksandarharalanov.chatguard.core.security.captcha;
+
+import io.github.aleksandarharalanov.chatguard.ChatGuard;
+import io.github.aleksandarharalanov.chatguard.core.data.CaptchaData;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Set;
+
+public final class CaptchaDetector {
+
+ private CaptchaDetector() {}
+
+ public static boolean doesPlayerTriggerCaptcha(String playerName, String message) {
+ String sanitizedMessage = message.toLowerCase();
+ Set whitelistTerms = new HashSet<>(ChatGuard.getConfig().getStringList("captcha.whitelist", new ArrayList<>()));
+
+ for (String term : whitelistTerms) {
+ sanitizedMessage = sanitizedMessage.replaceAll(term, "");
+ }
+
+ if (sanitizedMessage.isEmpty()) return false;
+
+ LinkedList messages = CaptchaData.getMessageHistory(playerName);
+ messages.add(sanitizedMessage);
+
+ if (messages.size() > CaptchaData.getThreshold()) {
+ messages.removeFirst();
+ }
+
+ return messages.size() == CaptchaData.getThreshold() &&
+ messages.stream().allMatch(msg -> msg.equals(messages.getFirst()));
+ }
+}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/core/security/captcha/CaptchaGenerator.java b/src/main/java/io/github/aleksandarharalanov/chatguard/core/security/captcha/CaptchaGenerator.java
new file mode 100644
index 0000000..ba516d4
--- /dev/null
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/core/security/captcha/CaptchaGenerator.java
@@ -0,0 +1,22 @@
+package io.github.aleksandarharalanov.chatguard.core.security.captcha;
+
+import io.github.aleksandarharalanov.chatguard.core.data.CaptchaData;
+
+import java.util.Random;
+
+public final class CaptchaGenerator {
+
+ private CaptchaGenerator() {}
+
+ public static String generateCaptchaCode() {
+ StringBuilder captcha = new StringBuilder(CaptchaData.getCodeLength());
+ Random random = new Random();
+
+ for (int i = 0; i < CaptchaData.getCodeLength(); i++) {
+ captcha.append(CaptchaData.getCodeCharacters()
+ .charAt(random.nextInt(CaptchaData.getCodeCharacters().length())));
+ }
+
+ return captcha.toString();
+ }
+}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/core/security/captcha/CaptchaHandler.java b/src/main/java/io/github/aleksandarharalanov/chatguard/core/security/captcha/CaptchaHandler.java
new file mode 100644
index 0000000..e4daac7
--- /dev/null
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/core/security/captcha/CaptchaHandler.java
@@ -0,0 +1,51 @@
+package io.github.aleksandarharalanov.chatguard.core.security.captcha;
+
+import io.github.aleksandarharalanov.chatguard.core.data.CaptchaData;
+import io.github.aleksandarharalanov.chatguard.core.log.logger.ConsoleLogger;
+import io.github.aleksandarharalanov.chatguard.core.log.logger.DiscordLogger;
+import io.github.aleksandarharalanov.chatguard.core.log.LogType;
+import io.github.aleksandarharalanov.chatguard.core.misc.AudioCuePlayer;
+import io.github.aleksandarharalanov.chatguard.util.auth.AccessUtil;
+import io.github.aleksandarharalanov.chatguard.util.misc.ColorUtil;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+
+public final class CaptchaHandler {
+
+ private CaptchaHandler() {}
+
+ public static boolean doesPlayerHaveActiveCaptcha(Player player) {
+ String captchaCode = CaptchaData.getPlayerCaptcha(player.getName());
+ if (captchaCode != null) {
+ player.sendMessage(ColorUtil.translateColorCodes("&c[ChatGuard] You have an active captcha verification."));
+ player.sendMessage(ColorUtil.translateColorCodes(String.format(
+ "&cUse &e/cg captcha &b%s &cto verify. Case-sensitive!", captchaCode
+ )));
+ AudioCuePlayer.play(LogType.CAPTCHA, player, false);
+ return true;
+ }
+ return false;
+ }
+
+ public static void processCaptchaTrigger(Player player, String content) {
+ String captchaCode = CaptchaGenerator.generateCaptchaCode();
+ CaptchaData.setPlayerCaptcha(player.getName(), captchaCode);
+
+ player.sendMessage(ColorUtil.translateColorCodes("&c[ChatGuard] Captcha verification triggered."));
+ player.sendMessage(ColorUtil.translateColorCodes(String.format(
+ "&cUse &e/cg captcha &b%s &cto verify. Case-sensitive!", captchaCode
+ )));
+
+ AudioCuePlayer.play(LogType.CAPTCHA, player, false);
+ ConsoleLogger.log(LogType.CAPTCHA, player, content);
+ DiscordLogger.log(LogType.CAPTCHA, player, content, null);
+
+ for (Player p : Bukkit.getServer().getOnlinePlayers()) {
+ if (AccessUtil.senderHasPermission(p, "chatguard.captcha")) {
+ p.sendMessage(ColorUtil.translateColorCodes(String.format(
+ "&c[ChatGuard] &e%s&c triggered captcha verification.", player.getName()
+ )));
+ }
+ }
+ }
+}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/core/security/filter/ContentFilter.java b/src/main/java/io/github/aleksandarharalanov/chatguard/core/security/filter/ContentFilter.java
new file mode 100644
index 0000000..515eda4
--- /dev/null
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/core/security/filter/ContentFilter.java
@@ -0,0 +1,53 @@
+package io.github.aleksandarharalanov.chatguard.core.security.filter;
+
+import io.github.aleksandarharalanov.chatguard.ChatGuard;
+import io.github.aleksandarharalanov.chatguard.core.log.LogType;
+import org.bukkit.entity.Player;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public final class ContentFilter {
+
+ private ContentFilter() {}
+
+ public static boolean isChatContentBlocked(Player player, String content) {
+ return isBlocked(LogType.CHAT, player, content);
+ }
+
+ public static boolean isSignContentBlocked(Player player, String[] content) {
+ return isBlocked(LogType.SIGN, player, mergeSignContent(content));
+ }
+
+ public static boolean isPlayerNameBlocked(Player player) {
+ return isBlocked(LogType.NAME, player, player.getName());
+ }
+
+ private static boolean isBlocked(LogType logType, Player player, String content) {
+ String sanitizedContent = sanitizeContent(content);
+ String trigger = TriggerDetector.getTrigger(sanitizedContent);
+
+ if (trigger == null) return false;
+
+ ContentHandler.handleBlockedContent(logType, player, content, trigger);
+ return true;
+ }
+
+ private static String sanitizeContent(String content) {
+ String sanitizedContent = content.toLowerCase();
+ Set whitelistTerms = new HashSet<>(ChatGuard.getConfig().getStringList("filter.rules.terms.whitelist", new ArrayList<>()));
+
+ for (String term : whitelistTerms) {
+ sanitizedContent = sanitizedContent.replaceAll(term, "");
+ }
+ return sanitizedContent;
+ }
+
+ private static String mergeSignContent(String[] content) {
+ return Stream.of(content)
+ .map(String::toLowerCase)
+ .collect(Collectors.joining(" "));
+ }
+}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/core/security/filter/ContentHandler.java b/src/main/java/io/github/aleksandarharalanov/chatguard/core/security/filter/ContentHandler.java
new file mode 100644
index 0000000..71a814d
--- /dev/null
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/core/security/filter/ContentHandler.java
@@ -0,0 +1,43 @@
+package io.github.aleksandarharalanov.chatguard.core.security.filter;
+
+import io.github.aleksandarharalanov.chatguard.ChatGuard;
+import io.github.aleksandarharalanov.chatguard.core.log.LogType;
+import io.github.aleksandarharalanov.chatguard.core.log.logger.ConsoleLogger;
+import io.github.aleksandarharalanov.chatguard.core.log.logger.DiscordLogger;
+import io.github.aleksandarharalanov.chatguard.core.log.logger.FileLogger;
+import io.github.aleksandarharalanov.chatguard.core.misc.AudioCuePlayer;
+import io.github.aleksandarharalanov.chatguard.core.security.penalty.MuteEnforcer;
+import io.github.aleksandarharalanov.chatguard.core.security.penalty.StrikeEnforcer;
+import io.github.aleksandarharalanov.chatguard.util.misc.ColorUtil;
+import org.bukkit.entity.Player;
+
+public final class ContentHandler {
+
+ private ContentHandler() {}
+
+ public static void handleBlockedContent(LogType logType, Player player, String content, String trigger) {
+ if (shouldWarnPlayer(logType)) {
+ player.sendMessage(ColorUtil.translateColorCodes(getWarningMessage(logType)));
+ }
+
+ AudioCuePlayer.play(logType, player, false);
+ ConsoleLogger.log(logType, player, content);
+ FileLogger.log(logType, player, content);
+ DiscordLogger.log(logType, player, content, trigger);
+ MuteEnforcer.processEssentialsMute(logType, player);
+ StrikeEnforcer.incrementStrikeTier(logType, player);
+ }
+
+ private static boolean shouldWarnPlayer(LogType logType) {
+ return logType != LogType.NAME && ChatGuard.getConfig().getBoolean("filter.warn-player", true);
+ }
+
+ private static String getWarningMessage(LogType logType) {
+ switch (logType) {
+ case SIGN:
+ return "&cSign censored due to bad words.";
+ default:
+ return "&cMessage cancelled due to bad words.";
+ }
+ }
+}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/core/security/filter/TriggerDetector.java b/src/main/java/io/github/aleksandarharalanov/chatguard/core/security/filter/TriggerDetector.java
new file mode 100644
index 0000000..e9716b1
--- /dev/null
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/core/security/filter/TriggerDetector.java
@@ -0,0 +1,44 @@
+package io.github.aleksandarharalanov.chatguard.core.security.filter;
+
+import io.github.aleksandarharalanov.chatguard.ChatGuard;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public final class TriggerDetector {
+
+ private TriggerDetector() {}
+
+ public static String getTrigger(String sanitizedContent) {
+ String trigger = checkBlacklistedTerms(sanitizedContent);
+ return (trigger != null) ? trigger : checkRegExPatterns(sanitizedContent);
+ }
+
+ private static String checkBlacklistedTerms(String sanitizedContent) {
+ Set blacklistTerms = new HashSet<>(ChatGuard.getConfig().getStringList("filter.rules.terms.blacklist", new ArrayList<>()));
+ String[] contentTerms = sanitizedContent.split("\\s+");
+
+ for (String term : contentTerms) {
+ if (blacklistTerms.contains(term)) {
+ return term;
+ }
+ }
+ return null;
+ }
+
+ private static String checkRegExPatterns(String sanitizedContent) {
+ Set regexList = new HashSet<>(ChatGuard.getConfig().getStringList("filter.rules.regex", new ArrayList<>()));
+
+ for (String regex : regexList) {
+ Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
+ Matcher matcher = pattern.matcher(sanitizedContent);
+ if (matcher.find()) {
+ return regex.replace("\\", "\\\\").replace("\"", "\\\"");
+ }
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/core/security/penalty/MuteEnforcer.java b/src/main/java/io/github/aleksandarharalanov/chatguard/core/security/penalty/MuteEnforcer.java
new file mode 100644
index 0000000..721acda
--- /dev/null
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/core/security/penalty/MuteEnforcer.java
@@ -0,0 +1,40 @@
+package io.github.aleksandarharalanov.chatguard.core.security.penalty;
+
+import com.earth2me.essentials.Essentials;
+import com.earth2me.essentials.User;
+import com.earth2me.essentials.Util;
+import io.github.aleksandarharalanov.chatguard.ChatGuard;
+import io.github.aleksandarharalanov.chatguard.core.data.PenaltyData;
+import io.github.aleksandarharalanov.chatguard.core.log.LogAttribute;
+import io.github.aleksandarharalanov.chatguard.core.log.LogType;
+import io.github.aleksandarharalanov.chatguard.util.misc.ColorUtil;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+
+public final class MuteEnforcer {
+
+ private MuteEnforcer() {}
+
+ public static void processEssentialsMute(LogType logType, Player player) {
+ boolean isEssentialsMuteEnabled = ChatGuard.getConfig().getBoolean("filter.essentials-mute.enabled", true);
+ if (!isEssentialsMuteEnabled) return;
+
+ if (!logType.hasAttribute(LogAttribute.MUTE) || logType == LogType.NAME) return;
+
+ Essentials essentials = (Essentials) Bukkit.getServer().getPluginManager().getPlugin("Essentials");
+ if (essentials == null || !essentials.isEnabled()) return;
+
+ User user = essentials.getUser(player.getName());
+ try {
+ user.setMuteTimeout(Util.parseDateDiff(PenaltyData.getMuteDuration(player), true));
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ user.setMuted(true);
+
+ Bukkit.getServer().broadcastMessage(ColorUtil.translateColorCodes(String.format(
+ "&c[ChatGuard] %s muted for %s. by system; content has bad words.",
+ player.getName(), PenaltyData.getMuteDuration(player)
+ )));
+ }
+}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/core/security/penalty/StrikeEnforcer.java b/src/main/java/io/github/aleksandarharalanov/chatguard/core/security/penalty/StrikeEnforcer.java
new file mode 100644
index 0000000..44357a8
--- /dev/null
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/core/security/penalty/StrikeEnforcer.java
@@ -0,0 +1,19 @@
+package io.github.aleksandarharalanov.chatguard.core.security.penalty;
+
+import io.github.aleksandarharalanov.chatguard.ChatGuard;
+import io.github.aleksandarharalanov.chatguard.core.data.PenaltyData;
+import io.github.aleksandarharalanov.chatguard.core.log.LogAttribute;
+import io.github.aleksandarharalanov.chatguard.core.log.LogType;
+import org.bukkit.entity.Player;
+
+public final class StrikeEnforcer {
+
+ public static void incrementStrikeTier(LogType logType, Player player) {
+ if (!logType.hasAttribute(LogAttribute.STRIKE) || logType == LogType.NAME) return;
+
+ if (PenaltyData.getStrike(player) <= 4) {
+ ChatGuard.getStrikes().setProperty(player.getName(), PenaltyData.getStrike(player) + 1);
+ ChatGuard.getStrikes().save();
+ }
+ }
+}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/core/security/spam/ChatRateLimiter.java b/src/main/java/io/github/aleksandarharalanov/chatguard/core/security/spam/ChatRateLimiter.java
new file mode 100644
index 0000000..ffc0889
--- /dev/null
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/core/security/spam/ChatRateLimiter.java
@@ -0,0 +1,41 @@
+package io.github.aleksandarharalanov.chatguard.core.security.spam;
+
+import io.github.aleksandarharalanov.chatguard.ChatGuard;
+import io.github.aleksandarharalanov.chatguard.core.data.PenaltyData;
+import io.github.aleksandarharalanov.chatguard.core.data.TimestampData;
+import io.github.aleksandarharalanov.chatguard.util.misc.ColorUtil;
+import org.bukkit.entity.Player;
+
+public final class ChatRateLimiter {
+
+ private ChatRateLimiter() {}
+
+ public static boolean isPlayerChatSpamming(Player player) {
+ long timestamp = System.currentTimeMillis();
+ String playerName = player.getName();
+
+ long lastTimestamp = TimestampData.getMessageTimestamp(playerName);
+
+ if (lastTimestamp != 0) {
+ long elapsed = timestamp - lastTimestamp;
+ int cooldown = ChatGuard.getConfig().getInt(String.format(
+ "spam-prevention.cooldown-ms.chat.s%d", PenaltyData.getStrike(player)
+ ), 0);
+
+ if (elapsed <= cooldown) {
+ boolean isWarnEnabled = ChatGuard.getConfig().getBoolean("spam-prevention.warn-player", true);
+ if (isWarnEnabled) {
+ double remainingTime = (cooldown - elapsed) / 1000.0;
+ player.sendMessage(ColorUtil.translateColorCodes(String.format(
+ "&cPlease wait %.2f sec. before sending another message.", remainingTime
+ )));
+ }
+ return true;
+ }
+ }
+
+ TimestampData.setMessageTimestamp(playerName, timestamp);
+ return false;
+ }
+}
+
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/core/security/spam/CommandRateLimiter.java b/src/main/java/io/github/aleksandarharalanov/chatguard/core/security/spam/CommandRateLimiter.java
new file mode 100644
index 0000000..9f2142c
--- /dev/null
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/core/security/spam/CommandRateLimiter.java
@@ -0,0 +1,39 @@
+package io.github.aleksandarharalanov.chatguard.core.security.spam;
+
+import io.github.aleksandarharalanov.chatguard.ChatGuard;
+import io.github.aleksandarharalanov.chatguard.core.data.TimestampData;
+import io.github.aleksandarharalanov.chatguard.util.misc.ColorUtil;
+import org.bukkit.entity.Player;
+
+public final class CommandRateLimiter {
+
+ private CommandRateLimiter() {}
+
+ public static boolean isPlayerCommandSpamming(Player player) {
+ long timestamp = System.currentTimeMillis();
+ String playerName = player.getName();
+
+ long lastTimestamp = TimestampData.getCommandTimestamp(playerName);
+
+ if (lastTimestamp != 0) {
+ long elapsed = timestamp - lastTimestamp;
+
+ int strikeTier = ChatGuard.getStrikes().getInt(playerName, 0);
+ int cooldown = ChatGuard.getConfig().getInt(String.format("spam-prevention.cooldown-ms.command.s%d", strikeTier), 0);
+
+ if (elapsed <= cooldown) {
+ boolean isWarnEnabled = ChatGuard.getConfig().getBoolean("spam-prevention.warn-player", true);
+ if (isWarnEnabled) {
+ double remainingTime = (cooldown - elapsed) / 1000.0;
+ player.sendMessage(ColorUtil.translateColorCodes(String.format(
+ "&cPlease wait %.2f sec. before running another command.", remainingTime
+ )));
+ }
+ return true;
+ }
+ }
+
+ TimestampData.setCommandTimestamp(playerName, timestamp);
+ return false;
+ }
+}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/handler/CaptchaHandler.java b/src/main/java/io/github/aleksandarharalanov/chatguard/handler/CaptchaHandler.java
deleted file mode 100644
index fa5a10a..0000000
--- a/src/main/java/io/github/aleksandarharalanov/chatguard/handler/CaptchaHandler.java
+++ /dev/null
@@ -1,80 +0,0 @@
-package io.github.aleksandarharalanov.chatguard.handler;
-
-import org.bukkit.entity.Player;
-
-import java.util.*;
-
-import static io.github.aleksandarharalanov.chatguard.ChatGuard.getConfig;
-import static io.github.aleksandarharalanov.chatguard.handler.LogHandler.performLogs;
-import static io.github.aleksandarharalanov.chatguard.handler.SoundHandler.playSoundCue;
-import static io.github.aleksandarharalanov.chatguard.util.AccessUtil.hasPermission;
-import static io.github.aleksandarharalanov.chatguard.util.ColorUtil.translate;
-import static org.bukkit.Bukkit.getServer;
-
-public class CaptchaHandler {
-
- private static final HashMap> playerMessages = new HashMap<>();
- private static final HashMap playerCaptcha = new HashMap<>();
-
- public static boolean isPlayerCaptchaActive(Player player) {
- String captchaCode = playerCaptcha.get(player.getName());
- if (captchaCode != null) {
- player.sendMessage(translate(String.format("&c[ChatGuard] Bot-like behavior detected. Code &b%s&c.", captchaCode)));
- player.sendMessage(translate("&cUse &e/cg captcha &cto verify. Case-sensitive!"));
- playSoundCue(player, false);
- return true;
- }
- return false;
- }
-
- public static boolean checkPlayerCaptcha(Player player, String message) {
- String sanitizedMessage = message.toLowerCase();
-
- Set whitelistTerms = new HashSet<>(getConfig().getStringList("captcha.whitelist", new ArrayList<>()));
- for (String term : whitelistTerms) sanitizedMessage = sanitizedMessage.replaceAll(term, "");
-
- if (sanitizedMessage.equalsIgnoreCase("")) return false;
-
- LinkedList messages = playerMessages.computeIfAbsent(player.getName(), k -> new LinkedList<>());
- messages.add(sanitizedMessage);
-
- int threshold = getConfig().getInt("captcha.threshold", 4);
- if (messages.size() > threshold) messages.removeFirst();
-
- if (messages.size() == threshold && messages.stream().allMatch(msg -> msg.equals(messages.getFirst()))) {
- String captchaCode = generateCaptchaCode();
- playerCaptcha.put(player.getName(), captchaCode);
-
- player.sendMessage(translate(String.format("&c[ChatGuard] Bot-like behavior detected. Code &b%s&c.", captchaCode)));
- player.sendMessage(translate("&cUse &e/cg captcha &cto verify. Case-sensitive!"));
-
- playSoundCue(player,false);
- performLogs("captcha", player, message);
-
- for (Player pl : getServer().getOnlinePlayers())
- if (hasPermission(pl, "chatguard.captcha"))
- pl.sendMessage(translate(String.format("&c[ChatGuard] &e%s&c prompted captcha for bot-like behavior.", player.getName())));
-
- return true;
- }
-
- return false;
- }
-
- private static String generateCaptchaCode() {
- String chars = getConfig().getString("captcha.code.characters");
- int length = getConfig().getInt("captcha.code.length", 5);
- StringBuilder captcha = new StringBuilder(length);
- Random random = new Random();
- for (int i = 0; i < length; i++) captcha.append(chars.charAt(random.nextInt(chars.length())));
- return captcha.toString();
- }
-
- public static HashMap getPlayerCaptcha() {
- return playerCaptcha;
- }
-
- public static HashMap> getPlayerMessages() {
- return playerMessages;
- }
-}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/handler/FilterHandler.java b/src/main/java/io/github/aleksandarharalanov/chatguard/handler/FilterHandler.java
deleted file mode 100644
index 22a104e..0000000
--- a/src/main/java/io/github/aleksandarharalanov/chatguard/handler/FilterHandler.java
+++ /dev/null
@@ -1,96 +0,0 @@
-package io.github.aleksandarharalanov.chatguard.handler;
-
-import com.earth2me.essentials.Essentials;
-import com.earth2me.essentials.User;
-import com.earth2me.essentials.Util;
-import org.bukkit.entity.Player;
-
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.Set;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import static io.github.aleksandarharalanov.chatguard.ChatGuard.*;
-import static io.github.aleksandarharalanov.chatguard.handler.LogHandler.performLogs;
-import static io.github.aleksandarharalanov.chatguard.handler.PunishmentHandler.getMuteDuration;
-import static io.github.aleksandarharalanov.chatguard.handler.PunishmentHandler.getStrike;
-import static io.github.aleksandarharalanov.chatguard.handler.SoundHandler.playSoundCue;
-import static io.github.aleksandarharalanov.chatguard.util.ColorUtil.translate;
-import static org.bukkit.Bukkit.getServer;
-
-public class FilterHandler {
-
- private static String trigger;
-
- public static boolean checkPlayerMessage(Player player, String message) throws Exception {
- String sanitizedMessage = message.toLowerCase();
- Set whitelistTerms = new HashSet<>(getConfig().getStringList("filter.rules.terms.whitelist", new ArrayList<>()));
- for (String term : whitelistTerms) sanitizedMessage = sanitizedMessage.replaceAll(term, "");
- if (containsBlacklistedTerms(sanitizedMessage) || matchesRegExPatterns(sanitizedMessage)) {
- cancelPlayerMessage(player, message);
- return true;
- }
- return false;
- }
-
- public static boolean shouldBlockUsername(String playerName) {
- String sanitizedMessage = playerName.toLowerCase();
- Set whitelistTerms = new HashSet<>(getConfig().getStringList("filter.rules.terms.whitelist", new ArrayList<>()));
- for (String term : whitelistTerms) sanitizedMessage = sanitizedMessage.replaceAll(term, "");
- return containsBlacklistedTerms(sanitizedMessage) || matchesRegExPatterns(sanitizedMessage);
- }
-
- private static boolean containsBlacklistedTerms(String message) {
- Set blacklistTerms = new HashSet<>(getConfig().getStringList("filter.rules.terms.blacklist", new ArrayList<>()));
- String[] messageTerms = message.split("\\s+");
- for (String term : messageTerms)
- if (blacklistTerms.contains(term)) {
- trigger = term;
- return true;
- }
- return false;
- }
-
- private static boolean matchesRegExPatterns(String message) {
- Set regexList = new HashSet<>(getConfig().getStringList("filter.rules.regex", new ArrayList<>()));
- for (String regex : regexList) {
- Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
- Matcher matcher = pattern.matcher(message);
- if (matcher.find()) {
- trigger = regex.replace("\\", "\\\\").replace("\"", "\\\"");
- return true;
- }
- }
- return false;
- }
-
- private static void cancelPlayerMessage(Player player, String message) throws Exception {
- boolean isWarnEnabled = getConfig().getBoolean("filter.warn-player", true);
- if (isWarnEnabled) player.sendMessage(translate("&cMessage cancelled for containing blocked words."));
-
- playSoundCue(player,false);
- performLogs("filter", player, message);
- issuePunishments(player);
- }
-
- private static void issuePunishments(Player player) throws Exception {
- Essentials essentials = (Essentials) getServer().getPluginManager().getPlugin("Essentials");
- boolean isMuteEnabled = getConfig().getBoolean("filter.mute.enabled", true);
- if (essentials != null && essentials.isEnabled() && isMuteEnabled) {
- User user = essentials.getUser(player.getName());
- user.setMuteTimeout(Util.parseDateDiff(getMuteDuration(player), true));
- user.setMuted(true);
- getServer().broadcastMessage(translate(String.format("&c[ChatGuard] %s muted for %s. by system; message contains blocked words.", player.getName(), getMuteDuration(player))));
- }
-
- if (getStrike(player) <= 4) {
- getStrikes().setProperty(player.getName(), getStrike(player) + 1);
- getStrikes().save();
- }
- }
-
- public static String getTrigger() {
- return trigger;
- }
-}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/handler/LogHandler.java b/src/main/java/io/github/aleksandarharalanov/chatguard/handler/LogHandler.java
deleted file mode 100644
index c29f917..0000000
--- a/src/main/java/io/github/aleksandarharalanov/chatguard/handler/LogHandler.java
+++ /dev/null
@@ -1,75 +0,0 @@
-package io.github.aleksandarharalanov.chatguard.handler;
-
-import io.github.aleksandarharalanov.chatguard.util.DiscordUtil;
-import org.bukkit.entity.Player;
-
-import java.awt.*;
-import java.io.IOException;
-import java.util.Arrays;
-
-import static io.github.aleksandarharalanov.chatguard.ChatGuard.getConfig;
-import static io.github.aleksandarharalanov.chatguard.ChatGuard.getInstance;
-import static io.github.aleksandarharalanov.chatguard.handler.FilterHandler.*;
-import static io.github.aleksandarharalanov.chatguard.handler.PunishmentHandler.getMuteDuration;
-import static io.github.aleksandarharalanov.chatguard.handler.PunishmentHandler.getStrike;
-import static io.github.aleksandarharalanov.chatguard.util.LoggerUtil.logWarning;
-import static io.github.aleksandarharalanov.chatguard.util.LoggerUtil.writeToLog;
-import static org.bukkit.Bukkit.getServer;
-
-public class LogHandler {
-
- public static void performLogs(String detection, Player player, String message) {
- boolean isConsoleLogEnabled = getConfig().getBoolean(String.format("%s.log.console", detection), true);
- if (isConsoleLogEnabled) {
- if (detection.equals("filter")) logWarning(String.format("[ChatGuard] <%s> %s", player.getName(), message));
- else logWarning(String.format("[ChatGuard] Detected player '%s' for bot-like behavior. Prompted captcha verification.", player.getName()));
- }
-
- boolean isLocalFileLogEnabled = getConfig().getBoolean(String.format("%s.log.local-file", detection), true);
- if (isLocalFileLogEnabled) {
- if (detection.equals("filter")) writeToLog(String.format("[FILTER] [%s] <%s> %s", player.getAddress().getAddress().getHostAddress(), player.getName(), message), true);
- else writeToLog(String.format("[CAPTCHA] [%s] <%s> %s", player.getAddress().getAddress().getHostAddress(), player.getName(), message), true);
- }
-
- boolean isDiscordWebhookLogEnabled = getConfig().getBoolean(String.format("%s.log.discord-webhook.enabled", detection), true);
- if (isDiscordWebhookLogEnabled) {
- getServer().getScheduler().scheduleAsyncDelayedTask(getInstance(), () -> {
- DiscordUtil webhook = new DiscordUtil(getConfig().getString(String.format("%s.log.discord-webhook.url", detection)));
- webhook.setUsername("ChatGuard");
- webhook.setAvatarUrl("https://raw.githubusercontent.com/AleksandarHaralanov/ChatGuard/refs/heads/master/assets/ChatGuard-Logo.png");
-
- final long unixTimestamp = System.currentTimeMillis() / 1000;
- DiscordUtil.EmbedObject embed = new DiscordUtil.EmbedObject();
-
- if (detection.equals("filter")) embed.setTitle("Filter Detection");
- else embed.setTitle("Captcha Verification");
-
- embed.setAuthor(player.getName(), null, String.format("https://minotar.net/helm/%s.png", player.getName()));
-
- if (detection.equals("filter")) {
- if (getStrike(player) <= 4) embed.setDescription(String.format("S%d > S%d ・ Mute Duration: %s", getStrike(player), getStrike(player) + 1, getMuteDuration(player)));
- else embed.setDescription(String.format("S%d ・ Mute Duration: %s", getStrike(player), getMuteDuration(player)));
- }
-
- embed.addField("Message:", message, false);
-
- if (detection.equals("filter")) embed.addField("Trigger:", String.format("`%s`", getTrigger()), true);
-
- embed.addField("IP:", player.getAddress().getAddress().getHostAddress(), true)
- .addField("Timestamp:", String.format("", unixTimestamp), true)
- .setFooter(String.format("ChatGuard v%s ・ Logger", getInstance().getDescription().getVersion()), null);
-
- if (detection.equals("filter")) embed.setColor(new Color(178, 34, 34));
- else embed.setColor(new Color(0, 152, 186));
-
- webhook.addEmbed(embed);
-
- try {
- webhook.execute();
- } catch (IOException e) {
- logWarning(Arrays.toString(e.getStackTrace()));
- }
- }, 0L);
- }
- }
-}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/handler/PunishmentHandler.java b/src/main/java/io/github/aleksandarharalanov/chatguard/handler/PunishmentHandler.java
deleted file mode 100644
index a43f5b0..0000000
--- a/src/main/java/io/github/aleksandarharalanov/chatguard/handler/PunishmentHandler.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package io.github.aleksandarharalanov.chatguard.handler;
-
-import org.bukkit.entity.Player;
-
-import static io.github.aleksandarharalanov.chatguard.ChatGuard.getConfig;
-import static io.github.aleksandarharalanov.chatguard.ChatGuard.getStrikes;
-
-public class PunishmentHandler {
-
- public static int getStrike(String playerName) {
- return getStrikes().getInt(playerName, 0);
- }
-
- public static int getStrike(Player player) {
- return getStrikes().getInt(player.getName(), 0);
- }
-
- public static String getMuteDuration(String playerName) {
- return getConfig().getString(String.format("filter.mute.duration.s%d", getStrikes().getInt(playerName, 0)));
- }
-
- public static String getMuteDuration(Player player) {
- return getConfig().getString(String.format("filter.mute.duration.s%d", getStrikes().getInt(player.getName(), 0)));
- }
-}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/handler/SoundHandler.java b/src/main/java/io/github/aleksandarharalanov/chatguard/handler/SoundHandler.java
deleted file mode 100644
index e2ccd7b..0000000
--- a/src/main/java/io/github/aleksandarharalanov/chatguard/handler/SoundHandler.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package io.github.aleksandarharalanov.chatguard.handler;
-
-import org.bukkit.Effect;
-import org.bukkit.Location;
-import org.bukkit.entity.Player;
-
-import static io.github.aleksandarharalanov.chatguard.ChatGuard.getConfig;
-
-public class SoundHandler {
-
- public static void playSoundCue(Player player, boolean pitch) {
- boolean isSoundCuesEnabled = getConfig().getBoolean("miscellaneous.sound-cues", true);
- if (isSoundCuesEnabled) {
- Location location = player.getLocation();
- location.setY(location.getY() + player.getEyeHeight());
- if (pitch) player.playEffect(location, Effect.CLICK1, 0);
- else player.playEffect(location, Effect.CLICK2, 0);
- }
- }
-}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/handler/spam/CommandSpamHandler.java b/src/main/java/io/github/aleksandarharalanov/chatguard/handler/spam/CommandSpamHandler.java
deleted file mode 100644
index 1160345..0000000
--- a/src/main/java/io/github/aleksandarharalanov/chatguard/handler/spam/CommandSpamHandler.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package io.github.aleksandarharalanov.chatguard.handler.spam;
-
-import org.bukkit.entity.Player;
-
-import java.util.HashMap;
-
-import static io.github.aleksandarharalanov.chatguard.ChatGuard.getConfig;
-import static io.github.aleksandarharalanov.chatguard.ChatGuard.getStrikes;
-import static io.github.aleksandarharalanov.chatguard.util.ColorUtil.translate;
-
-public class CommandSpamHandler {
-
- private static final HashMap playerCommandTimestamps = new HashMap<>();
-
- public static boolean isPlayerCommandSpamming(Player player) {
- long timestamp = System.currentTimeMillis();
-
- Long lastTimestamp = playerCommandTimestamps.get(player.getName());
- if (lastTimestamp != null) {
- long elapsed = timestamp - lastTimestamp;
-
- int strikeTier = getStrikes().getInt(player.getName(), 0);
- int cooldown = getConfig().getInt(String.format("spam-prevention.cooldown-ms.command.s%d", strikeTier), 0);
-
- if (elapsed <= cooldown) {
- boolean isWarnEnabled = getConfig().getBoolean("spam-prevention.warn-player", true);
- if (isWarnEnabled) {
- double remainingTime = (cooldown - elapsed) / 1000.0;
- player.sendMessage(translate(String.format("&cPlease wait %.2f sec. before running another command.", remainingTime)));
- }
-
- return true;
- }
- }
-
- playerCommandTimestamps.put(player.getName(), timestamp);
- return false;
- }
-}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/handler/spam/MessageSpamHandler.java b/src/main/java/io/github/aleksandarharalanov/chatguard/handler/spam/MessageSpamHandler.java
deleted file mode 100644
index f402a6d..0000000
--- a/src/main/java/io/github/aleksandarharalanov/chatguard/handler/spam/MessageSpamHandler.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package io.github.aleksandarharalanov.chatguard.handler.spam;
-
-import org.bukkit.entity.Player;
-
-import java.util.HashMap;
-
-import static io.github.aleksandarharalanov.chatguard.ChatGuard.getConfig;
-import static io.github.aleksandarharalanov.chatguard.ChatGuard.getStrikes;
-import static io.github.aleksandarharalanov.chatguard.util.ColorUtil.translate;
-
-public class MessageSpamHandler {
-
- private static final HashMap playerMessageTimestamps = new HashMap<>();
-
- public static boolean isPlayerMessageSpamming(Player player) {
- long timestamp = System.currentTimeMillis();
-
- Long lastTimestamp = playerMessageTimestamps.get(player.getName());
- if (lastTimestamp != null) {
- long elapsed = timestamp - lastTimestamp;
-
- int strikeTier = getStrikes().getInt(player.getName(), 0);
- int cooldown = getConfig().getInt(String.format("spam-prevention.cooldown-ms.message.s%d", strikeTier), 0);
-
- if (elapsed <= cooldown) {
- boolean isWarnEnabled = getConfig().getBoolean("spam-prevention.warn-player", true);
- if (isWarnEnabled) {
- double remainingTime = (cooldown - elapsed) / 1000.0;
- player.sendMessage(translate(String.format("&cPlease wait %.2f sec. before sending another message.", remainingTime)));
- }
-
- return true;
- }
- }
-
- playerMessageTimestamps.put(player.getName(), timestamp);
- return false;
- }
-}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/listener/block/SignChangeListener.java b/src/main/java/io/github/aleksandarharalanov/chatguard/listener/block/SignChangeListener.java
new file mode 100644
index 0000000..2228466
--- /dev/null
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/listener/block/SignChangeListener.java
@@ -0,0 +1,32 @@
+package io.github.aleksandarharalanov.chatguard.listener.block;
+
+import io.github.aleksandarharalanov.chatguard.ChatGuard;
+import io.github.aleksandarharalanov.chatguard.core.security.filter.ContentFilter;
+import io.github.aleksandarharalanov.chatguard.util.auth.AccessUtil;
+import org.bukkit.entity.Player;
+import org.bukkit.event.block.BlockListener;
+import org.bukkit.event.block.SignChangeEvent;
+
+public class SignChangeListener extends BlockListener {
+
+ @Override
+ public void onSignChange(SignChangeEvent event) {
+ Player player = event.getPlayer();
+
+ if (hasBypassPermission(player)) return;
+ if (handleSignFiltering(player, event)) return;
+ }
+
+ private static boolean hasBypassPermission(Player player) {
+ return AccessUtil.senderHasPermission(player, "chatguard.bypass");
+ }
+
+ private static boolean handleSignFiltering(Player player, SignChangeEvent event) {
+ boolean isSignFilterEnabled = ChatGuard.getConfig().getBoolean("filter.enabled.sign", true);
+ if (isSignFilterEnabled && ContentFilter.isSignContentBlocked(player, event.getLines())) {
+ event.setCancelled(true);
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/listener/player/PlayerChatListener.java b/src/main/java/io/github/aleksandarharalanov/chatguard/listener/player/PlayerChatListener.java
index 4af0e3f..6d7616e 100644
--- a/src/main/java/io/github/aleksandarharalanov/chatguard/listener/player/PlayerChatListener.java
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/listener/player/PlayerChatListener.java
@@ -2,90 +2,83 @@
import com.earth2me.essentials.Essentials;
import com.earth2me.essentials.User;
+import io.github.aleksandarharalanov.chatguard.ChatGuard;
+import io.github.aleksandarharalanov.chatguard.core.misc.TimeFormatter;
+import io.github.aleksandarharalanov.chatguard.core.security.captcha.CaptchaDetector;
+import io.github.aleksandarharalanov.chatguard.core.security.captcha.CaptchaHandler;
+import io.github.aleksandarharalanov.chatguard.core.security.filter.ContentFilter;
+import io.github.aleksandarharalanov.chatguard.core.security.spam.ChatRateLimiter;
+import io.github.aleksandarharalanov.chatguard.util.auth.AccessUtil;
+import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.player.PlayerChatEvent;
import org.bukkit.event.player.PlayerListener;
-import java.util.Arrays;
-
-import static io.github.aleksandarharalanov.chatguard.ChatGuard.getConfig;
-import static io.github.aleksandarharalanov.chatguard.handler.CaptchaHandler.checkPlayerCaptcha;
-import static io.github.aleksandarharalanov.chatguard.handler.CaptchaHandler.isPlayerCaptchaActive;
-import static io.github.aleksandarharalanov.chatguard.handler.FilterHandler.checkPlayerMessage;
-import static io.github.aleksandarharalanov.chatguard.handler.spam.MessageSpamHandler.isPlayerMessageSpamming;
-import static io.github.aleksandarharalanov.chatguard.util.AccessUtil.hasPermission;
-import static io.github.aleksandarharalanov.chatguard.util.ColorUtil.translate;
-import static io.github.aleksandarharalanov.chatguard.util.LoggerUtil.logWarning;
-import static org.bukkit.Bukkit.getServer;
-
public class PlayerChatListener extends PlayerListener {
@Override
- public void onPlayerChat(final PlayerChatEvent event) {
+ public void onPlayerChat(PlayerChatEvent event) {
Player player = event.getPlayer();
- Essentials essentials = (Essentials) getServer().getPluginManager().getPlugin("Essentials");
+ if (isPlayerEssentialsMuted(player, event)) return;
+ if (hasBypassPermission(player)) return;
+ if (handleActiveCaptchaVerification(player, event)) return;
+ if (handleSpamPrevention(player, event)) return;
+ if (handleChatFiltering(player, event)) return;
+ if (handleCaptchaTriggerCheck(player, event)) return;
+ }
+
+ private static boolean isPlayerEssentialsMuted(Player player, PlayerChatEvent event) {
+ Essentials essentials = (Essentials) Bukkit.getServer().getPluginManager().getPlugin("Essentials");
if (essentials != null && essentials.isEnabled()) {
User user = essentials.getUser(player.getName());
if (user.isMuted()) {
+ TimeFormatter.printFormattedMuteDuration(user);
event.setCancelled(true);
-
- long remainingMillis = user.getMuteTimeout() - System.currentTimeMillis();
- int seconds = (int) ((remainingMillis / 1000) % 60);
- int minutes = (int) ((remainingMillis / (1000 * 60)) % 60);
- int hours = (int) ((remainingMillis / (1000 * 60 * 60)) % 24);
- int days = (int) (remainingMillis / (1000 * 60 * 60 * 24));
-
- StringBuilder timeMessage = new StringBuilder("&7");
- if (days > 0) timeMessage.append(days).append(" day(s)");
- if (hours > 0) {
- if (timeMessage.length() > 2) timeMessage.append(", ");
- timeMessage.append(hours).append(" hour(s)");
- }
- if (minutes > 0) {
- if (timeMessage.length() > 2) timeMessage.append(", ");
- timeMessage.append(minutes).append(" minute(s)");
- }
- if (seconds > 0) {
- if (timeMessage.length() > 2) timeMessage.append(", ");
- timeMessage.append(seconds).append(" second(s)");
- }
-
- player.sendMessage(translate(timeMessage.toString()));
- return;
+ return true;
}
}
+ return false;
+ }
- if (hasPermission(player, "chatguard.bypass")) return;
+ private static boolean hasBypassPermission(Player player) {
+ return AccessUtil.senderHasPermission(player, "chatguard.bypass");
+ }
- boolean isCaptchaEnabled = getConfig().getBoolean("captcha.enabled", true);
- if (isCaptchaEnabled)
- if (isPlayerCaptchaActive(player)) {
- event.setCancelled(true);
- return;
- }
+ private static boolean handleActiveCaptchaVerification(Player player, PlayerChatEvent event) {
+ boolean isCaptchaEnabled = ChatGuard.getConfig().getBoolean("captcha.enabled", true);
+ if (isCaptchaEnabled && CaptchaHandler.doesPlayerHaveActiveCaptcha(player)) {
+ event.setCancelled(true);
+ return true;
+ }
+ return false;
+ }
- boolean isMessageSpamPreventionEnabled = getConfig().getBoolean("spam-prevention.enabled.message", true);
- if (isMessageSpamPreventionEnabled)
- if (isPlayerMessageSpamming(player)) {
- event.setCancelled(true);
- return;
- }
+ private static boolean handleSpamPrevention(Player player, PlayerChatEvent event) {
+ boolean isChatSpamPreventionEnabled = ChatGuard.getConfig().getBoolean("spam-prevention.enabled.chat", true);
+ if (isChatSpamPreventionEnabled && ChatRateLimiter.isPlayerChatSpamming(player)) {
+ event.setCancelled(true);
+ return true;
+ }
+ return false;
+ }
- boolean isFilterEnabled = getConfig().getBoolean("filter.enabled", true);
- if (isFilterEnabled) {
- try {
- if (checkPlayerMessage(player, event.getMessage())) {
- event.setCancelled(true);
- return;
- }
- } catch (Exception e) {
- logWarning(Arrays.toString(e.getStackTrace()));
- }
+ private static boolean handleChatFiltering(Player player, PlayerChatEvent event) {
+ boolean isChatFilterEnabled = ChatGuard.getConfig().getBoolean("filter.enabled.chat", true);
+ if (isChatFilterEnabled && ContentFilter.isChatContentBlocked(player, event.getMessage())) {
+ event.setCancelled(true);
+ return true;
}
+ return false;
+ }
- if (isCaptchaEnabled)
- if (checkPlayerCaptcha(player, event.getMessage()))
- event.setCancelled(true);
+ private static boolean handleCaptchaTriggerCheck(Player player, PlayerChatEvent event) {
+ boolean isCaptchaEnabled = ChatGuard.getConfig().getBoolean("captcha.enabled", true);
+ if (isCaptchaEnabled && CaptchaDetector.doesPlayerTriggerCaptcha(player.getName(), event.getMessage())) {
+ CaptchaHandler.processCaptchaTrigger(player, event.getMessage());
+ event.setCancelled(true);
+ return true;
+ }
+ return false;
}
}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/listener/player/PlayerCommandPreprocessListener.java b/src/main/java/io/github/aleksandarharalanov/chatguard/listener/player/PlayerCommandPreprocessListener.java
index 0bb61a4..47156e0 100644
--- a/src/main/java/io/github/aleksandarharalanov/chatguard/listener/player/PlayerCommandPreprocessListener.java
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/listener/player/PlayerCommandPreprocessListener.java
@@ -1,24 +1,32 @@
package io.github.aleksandarharalanov.chatguard.listener.player;
+import io.github.aleksandarharalanov.chatguard.ChatGuard;
+import io.github.aleksandarharalanov.chatguard.core.security.spam.CommandRateLimiter;
+import io.github.aleksandarharalanov.chatguard.util.auth.AccessUtil;
import org.bukkit.entity.Player;
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
import org.bukkit.event.player.PlayerListener;
-import static io.github.aleksandarharalanov.chatguard.ChatGuard.getConfig;
-import static io.github.aleksandarharalanov.chatguard.handler.spam.CommandSpamHandler.isPlayerCommandSpamming;
-import static io.github.aleksandarharalanov.chatguard.util.AccessUtil.hasPermission;
-
public class PlayerCommandPreprocessListener extends PlayerListener {
@Override
public void onPlayerCommandPreprocess(PlayerCommandPreprocessEvent event) {
Player player = event.getPlayer();
- if (hasPermission(player, "chatguard.bypass")) return;
+ if (hasBypassPermission(player)) return;
+ if (handleSpamPrevention(player, event)) return;
+ }
+
+ private static boolean hasBypassPermission(Player player) {
+ return AccessUtil.senderHasPermission(player, "chatguard.bypass");
+ }
- boolean isCommandSpamPreventionEnabled = getConfig().getBoolean("spam-prevention.enabled.command", true);
- if (isCommandSpamPreventionEnabled)
- if (isPlayerCommandSpamming(player))
- event.setCancelled(true);
+ private static boolean handleSpamPrevention(Player player, PlayerCommandPreprocessEvent event) {
+ boolean isCommandSpamPreventionEnabled = ChatGuard.getConfig().getBoolean("spam-prevention.enabled.command", true);
+ if (isCommandSpamPreventionEnabled && CommandRateLimiter.isPlayerCommandSpamming(player)) {
+ event.setCancelled(true);
+ return true;
+ }
+ return false;
}
}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/listener/player/PlayerJoinListener.java b/src/main/java/io/github/aleksandarharalanov/chatguard/listener/player/PlayerJoinListener.java
index 30d7e51..1a8ee53 100644
--- a/src/main/java/io/github/aleksandarharalanov/chatguard/listener/player/PlayerJoinListener.java
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/listener/player/PlayerJoinListener.java
@@ -1,39 +1,26 @@
package io.github.aleksandarharalanov.chatguard.listener.player;
+import io.github.aleksandarharalanov.chatguard.ChatGuard;
+import io.github.aleksandarharalanov.chatguard.core.security.filter.ContentFilter;
+import io.github.aleksandarharalanov.chatguard.core.data.PenaltyData;
+import io.github.aleksandarharalanov.chatguard.util.misc.ColorUtil;
import org.bukkit.entity.Player;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerListener;
-import org.bukkit.event.player.PlayerPreLoginEvent;
-
-import static io.github.aleksandarharalanov.chatguard.ChatGuard.getStrikes;
-import static io.github.aleksandarharalanov.chatguard.handler.FilterHandler.shouldBlockUsername;
-import static io.github.aleksandarharalanov.chatguard.util.ColorUtil.translate;
-import static io.github.aleksandarharalanov.chatguard.util.LoggerUtil.logWarning;
public class PlayerJoinListener extends PlayerListener {
@Override
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
- if (shouldBlockUsername(player.getName())) {
- player.kickPlayer(translate("&cKICKED: Username contains blocked words."));
- logWarning(String.format("[ChatGuard] Kicked player '%s' for bad username.", player.getName()));
- return;
- }
- if (getStrikes().getInt(player.getName(), -1) == -1) {
- getStrikes().setProperty(player.getName(), 0);
- getStrikes().save();
+ // Fallback if PlayerLoginEvent fails
+ boolean isNameFilterEnabled = ChatGuard.getConfig().getBoolean("filter.enabled.name", true);
+ if (isNameFilterEnabled && ContentFilter.isPlayerNameBlocked(player)) {
+ player.kickPlayer(ColorUtil.translateColorCodes("&cName contains bad words."));
+ return;
}
- }
- @Override
- public void onPlayerPreLogin(PlayerPreLoginEvent event) {
- String playerName = event.getName();
- if (shouldBlockUsername(playerName))
- event.disallow(
- PlayerPreLoginEvent.Result.KICK_BANNED,
- translate("&cKICKED: Username contains blocked words.")
- );
+ PenaltyData.setDefaultStrikeTier(player);
}
}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/listener/player/PlayerLoginListener.java b/src/main/java/io/github/aleksandarharalanov/chatguard/listener/player/PlayerLoginListener.java
new file mode 100644
index 0000000..bc47389
--- /dev/null
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/listener/player/PlayerLoginListener.java
@@ -0,0 +1,26 @@
+package io.github.aleksandarharalanov.chatguard.listener.player;
+
+import io.github.aleksandarharalanov.chatguard.core.security.filter.ContentFilter;
+import io.github.aleksandarharalanov.chatguard.core.data.LoginData;
+import io.github.aleksandarharalanov.chatguard.util.misc.ColorUtil;
+import org.bukkit.entity.Player;
+import org.bukkit.event.player.PlayerListener;
+import org.bukkit.event.player.PlayerLoginEvent;
+
+public class PlayerLoginListener extends PlayerListener {
+
+ @Override
+ public void onPlayerLogin(PlayerLoginEvent event) {
+ Player player = event.getPlayer();
+ if (event.getResult() == PlayerLoginEvent.Result.ALLOWED) {
+ LoginData.storePlayerIP(player.getName(), event.getKickMessage());
+ }
+
+ if (ContentFilter.isPlayerNameBlocked(player)) {
+ event.disallow(
+ PlayerLoginEvent.Result.KICK_OTHER,
+ ColorUtil.translateColorCodes("&cName contains bad words.")
+ );
+ }
+ }
+}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/listener/player/PlayerQuitListener.java b/src/main/java/io/github/aleksandarharalanov/chatguard/listener/player/PlayerQuitListener.java
index d618589..54e2918 100644
--- a/src/main/java/io/github/aleksandarharalanov/chatguard/listener/player/PlayerQuitListener.java
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/listener/player/PlayerQuitListener.java
@@ -1,15 +1,14 @@
package io.github.aleksandarharalanov.chatguard.listener.player;
+import io.github.aleksandarharalanov.chatguard.core.data.CaptchaData;
import org.bukkit.event.player.PlayerListener;
import org.bukkit.event.player.PlayerQuitEvent;
-import static io.github.aleksandarharalanov.chatguard.handler.CaptchaHandler.getPlayerMessages;
-
public class PlayerQuitListener extends PlayerListener {
@Override
public void onPlayerQuit(PlayerQuitEvent event) {
String playerName = event.getPlayer().getName();
- getPlayerMessages().remove(playerName);
+ CaptchaData.getPlayerMessages().remove(playerName);
}
}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/util/AccessUtil.java b/src/main/java/io/github/aleksandarharalanov/chatguard/util/AccessUtil.java
deleted file mode 100644
index 5c4c5ac..0000000
--- a/src/main/java/io/github/aleksandarharalanov/chatguard/util/AccessUtil.java
+++ /dev/null
@@ -1,77 +0,0 @@
-package io.github.aleksandarharalanov.chatguard.util;
-
-import org.bukkit.command.CommandSender;
-import org.bukkit.entity.Player;
-
-import java.util.logging.Logger;
-
-import static org.bukkit.Bukkit.getServer;
-import static io.github.aleksandarharalanov.chatguard.util.ColorUtil.translate;
-
-/**
- * Utility class for handling access control and command restrictions.
- *
- * This class provides methods to check permissions and enforce command usage restrictions based on the
- * sender's type (player or console).
- */
-public class AccessUtil {
-
- private static final Logger logger = getServer().getLogger();
-
- /**
- * Checks if the sender has the specified permission.
- *
- * If the sender is not a player (e.g., console), the method returns {@code true} by default.
- * If the sender does not have the required permission and is not an operator, a message is sent to the sender.
- *
- * @param sender the entity executing the command, can be a player or console
- * @param permission the permission node to check
- * @param message the message to send if the sender lacks the required permission
- * @return {@code true} if the sender has the permission or is an operator; {@code false} otherwise
- */
- public static boolean hasPermission(CommandSender sender, String permission, String message) {
- if (!(sender instanceof Player)) return true;
-
- boolean hasPermission = sender.hasPermission(permission);
- boolean isOp = sender.isOp();
- if (!(hasPermission || isOp)) {
- sender.sendMessage(translate(String.format("&c%s", message)));
- return false;
- } else return true;
- }
-
- /**
- * Checks if the sender has the specified permission.
- *
- * If the sender is not a player (e.g., console), the method returns {@code true} by default.
- * This method does not send any messages if the sender lacks permission.
- *
- * @param sender the entity executing the command, can be a player or console
- * @param permission the permission node to check
- * @return {@code true} if the sender has the permission or is an operator; {@code false} otherwise
- */
- public static boolean hasPermission(CommandSender sender, String permission) {
- if (!(sender instanceof Player)) return true;
-
- boolean hasPermission = sender.hasPermission(permission);
- boolean isOp = sender.isOp();
- return hasPermission || isOp;
- }
-
- /**
- * Ensures that the command can only be executed in-game by a player.
- *
- * If the sender is not a player (e.g., console), a message is logged to the console indicating that the command
- * must be executed in-game. This method is typically used to prevent console execution of player-only commands.
- *
- * @param sender the entity executing the command, typically the player or console
- * @return {@code true} if the sender is not a player, indicating that the command was blocked;
- * {@code false} if the sender is a player, allowing the command to proceed
- */
- public static boolean commandInGameOnly(CommandSender sender) {
- if (!(sender instanceof Player)) {
- logger.info("You must be in-game to run this command.");
- return true;
- } else return false;
- }
-}
\ No newline at end of file
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/util/ColorUtil.java b/src/main/java/io/github/aleksandarharalanov/chatguard/util/ColorUtil.java
deleted file mode 100644
index fe0cf1b..0000000
--- a/src/main/java/io/github/aleksandarharalanov/chatguard/util/ColorUtil.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package io.github.aleksandarharalanov.chatguard.util;
-
-/**
- * Utility class for translating color codes in text to Minecraft's color code format.
- *
- * This class provides a method to scan text for color codes prefixed with an ampersand ({@code &}) and replace them with
- * the appropriate Minecraft color code format using the section sign ({@code §}) symbol.
- */
-public class ColorUtil {
-
- /**
- * Translates color codes in the given text to Minecraft's color code format.
- *
- * This method scans the input text for the ampersand character ({@code &}) followed by a valid color code character
- * (0-9, a-f, A-F) and replaces the ampersand with the section sign ({@code §}). The following character is converted
- * to lowercase to ensure proper formatting for Minecraft color codes.
- *
- * Example: A string like {@code "&aHello"} will be converted to {@code "§aHello"}, where {@code §a} is the
- * color code for light green in Minecraft.
- *
- * @param text the input text containing color codes to be translated
- *
- * @return the translated text with Minecraft color codes, or the original text if no color codes are found
- */
- public static String translate(String text) {
- char[] translation = text.toCharArray();
- for (int i = 0; i < translation.length - 1; ++i)
- if (translation[i] == '&' && "0123456789AaBbCcDdEeFf".indexOf(translation[i + 1]) > -1) {
- translation[i] = 167;
- translation[i + 1] = Character.toLowerCase(translation[i + 1]);
- }
- return new String(translation);
- }
-}
\ No newline at end of file
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/util/auth/AccessUtil.java b/src/main/java/io/github/aleksandarharalanov/chatguard/util/auth/AccessUtil.java
new file mode 100644
index 0000000..47ee38d
--- /dev/null
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/util/auth/AccessUtil.java
@@ -0,0 +1,123 @@
+package io.github.aleksandarharalanov.chatguard.util.auth;
+
+import io.github.aleksandarharalanov.chatguard.util.misc.ColorUtil;
+import org.bukkit.Bukkit;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+import org.bukkit.plugin.java.JavaPlugin;
+
+import java.util.logging.Logger;
+
+/**
+ * Utility class for handling access control for commands.
+ *
+ * Provides methods to check permissions and enforce command usage restrictions based on the sender's type.
+ *
+ * @see Aleksandar's GitHub
+ *
+ * @author Aleksandar Haralanov (@AleksandarHaralanov)
+ */
+public final class AccessUtil {
+
+ private static final Logger logger = Bukkit.getServer().getLogger();
+
+ private AccessUtil() {}
+
+ /**
+ * Checks if the sender has the specified permission.
+ *
+ * If the sender is the console, the method returns {@code true} by default.
+ * If the sender does not have the required permission and is not an operator, a content is sent to the sender.
+ *
+ * @param sender the entity executing the command, can be a player or console
+ * @param permission the permission node to check
+ * @param noPermissionMessage the content to send if the sender lacks the required permission
+ *
+ * @return {@code true} if the sender has the permission or is an operator; {@code false} otherwise
+ */
+ public static boolean senderHasPermission(CommandSender sender, String permission, String noPermissionMessage) {
+ if (!(sender instanceof Player)) {
+ return true;
+ }
+
+ boolean hasPermission = sender.hasPermission(permission);
+ boolean isOp = sender.isOp();
+ if (!(hasPermission || isOp)) {
+ sender.sendMessage(ColorUtil.translateColorCodes(String.format(
+ "&c%s", noPermissionMessage
+ )));
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Checks if the sender has the specified permission.
+ *
+ * If the sender is the console, the method returns {@code true} by default.
+ * This method does not send any messages if the sender lacks permission.
+ *
+ * @param sender the entity executing the command, can be a player or console
+ * @param permission the permission node to check
+ *
+ * @return {@code true} if the sender has the permission or is an operator; {@code false} otherwise
+ */
+ public static boolean senderHasPermission(CommandSender sender, String permission) {
+ if (!(sender instanceof Player)) {
+ return true;
+ }
+
+ boolean hasPermission = sender.hasPermission(permission);
+ boolean isOp = sender.isOp();
+ return hasPermission || isOp;
+ }
+
+ /**
+ * Ensures that the command can only be executed in-game by a player.
+ *
+ * If the sender is the console, a content is logged indicating that the command must be executed in-game.
+ * This method is typically used to prevent console execution of player-only commands.
+ *
+ * @param sender the entity executing the command, typically the player or console
+ * @param plugin the plugin instance to display the plugin name
+ *
+ * @return {@code true} if the sender is not a player, indicating that the command was blocked;
+ * {@code false} if the sender is a player, allowing the command to proceed
+ */
+ public static boolean denyIfNotPlayer(CommandSender sender, JavaPlugin plugin) {
+ if (!(sender instanceof Player)) {
+ logger.info(String.format(
+ "[%s] You must be in-game to run this command.",
+ plugin.getDescription().getName()
+ ));
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Ensures that the command can only be executed through the console.
+ *
+ * If the sender is the player, a content is sent to them indicating that the command can't be executed in-game.
+ * This method is typically used to prevent player execution of console-only commands.
+ *
+ * @param sender the entity executing the command, typically the player or console
+ * @param plugin the plugin instance to display the plugin name
+ *
+ * @return {@code true} if the sender is not the console, indicating that the command was blocked;
+ * {@code false} if the sender is the console, allowing the command to proceed
+ */
+ public static boolean denyIfNotConsole(CommandSender sender, JavaPlugin plugin) {
+ if (sender instanceof Player) {
+ sender.sendMessage(ColorUtil.translateColorCodes(String.format(
+ "&c[%s] You can't run this command in-game.",
+ plugin.getDescription().getName()
+ )));
+ return true;
+ }
+
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/util/ConfigUtil.java b/src/main/java/io/github/aleksandarharalanov/chatguard/util/config/ConfigUtil.java
similarity index 70%
rename from src/main/java/io/github/aleksandarharalanov/chatguard/util/ConfigUtil.java
rename to src/main/java/io/github/aleksandarharalanov/chatguard/util/config/ConfigUtil.java
index e7d1171..b006a2a 100644
--- a/src/main/java/io/github/aleksandarharalanov/chatguard/util/ConfigUtil.java
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/util/config/ConfigUtil.java
@@ -1,5 +1,6 @@
-package io.github.aleksandarharalanov.chatguard.util;
+package io.github.aleksandarharalanov.chatguard.util.config;
+import org.bukkit.Bukkit;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.util.config.Configuration;
@@ -9,22 +10,22 @@
import java.nio.file.Files;
import java.util.logging.Logger;
-import static org.bukkit.Bukkit.getServer;
-
/**
* Utility class for managing plugin configuration files.
*
- * This class extends {@link Configuration} to provide custom methods for loading, saving, and managing
- * configuration files. It automatically handles the creation of parent directories and copies default configuration
- * files from the plugin's resources if they do not exist.
- *
- * Note: This class allows for flexible management of multiple configuration files, specified by their file name.
+ * Extends {@link Configuration} to provide custom methods for loading, saving, and managing configuration
+ * files. It automatically handles the creation of parent directories and copies default configuration files from the
+ * plugin's resources if they do not exist.
+ *
+ * @see Aleksandar's GitHub
+ *
+ * @author Aleksandar Haralanov (@AleksandarHaralanov)
*/
-public class ConfigUtil extends Configuration {
+public final class ConfigUtil extends Configuration {
+ private static final Logger logger = Bukkit.getServer().getLogger();
private final File configFile;
private final String pluginName;
- private static final Logger logger = getServer().getLogger();
/**
* Constructs a new instance of {@code ConfigUtil}.
@@ -56,12 +57,14 @@ public ConfigUtil(JavaPlugin plugin, String fileName) {
public void load() {
createParentDirectories();
- if (!configFile.exists()) copyDefaultConfig();
+ if (!configFile.exists()) {
+ copyDefaultConfig();
+ }
try {
super.load();
} catch (Exception e) {
- logger.severe(String.format("[%s] Failed to load config '%s': %s", pluginName, configFile.getName(), e.getMessage()));
+ logger.severe(String.format("[%s] Failed to load configuration '%s': %s", pluginName, configFile.getName(), e.getMessage()));
}
}
@@ -75,7 +78,7 @@ private void createParentDirectories() {
try {
Files.createDirectories(configFile.getParentFile().toPath());
} catch (IOException e) {
- logger.severe(String.format("[%s] Failed to create default config directory: %s", pluginName, e.getMessage()));
+ logger.severe(String.format("[%s] Failed to create default configuration directory: %s", pluginName, e.getMessage()));
}
}
@@ -91,44 +94,44 @@ private void copyDefaultConfig() {
try (InputStream input = getClass().getResourceAsStream(resourcePath)) {
if (input == null) {
- logger.severe(String.format("[%s] Default config '%s' wasn't found.", pluginName, configFile.getName()));
+ logger.severe(String.format("[%s] Default configuration '%s' wasn't found.", pluginName, configFile.getName()));
return;
}
Files.copy(input, configFile.toPath());
- logger.info(String.format("[%s] Default config '%s' created successfully.", pluginName, configFile.getName()));
+ logger.info(String.format("[%s] Default configuration '%s' created successfully.", pluginName, configFile.getName()));
} catch (IOException e) {
- logger.severe(String.format("[%s] Failed to create default config '%s': %s", pluginName, configFile.getName(), e.getMessage()));
+ logger.severe(String.format("[%s] Failed to create default configuration '%s': %s", pluginName, configFile.getName(), e.getMessage()));
}
}
/**
* Loads the configuration file and logs the result.
*
- * Calls {@link #load()} to load the configuration file and logs a message indicating whether the configuration
+ * Calls {@link #load()} to load the configuration file and logs a content indicating whether the configuration
* was loaded successfully.
*/
- public void loadConfig() {
+ public void loadAndLog() {
try {
this.load();
- logger.info(String.format("[%s] Config '%s' loaded successfully.", pluginName, configFile.getName()));
+ logger.info(String.format("[%s] Configuration '%s' loaded successfully.", pluginName, configFile.getName()));
} catch (Exception e) {
- logger.severe(String.format("[%s] Failed to load config '%s': %s", pluginName, configFile.getName(), e.getMessage()));
+ logger.severe(String.format("[%s] Failed to load configuration '%s': %s", pluginName, configFile.getName(), e.getMessage()));
}
}
/**
* Saves the configuration file and logs the result.
*
- * Attempts to save the configuration using the superclass' {@code save()} method and logs a message indicating
+ * Attempts to save the configuration using the superclass' {@code save()} method and logs a content indicating
* whether the configuration was saved successfully.
*/
- public void saveConfig() {
+ public void saveAndLog() {
try {
this.save();
- logger.info(String.format("[%s] Config '%s' saved successfully.", pluginName, configFile.getName()));
+ logger.info(String.format("[%s] Configuration '%s' saved successfully.", pluginName, configFile.getName()));
} catch (Exception e) {
- logger.severe(String.format("[%s] Failed to save config '%s': %s", pluginName, configFile.getName(), e.getMessage()));
+ logger.severe(String.format("[%s] Failed to save configuration '%s': %s", pluginName, configFile.getName(), e.getMessage()));
}
}
}
\ No newline at end of file
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/util/DiscordUtil.java b/src/main/java/io/github/aleksandarharalanov/chatguard/util/log/DiscordUtil.java
similarity index 87%
rename from src/main/java/io/github/aleksandarharalanov/chatguard/util/DiscordUtil.java
rename to src/main/java/io/github/aleksandarharalanov/chatguard/util/log/DiscordUtil.java
index 8b2fee5..d2c86c7 100644
--- a/src/main/java/io/github/aleksandarharalanov/chatguard/util/DiscordUtil.java
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/util/log/DiscordUtil.java
@@ -1,4 +1,6 @@
-package io.github.aleksandarharalanov.chatguard.util;
+package io.github.aleksandarharalanov.chatguard.util.log;
+
+import org.bukkit.Bukkit;
import javax.net.ssl.HttpsURLConnection;
import java.awt.Color;
@@ -13,18 +15,25 @@
import java.util.Set;
import java.util.logging.Logger;
-import static org.bukkit.Bukkit.getServer;
-
-// Thanks to k3kdude @ https://gist.github.com/k3kdude/fba6f6b37594eae3d6f9475330733bdb
-public class DiscordUtil {
-
+/**
+ * Utility class for sending messages and embeds to a Discord webhook.
+ *
+ * Provides methods for setting up a Discord webhook content with content, username, avatar, text-to-speech (TTS)
+ * options, and rich embed objects.
+ *
+ * @see Ron's DiscordWebhook GitHub Gist
+ *
+ * @author Ron (@k3kdude)
+ */
+public final class DiscordUtil {
+
+ private static final Logger logger = Bukkit.getServer().getLogger();
private final String url;
private String content;
private String username;
private String avatarUrl;
private boolean tts;
private final List embeds = new ArrayList<>();
- private static final Logger logger = getServer().getLogger();
public DiscordUtil(String url) {
this.url = url;
@@ -51,7 +60,9 @@ public void addEmbed(EmbedObject embed) {
}
public void execute() throws IOException {
- if (this.content == null && this.embeds.isEmpty()) logger.warning("Set content or add at least one EmbedObject!");
+ if (this.content == null && this.embeds.isEmpty()) {
+ logger.warning("Set content or add at least one EmbedObject.");
+ }
JSONObject json = new JSONObject();
@@ -62,10 +73,8 @@ public void execute() throws IOException {
if (!this.embeds.isEmpty()) {
List embedObjects = new ArrayList<>();
-
for (EmbedObject embed : this.embeds) {
JSONObject jsonEmbed = new JSONObject();
-
jsonEmbed.put("title", embed.getTitle());
jsonEmbed.put("description", embed.getDescription());
jsonEmbed.put("url", embed.getUrl());
@@ -87,7 +96,6 @@ public void execute() throws IOException {
if (footer != null) {
JSONObject jsonFooter = new JSONObject();
-
jsonFooter.put("text", footer.getText());
jsonFooter.put("icon_url", footer.getIconUrl());
jsonEmbed.put("footer", jsonFooter);
@@ -95,21 +103,18 @@ public void execute() throws IOException {
if (image != null) {
JSONObject jsonImage = new JSONObject();
-
jsonImage.put("url", image.getUrl());
jsonEmbed.put("image", jsonImage);
}
if (thumbnail != null) {
JSONObject jsonThumbnail = new JSONObject();
-
jsonThumbnail.put("url", thumbnail.getUrl());
jsonEmbed.put("thumbnail", jsonThumbnail);
}
if (author != null) {
JSONObject jsonAuthor = new JSONObject();
-
jsonAuthor.put("name", author.getName());
jsonAuthor.put("url", author.getUrl());
jsonAuthor.put("icon_url", author.getIconUrl());
@@ -119,15 +124,13 @@ public void execute() throws IOException {
List jsonFields = new ArrayList<>();
for (EmbedObject.Field field : fields) {
JSONObject jsonField = new JSONObject();
-
jsonField.put("name", field.getName());
jsonField.put("value", field.getValue());
jsonField.put("inline", field.isInline());
-
jsonFields.add(jsonField);
}
-
jsonEmbed.put("fields", jsonFields.toArray());
+
embedObjects.add(jsonEmbed);
}
@@ -151,11 +154,11 @@ public void execute() throws IOException {
}
public static class EmbedObject {
+
private String title;
private String description;
private String url;
private Color color;
-
private Footer footer;
private Thumbnail thumbnail;
private Image image;
@@ -244,6 +247,7 @@ public EmbedObject addField(String name, String value, boolean inline) {
}
private static class Footer {
+
private final String text;
private final String iconUrl;
@@ -262,6 +266,7 @@ private String getIconUrl() {
}
private static class Thumbnail {
+
private final String url;
private Thumbnail(String url) {
@@ -274,6 +279,7 @@ private String getUrl() {
}
private static class Image {
+
private final String url;
private Image(String url) {
@@ -286,6 +292,7 @@ private String getUrl() {
}
private static class Author {
+
private final String name;
private final String url;
private final String iconUrl;
@@ -310,6 +317,7 @@ private String getIconUrl() {
}
private static class Field {
+
private final String name;
private final String value;
private final boolean inline;
@@ -339,7 +347,9 @@ private static class JSONObject {
private final HashMap map = new HashMap<>();
void put(String key, Object value) {
- if (value != null) map.put(key, value);
+ if (value != null) {
+ map.put(key, value);
+ }
}
@Override
@@ -352,15 +362,20 @@ public String toString() {
for (Map.Entry entry : entrySet) {
Object val = entry.getValue();
builder.append(quote(entry.getKey())).append(":");
-
- if (val instanceof String) builder.append(quote(String.valueOf(val)));
- else if (val instanceof Integer) builder.append(Integer.valueOf(String.valueOf(val)));
- else if (val instanceof Boolean) builder.append(val);
- else if (val instanceof JSONObject) builder.append(val);
- else if (val.getClass().isArray()) {
+ if (val instanceof String) {
+ builder.append(quote(String.valueOf(val)));
+ } else if (val instanceof Integer) {
+ builder.append(Integer.valueOf(String.valueOf(val)));
+ } else if (val instanceof Boolean) {
+ builder.append(val);
+ } else if (val instanceof JSONObject) {
+ builder.append(val);
+ } else if (val.getClass().isArray()) {
builder.append("[");
int len = Array.getLength(val);
- for (int j = 0; j < len; j++) builder.append(Array.get(val, j).toString()).append(j != len - 1 ? "," : "");
+ for (int j = 0; j < len; j++) {
+ builder.append(Array.get(val, j).toString()).append(j != len - 1 ? "," : "");
+ }
builder.append("]");
}
@@ -374,4 +389,4 @@ private String quote(String string) {
return "\"" + string + "\"";
}
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/util/LoggerUtil.java b/src/main/java/io/github/aleksandarharalanov/chatguard/util/log/LogUtil.java
similarity index 55%
rename from src/main/java/io/github/aleksandarharalanov/chatguard/util/LoggerUtil.java
rename to src/main/java/io/github/aleksandarharalanov/chatguard/util/log/LogUtil.java
index 5ef8c36..b6b6983 100644
--- a/src/main/java/io/github/aleksandarharalanov/chatguard/util/LoggerUtil.java
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/util/log/LogUtil.java
@@ -1,7 +1,7 @@
-package io.github.aleksandarharalanov.chatguard.util;
+package io.github.aleksandarharalanov.chatguard.util.log;
+import org.bukkit.Bukkit;
import org.bukkit.plugin.java.JavaPlugin;
-import org.bukkit.util.config.Configuration;
import java.io.BufferedWriter;
import java.io.File;
@@ -11,76 +11,75 @@
import java.time.format.DateTimeFormatter;
import java.util.logging.Logger;
-import static org.bukkit.Bukkit.getServer;
-
/**
* Utility class for logging messages to the server console and managing a log file.
*
- * This class provides static methods for logging informational, warning, and severe messages
- * to the server's logger, simplifying the process of logging by avoiding the need to directly
- * access the logger. Additionally, it manages a log file within the plugin's data folder where
- * custom log messages can be written.
- *
- * By using this utility, you can log messages with a single method call, making your code cleaner
- * and easier to maintain. You can also initialize and manage a log file for additional logging purposes.
+ * Provides methods for logging info, warning, and severe messages through the server's logger into the console,
+ * simplifying the process of logging by avoiding the need to directly access the logger.
+ * Additionally, it allows to manage log files within the plugin's data folder where custom log messages can be written.
+ *
+ * @see Aleksandar's GitHub
+ *
+ * @author Aleksandar Haralanov (@AleksandarHaralanov)
*/
-public class LoggerUtil extends Configuration {
+public final class LogUtil {
+ private static final Logger logger = Bukkit.getServer().getLogger();
private static File logFile;
private static String pluginName;
- private static final Logger logger = getServer().getLogger();
/**
* Constructs a LoggerUtil instance.
*
- * This constructor initializes the LoggerUtil with the plugin's data folder and a specified
- * log file name. It also retrieves the plugin's name from its description for use in log messages.
+ * This constructor initializes the LoggerUtil with the plugin's data folder and a specified log file name.
+ * It also retrieves the plugin's name from its description for use in log messages.
*
- * @param plugin the plugin instance, used to access its data folder and description
- * @param fileName the name of the log file to be used for logging messages
+ * @param plugin the plugin instance, used to access its data folder and description
+ * @param logFileName the name of the log file to be used for logging messages
*/
- public LoggerUtil(JavaPlugin plugin, String fileName) {
- super(new File(plugin.getDataFolder(), fileName));
- logFile = new File(plugin.getDataFolder(), fileName);
+ public LogUtil(JavaPlugin plugin, String logFileName) {
+ logFile = new File(plugin.getDataFolder(), logFileName);
pluginName = plugin.getDescription().getName();
}
/**
* Initializes the log file.
*
- * This method checks if the log file exists in the plugin's data folder. If it does not exist,
- * it attempts to create the file. If the file is successfully created, an informational log message
- * is generated. If the file creation fails, a severe log message is logged.
+ * This method checks if the log file exists in the plugin's data folder. If it does not exist, it attempts to
+ * create the file. If the file is successfully created, an informational log content is generated. If the file
+ * creation fails, a severe log content is logged.
*/
- public void initializeLog() {
- if (!logFile.exists())
+ public void initializeLogFile() {
+ if (!logFile.exists()) {
try {
if (logFile.createNewFile()) {
logger.info(String.format("[%s] Log '%s' created successfully.", pluginName, logFile.getName()));
}
} catch (IOException e) {
- logger.severe(String.format("[%s] Could not create log '%s': %s", pluginName, logFile.getName(), e.getMessage()));
+ logger.severe(String.format("[%s] Failed to create log '%s': %s", pluginName, logFile.getName(), e.getMessage()));
}
+ }
}
/**
* Writes text to the log file with an optional timestamp and a newline at the end.
*
* This method appends the provided text to the log file, optionally prepending the current date and time.
- * If the log file does not exist, ensure it has been initialized using {@link #initializeLog()} before calling this method.
+ * If the log file does not exist, ensure it has been initialized using {@link #initializeLogFile()} before calling this method.
*
* @param text the text to write to the log file
* @param logDateTime if {@code true}, prepends the current date and time to the log entry
*/
- public static void writeToLog(String text, boolean logDateTime) {
+ public static void writeToLogFile(String text, boolean logDateTime) {
String logEntry;
if (logDateTime) {
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String timestamp = now.format(formatter);
-
logEntry = String.format("[%s] %s", timestamp, text);
- } else logEntry = text;
+ } else {
+ logEntry = text;
+ }
try (BufferedWriter writer = new BufferedWriter(new FileWriter(logFile, true))) {
writer.write(logEntry);
@@ -91,36 +90,36 @@ public static void writeToLog(String text, boolean logDateTime) {
}
/**
- * Logs an informational message to the server console.
+ * Logs an informational content to the server console.
*
* Use this method to log general information that can help with understanding server or plugin behavior.
*
- * @param message the message to log
+ * @param message the content to log
*/
- public static void logInfo(String message) {
+ public static void logConsoleInfo(String message) {
logger.info(message);
}
/**
- * Logs a warning message to the server console.
+ * Logs a warning content to the server console.
*
* Use this method to log warnings that indicate potential issues or problems that may not cause immediate failures
* but should be addressed.
*
- * @param message the message to log
+ * @param message the content to log
*/
- public static void logWarning(String message) {
+ public static void logConsoleWarning(String message) {
logger.warning(message);
}
/**
- * Logs a severe message to the server console.
+ * Logs a severe content to the server console.
*
* Use this method to log critical issues that may prevent the server or plugin from functioning correctly.
*
- * @param message the message to log
+ * @param message the content to log
*/
- public static void logSevere(String message) {
+ public static void logConsoleSevere(String message) {
logger.severe(message);
}
}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/util/UpdateUtil.java b/src/main/java/io/github/aleksandarharalanov/chatguard/util/log/UpdateUtil.java
similarity index 60%
rename from src/main/java/io/github/aleksandarharalanov/chatguard/util/UpdateUtil.java
rename to src/main/java/io/github/aleksandarharalanov/chatguard/util/log/UpdateUtil.java
index 894f904..dde1109 100644
--- a/src/main/java/io/github/aleksandarharalanov/chatguard/util/UpdateUtil.java
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/util/log/UpdateUtil.java
@@ -1,5 +1,6 @@
-package io.github.aleksandarharalanov.chatguard.util;
+package io.github.aleksandarharalanov.chatguard.util.log;
+import org.bukkit.Bukkit;
import org.bukkit.plugin.PluginDescriptionFile;
import org.bukkit.plugin.java.JavaPlugin;
@@ -12,25 +13,29 @@
import java.net.URL;
import java.util.logging.Logger;
-import static org.bukkit.Bukkit.getServer;
-
/**
* Utility class for checking and comparing plugin versions with the latest release on GitHub.
*
- * This class queries the GitHub API for the latest release version and compares it with the current plugin version.
- * It logs messages indicating whether an update is available or if the plugin is up to date.
+ * Provides a method to query the GitHub API for the latest release version and compares it with the current plugin version.
+ * It logs messages to the console indicating whether an update is available or if the plugin is up to date.
+ *
+ * @see Aleksandar's GitHub
+ *
+ * @author Aleksandar Haralanov (@AleksandarHaralanov)
*/
-public class UpdateUtil {
+public final class UpdateUtil {
+
+ private static final Logger logger = Bukkit.getServer().getLogger();
- private static final Logger logger = getServer().getLogger();
+ private UpdateUtil() {}
/**
- * Checks for updates by querying a given GitHub API URL and comparing the current version with the latest
- * available version.
+ * Checks for updates by querying a given GitHub API URL and comparing the current version with the latest available
+ * version.
*
- * This method formats the current version by appending {@code v} to the front of it, as this is the convention
- * used in GitHub release tags. It then compares the formatted version with the latest version retrieved from
- * the GitHub API. If an update is available, it logs information about the new version and a download link.
+ * This method formats the current version by appending {@code v} to the front of it, as this is the convention used
+ * in GitHub release tags. It then compares the formatted version with the latest version retrieved from the GitHub
+ * API. If an update is available, it logs information about the new version and a download link.
*
* Warning: This method only works with GitHub repositories. Ensure that the GitHub API URL points to
* the latest release information of your repository.
@@ -39,7 +44,7 @@ public class UpdateUtil {
* @param githubApiUrl the GitHub API URL to query for the latest release information; should be in the format
* {@code https://api.github.com/repos/USER/REPO/releases/latest}
*/
- public static void checkForUpdates(JavaPlugin plugin, String githubApiUrl) {
+ public static void checkAvailablePluginUpdates(JavaPlugin plugin, String githubApiUrl) {
PluginDescriptionFile pdf = plugin.getDescription();
HttpURLConnection connection = null;
try {
@@ -57,7 +62,9 @@ public static void checkForUpdates(JavaPlugin plugin, String githubApiUrl) {
BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
StringBuilder content = new StringBuilder();
String inputLine;
- while ((inputLine = in.readLine()) != null) content.append(inputLine);
+ while ((inputLine = in.readLine()) != null) {
+ content.append(inputLine);
+ }
in.close();
String responseBody = content.toString();
@@ -65,9 +72,14 @@ public static void checkForUpdates(JavaPlugin plugin, String githubApiUrl) {
String formattedCurrentVersion = "v" + pdf.getVersion();
compareVersions(pdf.getName(), formattedCurrentVersion, latestVersion, githubApiUrl);
} catch (IOException | URISyntaxException e) {
- logger.severe(String.format("[%s] Exception occurred while checking for a new version: %s", pdf.getName(), e.getMessage()));
+ logger.severe(String.format(
+ "[%s] Exception occurred while checking for a new version: %s",
+ pdf.getName(), e.getMessage()
+ ));
} finally {
- if (connection != null) connection.disconnect();
+ if (connection != null) {
+ connection.disconnect();
+ }
}
}
@@ -80,11 +92,17 @@ public static void checkForUpdates(JavaPlugin plugin, String githubApiUrl) {
* @param responseCode the HTTP response code received from the GitHub API
*/
private static void handleResponseError(String pluginName, int responseCode) {
- if (responseCode == 403 || responseCode == 429)
- logger.warning(String.format("[%s] Rate limited, can't check for a new plugin version. This should resolve itself within an hour.", pluginName));
- else
- logger.warning(String.format("[%s] Unexpected response code: %s. Unable to check for a new plugin version.", pluginName, responseCode));
-
+ if (responseCode == 403 || responseCode == 429) {
+ logger.warning(String.format(
+ "[%s] Rate limited, can't check for a new plugin version. This should resolve itself within an hour.",
+ pluginName
+ ));
+ } else {
+ logger.warning(String.format(
+ "[%s] Unexpected response code: %s. Unable to check for a new plugin version.",
+ pluginName, responseCode
+ ));
+ }
}
/**
@@ -94,16 +112,21 @@ private static void handleResponseError(String pluginName, int responseCode) {
* string. If the version cannot be found, it returns {@code null}.
*
* @param responseBody the JSON response from the GitHub API
+ *
* @return the latest version string, or {@code null} if it cannot be determined
*/
private static String getLatestVersion(String responseBody) {
String tagNameField = "\"tag_name\":\"";
int tagIndex = responseBody.indexOf(tagNameField);
- if (tagIndex == -1) return null;
+ if (tagIndex == -1) {
+ return null;
+ }
int startIndex = tagIndex + tagNameField.length();
int endIndex = responseBody.indexOf("\"", startIndex);
- if (endIndex == -1) return null;
+ if (endIndex == -1) {
+ return null;
+ }
return responseBody.substring(startIndex, endIndex);
}
@@ -111,8 +134,8 @@ private static String getLatestVersion(String responseBody) {
/**
* Compares the current plugin version with the latest version and logs the result.
*
- * If a newer version is available, this method logs a message indicating that the plugin is outdated and provides
- * a download link. If the plugin is up to date, it logs a message confirming this.
+ * If a newer version is available, this method logs a content indicating that the plugin is outdated and provides
+ * a download link. If the plugin is up to date, it logs a content confirming this.
*
* @param pluginName the name of the plugin
* @param pluginVersion the current version of the plugin, formatted with a 'v' prefix
@@ -127,8 +150,16 @@ private static void compareVersions(String pluginName, String pluginVersion, Str
if (!pluginVersion.equalsIgnoreCase(latestVersion)) {
String downloadLink = githubApiUrl.replace("api.github.com/repos", "github.com");
- logger.info(String.format("[%s] New stable %s available. You are running an outdated or experimental %s.", pluginName, latestVersion, pluginVersion));
- logger.info(String.format("[%s] Download the latest stable version from: %s", pluginName, downloadLink));
- } else logger.info(String.format("[%s] You are running the latest version.", pluginName));
+ logger.info(String.format(
+ "[%s] New stable %s available. You are running an outdated or experimental %s.",
+ pluginName, latestVersion, pluginVersion
+ ));
+ logger.info(String.format(
+ "[%s] Download the latest stable version from: %s",
+ pluginName, downloadLink
+ ));
+ } else {
+ logger.info(String.format("[%s] You are running the latest version.", pluginName));
+ }
}
}
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/util/AboutUtil.java b/src/main/java/io/github/aleksandarharalanov/chatguard/util/misc/AboutUtil.java
similarity index 60%
rename from src/main/java/io/github/aleksandarharalanov/chatguard/util/AboutUtil.java
rename to src/main/java/io/github/aleksandarharalanov/chatguard/util/misc/AboutUtil.java
index f18c171..1b46074 100644
--- a/src/main/java/io/github/aleksandarharalanov/chatguard/util/AboutUtil.java
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/util/misc/AboutUtil.java
@@ -1,5 +1,6 @@
-package io.github.aleksandarharalanov.chatguard.util;
+package io.github.aleksandarharalanov.chatguard.util.misc;
+import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
@@ -8,31 +9,33 @@
import java.util.logging.Logger;
import java.util.stream.Collectors;
-import static org.bukkit.Bukkit.getServer;
-import static io.github.aleksandarharalanov.chatguard.util.ColorUtil.translate;
-
/**
* Utility class for displaying plugin information to a command sender.
*
- * Provides methods to display detailed information about a plugin—including its name, version,
- * description, website, author(s), and contributor(s)—to a player or the server console.
+ * Provides methods to display detailed information about a plugin—including its name, version, description, website,
+ * author(s), and contributor(s)—to a player or the server console.
+ *
+ * @see Aleksandar's GitHub
+ *
+ * @author Aleksandar Haralanov (@AleksandarHaralanov)
*/
-public class AboutUtil {
+public final class AboutUtil {
+
+ private static final Logger logger = Bukkit.getServer().getLogger();
- private static final Logger logger = getServer().getLogger();
+ private AboutUtil() {}
/**
* Displays detailed information about the specified plugin to the given command sender.
*
- * This method formats and sends plugin details such as the name, version, description, website,
- * author(s), and contributor(s) to the specified {@link CommandSender}. If the plugin version contains
- * keywords like "snapshot", "alpha", "beta", or "rc", a warning is displayed indicating that the plugin is experimental.
+ * This method formats and sends plugin details such as the name, version, description, website, author(s), and
+ * contributor(s) to the specified {@link CommandSender}.
*
* @param sender the command sender who will receive the plugin information; can be a player or console
- * @param plugin the plugin whose information is to be displayed
+ * @param plugin the plugin instance whose information is to be displayed
* @param contributorsList the list of contributor names; may be {@code null} or empty
*/
- public static void about(CommandSender sender, JavaPlugin plugin, List contributorsList) {
+ public static void aboutPlugin(CommandSender sender, JavaPlugin plugin, List contributorsList) {
String name = plugin.getDescription().getName();
String version = plugin.getDescription().getVersion();
String website = plugin.getDescription().getWebsite();
@@ -40,25 +43,27 @@ public static void about(CommandSender sender, JavaPlugin plugin, List c
String authors = formatAuthors(plugin.getDescription().getAuthors());
String contributors = formatContributors(contributorsList);
- boolean isExperimental = isExperimentalVersion(version);
-
- if (sender instanceof Player)
- sendPlayerInfo((Player) sender, name, version, description, website, authors, contributors, isExperimental);
- else
- sendConsoleInfo(name, version, description, website, authors, contributors, isExperimental);
+ if (sender instanceof Player) {
+ sendPlayerInfo((Player) sender, name, version, description, website, authors, contributors);
+ } else {
+ sendConsoleInfo(name, version, description, website, authors, contributors);
+ }
}
/**
* Formats the list of authors into a single string.
*
- * If the list contains multiple authors, they are joined by commas. Each author's name is prefixed with {@code &e}
- * for coloring in the player chat.
+ * If the list contains multiple authors, they are joined by commas.
*
* @param authorsList the list of authors to format
+ *
* @return a formatted string of authors, or {@code null} if the list is {@code null} or empty
*/
private static String formatAuthors(List authorsList) {
- if (authorsList == null || authorsList.isEmpty()) return null;
+ if (authorsList == null || authorsList.isEmpty()) {
+ return null;
+ }
+
return authorsList.size() == 1 ? authorsList.get(0) : authorsList.stream()
.map(author -> "&e" + author)
.collect(Collectors.joining("&7, &e"));
@@ -67,39 +72,27 @@ private static String formatAuthors(List authorsList) {
/**
* Formats the list of contributors into a single string.
*
- * If the list contains multiple contributors, they are joined by commas. Each contributor's name is prefixed with {@code &e}
- * for coloring in the player chat.
+ * If the list contains multiple contributors, they are joined by commas.
*
* @param contributorsList the list of contributors to format
+ *
* @return a formatted string of contributors, or {@code null} if the list is {@code null} or empty
*/
private static String formatContributors(List contributorsList) {
- if (contributorsList == null || contributorsList.isEmpty()) return null;
+ if (contributorsList == null || contributorsList.isEmpty()) {
+ return null;
+ }
+
return contributorsList.size() == 1 ? contributorsList.get(0) : contributorsList.stream()
.map(contributor -> "&e" + contributor)
.collect(Collectors.joining("&7, &e"));
}
- /**
- * Determines if the plugin version is experimental based on its version string.
- *
- * A version is considered experimental if it contains "snapshot", "alpha", "beta", or "rc".
- *
- * @param version the version string to check
- * @return {@code true} if the version is experimental, otherwise {@code false}
- */
- private static boolean isExperimentalVersion(String version) {
- return (version.contains("snapshot") ||
- version.contains("alpha") ||
- version.contains("beta") ||
- version.contains("rc"));
- }
-
/**
* Sends the plugin information to a player.
*
* This method sends formatted information including the plugin's name, version, description, website, author(s),
- * and contributor(s) to the specified player. If the plugin version is experimental, warning messages are also sent.
+ * and contributor(s) to the specified player.
*
* @param player the player to receive the plugin information
* @param name the name of the plugin
@@ -108,14 +101,9 @@ private static boolean isExperimentalVersion(String version) {
* @param website the website of the plugin, or {@code null} if not available
* @param authors the formatted string of authors, or {@code null} if not available
* @param contributors the formatted string of contributors, or {@code null} if not available
- * @param experimental {@code true} if the plugin version is experimental, otherwise {@code false}
*/
- private static void sendPlayerInfo(Player player, String name, String version, String description, String website, String authors, String contributors, boolean experimental) {
- if (experimental) {
- player.sendMessage(translate("&cRunning an experimental version."));
- player.sendMessage(translate("&cMay contain bugs or other issues."));
- }
- player.sendMessage(translate(String.format("&b%s &ev%s", name, version)));
+ private static void sendPlayerInfo(Player player, String name, String version, String description, String website, String authors, String contributors) {
+ player.sendMessage(ColorUtil.translateColorCodes(String.format("&b%s &ev%s", name, version)));
outputMessage(player, "&bDescription: &7", description);
outputMessage(player, "&bWebsite: &e", website);
outputMessage(player, "&bAuthor(s): &e", authors);
@@ -128,7 +116,7 @@ private static void sendPlayerInfo(Player player, String name, String version, S
* Logs the plugin information to the server console.
*
* This method logs formatted information including the plugin's name, version, description, website, author(s),
- * and contributor(s) to the server console. If the plugin version is experimental, warning messages are also logged.
+ * and contributor(s) to the server console.
*
* @param name the name of the plugin
* @param version the version of the plugin
@@ -136,13 +124,8 @@ private static void sendPlayerInfo(Player player, String name, String version, S
* @param website the website of the plugin, or {@code null} if not available
* @param authors the formatted string of authors, or {@code null} if not available
* @param contributors the formatted string of contributors, or {@code null} if not available
- * @param experimental {@code true} if the plugin version is experimental, otherwise {@code false}
*/
- private static void sendConsoleInfo(String name, String version, String description, String website, String authors, String contributors, boolean experimental) {
- if (experimental) {
- logger.warning("Running an experimental version.");
- logger.warning("May contain bugs or other issues.");
- }
+ private static void sendConsoleInfo(String name, String version, String description, String website, String authors, String contributors) {
logger.info(String.format("%s v%s", name, version));
outputMessage("Description: ", description);
outputMessage("Website: ", website);
@@ -153,25 +136,25 @@ private static void sendConsoleInfo(String name, String version, String descript
}
/**
- * Sends a message to a player if the message is not {@code null}.
+ * Sends a content to a player if the content is not {@code null}.
*
- * The message is prefixed with a specified string before being sent.
+ * The content is prefixed with a specified string before being sent.
*
- * @param player the player to receive the message
- * @param prefix the prefix to add to the message
- * @param message the message to send, or {@code null} if no message should be sent
+ * @param player the player to receive the content
+ * @param prefix the prefix to add to the content
+ * @param message the content to send, or {@code null} if no content should be sent
*/
private static void outputMessage(Player player, String prefix, String message) {
if (message != null) {
- player.sendMessage(translate(prefix + message));
+ player.sendMessage(ColorUtil.translateColorCodes(prefix + message));
}
}
/**
- * Logs a message to the server console with a prefix if the message is not {@code null}.
+ * Logs a content to the server console with a prefix if the content is not {@code null}.
*
- * @param prefix the prefix to add to the message
- * @param message the message to log, or {@code null} if no message should be logged
+ * @param prefix the prefix to add to the content
+ * @param message the content to log, or {@code null} if no content should be logged
*/
private static void outputMessage(String prefix, String message) {
if (message != null) {
diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/util/misc/ColorUtil.java b/src/main/java/io/github/aleksandarharalanov/chatguard/util/misc/ColorUtil.java
new file mode 100644
index 0000000..f807be6
--- /dev/null
+++ b/src/main/java/io/github/aleksandarharalanov/chatguard/util/misc/ColorUtil.java
@@ -0,0 +1,41 @@
+package io.github.aleksandarharalanov.chatguard.util.misc;
+
+/**
+ * Utility class for translating color codes in text to Minecraft's color code format.
+ *
+ * Provides a method to scan text for color codes prefixed with an ampersand ({@code &}) and replace them with the
+ * appropriate Minecraft color code format using the section sign ({@code §}) symbol.
+ *
+ * @see Aleksandar's GitHub
+ *
+ * @author Aleksandar Haralanov (@AleksandarHaralanov)
+ */
+public final class ColorUtil {
+
+ private ColorUtil() {}
+
+ /**
+ * Translates color codes in the given text to Minecraft's color code format.
+ *
+ * This method scans the input text for the ampersand character ({@code &}) followed by a valid color code character
+ * ({@code 0-9}, {@code a-f}, {@code A-F}) and replaces the ampersand with the section sign ({@code §}).
+ * The following character is converted to lowercase to ensure proper formatting for Minecraft color codes.
+ *
+ * Example: A string like {@code "&aHello"} will be converted to {@code "§aHello"}, where {@code §a} is the
+ * color code for light green in Minecraft.
+ *
+ * @param message the content containing color codes to be translated
+ *
+ * @return the translated content with Minecraft color codes, or the original content if no color codes are found
+ */
+ public static String translateColorCodes(String message) {
+ char[] translation = message.toCharArray();
+ for (int i = 0; i < translation.length - 1; ++i) {
+ if (translation[i] == '&' && "0123456789AaBbCcDdEeFf".indexOf(translation[i + 1]) > -1) {
+ translation[i] = 167;
+ translation[i + 1] = Character.toLowerCase(translation[i + 1]);
+ }
+ }
+ return new String(translation);
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/captchas.yml b/src/main/resources/captchas.yml
new file mode 100644
index 0000000..e69de29
diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml
index 3a623e4..ab1553b 100644
--- a/src/main/resources/config.yml
+++ b/src/main/resources/config.yml
@@ -1,13 +1,13 @@
miscellaneous:
- sound-cues: true
+ audio-cues: true
spam-prevention:
enabled:
- message: true
+ chat: true
command: true
warn-player: true
cooldown-ms:
- message:
+ chat:
s0: 1000
s1: 2000
s2: 3000
@@ -28,24 +28,19 @@ captcha:
code:
characters: "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789"
length: 5
- log:
- console: true
- local-file: true
- discord-webhook:
- enabled: false
- url: ""
+ log-console: true
whitelist: []
filter:
- enabled: true
+ enabled:
+ chat: true
+ sign: true
+ name: true
warn-player: true
log:
console: true
local-file: true
- discord-webhook:
- enabled: false
- url: ""
- mute:
+ essentials-mute:
enabled: true
duration:
s0: "30m"
diff --git a/src/main/resources/discord.yml b/src/main/resources/discord.yml
new file mode 100644
index 0000000..3b3df0c
--- /dev/null
+++ b/src/main/resources/discord.yml
@@ -0,0 +1,37 @@
+webhook-url: ""
+
+embed-log:
+ type:
+ chat: false
+ sign: false
+ name: false
+ captcha: false
+ optional:
+ censor: true
+ data:
+ ip-address: true
+ timestamp: true
+
+customize:
+ player-avatar: "https://minotar.net/avatar/%player%.png"
+ type:
+ chat:
+ color: "#FF5555"
+ webhook:
+ name: "ChatGuard - Chat"
+ icon: "https://raw.githubusercontent.com/AleksandarHaralanov/ChatGuard/refs/heads/master/assets/ChatGuard-Logo.png"
+ sign:
+ color: "#FFAA00"
+ webhook:
+ name: "ChatGuard - Sign"
+ icon: "https://raw.githubusercontent.com/AleksandarHaralanov/ChatGuard/refs/heads/master/assets/ChatGuard-Logo-Gold.png"
+ name:
+ color: "#FFFF55"
+ webhook:
+ name: "ChatGuard - Name"
+ icon: "https://raw.githubusercontent.com/AleksandarHaralanov/ChatGuard/refs/heads/master/assets/ChatGuard-Logo-Yellow.png"
+ captcha:
+ color: "#AA00AA"
+ webhook:
+ name: "ChatGuard - Captcha"
+ icon: "https://raw.githubusercontent.com/AleksandarHaralanov/ChatGuard/refs/heads/master/assets/ChatGuard-Logo-Dark-Purple.png"
\ No newline at end of file
diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml
index c8919f5..4ac3ce3 100644
--- a/src/main/resources/plugin.yml
+++ b/src/main/resources/plugin.yml
@@ -1,15 +1,15 @@
main: io.github.aleksandarharalanov.chatguard.ChatGuard
-version: 4.1.1
+version: 5.0.0
name: ChatGuard
author: Beezle
website: github.com/AleksandarHaralanov/ChatGuard
-description: Prevents messages and usernames containing blocked terms or matching RegEx patterns, enforces mutes, stops chat and command spam, prompts captcha verification, logs actions, and applies escalating penalties.
+description: Prevents messages, usernames, and signs containing blocked terms or matching RegEx patterns, enforces mutes, stops chat and command spam, prompts captcha verification, logs actions, and applies escalating penalties.
softdepend: [Essentials]
commands:
chatguard:
- description: All of ChatGuard's features in one command.
- usage: /chatguard
+ description: ChatGuard main command.
+ usage: /
aliases: [cg]
permissions:
@@ -27,5 +27,5 @@ permissions:
description: Allows player to reload and modify the ChatGuard configuration.
default: op
chatguard.captcha:
- description: Allows player to be notified when someone is prompted a captcha verification.
+ description: Allows player to be notified when someone triggers a captcha verification.
default: op
\ No newline at end of file