Better username censoring (#3122)

## Description:

Many inapropriate names bypass the current filter. This PR does the
following:

1. Moves name censoring to server side so inappropriate names are
scrubbed before being sent to the client
2. Requests a list of profane words from the api, this allows us to
quickly add new profane words in the admin panel without having to
redeploy.

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

evan
This commit is contained in:
Evan
2026-02-09 21:05:59 -08:00
committed by GitHub
parent f7da20ddfd
commit 900cc89067
10 changed files with 330 additions and 163 deletions
-83
View File
@@ -1,92 +1,9 @@
import {
RegExpMatcher,
collapseDuplicatesTransformer,
englishDataset,
englishRecommendedTransformers,
resolveConfusablesTransformer,
resolveLeetSpeakTransformer,
skipNonAlphabeticTransformer,
} from "obscenity";
import { translateText } from "../../client/Utils";
import { UsernameSchema } from "../Schemas";
import { getClanTagOriginalCase, simpleHash } from "../Util";
const matcher = new RegExpMatcher({
...englishDataset.build(),
...englishRecommendedTransformers,
...resolveConfusablesTransformer(),
...skipNonAlphabeticTransformer(),
...collapseDuplicatesTransformer(),
...resolveLeetSpeakTransformer(),
});
export const MIN_USERNAME_LENGTH = 3;
export const MAX_USERNAME_LENGTH = 27;
const shadowNames = [
"NicePeopleOnly",
"BeKindPlz",
"LearningManners",
"StayClassy",
"BeNicer",
"NeedHugs",
"MakeFriends",
];
export function fixProfaneUsername(username: string): string {
if (isProfaneUsername(username)) {
return shadowNames[simpleHash(username) % shadowNames.length];
}
return username;
}
export function isProfaneUsername(username: string): boolean {
return matcher.hasMatch(username);
}
/**
* Sanitizes and censors profane usernames and clan tags.
* Profane username is overwritten, profane clan tag is removed.
*
* Preserves non-profane clan tag:
* prevents desync after clan team assignment because local player's own clan tag and name aren't overwritten
*
* Removing bad clan tags won't hurt existing clans nor cause desyncs:
* - full name including clan tag was overwritten in the past, if any part of name was bad
* - only each separate local player name with a profane clan tag will remain, no clan team assignment
*
* Examples:
* - "GoodName" -> "GoodName"
* - "BadName" -> "Censored"
* - "[CLAN]GoodName" -> "[CLAN]GoodName"
* - "[CLaN]BadName" -> "[CLaN] Censored"
* - "[BAD]GoodName" -> "GoodName"
* - "[BAD]BadName" -> "Censored"
*/
export function censorNameWithClanTag(username: string): string {
// Don't use getClanTag because that returns upperCase and if original isn't, str replace `[{$clanTag}]` won't match
const clanTag = getClanTagOriginalCase(username);
const nameWithoutClan = clanTag
? username.replace(`[${clanTag}]`, "").trim()
: username;
const clanTagIsProfane = clanTag ? isProfaneUsername(clanTag) : false;
const usernameIsProfane = isProfaneUsername(nameWithoutClan);
const censoredNameWithoutClan = usernameIsProfane
? fixProfaneUsername(nameWithoutClan)
: nameWithoutClan;
// Restore clan tag if it existed and is not profane
if (clanTag && !clanTagIsProfane) {
return `[${clanTag.toUpperCase()}] ${censoredNameWithoutClan}`;
}
// Don't restore profane or nonexistent clan tag
return censoredNameWithoutClan;
}
export function validateUsername(username: string): {
isValid: boolean;
error?: string;