Censor slurs split across clan tag and username

Catches names like "[HIT]LER" where neither the clan
tag nor the username is profane on its own, but concatenating them
forms a slur. Detected by running the matcher against clan+name and
checking whether any match spans the clan/name boundary.
This commit is contained in:
evanpelle
2026-04-16 12:36:48 -07:00
parent adeb6a306a
commit b32b81c86c
2 changed files with 53 additions and 4 deletions
+15 -4
View File
@@ -124,13 +124,24 @@ function censorUsernameWithMatcher(
? matcher.hasMatch(clanTag) || clanTag.toLowerCase() === "ss"
: false;
const usernameIsProfane = matcher.hasMatch(nameWithoutClan);
// Catch slurs split across clan tag and username (e.g. "[HIT]LER", "[NIG]ger")
// by looking for a match that spans the clan/name boundary.
const combinedSlurAcrossBoundary = clanTag
? matcher.getAllMatches(clanTag + nameWithoutClan).some(
(match) =>
// Match must start in the clan and extend into the name — otherwise
// it's already handled by the clan-only or name-only checks above.
match.startIndex < clanTag.length && match.endIndex >= clanTag.length,
)
: false;
const censoredName = usernameIsProfane
? shadowNames[simpleHash(nameWithoutClan) % shadowNames.length]
: nameWithoutClan;
const censoredName =
usernameIsProfane || combinedSlurAcrossBoundary
? shadowNames[simpleHash(nameWithoutClan) % shadowNames.length]
: nameWithoutClan;
// Restore clan tag only if it's clean, otherwise remove it entirely
if (clanTag && !clanTagIsProfane) {
if (clanTag && !clanTagIsProfane && !combinedSlurAcrossBoundary) {
return `[${clanTag.toUpperCase()}] ${censoredName}`;
}
+38
View File
@@ -230,6 +230,44 @@ describe("UsernameCensor", () => {
expect(shadowNames).toContain(result);
expect(result).not.toContain("[");
});
describe("clan tag + username combined forms a slur", () => {
test("censors when clan+name combined forms hitler", () => {
const result = checker.censorUsername("[HIT]LER");
expect(shadowNames).toContain(result);
expect(result).not.toContain("[");
});
test("censors when clan+name combined forms hitler (split differently)", () => {
const result = checker.censorUsername("[HI]TLER");
expect(shadowNames).toContain(result);
expect(result).not.toContain("[");
});
test("censors when clan+name combined forms adolf", () => {
const result = checker.censorUsername("[AD]OLF");
expect(shadowNames).toContain(result);
expect(result).not.toContain("[");
});
test("censors when clan+name combined forms nigger", () => {
const result = checker.censorUsername("[NIG]ger");
expect(shadowNames).toContain(result);
expect(result).not.toContain("[");
});
test("censors when clan+name combined forms nigger (clean parts)", () => {
const result = checker.censorUsername("[NI]gger");
expect(shadowNames).toContain(result);
expect(result).not.toContain("[");
});
test("censors leet speak combined across clan and name", () => {
const result = checker.censorUsername("[N1G]g3r");
expect(shadowNames).toContain(result);
expect(result).not.toContain("[");
});
});
});
test("returns deterministic shadow name for same input", () => {