diff --git a/build.gradle b/build.gradle index b45453c..ddde3ef 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ plugins { } group = 'io.github.thetrouper' -version = '0.0.1' +version = '0.0.2' repositories { mavenCentral() diff --git a/src/main/java/io/github/thetrouper/sentinel/Sentinel.java b/src/main/java/io/github/thetrouper/sentinel/Sentinel.java index 38741c2..8a64af1 100644 --- a/src/main/java/io/github/thetrouper/sentinel/Sentinel.java +++ b/src/main/java/io/github/thetrouper/sentinel/Sentinel.java @@ -7,9 +7,11 @@ package io.github.thetrouper.sentinel; import io.github.thetrouper.sentinel.commands.InfoCommand; import io.github.thetrouper.sentinel.commands.ReopCommand; import io.github.thetrouper.sentinel.data.Config; +import io.github.thetrouper.sentinel.events.ChatEvent; import io.github.thetrouper.sentinel.events.CmdBlockEvents; import io.github.thetrouper.sentinel.events.CommandEvent; import io.github.thetrouper.sentinel.events.NBTEvents; +import io.github.thetrouper.sentinel.server.functions.AntiSpam; import org.bukkit.Bukkit; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.entity.Player; @@ -56,6 +58,24 @@ public final class Sentinel extends JavaPlugin { // Plugin startup logic loadConfiguration(); log.info("Sentinel has loaded! (" + getDescription().getVersion() + ")"); + // Enable Functions + AntiSpam.enableAntiSpam(); + + prefix = Config.Plugin.getPrefix(); + + // Commands -> BE SURE TO REGISTER ANY NEW COMMANDS IN PLUGIN.YML (src/main/java/resources/plugin.yml)! + getCommand("sentinel").setExecutor(new InfoCommand()); + getCommand("sentinel").setTabCompleter(new InfoCommand()); + getCommand("reop").setExecutor(new ReopCommand()); + + // Events + manager.registerEvents(new CommandEvent(),this); + manager.registerEvents(new CmdBlockEvents(), this); + manager.registerEvents(new NBTEvents(), this); + manager.registerEvents(new ChatEvent(),this); + + // Scheduled timers + Bukkit.getScheduler().runTaskTimer(this, AntiSpam::decayHeat,0, 20); log.info("\n" + " ____ __ ___ \n" + "/\\ _`\\ /\\ \\__ __ /\\_ \\ \n" + @@ -64,18 +84,7 @@ public final class Sentinel extends JavaPlugin { " /\\ \\L\\ \\/\\ __//\\ \\/\\ \\ \\ \\_\\ \\ \\/\\ \\/\\ \\/\\ __/ \\_\\ \\_ \n" + " \\ `\\____\\ \\____\\ \\_\\ \\_\\ \\__\\\\ \\_\\ \\_\\ \\_\\ \\____\\/\\____\\\n" + " \\/_____/\\/____/\\/_/\\/_/\\/__/ \\/_/\\/_/\\/_/\\/____/\\/____/\n" + - " ]======------ Advanced Anti-Grief ------======["); - prefix = Config.Plugin.getPrefix(); - - // Commands -> BE SURE TO REGISTER ANY NEW COMMANDS IN PLUGIN.YML (src/main/java/resources/plugin.yml)! - getCommand("sentinel").setExecutor(new InfoCommand()); - getCommand("sentinel").setTabCompleter(new InfoCommand.Tabs()); - getCommand("reop").setExecutor(new ReopCommand()); - - // Events - manager.registerEvents(new CommandEvent(),this); - manager.registerEvents(new CmdBlockEvents(), this); - manager.registerEvents(new NBTEvents(), this); + " ]====---- Advanced Anti-Grief & Chat Filter ----====["); } /** @@ -118,7 +127,7 @@ public final class Sentinel extends JavaPlugin { dangerousCommands = config.getStringList("config.plugin.dangerous"); // Load log protected commands - logDangerousCommands = config.getBoolean("config.plugin.log-protected"); + logDangerousCommands = config.getBoolean("config.plugin.log-dangerous"); // Load logged commands loggedCommands = config.getStringList("config.plugin.logged"); diff --git a/src/main/java/io/github/thetrouper/sentinel/commands/InfoCommand.java b/src/main/java/io/github/thetrouper/sentinel/commands/InfoCommand.java index 1bf7953..c9cd596 100644 --- a/src/main/java/io/github/thetrouper/sentinel/commands/InfoCommand.java +++ b/src/main/java/io/github/thetrouper/sentinel/commands/InfoCommand.java @@ -4,12 +4,11 @@ package io.github.thetrouper.sentinel.commands; +import io.github.thetrouper.sentinel.discord.WebhookSender; import io.github.thetrouper.sentinel.exceptions.CmdExHandler; +import io.github.thetrouper.sentinel.server.functions.AntiSpam; import io.github.thetrouper.sentinel.server.util.TextUtils; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; -import org.bukkit.command.TabCompleter; +import org.bukkit.command.*; import java.util.ArrayList; import java.util.List; @@ -17,33 +16,45 @@ import java.util.List; /** * Example command */ -public class InfoCommand implements CommandExecutor { - +public class InfoCommand implements TabExecutor { + public static boolean debugmode; @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { try { - sender.sendMessage(TextUtils.prefix("This server is running Sentinel by TheTrouper. Hot reloading is not advised.")); + if (args.length == 0) { + sender.sendMessage(TextUtils.prefix("&cYou must specify an item to give.")); + return true; + } + switch (args[0]) { + case "debugmode" -> { + debugmode = !debugmode; + sender.sendMessage(TextUtils.prefix("Debug mode set to §b" + debugmode)); + } + case "webhooktest" -> { + sender.sendMessage(TextUtils.prefix("Testing the webhook...")); + WebhookSender.sendEmbedWarning(sender.getName(), "/sentinel webhooktest",true,true,false); + WebhookSender.sendHelloWorldEmbed(); + } + case "checkheat" -> { + sender.sendMessage(TextUtils.prefix("Your heat is §e" + AntiSpam.heatMap.get(sender).toString())); + } + } return true; } catch (Exception ex) { CmdExHandler handler = new CmdExHandler(ex,command); sender.sendMessage(handler.getErrorMessage()); + ex.printStackTrace(); return true; } } - /** - * Example command's tab completer - */ - public static class Tabs implements TabCompleter { - - @Override - public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { - List list = new ArrayList<>(); - switch (args.length) { - case 1 -> list.add("reload"); - } - list.removeIf(s -> !s.toLowerCase().contains(args[args.length - 1].toLowerCase())); - return list; - } + @Override + public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + return new TabComplBuilder(sender,command,alias,args) + .add(1,new String[]{ + "debugmode", + "webhooktest", + "checkheat" + }).build(); } } diff --git a/src/main/java/io/github/thetrouper/sentinel/commands/TabComplBuilder.java b/src/main/java/io/github/thetrouper/sentinel/commands/TabComplBuilder.java new file mode 100644 index 0000000..136deae --- /dev/null +++ b/src/main/java/io/github/thetrouper/sentinel/commands/TabComplBuilder.java @@ -0,0 +1,60 @@ +package io.github.thetrouper.sentinel.commands; + +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; + +import java.util.*; + +public class TabComplBuilder { + + private Map> entries = new HashMap<>(); + private final CommandSender sender; + private final Command command; + private final String alias; + private final String[] args; + + public TabComplBuilder(CommandSender sender, Command command, String alias, String[] args) { + this.sender = sender; + this.command = command; + this.alias = alias; + this.args = args; + } + + /** + * Adds to the tab completion + * @param atIndex should be a number above or equal to 1 + * @param entry string array + * @param condition condition + */ + public TabComplBuilder add(int atIndex, String[] entry, boolean condition) { + if (condition) add(atIndex,entry); + return this; + } + + /** + * Adds to the tab completion + * @param atIndex should be a number above or equal to 1 + * @param entry string array + */ + public TabComplBuilder add(int atIndex, String[] entry) { + atIndex = Math.max(1,atIndex); + entries.put(atIndex,Arrays.stream(entry).toList()); + return this; + } + + public TabComplBuilder add(int atIndex, List entry, boolean condition) { + if (condition) add(atIndex,entry); + return this; + } + + public TabComplBuilder add(int atIndex, List entry) { + entries.put(atIndex,entry); + return this; + } + + public List build() { + List list = new ArrayList<>(entries.get(args.length) != null ? entries.get(args.length) : new ArrayList<>()); + list.removeIf(s -> !s.toLowerCase().contains(args[args.length - 1].toLowerCase())); + return list; + } +} diff --git a/src/main/java/io/github/thetrouper/sentinel/data/Emojis.java b/src/main/java/io/github/thetrouper/sentinel/data/Emojis.java new file mode 100644 index 0000000..71908ac --- /dev/null +++ b/src/main/java/io/github/thetrouper/sentinel/data/Emojis.java @@ -0,0 +1,9 @@ +package io.github.thetrouper.sentinel.data; + +public class Emojis { + public static String success = "<:success:1125240412081238066>"; + public static String failure = "<:failure:1125241087909429369>"; + public static String rightArrow = "<:rightArrow:1125241843597189160>"; + public static String nuke = "<:nuke:1125244368807280702>"; + public static String member = "<:member:1125244044407218176>"; +} diff --git a/src/main/java/io/github/thetrouper/sentinel/discord/DiscordWebhook.java b/src/main/java/io/github/thetrouper/sentinel/discord/DiscordWebhook.java new file mode 100644 index 0000000..96f9f02 --- /dev/null +++ b/src/main/java/io/github/thetrouper/sentinel/discord/DiscordWebhook.java @@ -0,0 +1,390 @@ +package io.github.thetrouper.sentinel.discord; + +import javax.net.ssl.HttpsURLConnection; +import java.awt.Color; +import java.io.IOException; +import java.io.OutputStream; +import java.lang.reflect.Array; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Class used to execute Discord Webhooks with low effort + */ +public class DiscordWebhook { + + private final String url; + private String content; + private String username; + private String avatarUrl; + private boolean tts; + private List embeds = new ArrayList<>(); + + /** + * Constructs a new DiscordWebhook instance + * + * @param url The webhook URL obtained in Discord + */ + public DiscordWebhook(String url) { + this.url = url; + } + + public void setContent(String content) { + this.content = content; + } + + public void setUsername(String username) { + this.username = username; + } + + public void setAvatarUrl(String avatarUrl) { + this.avatarUrl = avatarUrl; + } + + public void setTts(boolean tts) { + this.tts = tts; + } + + public void addEmbed(EmbedObject embed) { + this.embeds.add(embed); + } + + public void execute() throws IOException { + if (this.content == null && this.embeds.isEmpty()) { + throw new IllegalArgumentException("Set content or add at least one EmbedObject"); + } + + JSONObject json = new JSONObject(); + + json.put("content", this.content); + json.put("username", this.username); + json.put("avatar_url", this.avatarUrl); + json.put("tts", this.tts); + + 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()); + + if (embed.getColor() != null) { + Color color = embed.getColor(); + int rgb = color.getRed(); + rgb = (rgb << 8) + color.getGreen(); + rgb = (rgb << 8) + color.getBlue(); + + jsonEmbed.put("color", rgb); + } + + EmbedObject.Footer footer = embed.getFooter(); + EmbedObject.Image image = embed.getImage(); + EmbedObject.Thumbnail thumbnail = embed.getThumbnail(); + EmbedObject.Author author = embed.getAuthor(); + List fields = embed.getFields(); + + if (footer != null) { + JSONObject jsonFooter = new JSONObject(); + + jsonFooter.put("text", footer.getText()); + jsonFooter.put("icon_url", footer.getIconUrl()); + jsonEmbed.put("footer", jsonFooter); + } + + 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()); + jsonEmbed.put("author", jsonAuthor); + } + + 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); + } + + json.put("embeds", embedObjects.toArray()); + } + + URL url = new URL(this.url); + HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); + connection.addRequestProperty("Content-Type", "application/json"); + connection.addRequestProperty("User-Agent", "Java-DiscordWebhook-BY-Gelox_"); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + + OutputStream stream = connection.getOutputStream(); + stream.write(json.toString().getBytes()); + stream.flush(); + stream.close(); + + connection.getInputStream().close(); //I'm not sure why but it doesn't work without getting the InputStream + connection.disconnect(); + } + + 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; + private Author author; + private List fields = new ArrayList<>(); + + public String getTitle() { + return title; + } + + public String getDescription() { + return description; + } + + public String getUrl() { + return url; + } + + public Color getColor() { + return color; + } + + public Footer getFooter() { + return footer; + } + + public Thumbnail getThumbnail() { + return thumbnail; + } + + public Image getImage() { + return image; + } + + public Author getAuthor() { + return author; + } + + public List getFields() { + return fields; + } + + public EmbedObject setTitle(String title) { + this.title = title; + return this; + } + + public EmbedObject setDescription(String description) { + this.description = description; + return this; + } + + public EmbedObject setUrl(String url) { + this.url = url; + return this; + } + + public EmbedObject setColor(Color color) { + this.color = color; + return this; + } + + public EmbedObject setFooter(String text, String icon) { + this.footer = new Footer(text, icon); + return this; + } + + public EmbedObject setThumbnail(String url) { + this.thumbnail = new Thumbnail(url); + return this; + } + + public EmbedObject setImage(String url) { + this.image = new Image(url); + return this; + } + + public EmbedObject setAuthor(String name, String url, String icon) { + this.author = new Author(name, url, icon); + return this; + } + + public EmbedObject addField(String name, String value, boolean inline) { + this.fields.add(new Field(name, value, inline)); + return this; + } + + private class Footer { + private String text; + private String iconUrl; + + private Footer(String text, String iconUrl) { + this.text = text; + this.iconUrl = iconUrl; + } + + private String getText() { + return text; + } + + private String getIconUrl() { + return iconUrl; + } + } + + private class Thumbnail { + private String url; + + private Thumbnail(String url) { + this.url = url; + } + + private String getUrl() { + return url; + } + } + + private class Image { + private String url; + + private Image(String url) { + this.url = url; + } + + private String getUrl() { + return url; + } + } + + private class Author { + private String name; + private String url; + private String iconUrl; + + private Author(String name, String url, String iconUrl) { + this.name = name; + this.url = url; + this.iconUrl = iconUrl; + } + + private String getName() { + return name; + } + + private String getUrl() { + return url; + } + + private String getIconUrl() { + return iconUrl; + } + } + + private class Field { + private String name; + private String value; + private boolean inline; + + private Field(String name, String value, boolean inline) { + this.name = name; + this.value = value; + this.inline = inline; + } + + private String getName() { + return name; + } + + private String getValue() { + return value; + } + + private boolean isInline() { + return inline; + } + } + } + + private class JSONObject { + + private final HashMap map = new HashMap<>(); + + void put(String key, Object value) { + if (value != null) { + map.put(key, value); + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + Set> entrySet = map.entrySet(); + builder.append("{"); + + int i = 0; + 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.toString()); + } 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 ? "," : ""); + } + builder.append("]"); + } + + builder.append(++i == entrySet.size() ? "}" : ","); + } + + return builder.toString(); + } + + private String quote(String string) { + return "\"" + string + "\""; + } + } +} \ No newline at end of file diff --git a/src/main/java/io/github/thetrouper/sentinel/discord/WebHook.java b/src/main/java/io/github/thetrouper/sentinel/discord/WebHook.java deleted file mode 100644 index 84f7d38..0000000 --- a/src/main/java/io/github/thetrouper/sentinel/discord/WebHook.java +++ /dev/null @@ -1,5 +0,0 @@ -package io.github.thetrouper.sentinel.discord; - -public class WebHook { - // To be implemented once I have learned how to use JDA -} diff --git a/src/main/java/io/github/thetrouper/sentinel/discord/WebhookSender.java b/src/main/java/io/github/thetrouper/sentinel/discord/WebhookSender.java new file mode 100644 index 0000000..0c3ea9b --- /dev/null +++ b/src/main/java/io/github/thetrouper/sentinel/discord/WebhookSender.java @@ -0,0 +1,125 @@ +package io.github.thetrouper.sentinel.discord; + +import io.github.thetrouper.sentinel.Sentinel; +import io.github.thetrouper.sentinel.commands.InfoCommand; +import io.github.thetrouper.sentinel.data.Emojis; +import io.github.thetrouper.sentinel.discord.DiscordWebhook; +import io.github.thetrouper.sentinel.server.util.ServerUtils; +import io.github.thetrouper.sentinel.server.util.TextUtils; +import org.bukkit.block.Block; +import org.bukkit.inventory.ItemStack; + +import java.awt.Color; +import java.io.IOException; + +public class WebhookSender { + + public static void sendHelloWorldEmbed() { + String webhookUrl = Sentinel.webhook; + + // Create a new DiscordWebhook instance + DiscordWebhook webhook = new DiscordWebhook(webhookUrl); + + // Create an EmbedObject and set its properties + DiscordWebhook.EmbedObject embed = new DiscordWebhook.EmbedObject() + .setDescription("Hello, World!") + .setColor(Color.GREEN); + + // Add the EmbedObject to the webhook + webhook.addEmbed(embed); + + try { + // Execute the webhook + webhook.execute(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static String successOrFail(boolean bool) { + if (bool) { + return Emojis.success; + } else { + return Emojis.failure; + } + } + public static void sendEmbedWarning(String player, String command, boolean denied, boolean removedOp, boolean banned) { + ServerUtils.sendDebugMessage("Creating Command Webhook..."); + final String description = + Emojis.rightArrow + " **Player:** " + player + " " + Emojis.member + "\\n" + + Emojis.rightArrow + " **Command:** " + command + " " + Emojis.nuke + "\\n" + + Emojis.rightArrow + " **Denied:** " + successOrFail(denied) + "\\n" + + Emojis.rightArrow + " **Removed OP:** " + successOrFail(removedOp) + "\\n" + + Emojis.rightArrow + " **Banned:** " + successOrFail(banned) + "\\n"; + + DiscordWebhook webhook = new DiscordWebhook(Sentinel.webhook); + webhook.setAvatarUrl("https://r2.e-z.host/d440b58a-ba90-4839-8df6-8bba298cf817/3lwit5nt.png"); + webhook.setUsername("Sentinel Anti-Nuke | Logs"); + DiscordWebhook.EmbedObject embed = new DiscordWebhook.EmbedObject() + .setAuthor("Anti-Nuke has been triggered","","") + .setTitle("The use of a dangerous command has been detected!") + .setDescription(description) + .setColor(Color.RED); + webhook.addEmbed(embed); + try { + ServerUtils.sendDebugMessage("Executing webhook..."); + webhook.execute(); + } catch (IOException e) { + ServerUtils.sendDebugMessage(TextUtils.prefix("Epic webhook failure!!!")); + Sentinel.log.info(e.toString()); + } + } + public static void sendEmbedWarning(String player, Block b, boolean denied, boolean removedOp, boolean banned) { + ServerUtils.sendDebugMessage("Creating Block Webhook..."); + final String description = + Emojis.rightArrow + " **Player:** " + player + " " + Emojis.member + "\\n" + + Emojis.rightArrow + " **Block:** " + b.getType() + " " + Emojis.nuke + "\\n" + + Emojis.rightArrow + " **Denied:** " + successOrFail(denied) + "\\n" + + Emojis.rightArrow + " **Removed OP:** " + successOrFail(removedOp) + "\\n" + + Emojis.rightArrow + " **Banned:** " + successOrFail(banned) + "\\n"; + + DiscordWebhook webhook = new DiscordWebhook(Sentinel.webhook); + webhook.setAvatarUrl("https://r2.e-z.host/d440b58a-ba90-4839-8df6-8bba298cf817/3lwit5nt.png"); + webhook.setUsername("Sentinel Anti-Nuke | Logs"); + DiscordWebhook.EmbedObject embed = new DiscordWebhook.EmbedObject() + .setAuthor("Anti-Nuke has been triggered","","") + .setTitle("The use of a dangerous block has been detected!") + .setDescription(description) + .setColor(Color.RED); + webhook.addEmbed(embed); + try { + ServerUtils.sendDebugMessage("Executing webhook..."); + webhook.execute(); + } catch (IOException e) { + ServerUtils.sendDebugMessage(TextUtils.prefix("Epic webhook failure!!!")); + Sentinel.log.info(e.toString()); + } + + } + public static void sendEmbedWarning(String player, ItemStack item, boolean denied, boolean removedOp, boolean banned) { + ServerUtils.sendDebugMessage("Creating Webhook..."); + final String description = + Emojis.rightArrow + " **Player:** " + player + " " + Emojis.member + "\\n" + + Emojis.rightArrow + " **Item:** " + item.getType() + " " + Emojis.nuke + "\\n" + + Emojis.rightArrow + " **Denied:** " + successOrFail(denied) + "\\n" + + Emojis.rightArrow + " **Removed OP:** " + successOrFail(removedOp) + "\\n" + + Emojis.rightArrow + " **Banned:** " + successOrFail(banned) + "\\n"; + + DiscordWebhook webhook = new DiscordWebhook(Sentinel.webhook); + webhook.setAvatarUrl("https://r2.e-z.host/d440b58a-ba90-4839-8df6-8bba298cf817/3lwit5nt.png"); + webhook.setUsername("Sentinel Anti-Nuke | Logs"); + DiscordWebhook.EmbedObject embed = new DiscordWebhook.EmbedObject() + .setAuthor("Anti-Nuke has been triggered","","") + .setTitle("The use of a dangerous item has been detected!") + .setDescription(description) + .setColor(Color.BLUE); + webhook.addEmbed(embed); + try { + ServerUtils.sendDebugMessage("Executing webhook..."); + webhook.execute(); + } catch (IOException e) { + ServerUtils.sendDebugMessage(TextUtils.prefix("Epic webhook failure!!!")); + Sentinel.log.info(e.toString()); + } + } +} diff --git a/src/main/java/io/github/thetrouper/sentinel/events/ChatEvent.java b/src/main/java/io/github/thetrouper/sentinel/events/ChatEvent.java new file mode 100644 index 0000000..931b698 --- /dev/null +++ b/src/main/java/io/github/thetrouper/sentinel/events/ChatEvent.java @@ -0,0 +1,13 @@ +package io.github.thetrouper.sentinel.events; + +import io.github.thetrouper.sentinel.server.functions.AntiSpam; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.AsyncPlayerChatEvent; + +public class ChatEvent implements Listener { + @EventHandler + public static void onChat(AsyncPlayerChatEvent e) { + AntiSpam.handleAntiSpam(e); + } +} diff --git a/src/main/java/io/github/thetrouper/sentinel/events/CmdBlockEvents.java b/src/main/java/io/github/thetrouper/sentinel/events/CmdBlockEvents.java index 165cdbd..7163375 100644 --- a/src/main/java/io/github/thetrouper/sentinel/events/CmdBlockEvents.java +++ b/src/main/java/io/github/thetrouper/sentinel/events/CmdBlockEvents.java @@ -22,7 +22,7 @@ public class CmdBlockEvents implements Listener { Player p = e.getPlayer(); if (!Sentinel.isTrusted(p)) { e.setCancelled(true); - Sentinel.log.info("Use of a command block has been denied for " + p.getName() + " at X:" + b.getX() + " Y:" + b.getY() + " Z:" + b.getZ()); + DeniedActions.handleDeniedAction(p,b); } } } @@ -30,16 +30,16 @@ public class CmdBlockEvents implements Listener { private void onCMDBlockPlace(BlockPlaceEvent e) { if (!Sentinel.preventCmdBlocks) return; Block b = e.getBlockPlaced(); - if (b.getType() == Material.COMMAND_BLOCK || b.getType() == Material.REPEATING_COMMAND_BLOCK || b.getType() == Material.CHAIN_COMMAND_BLOCK) { + if (b.getType() == Material.COMMAND_BLOCK || b.getType() == Material.CHAIN_COMMAND_BLOCK || b.getType() == Material.REPEATING_COMMAND_BLOCK ) { Player p = e.getPlayer(); if (!Sentinel.isTrusted(p)) { e.setCancelled(true); - Sentinel.log.info("Placing a command block has been denied for " + p.getName() + " at X:" + b.getX() + " Y:" + b.getY() + " Z:" + b.getZ()); + DeniedActions.handleDeniedAction(p,b); } } } @EventHandler - private void onCMDBlockMinecartPlace(PlayerInteractEntityEvent e) { + private void onCMDBlockMinecartUse(PlayerInteractEntityEvent e) { if (!Sentinel.preventCmdBlocks) return; if (e.getRightClicked().getType() == EntityType.MINECART_COMMAND) { Player p = e.getPlayer(); diff --git a/src/main/java/io/github/thetrouper/sentinel/events/CommandEvent.java b/src/main/java/io/github/thetrouper/sentinel/events/CommandEvent.java index 3f4fe65..677c913 100644 --- a/src/main/java/io/github/thetrouper/sentinel/events/CommandEvent.java +++ b/src/main/java/io/github/thetrouper/sentinel/events/CommandEvent.java @@ -2,6 +2,8 @@ package io.github.thetrouper.sentinel.events; import io.github.thetrouper.sentinel.Sentinel; import io.github.thetrouper.sentinel.server.util.DeniedActions; +import io.github.thetrouper.sentinel.server.util.ServerUtils; +import io.github.thetrouper.sentinel.server.util.TextUtils; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; @@ -13,16 +15,22 @@ public class CommandEvent implements Listener { private void onCommand(PlayerCommandPreprocessEvent e) { Player p = e.getPlayer(); String command = e.getMessage().substring(1).split(" ")[0]; + ServerUtils.sendDebugMessage(TextUtils.prefix("Checking command")); if (Sentinel.isDangerousCommand(command)) { + ServerUtils.sendDebugMessage(TextUtils.prefix( "Command is dangerous")); if (!Sentinel.isTrusted(p)) { e.setCancelled(true); - DeniedActions.handleDeniedAction(p,command); + ServerUtils.sendDebugMessage(TextUtils.prefix("Command is canceled")); + DeniedActions.handleDeniedAction(p,e.getMessage()); } } if (Sentinel.blockSpecificCommands) { + ServerUtils.sendDebugMessage(TextUtils.prefix("Checking command for specific")); if (command.contains(":")) { + ServerUtils.sendDebugMessage(TextUtils.prefix("Checking is specific")); if (!Sentinel.isTrusted(p)) { e.setCancelled(true); + ServerUtils.sendDebugMessage(TextUtils.prefix("Command is canceled")); DeniedActions.handleDeniedAction(p,command); } } diff --git a/src/main/java/io/github/thetrouper/sentinel/server/functions/AntiSpam.java b/src/main/java/io/github/thetrouper/sentinel/server/functions/AntiSpam.java new file mode 100644 index 0000000..28e1bd8 --- /dev/null +++ b/src/main/java/io/github/thetrouper/sentinel/server/functions/AntiSpam.java @@ -0,0 +1,47 @@ +package io.github.thetrouper.sentinel.server.functions; + +import io.github.thetrouper.sentinel.server.util.GPTUtils; +import io.github.thetrouper.sentinel.server.util.TextUtils; +import org.bukkit.ChatColor; +import org.bukkit.entity.Player; +import org.bukkit.event.player.AsyncPlayerChatEvent; + +import java.util.HashMap; +import java.util.Map; + +import static io.github.thetrouper.sentinel.server.util.GPTUtils.calculateSimilarity; + +public class AntiSpam { + public static Map heatMap; + public static Map lastMessageMap; + + public static void enableAntiSpam() { + heatMap = new HashMap<>(); + lastMessageMap = new HashMap<>(); + } + public static void handleAntiSpam(AsyncPlayerChatEvent event) { + Player player = event.getPlayer(); + String message = event.getMessage(); + if (!heatMap.containsKey(player)) heatMap.put(player, 0); + if (heatMap.get(player) > 10) { + event.setCancelled(true); + player.sendMessage(TextUtils.prefix("Rate limit exceeded! Please wait before sending another message.")); + return; + } + if (lastMessageMap.containsKey(player)) { + String lastMessage = lastMessageMap.get(player); + double similarity = calculateSimilarity(message, lastMessage); + if (similarity > 0.5) heatMap.put(player, heatMap.get(player) + 3); + if (similarity > 0.9) heatMap.put(player, heatMap.get(player) + 6); + } + lastMessageMap.put(player, message); + } + public static void decayHeat() { + for (Player player : heatMap.keySet()) { + int heat = heatMap.get(player); + if (heat > 0) { + heatMap.put(player, heat - 1); + } + } + } +} diff --git a/src/main/java/io/github/thetrouper/sentinel/server/util/DeniedActions.java b/src/main/java/io/github/thetrouper/sentinel/server/util/DeniedActions.java index 61f8086..4d2bcde 100644 --- a/src/main/java/io/github/thetrouper/sentinel/server/util/DeniedActions.java +++ b/src/main/java/io/github/thetrouper/sentinel/server/util/DeniedActions.java @@ -1,6 +1,7 @@ package io.github.thetrouper.sentinel.server.util; import io.github.thetrouper.sentinel.Sentinel; +import io.github.thetrouper.sentinel.discord.WebhookSender; import net.md_5.bungee.api.chat.ClickEvent; import net.md_5.bungee.api.chat.HoverEvent; import net.md_5.bungee.api.chat.TextComponent; @@ -13,27 +14,42 @@ import org.bukkit.inventory.ItemStack; public class DeniedActions { private static String logMessage; + private static boolean banned; + private static boolean opRemoved; + private static boolean denied; + public static void handleDeniedAction(Player p, String command) { + ServerUtils.sendDebugMessage(TextUtils.prefix("Handling denied command...")); if (!Sentinel.logDangerousCommands) return; + ServerUtils.sendDebugMessage(TextUtils.prefix("LDC is enabled")); logMessage = "]==-- Sentinel --==[\n" + "A Dangerous command has been attempted!\n" + "Player: " + p.getName() + "\n" + "Command: " + command + "\n"; if (Sentinel.deop) { + ServerUtils.sendDebugMessage(TextUtils.prefix("Deoping player")); p.setOp(false); logMessage = logMessage + "Operator Removed: ✔\n"; + opRemoved = true; } else { logMessage = logMessage + "Operator Removed: ✘\n"; + opRemoved = false; } if (Sentinel.ban) { + ServerUtils.sendDebugMessage(TextUtils.prefix("Banning player")); Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "ban " + p.getName() + " ]=- Sentinel Anti-Grief -=[ You have been banned for attempting a dangerous command. Contact an administrator if you believe this to be a mistake."); logMessage = logMessage + "Banned: ✔\n"; + banned = true; } else { logMessage = logMessage + "Banned: ✘\n"; + banned = false; } + ServerUtils.sendDebugMessage(TextUtils.prefix("Sending log")); logMessage = logMessage + "Denied: ✔"; + denied = true; Sentinel.log.info(logMessage); notifyTrusted(p, command); + WebhookSender.sendEmbedWarning(p.getName(),command,denied,opRemoved,banned); } public static void handleDeniedAction(Player p, Block block) { if (!Sentinel.logCmdBlocks) return; @@ -44,18 +60,24 @@ public class DeniedActions { if (Sentinel.deop) { p.setOp(false); logMessage = logMessage + "Operator Removed: ✔\n"; + opRemoved = true; } else { logMessage = logMessage + "Operator Removed: ✘\n"; + opRemoved = false; } if (Sentinel.ban) { Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "ban " + p.getName() + " ]=- Sentinel Anti-Grief -=[ You have been banned for attempting to use dangerous blocks. Contact an administrator if you believe this to be a mistake."); logMessage = logMessage + "Banned: ✔\n"; + banned = true; } else { logMessage = logMessage + "Banned: ✘\n"; + banned = false; } logMessage = logMessage + "Denied: ✔"; + denied = true; Sentinel.log.info(logMessage); notifyTrusted(p, block); + WebhookSender.sendEmbedWarning(p.getName(),block,denied,opRemoved,banned); } public static void handleDeniedAction(Player p, ItemStack i) { if (!Sentinel.logNBT) return; @@ -66,18 +88,24 @@ public class DeniedActions { if (Sentinel.deop) { p.setOp(false); logMessage = logMessage + "Operator Removed: ✔\n"; + opRemoved = true; } else { logMessage = logMessage + "Operator Removed: ✘\n"; + opRemoved = false; } if (Sentinel.ban) { - Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "ban " + p.getName() + " ]=- Sentinel Anti-Grief -=[ You have been banned for attempting a dangerous command. Contact an administrator if you believe this to be a mistake."); + Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "ban " + p.getName() + " ]=- Sentinel Anti-Grief -=[ You have been banned for attempting to use an NBT item. Contact an administrator if you believe this to be a mistake."); logMessage = logMessage + "Banned: ✔\n"; + banned = true; } else { logMessage = logMessage + "Banned: ✘\n"; + banned = false; } logMessage = logMessage + "Denied: ✔"; + denied = true; Sentinel.log.info(logMessage); notifyTrusted(p, i); + WebhookSender.sendEmbedWarning(p.getName(),i,denied,opRemoved,banned); } private static void notifyTrusted(Player p, String command) { TextComponent message = new TextComponent(TextUtils.prefix(p.getName() + " has attempted a dangerous command!")); diff --git a/src/main/java/io/github/thetrouper/sentinel/server/util/GPTUtils.java b/src/main/java/io/github/thetrouper/sentinel/server/util/GPTUtils.java new file mode 100644 index 0000000..3ca9ee9 --- /dev/null +++ b/src/main/java/io/github/thetrouper/sentinel/server/util/GPTUtils.java @@ -0,0 +1,35 @@ +package io.github.thetrouper.sentinel.server.util; + +public class GPTUtils { + // I'd be surprised if anyone knew how tf this shi works, I just asked GPT to write it. + public static double calculateSimilarity(String str1, String str2) { + int len1 = str1.length(); + int len2 = str2.length(); + + int[][] dp = new int[len1 + 1][len2 + 1]; + + for (int i = 0; i <= len1; i++) { + dp[i][0] = i; + } + + for (int j = 0; j <= len2; j++) { + dp[0][j] = j; + } + + for (int i = 1; i <= len1; i++) { + for (int j = 1; j <= len2; j++) { + if (str1.charAt(i - 1) == str2.charAt(j - 1)) { + dp[i][j] = dp[i - 1][j - 1]; + } else { + dp[i][j] = 1 + Math.min(dp[i - 1][j - 1], Math.min(dp[i][j - 1], dp[i - 1][j])); + } + } + } + + int maxLen = Math.max(len1, len2); + int distance = dp[len1][len2]; + + double similarity = ((double) (maxLen - distance) / maxLen) * 100; + return similarity; + } +} diff --git a/src/main/java/io/github/thetrouper/sentinel/server/util/ServerUtils.java b/src/main/java/io/github/thetrouper/sentinel/server/util/ServerUtils.java index 58f3d9b..e1a295b 100644 --- a/src/main/java/io/github/thetrouper/sentinel/server/util/ServerUtils.java +++ b/src/main/java/io/github/thetrouper/sentinel/server/util/ServerUtils.java @@ -5,6 +5,7 @@ package io.github.thetrouper.sentinel.server.util; import io.github.thetrouper.sentinel.Sentinel; +import io.github.thetrouper.sentinel.commands.InfoCommand; import org.bukkit.Bukkit; import org.bukkit.entity.Player; @@ -17,9 +18,16 @@ import java.util.Set; * Server utils */ public abstract class ServerUtils { - - - + public static void sendDebugMessage(String message) { + if (InfoCommand.debugmode) { + Sentinel.log.info(message); + for (Player trustedPlayer : Bukkit.getOnlinePlayers()) { + if (Sentinel.isTrusted(trustedPlayer)) { + trustedPlayer.sendMessage(message); + } + } + } + } /** * List of names of online players * @return list of names diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 926f776..57c3473 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,18 +1,27 @@ -# -# -# Configure the plugin here, the default config may not be adequate to your needs. -# -# -config: +# Sentinel 0.0.2 +# ____ __ ___ +#/\ _`\ /\ \__ __ /\_ \ +#\ \,\L\_\ __ ___\ \ ,_\/\_\ ___ __\//\ \ +# \/_\__ \ /'__`\/' _ `\ \ \/\/\ \ /' _ `\ /'__`\\ \ \ +# /\ \L\ \/\ __//\ \/\ \ \ \_\ \ \/\ \/\ \/\ __/ \_\ \_ +# \ `\____\ \____\ \_\ \_\ \__\\ \_\ \_\ \_\ \____\/\____\ +# \/_____/\/____/\/_/\/_/\/__/ \/_/\/_/\/_/\/____/\/____/ +# ]======------ Configuration & Setup Guide ------=====[ +# Sentinel is inspired by WickBot.com +# Be sure to check out their amazing discord bot! +config : plugin: + # ------------------------------- + # Important Setup (Do this first) + # ------------------------------- prefix: "§d§lSentinel §8» §7" # Prefix of the plugin. Line below is the discord webhook for logs to be sent to - webhook: "https://discord.com/api/webhooks/1124908469842096211/https://discord.com/api/webhooks/1124908469842096211/7NGOeFvtmxQ4n0_hSvbqhZUjnzRHIicLpHKETYU92n9JaLUPPsueBSn7w4wUfAnhjlLF" + webhook: "https://discord.com/api/webhooks/1124908469842096211/7NGOeFvtmxQ4n0_hSvbqhZUjnzRHIicLpHKETYU92n9JaLUPPsueBSn7w4wUfAnhjlLF" trusted: # List the UUIDs of players who are trusted, will bypass the plugin and be immune to logs and are able to re-op themeselves - "049460f7-21cb-42f5-8059-d42752bf406f" # obvWolf block-specific: true # Defaulted true | Weather or not to block ALL plugin specific commands from non-trusted members (EX: minecraft:ban) these will not be logged. prevent-nbt: true # Defaulted true | Should NBT items be blocked from the creative hotbar log-nbt: true # Defaulted true | Should items and their NBT's be logged - prevent-cmdblocks: true # Defaulted true | Should all command block actions be blocked + prevent-cmdblocks: true # Defaulted true | Should all command block actions be blocked log-cmdblocks: true # Defaulted true | Log attempts of command-block place-ery dangerous: # These commands can only be run by "trusted" users - "op" @@ -27,4 +36,14 @@ config: - "give" deop: true # Defaulted true | This will remove an untrusted player's operator permissions whenever they attempt dangerous actions ban: false # Defaulted false | This will ban a player when they attempt dangerous actions - reop-command: false # Defaulted false | This enables the command allowing trusted players to op themselves if they get deoped. \ No newline at end of file + reop-command: false # Defaulted false | This enables the command allowing trusted players to op themselves if they get deoped. + # ------------------------------- + # Chat Filter Setup & AntiSpam + # ------------------------------- + chat: + # AntiSpam Heat system + anti-spam: true # Default true | Enables the anti-spam + default-gain: 1 # Heat gained as base for every message + medium-gain: 3 # Heat gained when your message is 50% similar + high-gain: 6 # Heat gained for + max-heat: 15 # Highest value of heat a player can reach \ No newline at end of file