mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 11:20:45 +00:00
sanitize profane usernames
This commit is contained in:
Generated
+10
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user