mirror of
https://github.com/bdnugget/rs-sdk.git
synced 2026-11-07 09:07:30 +00:00
feat: NODE_PROFANITY_FILTER option to disable chat censoring
The filter runs in two independent layers, both now gated: - Engine: WordEnc.filter skips censoring (bad words, domains, TLDs, fragments) when NODE_PROFANITY_FILTER=false, keeping casing normalization. Covers public chat ingest and PM encode. - Browser client: the setting is injected via bot.ejs/client.ejs into the Client constructor and sets WordFilter.enabled, so the local re-filter follows the server automatically. - Lite client: new SessionOptions.profanityFilter; the runner reads PROFANITY_FILTER from bot.env / process env (no server config channel exists headless). Default unchanged (filter on). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
515f29ded5
commit
933fe0dbe0
10 files changed
+68
-23
No files matched your search
@@ -91,6 +91,8 @@ Running the server locally has many advantages, primary being the ability to set
|
||||
|
||||
You can set tickrate in `server/engine/.env` via the `NODE_TICKRATE` variable (default is 400ms, try 200ms or 30ms for faster gameplay, especially useful for headless testing). You can also change it at runtime with the in-game `::speed <ms>` command (minimum 20ms, doesn't persist across restarts).
|
||||
|
||||
The chat profanity filter can be disabled with `NODE_PROFANITY_FILTER=false` (default on). The server censors chat before broadcasting and injects the setting into the browser client; headless lite runners have no config channel from the server, so also set `PROFANITY_FILTER=false` in the bot's `bot.env` (or process env) to stop their local re-censoring.
|
||||
|
||||
You want all three of these running:
|
||||
|
||||
```sh
|
||||
|
||||
+13
-10
@@ -4,6 +4,7 @@ import WordEncFragments from '#/cache/wordenc/WordEncFragments.js';
|
||||
import WordEncTlds from '#/cache/wordenc/WordEncTlds.js';
|
||||
import Jagfile from '#/io/Jagfile.js';
|
||||
import Packet from '#/io/Packet.js';
|
||||
import Environment from '#/util/Environment.js';
|
||||
|
||||
export default class WordEnc {
|
||||
static PERIOD = new Uint16Array(
|
||||
@@ -70,16 +71,18 @@ export default class WordEnc {
|
||||
const trimmed = characters.join('').trim();
|
||||
const lowercase = trimmed.toLowerCase();
|
||||
const filtered = [...lowercase];
|
||||
this.wordEncTlds.filter(filtered);
|
||||
this.wordEncBadWords.filter(filtered);
|
||||
this.wordEncDomains.filter(filtered);
|
||||
this.wordEncFragments.filter(filtered);
|
||||
for (let index = 0; index < this.whitelist.length; index++) {
|
||||
let offset = -1;
|
||||
while ((offset = lowercase.indexOf(this.whitelist[index], offset + 1)) !== -1) {
|
||||
const whitelisted: string[] = [...this.whitelist[index]];
|
||||
for (let charIndex = 0; charIndex < whitelisted.length; charIndex++) {
|
||||
filtered[charIndex + offset] = whitelisted[charIndex];
|
||||
if (Environment.NODE_PROFANITY_FILTER) {
|
||||
this.wordEncTlds.filter(filtered);
|
||||
this.wordEncBadWords.filter(filtered);
|
||||
this.wordEncDomains.filter(filtered);
|
||||
this.wordEncFragments.filter(filtered);
|
||||
for (let index = 0; index < this.whitelist.length; index++) {
|
||||
let offset = -1;
|
||||
while ((offset = lowercase.indexOf(this.whitelist[index], offset + 1)) !== -1) {
|
||||
const whitelisted: string[] = [...this.whitelist[index]];
|
||||
for (let charIndex = 0; charIndex < whitelisted.length; charIndex++) {
|
||||
filtered[charIndex + offset] = whitelisted[charIndex];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,11 @@ export default {
|
||||
// stalls at ~60% ("Connecting to update server"). Required for the bot client to load.
|
||||
NODE_WS_ONDEMAND: tryParseBoolean(process.env.NODE_WS_ONDEMAND, true),
|
||||
NODE_INFINITE_RUN: tryParseBoolean(process.env.NODE_INFINITE_RUN, true),
|
||||
// When false, WordEnc.filter stops censoring chat (bad words, domains, TLDs)
|
||||
// but still normalizes casing. Also injected into the browser client via
|
||||
// bot.ejs/client.ejs so its local WordFilter matches; the lite client has no
|
||||
// config channel and reads PROFANITY_FILTER from bot.env instead.
|
||||
NODE_PROFANITY_FILTER: tryParseBoolean(process.env.NODE_PROFANITY_FILTER, true),
|
||||
NODE_RANDOM_EVENTS: tryParseBoolean(process.env.NODE_RANDOM_EVENTS, false),
|
||||
NODE_TICKRATE: tryParseInt(process.env.NODE_TICKRATE, 400),
|
||||
HISCORES_WEB_PORT: tryParseInt(process.env.HISCORES_WEB_PORT, 8889),
|
||||
|
||||
@@ -24,6 +24,7 @@ export async function handleClientPage(url: URL): Promise<Response | null> {
|
||||
members: Environment.NODE_MEMBERS,
|
||||
botUsername,
|
||||
maxMessageLength: Environment.node.maxMessageLength,
|
||||
profanityFilter: Environment.NODE_PROFANITY_FILTER,
|
||||
cachebust: CLIENT_CACHEBUST,
|
||||
per_deployment_token: Environment.WEB_SOCKET_TOKEN_PROTECTION ? getPublicPerDeploymentToken() : ''
|
||||
}), {
|
||||
@@ -39,6 +40,7 @@ export async function handleClientPage(url: URL): Promise<Response | null> {
|
||||
nodeid: Environment.NODE_ID,
|
||||
lowmem,
|
||||
members: Environment.NODE_MEMBERS,
|
||||
profanityFilter: Environment.NODE_PROFANITY_FILTER,
|
||||
cachebust: CLIENT_CACHEBUST,
|
||||
per_deployment_token: Environment.WEB_SOCKET_TOKEN_PROTECTION ? getPublicPerDeploymentToken() : ''
|
||||
}), {
|
||||
@@ -64,6 +66,7 @@ export async function handleClientPage(url: URL): Promise<Response | null> {
|
||||
nodeid: Environment.NODE_ID,
|
||||
lowmem,
|
||||
members: Environment.NODE_MEMBERS,
|
||||
profanityFilter: Environment.NODE_PROFANITY_FILTER,
|
||||
cachebust: CLIENT_CACHEBUST,
|
||||
per_deployment_token: Environment.WEB_SOCKET_TOKEN_PROTECTION ? getPublicPerDeploymentToken() : ''
|
||||
}), {
|
||||
|
||||
@@ -437,7 +437,7 @@
|
||||
let clientInstance = null;
|
||||
|
||||
(() => {
|
||||
clientInstance = new Client(<%= nodeid %>, <%= lowmem %>, <%= members %>, <%= maxMessageLength %>);
|
||||
clientInstance = new Client(<%= nodeid %>, <%= lowmem %>, <%= members %>, <%= maxMessageLength %>, <%= profanityFilter %>);
|
||||
window.gameClient = clientInstance;
|
||||
// Auto-initialize from URL params (login, fps)
|
||||
autoInitFromUrlParams();
|
||||
|
||||
@@ -415,7 +415,7 @@
|
||||
import { Client } from './client/client.js?v=<%= cachebust %>';
|
||||
|
||||
(() => {
|
||||
new Client(<%= nodeid %>, <%= lowmem %>, <%= members %>);
|
||||
new Client(<%= nodeid %>, <%= lowmem %>, <%= members %>, undefined, <%= profanityFilter %>);
|
||||
})();
|
||||
|
||||
loadSettings();
|
||||
|
||||
@@ -659,7 +659,7 @@ export class Client extends GameShell {
|
||||
|
||||
// ----
|
||||
|
||||
constructor(nodeid: number, lowmem: boolean, members: boolean, maxMessageLength?: number) {
|
||||
constructor(nodeid: number, lowmem: boolean, members: boolean, maxMessageLength?: number, profanityFilter?: boolean) {
|
||||
super();
|
||||
this.searchParams = new URLSearchParams(window.location.search);
|
||||
|
||||
@@ -669,6 +669,12 @@ export class Client extends GameShell {
|
||||
Client.maxMessageLength = maxMessageLength;
|
||||
}
|
||||
|
||||
// rs-sdk: server-injected (bot.ejs/client.ejs) so the local filter matches
|
||||
// the server's NODE_PROFANITY_FILTER; the server censors independently.
|
||||
if (profanityFilter === false) {
|
||||
WordFilter.enabled = false;
|
||||
}
|
||||
|
||||
if (typeof nodeid === 'undefined' || typeof lowmem === 'undefined' || typeof members === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -258,10 +258,15 @@ const env = Object.fromEntries(
|
||||
);
|
||||
|
||||
const host = env.SERVER || 'localhost';
|
||||
// No config channel from the server (see LiteClient's maxMessageLength note), so
|
||||
// mirror the server's NODE_PROFANITY_FILTER by hand: PROFANITY_FILTER=false in
|
||||
// bot.env or the process env disables local chat censoring.
|
||||
const profanityRaw = (env.PROFANITY_FILTER ?? process.env.PROFANITY_FILTER ?? '').toLowerCase();
|
||||
const session = await startSession({
|
||||
host,
|
||||
username: env.BOT_USERNAME!,
|
||||
password: env.PASSWORD!,
|
||||
profanityFilter: ['false', '0', 'off', 'no'].includes(profanityRaw) ? false : undefined,
|
||||
quiet: true
|
||||
});
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import { GameConnection } from './net/GameConnection.js';
|
||||
import { LiteClient } from './LiteClient.js';
|
||||
import { LocIndex } from './world/LocIndex.js';
|
||||
import { ClientProt } from '#/io/ClientProt.js';
|
||||
import WordFilter from '#/wordfilter/WordFilter.js';
|
||||
|
||||
const LOOP_MS = 20;
|
||||
/** Engine drops a socket after TIMEOUT_NO_RESPONSE; NO_TIMEOUT keeps it alive. */
|
||||
@@ -39,6 +40,13 @@ export interface SessionOptions {
|
||||
cacheDir?: string;
|
||||
members?: boolean;
|
||||
maxMessageLength?: number;
|
||||
/**
|
||||
* Local chat censoring (WordFilter). Default on. Process-wide (WordFilter is
|
||||
* static), so in a multi-bot process like swarm.ts the last session started
|
||||
* wins. Cosmetic unless the server's NODE_PROFANITY_FILTER is also off - the
|
||||
* engine censors chat before it reaches any client.
|
||||
*/
|
||||
profanityFilter?: boolean;
|
||||
quiet?: boolean;
|
||||
/**
|
||||
* Called exactly once when the packet loop ends, whatever the cause. Fires
|
||||
@@ -76,6 +84,9 @@ export interface LiteSession {
|
||||
}
|
||||
|
||||
export async function startSession(opts: SessionOptions): Promise<LiteSession> {
|
||||
if (opts.profanityFilter !== undefined) {
|
||||
WordFilter.enabled = opts.profanityFilter;
|
||||
}
|
||||
const secured = opts.secured ?? !(opts.host.startsWith('localhost') || opts.host.startsWith('127.'));
|
||||
const origin = `${secured ? 'https' : 'http'}://${opts.host}`;
|
||||
const cacheDir = opts.cacheDir ?? `${process.env.HOME}/.cache/rs-sdk-lite`;
|
||||
|
||||
@@ -2,6 +2,14 @@ import JagFile from '#/io/JagFile.js';
|
||||
import Packet from '#/io/Packet.js';
|
||||
|
||||
export default class WordFilter {
|
||||
/**
|
||||
* When false, filter() still normalizes casing (WordPack lowercases the wire
|
||||
* text) but censors nothing. Note the server censors independently
|
||||
* (WordEnc.filter, gated by NODE_PROFANITY_FILTER) - disabling only the
|
||||
* client filter has no visible effect unless the server's is off too.
|
||||
*/
|
||||
static enabled: boolean = true;
|
||||
|
||||
private static readonly PERIOD: Uint16Array = new Uint16Array(
|
||||
['d', 'o', 't']
|
||||
.join('')
|
||||
@@ -46,16 +54,18 @@ export default class WordFilter {
|
||||
const trimmed: string = characters.join('').trim();
|
||||
const lowercase: string = trimmed.toLowerCase();
|
||||
const filtered: string[] = [...lowercase];
|
||||
this.filterTlds(filtered);
|
||||
this.filterBadWords(filtered);
|
||||
this.filterDomains(filtered);
|
||||
this.filterFragments(filtered);
|
||||
for (let index: number = 0; index < this.whitelist.length; index++) {
|
||||
let offset: number = -1;
|
||||
while ((offset = lowercase.indexOf(this.whitelist[index], offset + 1)) !== -1) {
|
||||
const whitelisted: string[] = [...this.whitelist[index]];
|
||||
for (let charIndex: number = 0; charIndex < whitelisted.length; charIndex++) {
|
||||
filtered[charIndex + offset] = whitelisted[charIndex];
|
||||
if (this.enabled) {
|
||||
this.filterTlds(filtered);
|
||||
this.filterBadWords(filtered);
|
||||
this.filterDomains(filtered);
|
||||
this.filterFragments(filtered);
|
||||
for (let index: number = 0; index < this.whitelist.length; index++) {
|
||||
let offset: number = -1;
|
||||
while ((offset = lowercase.indexOf(this.whitelist[index], offset + 1)) !== -1) {
|
||||
const whitelisted: string[] = [...this.whitelist[index]];
|
||||
for (let charIndex: number = 0; charIndex < whitelisted.length; charIndex++) {
|
||||
filtered[charIndex + offset] = whitelisted[charIndex];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user