From 2fa576c841ca8ad42aa2028c63bf06c64b2f9df6 Mon Sep 17 00:00:00 2001 From: Evan Date: Sat, 8 Feb 2025 19:00:35 -0800 Subject: [PATCH] sanitize profane usernames --- package-lock.json | 10 ++++++++ package.json | 3 ++- src/client/ClientGameRunner.ts | 34 ++++++++++++++------------ src/core/GameRunner.ts | 7 +++--- src/core/execution/ExecutionManager.ts | 23 ++++++++--------- src/core/validations/username.ts | 33 +++++++++++++++++++++++++ src/core/worker/Worker.worker.ts | 7 +++--- src/core/worker/WorkerClient.ts | 9 +++++-- src/core/worker/WorkerMessages.ts | 3 ++- 9 files changed, 92 insertions(+), 37 deletions(-) diff --git a/package-lock.json b/package-lock.json index 930c14c6a..c5cd8241c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 713c95073..22faecb11 100644 --- a/package.json +++ b/package.json @@ -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" -} \ No newline at end of file +} diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 79757c1b9..298fbaa5e 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -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 { 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 + ) ); } }); diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts index a26ccc182..b9d07fcca 100644 --- a/src/core/GameRunner.ts +++ b/src/core/GameRunner.ts @@ -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 { 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; } diff --git a/src/core/execution/ExecutionManager.ts b/src/core/execution/ExecutionManager.ts index 49a6e825f..e4ac0db03 100644 --- a/src/core/execution/ExecutionManager.ts +++ b/src/core/execution/ExecutionManager.ts @@ -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 diff --git a/src/core/validations/username.ts b/src/core/validations/username.ts index 12c9227c7..069d6cd86 100644 --- a/src/core/validations/username.ts +++ b/src/core/validations/username.ts @@ -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; diff --git a/src/core/worker/Worker.worker.ts b/src/core/worker/Worker.worker.ts index 62f5bc35d..e309302b1 100644 --- a/src/core/worker/Worker.worker.ts +++ b/src/core/worker/Worker.worker.ts @@ -27,14 +27,15 @@ ctx.addEventListener("message", async (e: MessageEvent) => { 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) => { const actions = (await gameRunner).playerActions( message.playerID, message.x, - message.y, + message.y ); sendMessage({ type: "player_actions_result", diff --git a/src/core/worker/WorkerClient.ts b/src/core/worker/WorkerClient.ts index 361628266..843bf0ea2 100644 --- a/src/core/worker/WorkerClient.ts +++ b/src/core/worker/WorkerClient.ts @@ -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 diff --git a/src/core/worker/WorkerMessages.ts b/src/core/worker/WorkerMessages.ts index e3cf6e8e8..28ce03e35 100644 --- a/src/core/worker/WorkerMessages.ts +++ b/src/core/worker/WorkerMessages.ts @@ -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 {