diff --git a/src/main/java/io/github/thetrouper/sentinel/Sentinel.java b/src/main/java/io/github/thetrouper/sentinel/Sentinel.java index 7c52a00..f28f323 100644 --- a/src/main/java/io/github/thetrouper/sentinel/Sentinel.java +++ b/src/main/java/io/github/thetrouper/sentinel/Sentinel.java @@ -110,6 +110,7 @@ public final class Sentinel extends JavaPlugin { new ReplyCommand().register(); new ReopCommand().register(); new SocialSpyCommand().register(); + new ChatClickCallback().register(); // Events manager.registerEvents(new CommandEvent(),this); diff --git a/src/main/java/io/github/thetrouper/sentinel/commands/ChatClickCallback.java b/src/main/java/io/github/thetrouper/sentinel/commands/ChatClickCallback.java new file mode 100644 index 0000000..3300f11 --- /dev/null +++ b/src/main/java/io/github/thetrouper/sentinel/commands/ChatClickCallback.java @@ -0,0 +1,56 @@ +/** + * This file is for tutorial purposes made by ImproperIssues. Distribute if you want :) + */ + +package io.github.thetrouper.sentinel.commands; + +import io.github.thetrouper.sentinel.server.functions.ProfanityFilter; +import io.github.thetrouper.sentinel.server.functions.ReportFalsePositives; +import io.github.thetrouper.sentinel.server.util.Cooldown; +import io.github.thetrouper.sentinel.server.util.TextUtils; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.event.player.AsyncPlayerChatEvent; + +import java.util.HashSet; +import java.util.Map; +import java.util.UUID; + +/** + * Example command + */ +public class ChatClickCallback extends CustomCommand { + public static Cooldown fpReportCooldown; + public ChatClickCallback() { + super("sentinelcallback"); + this.setPrintStacktrace(true); + } + + @Override + public void dispatchCommand(CommandSender sender, Command command, String label, String[] args) { + Player p = (Player) sender; + switch (args[0]) { + case "fpreport" -> { + if (fpReportCooldown.isOnCooldown(p.getUniqueId()) && !p.isOp()) { + p.sendMessage(TextUtils.prefix("This action is on cooldown! " + fpReportCooldown.getCooldown(p.getUniqueId()))); + } else { + ReportFalsePositives.sendFalsePositiveReport(args[1]); + p.sendMessage(TextUtils.prefix("Successfully reported a false positive!")); + } + } + } + } + + @Override + public void registerCompletions(CompletionBuilder builder) { + builder.addCompletion(1,"you"); + builder.addCompletion(1,"must"); + builder.addCompletion(1,"be"); + builder.addCompletion(1,"called"); + builder.addCompletion(1,"before"); + builder.addCompletion(1,"running"); + builder.addCompletion(1,"a"); + builder.addCompletion(1,"callback"); + } +} diff --git a/src/main/java/io/github/thetrouper/sentinel/commands/SentinelCommand.java b/src/main/java/io/github/thetrouper/sentinel/commands/SentinelCommand.java index 309a420..6808f8e 100644 --- a/src/main/java/io/github/thetrouper/sentinel/commands/SentinelCommand.java +++ b/src/main/java/io/github/thetrouper/sentinel/commands/SentinelCommand.java @@ -5,6 +5,7 @@ package io.github.thetrouper.sentinel.commands; import io.github.thetrouper.sentinel.server.functions.ProfanityFilter; +import io.github.thetrouper.sentinel.server.functions.ReportFalsePositives; import io.github.thetrouper.sentinel.server.util.TextUtils; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; @@ -12,6 +13,8 @@ import org.bukkit.entity.Player; import org.bukkit.event.player.AsyncPlayerChatEvent; import java.util.HashSet; +import java.util.Map; +import java.util.UUID; /** * Example command diff --git a/src/main/java/io/github/thetrouper/sentinel/server/functions/ProfanityFilter.java b/src/main/java/io/github/thetrouper/sentinel/server/functions/ProfanityFilter.java index 3b090f7..05e32e4 100644 --- a/src/main/java/io/github/thetrouper/sentinel/server/functions/ProfanityFilter.java +++ b/src/main/java/io/github/thetrouper/sentinel/server/functions/ProfanityFilter.java @@ -41,31 +41,31 @@ public class ProfanityFilter { ServerUtils.sendDebugMessage("AntiSwear Flag, Message: " + message + " Concentrated: " + fullSimplify(message) + " Severity: " + severity + " Previous Score: " + scoreMap.get(p) +" Adding Score: " + Config.lowScore); scoreMap.put(p, scoreMap.get(p) + Config.lowScore); e.setCancelled(true); - blockSwear(p,highlightProfanity(message),message,severity); + blockSwear(p,highlightProfanity(message),message,severity,e); } case "medium-low" -> { ServerUtils.sendDebugMessage("AntiSwear Flag, Message: " + message + " Concentrated: " + fullSimplify(message) + " Severity: " + severity + " Previous Score: " + scoreMap.get(p) +" Adding Score: " + Config.mediumLowScore); scoreMap.put(p, scoreMap.get(p) + Config.mediumLowScore); e.setCancelled(true); - blockSwear(p,highlightProfanity(message),message,severity); + blockSwear(p,highlightProfanity(message),message,severity,e); } case "medium" -> { ServerUtils.sendDebugMessage("AntiSwear Flag, Message: " + message + " Concentrated: " + fullSimplify(message) + " Severity: " + severity + " Previous Score: " + scoreMap.get(p) +" Adding Score: " + Config.mediumScore); scoreMap.put(p, scoreMap.get(p) + Config.mediumScore); e.setCancelled(true); - blockSwear(p,highlightProfanity(message),message,severity); + blockSwear(p,highlightProfanity(message),message,severity,e); } case "medium-high" -> { ServerUtils.sendDebugMessage("AntiSwear Flag, Message: " + message + " Concentrated: " + fullSimplify(message) + " Severity: " + severity + " Previous Score: " + scoreMap.get(p) +" Adding Score: " + Config.mediumHighScore); scoreMap.put(p, scoreMap.get(p) + Config.mediumHighScore); e.setCancelled(true); - blockSwear(p,highlightProfanity(message),message,severity); + blockSwear(p,highlightProfanity(message),message,severity,e); } case "high" -> { ServerUtils.sendDebugMessage("AntiSwear Flag, Message: " + message + " Concentrated: " + fullSimplify(message) + " Severity: " + severity + " Previous Score: " + scoreMap.get(p) +" Adding Score: " + Config.highScore); scoreMap.put(p, scoreMap.get(p) + Config.highScore); e.setCancelled(true); - blockSwear(p,highlightProfanity(message),message,severity); + blockSwear(p,highlightProfanity(message),message,severity,e); } case "slur" -> { // Insta-Punish @@ -107,14 +107,14 @@ public class ProfanityFilter { }); if (Config.logSwear) WebhookSender.sendSlurLog(player,origMessage,scoreMap.get(player)); } - public static void blockSwear(Player player, String highlightedMSG, String origMessage, String severity) { + public static void blockSwear(Player player, String highlightedMSG, String origMessage, String severity, AsyncPlayerChatEvent e) { player.sendMessage(TextUtils.prefix(("§cPlease do not swear in chat! Attempting to bypass this filter will result in a mute!"))); - String hover = ("§bOriginal: §f" + origMessage + "\n§bSanitized: §f" + highlightedMSG + "\n§bSeverity: §c" + severity + "\n§7§o(click to copy)"); + String hover = ("§bOriginal: §f" + origMessage + "\n§bSanitized: §f" + highlightedMSG + "\n§bSeverity: §c" + severity + "\n§7§o(click to report false positive)"); TextComponent text = new TextComponent(); text.setText(TextUtils.prefix( ("§b§n" + player.getName() + "§7 has triggered the anti-swear! §8(§c" + scoreMap.get(player) + "§7/§4" + Config.punishScore + "§8)"))); text.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, TextComponent.fromLegacyText(hover))); - text.setClickEvent(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, origMessage)); + text.setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "sentinelcallback fpreport " + ReportFalsePositives.generateReport(e))); ServerUtils.forEachStaff(staff -> { staff.spigot().sendMessage(text); @@ -221,13 +221,10 @@ public class ProfanityFilter { return "safe"; } - private static String removeFalsePositives(String text) { - for (String falsePositive : swearWhitelist) { - text = text.replace(falsePositive, ""); - } - return text; - } + public static boolean ContainsProfanity(String text) { + return containsSwears(text) || containsSlurs(text); + } private static boolean containsSwears(String text) { ServerUtils.sendDebugMessage("Debug: [AntiSwear] Checking for swears: " + swearBlacklist.toString()); for (String swear : swearBlacklist) { @@ -242,23 +239,28 @@ public class ProfanityFilter { } return false; } - - private static String convertLeetSpeakCharacters(String text) { + public static String removeFalsePositives(String text) { + for (String falsePositive : swearWhitelist) { + text = text.replace(falsePositive, ""); + } + return text; + } + public static String convertLeetSpeakCharacters(String text) { text = TextUtils.fromLeetString(text); return text; } - private static String stripSpecialCharacters(String text) { + public static String stripSpecialCharacters(String text) { text = text.replaceAll("[^A-Za-z0-9.,!?;:'\"()\\[\\]{}]", "").trim(); return text; } - private static String simplifyRepeatingLetters(String text) { + public static String simplifyRepeatingLetters(String text) { text = TextUtils.replaceRepeatingLetters(text); return text; } - private static String removePeriodsAndSpaces(String text) { + public static String removePeriodsAndSpaces(String text) { return text.replaceAll("[^A-Za-z0-9]", "").replace(" ", ""); } public static void decayScore() { diff --git a/src/main/java/io/github/thetrouper/sentinel/server/functions/ReportFalsePositives.java b/src/main/java/io/github/thetrouper/sentinel/server/functions/ReportFalsePositives.java new file mode 100644 index 0000000..1b69073 --- /dev/null +++ b/src/main/java/io/github/thetrouper/sentinel/server/functions/ReportFalsePositives.java @@ -0,0 +1,84 @@ +package io.github.thetrouper.sentinel.server.functions; + +import io.github.thetrouper.sentinel.Sentinel; +import io.github.thetrouper.sentinel.data.Config; +import io.github.thetrouper.sentinel.data.Emojis; +import io.github.thetrouper.sentinel.discord.DiscordWebhook; +import io.github.thetrouper.sentinel.server.util.Randomizer; +import io.github.thetrouper.sentinel.server.util.ServerUtils; +import io.github.thetrouper.sentinel.server.util.TextUtils; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.player.AsyncPlayerChatEvent; + +import java.awt.*; +import java.io.IOException; +import java.util.Map; + +public class ReportFalsePositives { + public static Map reportMap; + public static String generateReport(AsyncPlayerChatEvent e) { + final long reportLong = Randomizer.generateID(); + final String reportID = Long.toString(reportLong); + ServerUtils.sendDebugMessage(TextUtils.prefix("DEBUG: Generating chat filter report")); + reportMap.put(reportID,e); + ServerUtils.sendDebugMessage(TextUtils.prefix("DEBUG: Generated report. ID:" + reportID + " Message: \"" + reportMap.get(reportID).getMessage() + "\" Expires in 60 seconds")); + Bukkit.getScheduler().runTaskLater(Sentinel.getInstance(),()->{ + reportMap.remove(reportID); + ServerUtils.sendDebugMessage(TextUtils.prefix("DEBUG: A report expired. ID: " + reportID)); + },60000); + return reportID; + } + public static void sendFalsePositiveReport(String reportID) { + AsyncPlayerChatEvent e = reportMap.get(reportID); + String orig = e.getMessage(); + + String lowercasedText = orig.toLowerCase(); + String remFP = ProfanityFilter.removeFalsePositives(lowercasedText); + String convertedLeet = ProfanityFilter.convertLeetSpeakCharacters(remFP); + String remSpecials = ProfanityFilter.stripSpecialCharacters(convertedLeet); + String simplifyRep = ProfanityFilter.simplifyRepeatingLetters(remSpecials); + String sanitized = ProfanityFilter. removePeriodsAndSpaces(simplifyRep); + + sendEmbed(e.getPlayer(),orig,lowercasedText,remFP,convertedLeet,remSpecials,simplifyRep,sanitized); + + } + + public static void sendEmbed(Player player, + String message, + String lowercased, + String remFP, + String convertedLeet, + String remSpecials, + String simplifyRep, + String sanitized) { + ServerUtils.sendDebugMessage("Creating FalsePositive Webhook..."); + DiscordWebhook webhook = new DiscordWebhook(Config.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-Swear False Positive","","") + .setTitle("Flag Report:") + .setDescription( + Emojis.rightSort + "Player: " + player.getName() + " " + Emojis.target + "\\n" + + Emojis.space + Emojis.arrowRight + "UUID: `" + player.getUniqueId() + "`\\n" + ) + .addField("Original Message", "`" + message + "` " + (ProfanityFilter.ContainsProfanity(message) ? Emojis.alarm : ""), false) + .addField("Lowercase", "`" + lowercased + "` " + (ProfanityFilter.ContainsProfanity(lowercased) ? Emojis.alarm : ""), false) + .addField("Removed FPs", "`" + remFP + "` " + (ProfanityFilter.ContainsProfanity(remFP) ? Emojis.alarm : ""), false) + .addField("Converted Leet", "`" + convertedLeet + "` " + (ProfanityFilter.ContainsProfanity(convertedLeet) ? Emojis.alarm : ""), false) + .addField("Removed Specials", "`" + remSpecials + "` " + (ProfanityFilter.ContainsProfanity(remSpecials) ? Emojis.alarm : ""), false) + .addField("Simplify Repeats", "`" + simplifyRep + "` " + (ProfanityFilter.ContainsProfanity(simplifyRep) ? Emojis.alarm : ""), false) + .addField("Fully Sanitized Message", ProfanityFilter.highlightProfanity(sanitized,"`", "`") + " " + Emojis.noDM, false) + .setColor(Color.green) + .setThumbnail("https://crafatar.com/avatars/" + player.getUniqueId() + "?size=64&&overlay"); + 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/server/util/Cooldown.java b/src/main/java/io/github/thetrouper/sentinel/server/util/Cooldown.java new file mode 100644 index 0000000..f772a05 --- /dev/null +++ b/src/main/java/io/github/thetrouper/sentinel/server/util/Cooldown.java @@ -0,0 +1,38 @@ +package io.github.thetrouper.sentinel.server.util; + +import java.util.HashMap; +import java.util.Map; + +public class Cooldown { + + private final Map timer; + + public Cooldown() { + this.timer = new HashMap<>(); + } + + private O getOrDefault(O value, O def) { + return value != null ? value : def; + } + + public long getCooldown(T obj) { + return Math.max(getOrDefault(timer.get(obj), 0L) - System.currentTimeMillis(), 0L); + } + + public double getCooldownSec(T obj) { + final long cooldown = this.getCooldown(obj); + return MathUtils.round(cooldown / 1000.0, 100); + } + + public boolean isOnCooldown(T obj) { + return getCooldown(obj) > 0L; + } + + public void setCooldown(T obj, long millis) { + timer.put(obj, System.currentTimeMillis() + millis); + } + + public void addCooldown(T obj, long millis) { + setCooldown(obj, getCooldown(obj) + millis); + } +} diff --git a/src/main/java/io/github/thetrouper/sentinel/server/util/MathUtils.java b/src/main/java/io/github/thetrouper/sentinel/server/util/MathUtils.java new file mode 100644 index 0000000..e33c4c2 --- /dev/null +++ b/src/main/java/io/github/thetrouper/sentinel/server/util/MathUtils.java @@ -0,0 +1,23 @@ +package io.github.thetrouper.sentinel.server.util; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +public final class MathUtils { + + public static double avg(Integer... ints) { + final List list = Arrays.stream(ints).filter(Objects::nonNull).toList(); + return avg(list); + } + + public static double avg(List ints) { + double sum = 0.0; + for (Integer i : ints) sum += i; + return sum / ints.size(); + } + + public static double round(double value, int nthPlace) { + return Math.floor(value * nthPlace) / nthPlace; + } +}