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 <josh@slendy.pw>

* A few more code review suggestions

* Clarify character limit constant naming

---------

Co-authored-by: Rosie <alkalinethunder@gmail.com>
Co-authored-by: Josh <josh@slendy.pw>
This commit is contained in:
koko 2023-06-16 17:35:15 -04:00 committed by GitHub
commit bfb7f4b107
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 48 additions and 47 deletions

View file

@ -25,11 +25,14 @@ public class CensorConfiguration : ConfigurationBase<CensorConfiguration>
public FilterMode UserInputFilterMode { get; set; } = FilterMode.None;
// ReSharper disable once StringLiteralTypo
public List<string> FilteredWordList { get; set; } = new()
{
"cunt",
"fag",
"faggot",
"tranny",
"dyke",
"horny",
"kook",
"kys",
@ -42,8 +45,8 @@ public class CensorConfiguration : ConfigurationBase<CensorConfiguration>
"retarded",
"vagina",
"vore",
"restitched",
"h4h",
"porn",
"pornography",
};
public override ConfigurationBase<CensorConfiguration> Deserialize(IDeserializer deserializer, string text) => deserializer.Deserialize<CensorConfiguration>(text);

View file

@ -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<int> 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;
}
}
private static string Censor(int profanityIndex, int profanityLength, string message)
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 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++)
{
if (message[i] == ' ')
{
sb.Append(' ');
}
else
for (int i = profanityIndex; i < profanityIndex + profanityLength; i++)
{
if (char.IsWhiteSpace(message[i])) continue;
char randomChar = randomCharacters[CryptoHelper.GenerateRandomInt32(0, randomCharacters.Length)];
if (randomChar == prevRandomChar) randomChar = randomCharacters[CryptoHelper.GenerateRandomInt32(0, randomCharacters.Length)];
while (randomChar == prevRandomChar)
randomChar = randomCharacters[CryptoHelper.GenerateRandomInt32(0, randomCharacters.Length)];
prevRandomChar = randomChar;
sb.Append(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;
}
}