From bfb7f4b107f969e7f4d134f1ed55cfff513e0a18 Mon Sep 17 00:00:00 2001 From: koko Date: Fri, 16 Jun 2023 17:35:15 -0400 Subject: [PATCH] Optimize censor filter system (#795) * Log censored messages through filter & dead code removal * Log original message instead of filtered message * Better use of StringBuilder in CensorHelper, one can now send the server 64 MB of just the word fuck without things exploding into a mess of GC. * Fix infinite loop caused by my migraine <3 * Minor code nitpicks and remove "restitched"/"h4h" from default censor list Realistically "restitched" and "h4h" should not be in the default content filter list as they aren't causing harm to anyone. It seems these were added jokingly and nobody took them out. * Add a few more definitions to the default swear filter list * Fix possible crash when censoring using uwu mode * Apply suggestions from code review * "I am...inevitable." - Errant Whitespace, 2023 Apply code review suggestions Co-authored-by: Josh * A few more code review suggestions * Clarify character limit constant naming --------- Co-authored-by: Rosie Co-authored-by: Josh --- .../Configuration/CensorConfiguration.cs | 7 +- ProjectLighthouse/Helpers/CensorHelper.cs | 88 +++++++++---------- 2 files changed, 48 insertions(+), 47 deletions(-) diff --git a/ProjectLighthouse/Configuration/CensorConfiguration.cs b/ProjectLighthouse/Configuration/CensorConfiguration.cs index dd87824d..170e5791 100644 --- a/ProjectLighthouse/Configuration/CensorConfiguration.cs +++ b/ProjectLighthouse/Configuration/CensorConfiguration.cs @@ -25,11 +25,14 @@ public class CensorConfiguration : ConfigurationBase public FilterMode UserInputFilterMode { get; set; } = FilterMode.None; + // ReSharper disable once StringLiteralTypo public List FilteredWordList { get; set; } = new() { "cunt", "fag", "faggot", + "tranny", + "dyke", "horny", "kook", "kys", @@ -42,8 +45,8 @@ public class CensorConfiguration : ConfigurationBase "retarded", "vagina", "vore", - "restitched", - "h4h", + "porn", + "pornography", }; public override ConfigurationBase Deserialize(IDeserializer deserializer, string text) => deserializer.Deserialize(text); diff --git a/ProjectLighthouse/Helpers/CensorHelper.cs b/ProjectLighthouse/Helpers/CensorHelper.cs index c37ecb38..ead5e981 100644 --- a/ProjectLighthouse/Helpers/CensorHelper.cs +++ b/ProjectLighthouse/Helpers/CensorHelper.cs @@ -1,7 +1,9 @@ using System; -using System.IO; +using System.Collections.Generic; using System.Text; using LBPUnion.ProjectLighthouse.Configuration; +using LBPUnion.ProjectLighthouse.Logging; +using LBPUnion.ProjectLighthouse.Types.Logging; namespace LBPUnion.ProjectLighthouse.Helpers; @@ -20,79 +22,75 @@ public static class CensorHelper public static string FilterMessage(string message) { if (CensorConfiguration.Instance.UserInputFilterMode == FilterMode.None) return message; + StringBuilder stringBuilder = new(message); - int profaneIndex; + const int lbpCharLimit = 94; + + int profaneCount = 0; foreach (string profanity in CensorConfiguration.Instance.FilteredWordList) + { + int lastFoundProfanity = 0; + int profaneIndex; + List censorIndices = new(); do { - profaneIndex = message.ToLower().IndexOf(profanity, StringComparison.Ordinal); - if (profaneIndex != -1) message = Censor(profaneIndex, profanity.Length, message); + profaneIndex = message.IndexOf(profanity, lastFoundProfanity, StringComparison.OrdinalIgnoreCase); + if (profaneIndex == -1) continue; + + censorIndices.Add(profaneIndex); + + lastFoundProfanity = profaneIndex + profanity.Length; } while (profaneIndex != -1); - return message; + for (int i = censorIndices.Count - 1; i >= 0; i--) + { + Censor(censorIndices[i], profanity.Length, stringBuilder); + profaneCount += 1; + } + } + + if (ServerConfiguration.Instance.LogChatFiltering && profaneCount > 0 && message.Length <= lbpCharLimit) + Logger.Info($"Censored {profaneCount} profane words from message \"{message}\"", LogArea.Filter); + + return stringBuilder.ToString(); } - private static string Censor(int profanityIndex, int profanityLength, string message) + private static void Censor(int profanityIndex, int profanityLength, StringBuilder message) { - StringBuilder sb = new(); - - sb.Append(message.AsSpan(0, profanityIndex)); - switch (CensorConfiguration.Instance.UserInputFilterMode) { case FilterMode.Random: char prevRandomChar = '\0'; - for (int i = 0; i < profanityLength; i++) + for (int i = profanityIndex; i < profanityIndex + profanityLength; i++) { - if (message[i] == ' ') - { - sb.Append(' '); - } - else - { - char randomChar = randomCharacters[CryptoHelper.GenerateRandomInt32(0, randomCharacters.Length)]; - if (randomChar == prevRandomChar) randomChar = randomCharacters[CryptoHelper.GenerateRandomInt32(0, randomCharacters.Length)]; + if (char.IsWhiteSpace(message[i])) continue; + + char randomChar = randomCharacters[CryptoHelper.GenerateRandomInt32(0, randomCharacters.Length)]; + while (randomChar == prevRandomChar) + randomChar = randomCharacters[CryptoHelper.GenerateRandomInt32(0, randomCharacters.Length)]; - prevRandomChar = randomChar; - sb.Append(randomChar); - } + prevRandomChar = randomChar; + message[i] = randomChar; } break; case FilterMode.Asterisks: - for(int i = 0; i < profanityLength; i++) + for(int i = profanityIndex; i < profanityIndex + profanityLength; i++) { - sb.Append(message[i] == ' ' ? ' ' : '*'); - } + if (char.IsWhiteSpace(message[i])) continue; + message[i] = '*'; + } break; case FilterMode.Furry: string randomWord = randomFurry[CryptoHelper.GenerateRandomInt32(0, randomFurry.Length)]; - sb.Append(randomWord); + message.Remove(profanityIndex, profanityLength); + message.Insert(profanityIndex, randomWord); break; case FilterMode.None: break; default: throw new ArgumentOutOfRangeException(nameof(message)); } - - sb.Append(message.AsSpan(profanityIndex + profanityLength)); - - return sb.ToString(); - } - - public static string MaskEmail(string email) - { - if (string.IsNullOrEmpty(email) || !email.Contains('@')) return email; - - string[] emailArr = email.Split('@'); - string domainExt = Path.GetExtension(email); - - // Hides everything except the first and last character of the username and domain, preserves the domain extension (.net, .com) - string maskedEmail = $"{emailArr[0][0]}****{emailArr[0][^1..]}@{emailArr[1][0]}****{emailArr[1] - .Substring(emailArr[1].Length - domainExt.Length - 1, - 1)}{domainExt}"; - - return maskedEmail; } } \ No newline at end of file