mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-10 01:15:42 +00:00
fix: stop name censor flagging innocent words like "cons"
The collapse matcher (which catches elongation bypasses like "niiigger") pre-collapses banned words by reducing every run of a repeated letter to one. That turns short slurs with a natural double letter into common substrings — "coons" -> "cons", "boong" -> "bong", "bussy" -> "busy", "nigger" -> "niger" — and since it substring-matches, those censor innocent names like console, icons, busy, Bongo, Nigeria, and negative. Against the live banned list this falsely censored ~half of ordinary test names. A collapse hit is only a genuine elongation bypass when the matched text actually repeats a letter. Filter collapse matches to keep only those whose span (widened one char each side, to catch a trailing collapsed duplicate such as "nigg" -> "nig") contains a repeated letter. This can only remove collapse matches, never add them: a canonically-spelled slur with no repeat equals its deduped pattern and is still caught by the literal substring matcher, so no real slur is lost. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+21
-2
@@ -90,14 +90,33 @@ export function createMatcher(bannedWords: string[]): RegExpMatcher {
|
||||
skipNonAlphabeticTransformer(),
|
||||
],
|
||||
});
|
||||
// A collapse-matcher hit is only a genuine elongation bypass when the matched
|
||||
// text actually repeats a letter (e.g. "niiigger", "cooons", "nigg"). The
|
||||
// deduped patterns collapse a banned word's natural double into a shorter,
|
||||
// common substring ("coons" -> "cons", "boong" -> "bong", "nigger" -> "niger"),
|
||||
// so without this guard the collapse matcher censors innocent names like
|
||||
// "console", "Nigeria", or "busy". A canonical slur with no repeated letter is
|
||||
// identical to its deduped pattern and is still caught by substringMatcher, so
|
||||
// requiring a repeat only drops false positives, never real slurs. The span is
|
||||
// widened by one char each side because a collapsed duplicate at the leading or
|
||||
// trailing edge (e.g. "nigg" matching "nig") falls just outside the match.
|
||||
const elongatedCollapseMatches = (input: string, sorted?: boolean) =>
|
||||
collapseMatcher.getAllMatches(input, sorted).filter((m) =>
|
||||
/(.)\1/.test(
|
||||
input
|
||||
.slice(Math.max(0, m.startIndex - 1), m.endIndex + 2)
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z]/g, ""),
|
||||
),
|
||||
);
|
||||
return {
|
||||
hasMatch: (input: string) =>
|
||||
input.toLowerCase().includes("kkk") ||
|
||||
substringMatcher.hasMatch(input) ||
|
||||
collapseMatcher.hasMatch(input),
|
||||
elongatedCollapseMatches(input).length > 0,
|
||||
getAllMatches: (input: string, sorted?: boolean) => [
|
||||
...substringMatcher.getAllMatches(input, sorted),
|
||||
...collapseMatcher.getAllMatches(input, sorted),
|
||||
...elongatedCollapseMatches(input, sorted),
|
||||
],
|
||||
} as unknown as RegExpMatcher;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,12 @@ const bannedWords = [
|
||||
"faggot",
|
||||
"retard",
|
||||
"chair", // Test word to verify custom banned words work
|
||||
// Banned words with a natural double letter: the collapse matcher deduped
|
||||
// these to common substrings ("coons" -> "cons", "boong" -> "bong") and used
|
||||
// to censor innocent names. See the elongation regression tests below.
|
||||
"coons",
|
||||
"boong",
|
||||
"bussy",
|
||||
];
|
||||
|
||||
const matcher = createMatcher(bannedWords);
|
||||
@@ -165,6 +171,32 @@ describe("UsernameCensor", () => {
|
||||
expect(matcher.hasMatch("snigger")).toBe(false);
|
||||
});
|
||||
|
||||
test("does not censor innocent words that collide with a deduped slur", () => {
|
||||
// The collapse matcher deduped "coons" -> "cons", "boong" -> "bong",
|
||||
// "bussy" -> "busy", "nigger" -> "niger". Without the repeated-letter
|
||||
// guard, every word containing those substrings was censored.
|
||||
expect(matcher.hasMatch("cons")).toBe(false);
|
||||
expect(matcher.hasMatch("console")).toBe(false);
|
||||
expect(matcher.hasMatch("icons")).toBe(false);
|
||||
expect(matcher.hasMatch("constitution")).toBe(false);
|
||||
expect(matcher.hasMatch("busy")).toBe(false);
|
||||
expect(matcher.hasMatch("bong")).toBe(false);
|
||||
expect(matcher.hasMatch("Bongo")).toBe(false);
|
||||
expect(matcher.hasMatch("Nigeria")).toBe(false);
|
||||
expect(matcher.hasMatch("Nigerian")).toBe(false);
|
||||
});
|
||||
|
||||
test("still catches elongated slurs (collapse matcher)", () => {
|
||||
// The repeated-letter guard must not weaken elongation detection: these
|
||||
// all have a real repeated letter in the matched span.
|
||||
expect(matcher.hasMatch("niiigger")).toBe(true);
|
||||
expect(matcher.hasMatch("niiigger123")).toBe(true);
|
||||
expect(matcher.hasMatch("cooons")).toBe(true);
|
||||
expect(matcher.hasMatch("coons")).toBe(true);
|
||||
expect(matcher.hasMatch("boooong")).toBe(true);
|
||||
expect(matcher.hasMatch("bussy")).toBe(true);
|
||||
});
|
||||
|
||||
test("catches kkk as substring", () => {
|
||||
expect(matcher.hasMatch("kkk")).toBe(true);
|
||||
expect(matcher.hasMatch("KKK")).toBe(true);
|
||||
|
||||
Reference in New Issue
Block a user