diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..ce12592 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# Global owners +* @AleksandarHaralanov diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 4713876..1b7e1d3 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,18 +1,37 @@ # Contributing ChatGuard has a devlog which you can visit [here](https://github.com/users/AleksandarHaralanov/projects/1). -Optionally, you can add your name [here](https://github.com/AleksandarHaralanov/ChatGuard/blob/5daf57b1412584b5b450a59c4ddead1585bcae2b/src/main/java/io/github/aleksandarharalanov/chatguard/command/ChatGuardCommand.java#L46) when you contribute code. +## Contribution Workflow +All contributions must be made to the `staging` branch via pull requests (PRs). + +- **Write-access users** must create a **feature branch** from `staging`, push changes to it, and open a pull request to `staging`. +- **External contributors** must fork the repository, make changes in the `staging` branch of their fork, and open a pull request to `staging`. + +Once reviewed and approved, changes from `staging` will be **squash merged** into `master` to maintain a clean history. + +> ![NOTE] +> **Pull requests that do not follow these instructions will be rejected.**
+> You will receive a comment explaining what was incorrect, so you can make the necessary changes and resubmit. + +### Optional Contributor Recognition +You can add your name [here]() to be credited in the plugin's about command for your contribution. ## Coding Conventions -This is an open-source project. Keep in mind the people who will read your code — make it clean and easy to understand. +This is an open-source project. Keep in mind the people who will read and work with your code—make it clean and easy to understand. When reading the code, you'll quickly get the hang of it. Focus on optimizing for readability: - Do not leave zombie code behind. -- If you feel the need to add comments, don't hesitate; they are always appreciated. -- Use an indentation of four spaces whenever possible; however, two spaces are also acceptable if preferred. -- Always include spaces after list items and method parameters (e.g., `[1, 2, 3]`, not `[1,2,3]`), around operators (e.g., `x += 1`, not `x+=1`), around hash arrows, curly braces etc. -- Avoid utilizing algorithms with bad space and time complexity. - - You can read more about Big O notation [here](https://en.wikipedia.org/wiki/Big_O_notation). +- Clean and concise comments are encouraged if they improve understanding. +- Use an indentation of **four spaces** whenever possible; **two spaces are also acceptable** if preferred. +- Follow object-oriented principles such as SOLID to improve code maintainability. +- Prefer Java Streams and functional programming when applicable, as they improve code readability and performance. +- Use proper Java keywords and modifiers to ensure encapsulation and best practices: +- Always include spaces: + - After list items and method parameters (e.g., `[1, 2, 3]`, not `[1,2,3]`). + - Around operators (e.g., `x += 1`, not `x+=1`). + - Around hash arrows, curly braces, etc. +- If possible, please avoid algorithms with bad space and time complexity. + - Learn more about Big O notation [here](https://en.wikipedia.org/wiki/Big_O_notation). -Following general coding conventions is recommended as a best practice, no matter what you are working on. +Following these conventions will help maintain high-quality code and make contributions easier for everyone. diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml new file mode 100644 index 0000000..00f30c1 --- /dev/null +++ b/.github/workflows/build-and-test.yaml @@ -0,0 +1,29 @@ +name: build-and-test +on: + pull_request: + branches: + - master + - staging + +jobs: + build-and-test: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK 1.8 + uses: actions/setup-java@v2 + with: + java-version: 8 + distribution: 'temurin' + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v3 + + - name: Build with Gradle + run: ./gradlew build + + - name: Test with Gradle + run: ./gradlew test diff --git a/README.md b/README.md index e14944f..e24aa25 100644 --- a/README.md +++ b/README.md @@ -5,23 +5,24 @@ **ChatGuard** is a Minecraft plugin designed for servers running version b1.7.3. - Cancels messages containing blocked terms or matching RegEx patterns. +- Censors signs containing blocked terms or matching RegEx patterns. - Prevents players from joining with usernames containing blocked terms or matching RegEx patterns. - Logs offenders (via Discord webhook, server console, or local file). -- Prevents chat message and command spam. -- Prompts captcha verification on suspected bot-like behavior. -- Issues temporary mutes (requires [Essentials v2.5.8](#requirements) as of ChatGuard `v4.1.1`). +- Implements chat and command rate limiter to decrease spam. +- Triggers captcha verification on repeated message spam. +- Issues temporary mutes (requires [Essentials v2.5.8](#Requirements & Optional) as of ChatGuard `v5.0.0`). - Enforces escalating penalties via a six-strike tier system. -- Plays local sound cues for offending players upon detection. +- Plays local audio cues for offending players upon detection. The plugin is entirely configurable. --- -## Contributing Code & Reporting Issues +## Contributions, Suggestions, and Issues Consider helping ChatGuard become even more versatile and robust. -Visit the [CONTRIBUTING](https://github.com/AleksandarHaralanov/ChatGuard/blob/master/.github/CONTRIBUTING.md) guide for details on how to get started and where to focus your efforts. +It is **highly recommended** to visit the [CONTRIBUTING](https://github.com/AleksandarHaralanov/ChatGuard/blob/master/.github/CONTRIBUTING.md) guide for details on how to get started and where to focus your efforts. -For any issues with the plugin, or suggestions, please report them [here](https://github.com/AleksandarHaralanov/ChatGuard/issues). +For any issues with the plugin, or suggestions, please submit them [here](https://github.com/AleksandarHaralanov/ChatGuard/issues). --- ## Download @@ -32,10 +33,10 @@ The plugin is fully open-source and transparent.
If you'd like additional peace of mind, you're welcome to scan the `.jar` file using [VirusTotal](https://www.virustotal.com/gui/home/upload). --- -## Requirements +## Requirements & Optional Your server must be running one of the following APIs: CB1060-CB1092, [Project Poseidon](https://github.com/retromcorg/Project-Poseidon) or [UberBukkit](https://github.com/Moresteck/Project-Poseidon-Uberbukkit). -It also needs to be running **Essentials v2.5.8 or newer** (as of ChatGuard `v4.1.1`).
You can download it from [here](https://github.com/AleksandarHaralanov/ChatGuard/raw/refs/heads/master/libs/Essentials.jar). +You can download **Essentials v2.5.8** from [here](https://github.com/AleksandarHaralanov/ChatGuard/raw/refs/heads/master/libs/Essentials.jar). --- ## Usage @@ -61,26 +62,22 @@ Use PermissionsEx or similar plugins to grant groups the permission, enabling th --- ## Configurations -Generates `config.yml` and `strikes.yml` located at `plugins/ChatGuard`. +ChatGuard generates two configuration files using the default settings in the **config** directory. -> [!CAUTION] -> 🔖`v4.1.1`: If your server is not running **Essentials v2.5.8 or newer**, make sure to download and install it. Without it, the entire plugin will break, and in-game messages will fail to send properly. -> -> You can find the download [here](#requirements) in the requirements heading. +Additionally, it creates two data files, `captchas.yml` and `strikes.yml`, in the **data** directory. -### Config -This is the default `config.yml` configuration file: +#### Main Config `config.yml`: ```yaml -miscellaneous: # QoL Configurations - sound-cues: true # Offending player hears a local sound cue upon detection +miscellaneous: # Misc Configurations + audio-cues: true # Offending player hears a local audio cue upon detection spam-prevention: # Spam Prevention Configuration enabled: # Toggles spam prevention for chat messages and commands - message: true + chat: true command: true warn-player: true # Warns offending player upon detection cooldown-ms: # Cooldown durations in milliseconds for strike tiers - message: + chat: s0: 1000 s1: 2000 s2: 3000 @@ -101,24 +98,19 @@ captcha: # Captcha Configuration code: # Captcha characters and length characters: "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789" length: 5 - log: # Logs captcha triggers to: - console: true # Server console - local-file: true # Local file - discord-webhook: # Discord webhook by an embed - enabled: false # Toggles Discord webhook - url: "" # Discord webhook URL + log-console: true # Log captcha trigger to server console whitelist: [] # Allowed captcha bypass terms for sanitizing filter: # Filter Configuration - enabled: true # Toggles filtering of chat messages and player usernames + enabled: # Toggles filtration for chat messages, player usernames, and signs + chat: true + sign: true + name: true warn-player: true # Warns offending player upon detection - log: # Logs captcha triggers to: + log: # Log filter trigger to: console: true # Server console local-file: true # Local file - discord-webhook: # Discord webhook by an embed - enabled: false # Toggles Discord webhook - url: "" # Discord webhook URL - mute: # Mute Configuration + essentials-mute: # Essentials Mute Configuration enabled: true # Toggles automatic mutes upon filter detection duration: # Mute durations for strike tiers s0: "30m" @@ -133,9 +125,58 @@ filter: # Filter Configuration whitelist: [] # Allowed chat message and player username bypass terms for sanitizing blacklist: [] # Disallowed chat message and player username bypass terms ``` +
-### Strikes -The default `strikes.yml` configuration file is initially empty. When a player joins for the first time after ChatGuard is installed on the server, they are added to the configuration with 0 strikes. From there, the plugin manages their strikes, incrementing them up to a maximum of 5 as necessary. Read note below on how that works. +#### Discord Embed Config `discord.yml`: +```yaml +webhook-url: "" # Discord Webhook URL + +embed-log: # Embed Configurations + type: # Logs to embed + chat: false + sign: false + name: false + captcha: false + optional: + censor: true # Censors sensitive data, such as IP addresses and the filter trigger in the embed + data: + ip-address: true # Includes player IP address in the embed + timestamp: true # Includes timestamp in the embed + +customize: # Embed customization options + player-avatar: "https://minotar.net/avatar/%player%.png" # Place %player% where the player's username would usually go + type: # Various embed type log customizations + 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" +``` + +> [!CAUTION] +> If your server is not running **Essentials v2.5.8**, you must do one of the following: +> - **Disable Essentials mute support** by setting `filter.essentials-mute.enabled` to `false` in `config/config.yml`. +> - **Install Essentials** to use the temporary mute feature. +> - You can download **Essentials v2.5.8** from [here](#Requirements & Optional). +> +> Without one of the two, ChatGuard could break, and in-game messages might fail to send. -> [!NOTE] -> 🔖`v4.1.1`: Strike tiers will increment only when the filter is enabled, and a disallowed term or matching regex pattern is detected in a message. Otherwise, all strike tiers will default to 0 unless manually modified in the configuration file or through the included command. +> [!NOTE] +> Strike tiers increment only when the filter is enabled and a disallowed term or matching regex pattern is detected. +> +> Otherwise, all strike tiers default to `0` unless manually modified in `data/strikes.yml` or via the staff command. diff --git a/build.gradle b/build.gradle index 45a1720..c7b9c6f 100644 --- a/build.gradle +++ b/build.gradle @@ -15,31 +15,45 @@ dependencies { File ymlFile = file('src/main/resources/plugin.yml') as File -if (!ymlFile.exists()) throw new GradleException("The 'plugin.yml' file does not exist in 'src/main/resources'!") +if (!ymlFile.exists()) { + throw new GradleException("The 'plugin.yml' file does not exist in 'src/main/resources'.") +} -String pluginVersion -String pluginName String ymlText = ymlFile.text - Matcher mainMatcher = (ymlText =~ /main:\s*(\S+)/) Matcher versionMatcher = (ymlText =~ /version:\s*(\S+)/) Matcher nameMatcher = (ymlText =~ /name:\s*(\S+)/) -if (!mainMatcher.find()) throw new GradleException("The 'main' attribute wasn't found in the 'plugin.yml' file!") +if (!mainMatcher.find()) { + throw new GradleException("The 'main' attribute wasn't found in the 'plugin.yml' file.") +} + +if (versionMatcher.find()) { + version = versionMatcher.group(1) +} else { + throw new GradleException("The 'version' attribute wasn't found in the 'plugin.yml' file.") +} + +if (!nameMatcher.find()) { + throw new GradleException("The 'name' attribute wasn't found in the 'plugin.yml' file.") +} -if (versionMatcher.find()) pluginVersion = versionMatcher.group(1) -else throw new GradleException("The 'version' attribute wasn't found in the 'plugin.yml' file!") +tasks.named('build') { + mustRunAfter 'clean' +} -if (nameMatcher.find()) pluginName = nameMatcher.group(1) -else throw new GradleException("The 'name' attribute wasn't found in the 'plugin.yml' file!") +tasks.register('cleanBuild') { + dependsOn 'clean', 'build' + group = 'custom' +} jar { manifest { attributes( - 'Implementation-Title': pluginName, - 'Implementation-Version': pluginVersion + 'Implementation-Title': "${project.name}", + 'Implementation-Version': "${project.version}" ) } - archiveVersion.set(pluginVersion) - archiveBaseName.set(pluginName) + + archiveFileName = "${project.name}-${project.version}.jar" } diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/settings.gradle b/settings.gradle index e79d0c2..6061720 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1 @@ rootProject.name = 'ChatGuard' - diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/ChatGuard.java b/src/main/java/io/github/aleksandarharalanov/chatguard/ChatGuard.java index d80b020..558296c 100644 --- a/src/main/java/io/github/aleksandarharalanov/chatguard/ChatGuard.java +++ b/src/main/java/io/github/aleksandarharalanov/chatguard/ChatGuard.java @@ -1,62 +1,74 @@ package io.github.aleksandarharalanov.chatguard; import io.github.aleksandarharalanov.chatguard.command.ChatGuardCommand; -import io.github.aleksandarharalanov.chatguard.listener.player.PlayerChatListener; -import io.github.aleksandarharalanov.chatguard.listener.player.PlayerCommandPreprocessListener; -import io.github.aleksandarharalanov.chatguard.listener.player.PlayerJoinListener; -import io.github.aleksandarharalanov.chatguard.listener.player.PlayerQuitListener; -import io.github.aleksandarharalanov.chatguard.util.ConfigUtil; -import io.github.aleksandarharalanov.chatguard.util.LoggerUtil; +import io.github.aleksandarharalanov.chatguard.listener.block.SignChangeListener; +import io.github.aleksandarharalanov.chatguard.listener.player.*; +import io.github.aleksandarharalanov.chatguard.util.config.ConfigUtil; +import io.github.aleksandarharalanov.chatguard.util.log.LogUtil; +import io.github.aleksandarharalanov.chatguard.util.log.UpdateUtil; import org.bukkit.event.Event.Priority; import org.bukkit.event.Event.Type; import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.java.JavaPlugin; -import static io.github.aleksandarharalanov.chatguard.util.LoggerUtil.logInfo; -import static io.github.aleksandarharalanov.chatguard.util.UpdateUtil.checkForUpdates; - public class ChatGuard extends JavaPlugin { private static ChatGuard plugin; private static ConfigUtil config; + private static ConfigUtil discord; private static ConfigUtil strikes; + private static ConfigUtil captchas; @Override public void onEnable() { - checkForUpdates(this, "https://api.github.com/repos/AleksandarHaralanov/ChatGuard/releases/latest"); + UpdateUtil.checkAvailablePluginUpdates(this, "https://api.github.com/repos/AleksandarHaralanov/ChatGuard/releases/latest"); plugin = this; - config = new ConfigUtil(this, "config.yml"); + // Configurations + config = new ConfigUtil(this, "config/config.yml"); config.load(); + discord = new ConfigUtil(this, "config/discord.yml"); + discord.load(); - strikes = new ConfigUtil(this, "strikes.yml"); + // Data + strikes = new ConfigUtil(this, "data/strikes.yml"); strikes.load(); + captchas = new ConfigUtil(this, "data/captchas.yml"); + captchas.load(); - final LoggerUtil log = new LoggerUtil(this, "log.txt"); - log.initializeLog(); + // Local File Log + final LogUtil log = new LogUtil(this, "log.txt"); + log.initializeLogFile(); PluginManager pM = getServer().getPluginManager(); + + // Player Listeners final PlayerCommandPreprocessListener pCPL = new PlayerCommandPreprocessListener(); final PlayerChatListener pCL = new PlayerChatListener(); final PlayerJoinListener pJL = new PlayerJoinListener(); + final PlayerLoginListener pLL = new PlayerLoginListener(); final PlayerQuitListener pQL = new PlayerQuitListener(); pM.registerEvent(Type.PLAYER_COMMAND_PREPROCESS, pCPL, Priority.Lowest, this); - pM.registerEvent(Type.PLAYER_PRELOGIN, pJL, Priority.Lowest, this); + pM.registerEvent(Type.PLAYER_LOGIN, pLL, Priority.Lowest, this); pM.registerEvent(Type.PLAYER_CHAT, pCL, Priority.Lowest, this); - pM.registerEvent(Type.PLAYER_JOIN, pJL, Priority.Normal, this); - pM.registerEvent(Type.PLAYER_JOIN, pJL, Priority.Normal, this); - pM.registerEvent(Type.PLAYER_QUIT, pQL, Priority.Normal, this); + pM.registerEvent(Type.PLAYER_JOIN, pJL, Priority.Lowest, this); + pM.registerEvent(Type.PLAYER_QUIT, pQL, Priority.Lowest, this); + // Block Listeners + final SignChangeListener sCL = new SignChangeListener(); + pM.registerEvent(Type.SIGN_CHANGE, sCL, Priority.Lowest, this); + + // Main Command final ChatGuardCommand command = new ChatGuardCommand(this); getCommand("chatguard").setExecutor(command); - logInfo(String.format("[%s] v%s Enabled.", getDescription().getName(), getDescription().getVersion())); + LogUtil.logConsoleInfo(String.format("[%s] v%s Enabled.", getDescription().getName(), getDescription().getVersion())); } @Override public void onDisable() { - logInfo(String.format("[%s] v%s Disabled.", getDescription().getName(), getDescription().getVersion())); + LogUtil.logConsoleInfo(String.format("[%s] v%s Disabled.", getDescription().getName(), getDescription().getVersion())); } public static ChatGuard getInstance() { @@ -67,7 +79,15 @@ public static ConfigUtil getConfig() { return config; } + public static ConfigUtil getDiscord() { + return discord; + } + public static ConfigUtil getStrikes() { return strikes; } + + public static ConfigUtil getCaptchas() { + return captchas; + } } \ No newline at end of file diff --git a/src/main/java/io/github/aleksandarharalanov/chatguard/command/ChatGuardCommand.java b/src/main/java/io/github/aleksandarharalanov/chatguard/command/ChatGuardCommand.java index e01d51a..20abf6b 100644 --- a/src/main/java/io/github/aleksandarharalanov/chatguard/command/ChatGuardCommand.java +++ b/src/main/java/io/github/aleksandarharalanov/chatguard/command/ChatGuardCommand.java @@ -1,165 +1,38 @@ package io.github.aleksandarharalanov.chatguard.command; import io.github.aleksandarharalanov.chatguard.ChatGuard; +import io.github.aleksandarharalanov.chatguard.command.subcommand.*; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; +import java.util.HashMap; +import java.util.Map; -import static io.github.aleksandarharalanov.chatguard.ChatGuard.*; -import static io.github.aleksandarharalanov.chatguard.handler.CaptchaHandler.getPlayerCaptcha; -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.AboutUtil.about; -import static io.github.aleksandarharalanov.chatguard.util.AccessUtil.commandInGameOnly; -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.logInfo; +public final class ChatGuardCommand implements CommandExecutor { -public class ChatGuardCommand implements CommandExecutor { - - private final ChatGuard plugin; + private final Map subcommands = new HashMap<>(); public ChatGuardCommand(ChatGuard plugin) { - this.plugin = plugin; + subcommands.put("about", new AboutCommand(plugin)); + subcommands.put("reload", new ReloadCommand()); + subcommands.put("strike", new StrikeCommand()); + subcommands.put("captcha", new CaptchaCommand(plugin)); } @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { - if (!command.getName().equalsIgnoreCase("chatguard") && !command.getName().equalsIgnoreCase("cg")) - return true; - if (args.length == 0) { - helpCommand(sender); + HelpCommand.sendHelp(sender); return true; } - if (args.length == 1) { - switch (args[0].toLowerCase()) { - case "about": - // Contributors: Add your name here, if you wish to be credited, when you contribute code - List contributorsList = new ArrayList<>(Arrays.asList( - "moderator_man" - )); - about(sender, plugin, contributorsList); - break; - case "reload": - reloadCommand(sender); - break; - default: - helpCommand(sender); - } - return true; - } - - if (args.length <= 3) { - switch (args[0].toLowerCase()) { - case "strike": - if (!hasPermission(sender, "chatguard.config", "[ChatGuard] You don't have permission to modify the config.")) break; - - String playerName = args[1]; - List keys = getStrikes().getKeys(); - String foundKey = keys.stream() - .filter(key -> key.equalsIgnoreCase(playerName)) - .findFirst() - .orElse(null); - int playerStrike = foundKey != null ? getStrike(foundKey) : -1; - - switch (args.length) { - case 2: - if (foundKey != null) sender.sendMessage(translate(String.format("&c[ChatGuard] &e%s &cis on strike &e%d&c.", foundKey, playerStrike))); - else playerNotFound(sender, playerName); - break; - case 3: - if (foundKey != null) setPlayerStrikes(sender, foundKey, playerStrike, args[2]); - else playerNotFound(sender, playerName); - break; - } - break; - case "captcha": - if (commandInGameOnly(sender)) break; - - Player player = (Player) sender; - String captchaCode = getPlayerCaptcha().get(player.getName()); - if (captchaCode != null) { - String input = args[1]; - if (input.equals(captchaCode)) { - getPlayerCaptcha().remove(player.getName()); - player.sendMessage(translate("&a[ChatGuard] Captcha verification passed.")); - playSoundCue(player,true); - logInfo(String.format("[ChatGuard] Player '%s' passed captcha verification.", player.getName())); - } else { - player.sendMessage(translate("&c[ChatGuard] Please enter the correct captcha code.")); - playSoundCue(player,false); - } - } else player.sendMessage(translate("&c[ChatGuard] You don't have an active captcha verification.")); - break; - default: - break; - } - return true; + CommandExecutor subcommand = subcommands.get(args[0].toLowerCase()); + if (subcommand != null) { + return subcommand.onCommand(sender, command, label, args); } - helpCommand(sender); + HelpCommand.sendHelp(sender); return true; } - - private static void helpCommand(CommandSender sender) { - String[] messages = { - "&bChatGuard commands:", - "&e/cg &7- Displays this message.", - "&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(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