From dd2a4e772fbaebadbe271ce5cae1bc9d78b7d0c9 Mon Sep 17 00:00:00 2001 From: TheTrouper <93684527+thetrouper@users.noreply.github.com> Date: Tue, 17 Oct 2023 17:01:12 -0500 Subject: [PATCH] New similarity calculator (Levenshtein distance) + fixes to the dictionary and chat notifications. Debug commands need work, and then testing for release can begin --- .../sentinel/commands/SentinelCommand.java | 61 ++++++++++++++----- .../github/thetrouper/sentinel/data/FAT.java | 2 +- .../sentinel/data/FilterAction.java | 12 ++-- .../sentinel/server/functions/AntiSpam.java | 38 ++++++------ .../sentinel/server/util/GPTUtils.java | 33 ++++++++++ src/main/resources/lang/en_us.json | 14 ++--- 6 files changed, 111 insertions(+), 49 deletions(-) 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 1c57311..9d2872b 100644 --- a/src/main/java/io/github/thetrouper/sentinel/commands/SentinelCommand.java +++ b/src/main/java/io/github/thetrouper/sentinel/commands/SentinelCommand.java @@ -4,8 +4,12 @@ package io.github.thetrouper.sentinel.commands; +import io.github.thetrouper.sentinel.Sentinel; +import io.github.thetrouper.sentinel.data.Config; +import io.github.thetrouper.sentinel.server.functions.AntiSpam; import io.github.thetrouper.sentinel.server.functions.ProfanityFilter; import io.github.thetrouper.sentinel.server.util.Text; +import org.bukkit.Bukkit; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; @@ -28,28 +32,53 @@ public class SentinelCommand extends CustomCommand { public void dispatchCommand(CommandSender sender, Command command, String label, String[] args) { Player p = (Player) sender; switch (args[0]) { - case "debugmode" -> { - debugmode = !debugmode; - p.sendMessage(Text.prefix((debugmode ? "enabled" : "disabled") + " debug mode.")); - } - case "testantiswear" -> { - HashSet players = new HashSet<>(); - players.add((Player) sender); - String msg = ""; - for (int i = 1; i < args.length; i++) { - msg = msg.concat(" " + args[i]); + case "debug" -> { + switch (args[1]) { + case "testantiswear" -> { + HashSet players = new HashSet<>(); + players.add((Player) sender); + String msg = ""; + for (int i = 1; i < args.length; i++) { + msg = msg.concat(" " + args[i]); + } + msg = msg.trim(); + AsyncPlayerChatEvent e = new AsyncPlayerChatEvent(true, (Player) sender, msg, players); + ProfanityFilter.handleProfanityFilter(e); + } + case "testantispam" -> { + HashSet players = new HashSet<>(); + players.add((Player) sender); + String msg = ""; + for (int i = 1; i < args.length; i++) { + msg = msg.concat(" " + args[i]); + } + msg = msg.trim(); + AsyncPlayerChatEvent e = new AsyncPlayerChatEvent(true, (Player) sender, msg, players); + AntiSpam.handleAntiSpam(e); + } + case "testlang" -> { + p.sendMessage(Sentinel.dict.get("exmaple-message")); + } + case "toggle" -> { + debugmode = !debugmode; + p.sendMessage(Text.prefix((debugmode ? "enabled" : "disabled") + " debug mode.")); + } } - msg = msg.trim(); - AsyncPlayerChatEvent e = new AsyncPlayerChatEvent(true, (Player) sender, msg, players); - ProfanityFilter.handleProfanityFilter(e); + } + case "getHeat" -> { + Player target = Bukkit.getPlayer(args[1]); + if (target == null) { + p.sendMessage(Text.prefix("Invalid Player!")); + return; + } + p.sendMessage(Text.prefix("Heat of " + target.getName() + ": &8(&c" + AntiSpam.heatMap.get(target) + "&7/&4" + Config.punishHeat + "&8)")); } } } @Override public void registerCompletions(CompletionBuilder builder) { - builder.addCompletion(1,"debugmode"); - builder.addCompletion(1,"testantiswear"); - + builder.addCompletion(1,"debug"); + builder.addCompletion(1,"getHeat"); } } diff --git a/src/main/java/io/github/thetrouper/sentinel/data/FAT.java b/src/main/java/io/github/thetrouper/sentinel/data/FAT.java index 7fab4ba..1f95665 100644 --- a/src/main/java/io/github/thetrouper/sentinel/data/FAT.java +++ b/src/main/java/io/github/thetrouper/sentinel/data/FAT.java @@ -6,7 +6,7 @@ public enum FAT { // I couldn't miss the opportunity to call the "Filter Action Type" FAT // Its rly just to make the tab completion of FilterAction easier BLOCK_SWEAR("Sentinel Profanity Filter",null,"swear-block-warn", "swear-block-notification", null,null), - BLOCK_SPAM("Sentinel Anti-Spam", null, "spam-warning", "spam-notification",null,null), + BLOCK_SPAM("Sentinel Anti-Spam", null, "spam-block-warn", "spam-notification",null,null), SWEAR("Sentinel Anti-Swear Log","Anti-Swear", "profanity-mute-warn", "profanity-mute-notification", Config.swearPunishCommand, Color.orange), SLUR("Sentinel Anti-Slur Log", "Anti-Slur", "slur-mute-warn", "slur-mute-notification", Config.strictPunishCommand, Color.red), SPAM("Sentinel Anti-Spam Log", "Anti-Spam", "spam-mute-warn", "spam-mute-notification", Config.spamPunishCommand, Color.pink); diff --git a/src/main/java/io/github/thetrouper/sentinel/data/FilterAction.java b/src/main/java/io/github/thetrouper/sentinel/data/FilterAction.java index 268f8fe..ee2dd50 100644 --- a/src/main/java/io/github/thetrouper/sentinel/data/FilterAction.java +++ b/src/main/java/io/github/thetrouper/sentinel/data/FilterAction.java @@ -35,13 +35,15 @@ public class FilterAction { fs.setRoundingMode(RoundingMode.DOWN); TextComponent notif = new TextComponent(); - notif.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, TextComponent.fromLegacyText((type != FAT.SPAM ? Sentinel.dict.get("severity-notification-hover").formatted(e.getMessage(), highlighted, severity) : Sentinel.dict.get("spam-notification-hover").formatted(e.getMessage(),lastMessageMap.get(offender),fs.format(similarity)))))); + notif.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, TextComponent.fromLegacyText((type != FAT.SPAM && type != FAT.BLOCK_SPAM ? Sentinel.dict.get("severity-notification-hover").formatted(e.getMessage(), highlighted, severity) : Sentinel.dict.get("spam-notification-hover").formatted(e.getMessage(),lastMessageMap.get(offender),fs.format(similarity)))))); notif.setClickEvent(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/sentinelcallback fpreport " + report)); warn.setText(Text.prefix(Sentinel.dict.get(type.getWarnTranslationKey()))); offender.spigot().sendMessage(warn); - notif.setText(Text.prefix(Sentinel.dict.get(type.getNotifTranslationKey()).formatted(offender.getName(), scoreMap.get(offender), Config.punishScore))); + String notiftext = Sentinel.dict.get(type.getNotifTranslationKey()); + + notif.setText(Text.prefix((type != FAT.SPAM && type != FAT.BLOCK_SPAM ? notiftext.formatted(offender.getName(), scoreMap.get(offender), Config.punishScore) : notiftext.formatted(offender.getName(),heatMap.get(offender),Config.punishHeat)))); ServerUtils.forEachStaff(staffmember -> { staffmember.spigot().sendMessage(notif); }); @@ -67,10 +69,10 @@ public class FilterAction { public static void sendConsoleLog(Player offender, AsyncPlayerChatEvent e, FAT type) { String log = "]=-" + type.getTitle() + "-=[\n" + "Player: " + offender.getName() + - (type != FAT.BLOCK_SPAM ? "> Score: `" + scoreMap.get(offender) + "/" + Config.punishScore : "> Heat: `" + heatMap.get(offender) + "/" + Config.punishHeat) + "\n" + + (type != FAT.BLOCK_SPAM && type != FAT.SPAM ? "> Score: `" + scoreMap.get(offender) + "/" + Config.punishScore : "> Heat: `" + heatMap.get(offender) + "/" + Config.punishHeat) + "\n" + "> UUID: " + offender.getUniqueId() + "\n" + - (type != FAT.BLOCK_SPAM ? "Message: " + e.getMessage() : "Previous: " + lastMessageMap.get(offender)) + "\n" + - (type != FAT.BLOCK_SPAM ? "Reduced: " + fullSimplify(e.getMessage()) : "Current: " + e.getMessage()) + "\n" + + (type != FAT.BLOCK_SPAM && type != FAT.SPAM ? "Message: " + e.getMessage() : "Previous: " + lastMessageMap.get(offender)) + "\n" + + (type != FAT.BLOCK_SPAM && type != FAT.SPAM ? "Reduced: " + fullSimplify(e.getMessage()) : "Current: " + e.getMessage()) + "\n" + (type.getExecutedCommand() != null ? "Executed: " + type.getExecutedCommand() : "Executed: Nothing, its a standard flag. You shouldn't be seeing this, please report it."); Sentinel.log.info(log); } 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 index 40f39f0..366acae 100644 --- a/src/main/java/io/github/thetrouper/sentinel/server/functions/AntiSpam.java +++ b/src/main/java/io/github/thetrouper/sentinel/server/functions/AntiSpam.java @@ -1,25 +1,16 @@ 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.FAT; import io.github.thetrouper.sentinel.data.FilterAction; -import io.github.thetrouper.sentinel.discord.WebhookSender; import io.github.thetrouper.sentinel.server.util.GPTUtils; -import io.github.thetrouper.sentinel.server.util.ServerUtils; import io.github.thetrouper.sentinel.server.util.Text; -import net.md_5.bungee.api.chat.HoverEvent; -import net.md_5.bungee.api.chat.TextComponent; import org.bukkit.entity.Player; import org.bukkit.event.player.AsyncPlayerChatEvent; -import java.math.RoundingMode; -import java.text.DecimalFormat; 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; @@ -31,27 +22,34 @@ public class AntiSpam { public static void handleAntiSpam(AsyncPlayerChatEvent e) { Player p = e.getPlayer(); String message = Text.removeFirstColor(e.getMessage()); - Double sim = calculateSimilarity(e.getMessage(),lastMessageMap.get(p)); - if (!heatMap.containsKey(p)) heatMap.put(p, 0); + + if (!lastMessageMap.containsKey(p)) { + lastMessageMap.put(p,"/* Placeholder Message from Sentinel */"); + } + if (!heatMap.containsKey(p)) { + heatMap.put(p,0); + } + + if (lastMessageMap.containsKey(p)) { + String lastMessage = lastMessageMap.get(p); + double similarity = GPTUtils.calcSim(message, lastMessage); + if (similarity > 0.25) heatMap.put(p, heatMap.get(p) + Config.lowGain); + if (similarity > 0.5) heatMap.put(p, heatMap.get(p) + Config.mediumGain); + if (similarity > 0.9) heatMap.put(p, heatMap.get(p) + Config.highGain); + } + if (heatMap.get(p) > Config.punishHeat) { e.setCancelled(true); - FilterAction.filterAction(p,e,null,null, sim, FAT.SPAM); + FilterAction.filterAction(p,e,null,null, GPTUtils.calcSim(e.getMessage(),lastMessageMap.get(p)), FAT.SPAM); return; } if (heatMap.get(p) > Config.blockHeat) { e.setCancelled(true); - FilterAction.filterAction(p,e,null,null, sim, FAT.BLOCK_SPAM); + FilterAction.filterAction(p,e,null,null, GPTUtils.calcSim(e.getMessage(),lastMessageMap.get(p)), FAT.BLOCK_SPAM); heatMap.put(p, heatMap.get(p) + Config.highGain); return; } - if (lastMessageMap.containsKey(p)) { - String lastMessage = lastMessageMap.get(p); - double similarity = calculateSimilarity(message, lastMessage); - if (similarity > 0.25) heatMap.put(p, heatMap.get(p) + Config.lowGain); - if (similarity > 0.5) heatMap.put(p, heatMap.get(p) + Config.mediumGain); - if (similarity > 0.9) heatMap.put(p, heatMap.get(p) + Config.highGain); - } lastMessageMap.put(p, message); } public static void decayHeat() { 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 index 3ca9ee9..1f2cf4d 100644 --- a/src/main/java/io/github/thetrouper/sentinel/server/util/GPTUtils.java +++ b/src/main/java/io/github/thetrouper/sentinel/server/util/GPTUtils.java @@ -32,4 +32,37 @@ public class GPTUtils { double similarity = ((double) (maxLen - distance) / maxLen) * 100; return similarity; } + + public static double calcSim(String s1, String s2) { + int maxLength = Math.max(s1.length(), s2.length()); + if (maxLength == 0) { + return 100.0; + } + + int distance = calculateLevenshteinDistance(s1, s2); + double similarity = ((double) (maxLength - distance) / maxLength) * 100.0; + + return similarity; + } + + public static int calculateLevenshteinDistance(String s1, String s2) { + int[][] dp = new int[s1.length() + 1][s2.length() + 1]; + + for (int i = 0; i <= s1.length(); i++) { + dp[i][0] = i; + } + + for (int j = 0; j <= s2.length(); j++) { + dp[0][j] = j; + } + + for (int i = 1; i <= s1.length(); i++) { + for (int j = 1; j <= s2.length(); j++) { + int cost = (s1.charAt(i - 1) == s2.charAt(j - 1)) ? 0 : 1; + dp[i][j] = Math.min(Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1), dp[i - 1][j - 1] + cost); + } + } + + return dp[s1.length()][s2.length()]; + } } diff --git a/src/main/resources/lang/en_us.json b/src/main/resources/lang/en_us.json index 924dba0..eeabbd3 100644 --- a/src/main/resources/lang/en_us.json +++ b/src/main/resources/lang/en_us.json @@ -14,25 +14,25 @@ "no-user-reply" : "§cYou have nobody to reply to!", "spy-enabled" : "SocialSpy is now enabled.", "spy-disabled" : "SocialSpy is now disabled.", - "spam-warning" : "Do not spam in chat! Please wait before sending another message.", "action-automatic" : "§7This action was preformed automatically\n§7by the §bSentinel Anti-Spam§7 algorithm.", - "spam-notification" : "§b§n%1$s§7 might be spamming! §8(§c%2$s§7/§4%3$s§8)", - "spam-notification-hover" : "§8]==-- §d§lSentinel §8--==[\n§bPrevious: §f%1$s\n§bCurrent: §f%2$s\n§bSimilarity §f%3$s", - "spam-punished" : "§cYou have been auto-punished for violating the anti-spam repetitively!", - "spam-punish-notification" : "§b§n%1$s§7 has been auto-muted by the anti spam! §8(§c%2$s§7/§4%3$s§8)", + "action-automatic-reportable" : "§7This action was preformed automatically \n§7by the §bSentinel Profanity Filter§7 algorithm!\n§8§o(Click to report false positive)", "unicode-warn" : "§cDo not send non standard unicode in chat!", "message-sent" : "§d§lMessage §8» §b[§fYou §e>§f %1$s§b] §7%2$s", "message-received" : "§d§lMessage §8» §b[§f%1$s §e>§f You§b] §7%2$s", "spy-message" : "§d§lSpy §8» §b§n%1$s§7 has messaged §b§n%2$s§7.", "spy-message-hover" : "§8]==-- §d§lSocialSpy §8--==[\n§bSender: §f%1$S\n§bReceiver: §f%2$S\n§bMessage: §f%3$S", - "action-automatic-reportable" : "§7This action was preformed automatically \n§7by the §bSentinel Profanity Filter§7 algorithm!\n§8§o(Click to report false positive)", "profanity-mute-warn" : "You have been auto muted for repeated violation of the profanity filter! §7§o(Hover for more info)", "profanity-mute-notification" : "§b§n%1$s§7 has been auto-muted by the anti-swear! §8(§c%2$s§7/§4%3$s§8)", "slur-mute-warn" : "§cYou have been insta-punished by the anti-slur! §7§o(Hover for more info)", "slur-mute-notification" : "§b§n%1$s§7 has been insta-muted by the anti-swear! §8(§c%2$s§7/§4%3$s§8)", "swear-block-warn" : "§cPlease do not swear in chat! Attempting to bypass this filter will result in a mute! §7§o(Hover for more info)", "swear-block-notification" : "§b§n%1$s§7 has triggered the anti-swear! §8(§c%2$s§7/§4%3$s§8)", + "spam-notification" : "§b§n%1$s§7 might be spamming! §8(§c%2$s§7/§4%3$s§8)", + "spam-notification-hover" : "§8]==-- §d§lSentinel §8--==[\n§bPrevious: §f%1$s\n§bCurrent: §f%2$s\n§bSimilarity §f%3$s", + "spam-block-warn" : "Do not spam in chat! Please wait before sending another message.", + "spam-mute-warn" : "§cYou have been auto-punished for violating the anti-spam repetitively!", + "spam-mute-notification" : "§b§n%1$s§7 has been auto-muted by the anti spam! §8(§c%2$s§7/§4%3$s§8)", "filter-notification-hover" : "§bOriginal: §f%1$s\n§bSanitized: §f%2$s\n§8§o(Click to report false positive)", - "severity-notification-hover" : "§bOriginal: §f%1$s\n§bSanitized: §f%2$s\n§bSeverity: §c%3$s\n§7§o(click to report false positive)", + "severity-notification-hover" : "§bOriginal: §f%1$s\n§bSanitized: §f%2$s\n§bSeverity: §c%3$s\n§7§o(click to report false positive)" } } \ No newline at end of file