sanitize profane usernames

This commit is contained in:
Evan
2025-02-08 19:00:35 -08:00
parent 1594a45dac
commit 2fa576c841
9 changed files with 92 additions and 37 deletions
+10
View File
@@ -36,6 +36,7 @@
"nanoid": "^5.0.9",
"node-addon-api": "^8.1.0",
"node-gyp": "^10.2.0",
"obscenity": "^0.4.3",
"priority-queue-typescript": "^1.0.1",
"protobufjs": "^7.3.2",
"pureimage": "^0.4.13",
@@ -11798,6 +11799,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/obscenity": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/obscenity/-/obscenity-0.4.3.tgz",
"integrity": "sha512-DZKM0xUEksY5dVGaZoyC5VmIRMSbI6O0Gyb/07L+77d4zWJKKf5tQZrhlT0aiVYD6prTmnxiS3RhN0sfh5Q95Q==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/obuf": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
+2 -1
View File
@@ -87,6 +87,7 @@
"nanoid": "^5.0.9",
"node-addon-api": "^8.1.0",
"node-gyp": "^10.2.0",
"obscenity": "^0.4.3",
"priority-queue-typescript": "^1.0.1",
"protobufjs": "^7.3.2",
"pureimage": "^0.4.13",
@@ -98,4 +99,4 @@
"zod": "^3.23.8"
},
"type": "module"
}
}
+19 -15
View File
@@ -60,13 +60,13 @@ export interface LobbyConfig {
export function joinLobby(
lobbyConfig: LobbyConfig,
onjoin: () => void,
onjoin: () => void
): () => void {
const eventBus = new EventBus();
initRemoteSender(eventBus);
consolex.log(
`joinging lobby: gameID: ${lobbyConfig.gameID}, clientID: ${lobbyConfig.clientID}, persistentID: ${lobbyConfig.persistentID}`,
`joinging lobby: gameID: ${lobbyConfig.gameID}, clientID: ${lobbyConfig.clientID}, persistentID: ${lobbyConfig.persistentID}`
);
const serverConfig = getServerConfig();
@@ -84,7 +84,7 @@ export function joinLobby(
lobbyConfig,
gameConfig,
eventBus,
serverConfig,
serverConfig
);
const onconnect = () => {
@@ -96,7 +96,7 @@ export function joinLobby(
consolex.log("lobby: game started");
onjoin();
createClientGame(lobbyConfig, message.config, eventBus, transport).then(
(r) => r.start(),
(r) => r.start()
);
}
};
@@ -111,18 +111,22 @@ export async function createClientGame(
lobbyConfig: LobbyConfig,
gameConfig: GameConfig,
eventBus: EventBus,
transport: Transport,
transport: Transport
): Promise<ClientGameRunner> {
const config = getConfig(gameConfig);
const gameMap = await loadTerrainMap(gameConfig.gameMap);
const worker = new WorkerClient(lobbyConfig.gameID, gameConfig);
const worker = new WorkerClient(
lobbyConfig.gameID,
gameConfig,
lobbyConfig.clientID
);
await worker.initialize();
const gameView = new GameView(
worker,
config,
gameMap.gameMap,
lobbyConfig.clientID,
lobbyConfig.clientID
);
consolex.log("going to init path finder");
@@ -132,11 +136,11 @@ export async function createClientGame(
canvas,
gameView,
eventBus,
lobbyConfig.clientID,
lobbyConfig.clientID
);
consolex.log(
`creating private game got difficulty: ${gameConfig.difficulty}`,
`creating private game got difficulty: ${gameConfig.difficulty}`
);
return new ClientGameRunner(
@@ -146,7 +150,7 @@ export async function createClientGame(
new InputHandler(canvas, eventBus),
transport,
worker,
gameView,
gameView
);
}
@@ -164,7 +168,7 @@ export class ClientGameRunner {
private input: InputHandler,
private transport: Transport,
private worker: WorkerClient,
private gameView: GameView,
private gameView: GameView
) {}
public start() {
@@ -212,7 +216,7 @@ export class ClientGameRunner {
}
if (this.turnsSeen != message.turn.turnNumber) {
consolex.error(
`got wrong turn have turns ${this.turnsSeen}, received turn ${message.turn.turnNumber}`,
`got wrong turn have turns ${this.turnsSeen}, received turn ${message.turn.turnNumber}`
);
} else {
this.worker.sendTurn(message.turn);
@@ -235,7 +239,7 @@ export class ClientGameRunner {
}
const cell = this.renderer.transformHandler.screenToWorldCoordinates(
event.x,
event.y,
event.y
);
if (!this.gameView.isValidCoord(cell.x, cell.y)) {
return;
@@ -265,8 +269,8 @@ export class ClientGameRunner {
this.eventBus.emit(
new SendAttackIntentEvent(
this.gameView.owner(tile).id(),
this.myPlayer.troops() * this.renderer.uiState.attackRatio,
),
this.myPlayer.troops() * this.renderer.uiState.attackRatio
)
);
}
});
+3 -4
View File
@@ -22,14 +22,13 @@ import { NameViewData } from "./game/Game";
import { GameUpdateType } from "./game/GameUpdates";
import { createGame } from "./game/GameImpl";
import { loadTerrainMap as loadGameMap } from "./game/TerrainMapLoader";
import { GameConfig, Turn } from "./Schemas";
import { ClientID, GameConfig, Turn } from "./Schemas";
import { GameUpdateViewData } from "./game/GameUpdates";
import { andFN, manhattanDistFN, TileRef } from "./game/GameMap";
import { targetTransportTile } from "./Util";
export async function createGameRunner(
gameID: string,
gameConfig: GameConfig,
clientID: ClientID,
callBack: (gu: GameUpdateViewData) => void
): Promise<GameRunner> {
const config = getConfig(gameConfig);
@@ -40,7 +39,7 @@ export async function createGameRunner(
gameMap.nationMap,
config
);
const gr = new GameRunner(game as Game, new Executor(game, gameID), callBack);
const gr = new GameRunner(game as Game, new Executor(game, gameID, clientID), callBack);
gr.init();
return gr;
}
+12 -11
View File
@@ -12,6 +12,7 @@ import {
import {
AttackIntent,
BoatAttackIntentSchema,
ClientID,
GameID,
Intent,
Turn,
@@ -22,29 +23,26 @@ import { BotSpawner } from "./BotSpawner";
import { TransportShipExecution } from "./TransportShipExecution";
import { PseudoRandom } from "../PseudoRandom";
import { FakeHumanExecution } from "./FakeHumanExecution";
import { generateID, processName, sanitize, simpleHash } from "../Util";
import { sanitize, simpleHash } from "../Util";
import { AllianceRequestExecution } from "./alliance/AllianceRequestExecution";
import { AllianceRequestReplyExecution } from "./alliance/AllianceRequestReplyExecution";
import { BreakAllianceExecution } from "./alliance/BreakAllianceExecution";
import { TargetPlayerExecution } from "./TargetPlayerExecution";
import { EmojiExecution } from "./EmojiExecution";
import { DonateExecution } from "./DonateExecution";
import { NukeExecution } from "./NukeExecution";
import { SetTargetTroopRatioExecution } from "./SetTargetTroopRatioExecution";
import { WarshipExecution } from "./WarshipExecution";
import { PortExecution } from "./PortExecution";
import { MissileSiloExecution } from "./MissileSiloExecution";
import { DefensePostExecution } from "./DefensePostExecution";
import { CityExecution } from "./CityExecution";
import { TileRef } from "../game/GameMap";
import { MirvExecution } from "./MIRVExecution";
import { ConstructionExecution } from "./ConstructionExecution";
import { fixProfaneUsername, isProfaneUsername } from "../validations/username";
export class Executor {
// private random = new PseudoRandom(999)
private random: PseudoRandom = null;
constructor(private mg: Game, private gameID: GameID) {
constructor(
private mg: Game,
private gameID: GameID,
private clientID: ClientID
) {
// Add one to avoid id collisions with bots.
this.random = new PseudoRandom(simpleHash(gameID) + 1);
}
@@ -66,7 +64,10 @@ export class Executor {
case "spawn":
return new SpawnExecution(
new PlayerInfo(
sanitize(intent.name),
// Players see their original name, others see a sanitized version
intent.clientID == this.clientID
? sanitize(intent.name)
: fixProfaneUsername(sanitize(intent.name)),
intent.playerType,
intent.clientID,
intent.playerID
+33
View File
@@ -1,8 +1,41 @@
import {
RegExpMatcher,
englishDataset,
englishRecommendedTransformers,
} from "obscenity";
import { simpleHash } from "../Util";
const matcher = new RegExpMatcher({
...englishDataset.build(),
...englishRecommendedTransformers,
});
export const MIN_USERNAME_LENGTH = 3;
export const MAX_USERNAME_LENGTH = 20;
const validPattern = /^[a-zA-Z0-9_ ]+$/;
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);
}
export function validateUsername(username: string): {
isValid: boolean;
error?: string;
+4 -3
View File
@@ -27,14 +27,15 @@ ctx.addEventListener("message", async (e: MessageEvent<MainThreadMessage>) => {
switch (message.type) {
case "heartbeat":
(await gameRunner).executeNextTick()
(await gameRunner).executeNextTick();
break;
case "init":
try {
gameRunner = createGameRunner(
message.gameID,
message.gameConfig,
gameUpdate,
message.clientID,
gameUpdate
).then((gr) => {
sendMessage({
type: "initialized",
@@ -71,7 +72,7 @@ ctx.addEventListener("message", async (e: MessageEvent<MainThreadMessage>) => {
const actions = (await gameRunner).playerActions(
message.playerID,
message.x,
message.y,
message.y
);
sendMessage({
type: "player_actions_result",
+7 -2
View File
@@ -5,7 +5,7 @@ import {
PlayerProfile,
} from "../game/Game";
import { ErrorUpdate, GameUpdateViewData } from "../game/GameUpdates";
import { GameConfig, GameID, Turn } from "../Schemas";
import { ClientID, GameConfig, GameID, Turn } from "../Schemas";
import { generateID } from "../Util";
import { WorkerMessage } from "./WorkerMessages";
@@ -17,7 +17,11 @@ export class WorkerClient {
update: GameUpdateViewData | ErrorUpdate
) => void;
constructor(private gameID: GameID, private gameConfig: GameConfig) {
constructor(
private gameID: GameID,
private gameConfig: GameConfig,
private clientID: ClientID
) {
this.worker = new Worker(new URL("./Worker.worker.ts", import.meta.url));
this.messageHandlers = new Map();
@@ -65,6 +69,7 @@ export class WorkerClient {
id: messageId,
gameID: this.gameID,
gameConfig: this.gameConfig,
clientID: this.clientID,
});
// Add timeout for initialization
+2 -1
View File
@@ -1,5 +1,5 @@
import { GameUpdateViewData } from "../game/GameUpdates";
import { GameConfig, GameID, Turn } from "../Schemas";
import { ClientID, GameConfig, GameID, Turn } from "../Schemas";
import { PlayerActions, PlayerID, PlayerProfile } from "../game/Game";
export type WorkerMessageType =
@@ -28,6 +28,7 @@ export interface InitMessage extends BaseWorkerMessage {
type: "init";
gameID: GameID;
gameConfig: GameConfig;
clientID: ClientID;
}
export interface TurnMessage extends BaseWorkerMessage {