This commit is contained in:
mochaisvibecoded
2026-08-24 17:00:26 -04:00
parent cc1148050c
commit 42f7f57b72
401 changed files with 1108081 additions and 0 deletions
+846
View File
@@ -0,0 +1,846 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
namespace Mocha2023.Classes
{
public enum ProfanitySeverity
{
None,
Mild,
Severe,
SevereBypass
}
public sealed record ProfanityMatch(
string CanonicalWord,
string MatchedText,
ProfanitySeverity Severity,
int Index,
int Length,
bool WasBypassed
);
public sealed record ProfanityAnalysis(
ProfanitySeverity Severity,
string SanitizedText,
IReadOnlyList<ProfanityMatch> Matches
);
public static class ProfanityFilter
{
private const int MaxRepeatedCharacterCount = 16;
private const int MaxSeparatorsBetweenLetters = 12;
private const int MaxAnalyzedCharacters = 4096;
private static readonly TimeSpan RegexTimeout =
TimeSpan.FromMilliseconds(150);
private static readonly string[] MildWords =
{
"fuck",
"fucks",
"fucked",
"fucker",
"fuckers",
"fucking",
"motherfucker",
"motherfuckers",
"motherfucking",
"shit",
"shits",
"shitty",
"shitting",
"bullshit",
"bitch",
"bitches",
"bitchy",
"ass",
"asses",
"asshole",
"assholes",
"dumbass",
"jackass",
"dick",
"dicks",
"dickhead",
"dickheads",
"cock",
"cocks",
"pussy",
"pussies",
"whore",
"whores",
"slut",
"sluts",
"bastard",
"bastards",
"twat",
"twats",
"damn",
"dammit",
"hell",
"crap",
"piss",
"pissed",
"sex",
"sexy",
"cum",
"dildo",
"dildos",
"dealdough"
};
private static readonly string[] SevereWords =
{
"nigger",
"niggers",
"nigga",
"niggas",
"faggot",
"faggots",
"fag",
"fags",
"retard",
"retards",
"retarded",
"cunt",
"cunts",
"rape",
"rapes",
"raped",
"raping",
"rapist",
"rapists",
"cumslut",
"cumsluts",
"kike",
"kikes",
"chink",
"chinks",
"spic",
"spics",
"wetback",
"wetbacks",
"tranny",
"trannies"
};
private static readonly Dictionary<char, string[]> CharacterAliases =
new()
{
['a'] =
[
"4", "@", "à", "á", "â", "ã", "ä", "å",
"ɑ", "α", "а"
],
['b'] =
[
"8", "ß", "þ", "Ь", "в"
],
['c'] =
[
"k", "(", "<", "¢", "с"
],
['d'] =
[
"đ", "ԁ"
],
['e'] =
[
"3", "€", "è", "é", "ê", "ë",
"ε", "е"
],
['f'] =
[
"ph", "ƒ"
],
['g'] =
[
"9", "q", "ɡ", "ģ"
],
['h'] =
[
"#", "н"
],
['i'] =
[
"1", "!", "|", "l", "ì", "í", "î", "ï",
"ι", "і"
],
['j'] =
[
"ј"
],
['k'] =
[
"c", "|<", "κ", "к"
],
['l'] =
[
"1", "!", "|", "i", "ł", "ⅼ"
],
['m'] =
[
"rn", "м"
],
['n'] =
[
"ñ", "η", "п"
],
['o'] =
[
"0", "°", "ò", "ó", "ô", "õ", "ö",
"ο", "о"
],
['p'] =
[
"ρ", "р"
],
['q'] =
[
"9"
],
['r'] =
[
"®", "ŕ", "я"
],
['s'] =
[
"5", "$", "z", "ś", "ѕ"
],
['t'] =
[
"7", "+", "ţ", "т"
],
['u'] =
[
"v", "ù", "ú", "û", "ü",
"υ", "ц"
],
['v'] =
[
"u", "\\/"
],
['w'] =
[
"vv", "\\/\\/"
],
['x'] =
[
"%", "×", "х"
],
['y'] =
[
"¥", "ý", "ÿ", "у"
],
['z'] =
[
"2", "ѕ"
]
};
private static readonly Dictionary<char, char>
NormalizedConfusableCharacters =
BuildNormalizedConfusableCharacters();
private static readonly CompiledRule[] MildRules =
BuildRules(MildWords, ProfanitySeverity.Mild);
private static readonly CompiledRule[] SevereRules =
BuildRules(SevereWords, ProfanitySeverity.Severe);
public static ProfanityAnalysis Analyze(string? input)
{
if (string.IsNullOrEmpty(input))
{
return new ProfanityAnalysis(
ProfanitySeverity.None,
input ?? string.Empty,
Array.Empty<ProfanityMatch>()
);
}
string analyzedInput = input.Length > MaxAnalyzedCharacters
? input[..MaxAnalyzedCharacters]
: input;
var candidates = new List<ProfanityMatch>();
AddMatches(analyzedInput, SevereRules, candidates);
AddMatches(analyzedInput, MildRules, candidates);
AddNormalizedMatches(analyzedInput, SevereRules, candidates);
AddNormalizedMatches(analyzedInput, MildRules, candidates);
var selectedMatches = RemoveOverlappingMatches(candidates);
ProfanitySeverity finalSeverity;
if (selectedMatches.Any(x =>
x.Severity == ProfanitySeverity.SevereBypass))
{
finalSeverity = ProfanitySeverity.SevereBypass;
}
else if (selectedMatches.Any(x =>
x.Severity == ProfanitySeverity.Severe))
{
finalSeverity = ProfanitySeverity.Severe;
}
else if (selectedMatches.Any(x =>
x.Severity == ProfanitySeverity.Mild))
{
finalSeverity = ProfanitySeverity.Mild;
}
else
{
finalSeverity = ProfanitySeverity.None;
}
string sanitized = ApplyBlur(input, selectedMatches);
return new ProfanityAnalysis(
finalSeverity,
sanitized,
selectedMatches
);
}
public static ProfanitySeverity GetSeverity(string input)
{
return Analyze(input).Severity;
}
public static string Blur(string input)
{
if (string.IsNullOrEmpty(input))
return input;
return Analyze(input).SanitizedText;
}
public static bool IsPure(string? input)
{
return Analyze(input).Severity == ProfanitySeverity.None;
}
public static bool ContainsProfanity(string? input)
{
return Analyze(input).Severity != ProfanitySeverity.None;
}
public static IReadOnlyList<ProfanityMatch> GetMatches(string? input)
{
return Analyze(input).Matches;
}
private static void AddMatches(
string input,
IEnumerable<CompiledRule> rules,
ICollection<ProfanityMatch> results)
{
foreach (CompiledRule rule in rules)
{
try
{
MatchCollection matches = rule.FuzzyRegex.Matches(input);
foreach (Match match in matches)
{
if (!match.Success || match.Length == 0)
continue;
bool wasBypassed =
!rule.ExactRegex.IsMatch(match.Value);
ProfanitySeverity severity = rule.BaseSeverity;
if (severity == ProfanitySeverity.Severe &&
wasBypassed)
{
severity = ProfanitySeverity.SevereBypass;
}
results.Add(new ProfanityMatch(
rule.CanonicalWord,
match.Value,
severity,
match.Index,
match.Length,
wasBypassed
));
}
}
catch (RegexMatchTimeoutException)
{
}
}
}
private static void AddNormalizedMatches(
string input,
IEnumerable<CompiledRule> rules,
ICollection<ProfanityMatch> results)
{
NormalizedDetectionText normalized =
NormalizeForDetection(input);
if (!normalized.Changed || normalized.Text.Length == 0)
return;
foreach (CompiledRule rule in rules)
{
try
{
foreach (Match match in rule.FuzzyRegex.Matches(
normalized.Text))
{
if (!match.Success || match.Length == 0)
continue;
int normalizedEnd = match.Index + match.Length - 1;
if (match.Index >= normalized.SourceStarts.Count ||
normalizedEnd >= normalized.SourceEnds.Count)
{
continue;
}
int sourceStart =
normalized.SourceStarts[match.Index];
int sourceEnd =
normalized.SourceEnds[normalizedEnd];
int sourceLength = sourceEnd - sourceStart;
if (sourceLength <= 0)
continue;
bool duplicate = results.Any(existing =>
existing.CanonicalWord.Equals(
rule.CanonicalWord,
StringComparison.OrdinalIgnoreCase) &&
existing.Index == sourceStart &&
existing.Length == sourceLength);
if (duplicate)
continue;
ProfanitySeverity severity =
rule.BaseSeverity ==
ProfanitySeverity.Severe
? ProfanitySeverity.SevereBypass
: rule.BaseSeverity;
results.Add(new ProfanityMatch(
rule.CanonicalWord,
input.Substring(sourceStart, sourceLength),
severity,
sourceStart,
sourceLength,
WasBypassed: true));
}
}
catch (RegexMatchTimeoutException)
{
}
}
}
private static NormalizedDetectionText NormalizeForDetection(
string input)
{
var text = new StringBuilder(input.Length);
var starts = new List<int>(input.Length);
var ends = new List<int>(input.Length);
bool changed = false;
int sourceIndex = 0;
foreach (Rune sourceRune in input.EnumerateRunes())
{
int sourceEnd =
sourceIndex + sourceRune.Utf16SequenceLength;
string compatibilityForm = sourceRune
.ToString()
.Normalize(NormalizationForm.FormKD);
int outputStart = text.Length;
foreach (Rune normalizedRune in
compatibilityForm.EnumerateRunes())
{
UnicodeCategory category =
Rune.GetUnicodeCategory(normalizedRune);
if (category is
UnicodeCategory.NonSpacingMark or
UnicodeCategory.SpacingCombiningMark or
UnicodeCategory.EnclosingMark)
{
changed = true;
continue;
}
char output;
if (normalizedRune.IsAscii)
{
output = char.ToLowerInvariant(
(char)normalizedRune.Value);
}
else if (normalizedRune.Value <= char.MaxValue &&
NormalizedConfusableCharacters.TryGetValue(
(char)normalizedRune.Value,
out char mapped))
{
output = mapped;
}
else if (Rune.IsLetterOrDigit(normalizedRune) &&
normalizedRune.Value <= char.MaxValue)
{
output = char.ToLowerInvariant(
(char)normalizedRune.Value);
}
else
{
output = ' ';
}
text.Append(output);
starts.Add(sourceIndex);
ends.Add(sourceEnd);
}
if (text.Length == outputStart)
{
text.Append(' ');
starts.Add(sourceIndex);
ends.Add(sourceEnd);
}
string produced = text
.ToString(outputStart, text.Length - outputStart);
if (!produced.Equals(
sourceRune.ToString(),
StringComparison.Ordinal))
{
changed = true;
}
sourceIndex = sourceEnd;
}
return new NormalizedDetectionText(
text.ToString(),
starts,
ends,
changed);
}
private static Dictionary<char, char>
BuildNormalizedConfusableCharacters()
{
var result = new Dictionary<char, char>();
foreach ((char canonical, string[] aliases) in CharacterAliases)
{
foreach (string alias in aliases)
{
if (alias.Length != 1)
continue;
char candidate = alias[0];
if (candidate <= 127)
continue;
UnicodeCategory category =
CharUnicodeInfo.GetUnicodeCategory(candidate);
if (category is
UnicodeCategory.UppercaseLetter or
UnicodeCategory.LowercaseLetter or
UnicodeCategory.TitlecaseLetter or
UnicodeCategory.ModifierLetter or
UnicodeCategory.OtherLetter)
{
result.TryAdd(
char.ToLowerInvariant(candidate),
canonical);
}
}
}
return result;
}
private static IReadOnlyList<ProfanityMatch>
RemoveOverlappingMatches(IEnumerable<ProfanityMatch> matches)
{
var prioritized = matches
.OrderByDescending(x => GetSeverityPriority(x.Severity))
.ThenByDescending(x => x.Length)
.ThenBy(x => x.Index)
.ToList();
var selected = new List<ProfanityMatch>();
foreach (ProfanityMatch candidate in prioritized)
{
bool overlapsExisting = selected.Any(existing =>
RangesOverlap(
candidate.Index,
candidate.Length,
existing.Index,
existing.Length
));
if (!overlapsExisting)
selected.Add(candidate);
}
return selected
.OrderBy(x => x.Index)
.ToList();
}
private static int GetSeverityPriority(
ProfanitySeverity severity)
{
return severity switch
{
ProfanitySeverity.SevereBypass => 3,
ProfanitySeverity.Severe => 2,
ProfanitySeverity.Mild => 1,
_ => 0
};
}
private static bool RangesOverlap(
int firstIndex,
int firstLength,
int secondIndex,
int secondLength)
{
int firstEnd = firstIndex + firstLength;
int secondEnd = secondIndex + secondLength;
return firstIndex < secondEnd &&
secondIndex < firstEnd;
}
private static string ApplyBlur(
string input,
IEnumerable<ProfanityMatch> matches)
{
char[] characters = input.ToCharArray();
foreach (ProfanityMatch match in matches)
{
int start = Math.Max(0, match.Index);
int end = Math.Min(
characters.Length,
match.Index + match.Length
);
for (int i = start; i < end; i++)
{
if (!char.IsWhiteSpace(characters[i]))
characters[i] = '*';
}
}
return new string(characters);
}
private static CompiledRule[] BuildRules(
IEnumerable<string> words,
ProfanitySeverity baseSeverity)
{
return words
.Where(word => !string.IsNullOrWhiteSpace(word))
.Select(word => word.Trim().ToLowerInvariant())
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderByDescending(word => word.Length)
.Select(word => new CompiledRule(
word,
baseSeverity,
BuildFuzzyRegex(word),
BuildExactRegex(word)
))
.ToArray();
}
private static Regex BuildFuzzyRegex(string word)
{
string pattern = BuildFuzzyWordPattern(word);
return new Regex(
pattern,
RegexOptions.IgnoreCase |
RegexOptions.CultureInvariant |
RegexOptions.Compiled,
RegexTimeout
);
}
private static Regex BuildExactRegex(string word)
{
string escapedWord = Regex.Escape(word);
string pattern =
$@"(?<![\p{{L}}\p{{N}}])" +
escapedWord +
$@"(?![\p{{L}}\p{{N}}])";
return new Regex(
pattern,
RegexOptions.IgnoreCase |
RegexOptions.CultureInvariant |
RegexOptions.Compiled,
RegexTimeout
);
}
private static string BuildFuzzyWordPattern(string word)
{
var builder = new StringBuilder();
builder.Append(@"(?<![\p{L}\p{N}])");
for (int i = 0; i < word.Length; i++)
{
char character = char.ToLowerInvariant(word[i]);
builder.Append(
BuildCharacterPattern(character)
);
if (i < word.Length - 1)
{
builder.Append(
$@"[\s\p{{P}}\p{{S}}\p{{Cf}}]" +
$"{{0,{MaxSeparatorsBetweenLetters}}}"
);
}
}
builder.Append(@"(?![\p{L}\p{N}])");
return builder.ToString();
}
private static string BuildCharacterPattern(char character)
{
var alternatives = new HashSet<string>(
StringComparer.OrdinalIgnoreCase)
{
character.ToString()
};
if (character is >= 'a' and <= 'z')
{
char fullWidthCharacter =
(char)('\uFF41' + (character - 'a'));
alternatives.Add(fullWidthCharacter.ToString());
}
if (CharacterAliases.TryGetValue(
character,
out string[]? aliases))
{
foreach (string alias in aliases)
alternatives.Add(alias);
}
string alternativesPattern = string.Join(
"|",
alternatives
.OrderByDescending(value => value.Length)
.Select(Regex.Escape)
);
return
$"(?:{alternativesPattern})" +
$"{{1,{MaxRepeatedCharacterCount}}}";
}
private sealed class CompiledRule
{
public string CanonicalWord { get; }
public ProfanitySeverity BaseSeverity { get; }
public Regex FuzzyRegex { get; }
public Regex ExactRegex { get; }
public CompiledRule(
string canonicalWord,
ProfanitySeverity baseSeverity,
Regex fuzzyRegex,
Regex exactRegex)
{
CanonicalWord = canonicalWord;
BaseSeverity = baseSeverity;
FuzzyRegex = fuzzyRegex;
ExactRegex = exactRegex;
}
}
private sealed record NormalizedDetectionText(
string Text,
IReadOnlyList<int> SourceStarts,
IReadOnlyList<int> SourceEnds,
bool Changed);
}
}