Files
OpenFrontIO/src/core/validations/username.ts
T
Ryan 5e6c90d9bb Main Menu UI Overhaul (#2829)
## Description:

Overhauls the Main Menu UI, visit https://menu.openfront.dev to see
everything.

## 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:

w.o.n
2026-01-09 20:26:34 -08:00

130 lines
3.6 KiB
TypeScript

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;
} {
const parsed = UsernameSchema.safeParse(username);
if (!parsed.success) {
const errType = parsed.error.issues[0].code;
if (errType === "invalid_type") {
return { isValid: false, error: translateText("username.not_string") };
}
if (errType === "too_small") {
return {
isValid: false,
error: translateText("username.too_short", {
min: MIN_USERNAME_LENGTH,
}),
};
}
if (errType === "too_big") {
return {
isValid: false,
error: translateText("username.too_long", {
max: MAX_USERNAME_LENGTH,
}),
};
}
// Invalid regex, or any other issue
else {
return { isValid: false, error: translateText("username.invalid_chars") };
}
}
// All checks passed
return { isValid: true };
}