haxball-message-filter
v1.0.0
Published
Lightweight, modular chat filter for HaxBall — and any Node.js project.
Downloads
170
Maintainers
Readme
HaxFilter is a lightweight, modular chat moderation library for HaxBall rooms — and any Node.js project.
It is not a full moderation platform. It is not a bot framework. It is a small, well-designed filter core that gives you banned word detection, anti-spam, anti-flood and automatic word replacement without any unnecessary dependencies.
📦 Installation
npm install haxball-message-filter// ES Module (recommended)
import { HaxFilter } from 'haxball-message-filter';
// CommonJS
const { HaxFilter } = require('haxball-message-filter');Or use the standalone script directly in the HaxBall headless console (no npm needed):
// Paste haxfilter-room.js into https://www.haxball.com/headless?token=YOUR_TOKEN
// All logic is self-contained — no imports required🧠 Core Idea
HaxFilter introduces a processing pipeline over every incoming chat message that enables:
- banned word detection regardless of casing, accents, symbols or repeated characters
- automatic word replacement or censorship
- spam detection via Dice coefficient similarity
- flood detection with a sliding time window
- clean, typed results with full message context
Everything is built around a single principle:
One class. One method. One result object.
⚙️ Architecture
| Layer | Module | Responsibility |
| :--- | :--- | :--- |
| Orchestrator | core/filter.ts | Runs the full pipeline in order: flood → normalize → spam → match → act → record |
| Normalizer | core/normalize.ts | Strips accents, leet-speak, symbols and repeated characters before matching |
| Matcher | core/matcher.ts | Stateless banned word detection against the normalized message |
| Replacer | core/replacer.ts | Applies replace or censor mode to the original message text |
| Anti-spam | core/spam.ts | Dice bigram similarity check across recent player messages |
| Anti-flood | core/flood.ts | Sliding window message count per player |
| History | data/history.ts | Centralized per-player message log shared by spam and flood |
| Loader | data/loader.ts | Reads banned.txt and replacements.txt from disk at init time |
| Config | config.ts | Default values for every configurable option |
| Types | types.ts | All public interfaces, enums and result types |
| Public API | index.ts | HaxFilter class and exported types — the only public surface |
📦 Project Structure
haxball-message-filter/
│
├── src/
│ ├── core/
│ │ ├── filter.ts ← orchestrator
│ │ ├── matcher.ts ← banned word detection
│ │ ├── normalize.ts ← text normalization
│ │ ├── replacer.ts ← replace / censor
│ │ ├── spam.ts ← anti-spam
│ │ └── flood.ts ← anti-flood
│ │
│ ├── data/
│ │ ├── history.ts ← centralized player history
│ │ └── loader.ts ← file loader
│ │
│ ├── config.ts ← default configuration
│ ├── types.ts ← interfaces and types
│ └── index.ts ← public API
│
├── data/
│ ├── banned.txt ← one banned word per line
│ └── replacements.txt ← one replacement word per line
│
├── dist/ ← compiled output (generated by npm run build)
├── package.json
├── tsconfig.json
└── LICENSE🚀 Getting Started
Option A — npm (recommended)
npm install haxball-message-filterimport { HaxFilter } from 'haxball-message-filter';
const filter = new HaxFilter({ mode: 'replace' });
const result = filter.process(player.id, message);
if (result.blocked) {
console.log('Blocked:', result.reason);
} else {
console.log('Final message:', result.message);
}Option B — Standalone script (HaxBall headless console)
Download haxfilter-room.js and paste it directly into the HaxBall headless console. No build step, no npm, no imports. Edit the CONFIG, BANNED_WORDS and REPLACEMENT_WORDS sections at the top of the file.
Option C — Build from source
git clone https://github.com/mcvn2wrgx2-cpu/haxball-message-filter
cd haxball-message-filter
npm install
npm run build # → dist/🧩 Public API
Create a filter
const filter = new HaxFilter(config?);All config options are optional. See Configuration for defaults.
Process a message
const result = filter.process(playerId: number, message: string): FilterResultAllowed message (clean or modified):
{
allowed: true,
blocked: false,
modified: true,
original: "Eres un putoooo",
normalized: "eres un puto",
message: "Eres un crack",
matches: ["puto"],
reason: null
}Blocked message:
{
allowed: false,
blocked: true,
modified: false,
original: "",
reason: "banned_word" // | "spam" | "flood"
}Manage player history
filter.clearPlayer(playerId); // call on player leave
filter.clearAll(); // call on room reset⚙️ Configuration
All options are optional. Shown values are the defaults.
const filter = new HaxFilter({
// Action mode
mode: 'replace', // 'replace' | 'censor' | 'block'
// Normalization
ignoreCase: true,
ignoreAccents: true,
ignoreSymbols: true,
collapseRepeatedChars: true,
maxRepeatedChars: 2,
collapseSpaces: true,
// Anti-spam
antiSpam: true,
spamSimilarityThreshold: 0.85,
maxSimilarMessages: 3,
// Anti-flood
antiFlood: true,
maxMessages: 4,
floodInterval: 3000,
// Data files
bannedWordsPath: './data/banned.txt',
replacementsPath: './data/replacements.txt',
});🎨 Modes
replace (default)
Banned words are swapped for a random word from replacements.txt.
"Eres un puto" → "Eres un crack"censor
Banned words are replaced with asterisks of the same length.
"Eres un puto" → "Eres un ****"block
The entire message is blocked. result.blocked will be true.
🔒 Design Decisions
| Decision | Reason |
| :--- | :--- |
| Centralized PlayerHistory | Spam and flood share one store — no duplicated state |
| Separate matcher.ts (stateless) | Detection is pure: same input always gives the same output |
| Pre-normalized banned words | Normalization runs once at init, not on every message |
| Pipeline order: flood → normalize → spam → match | Cheapest checks run first; normalization only happens once |
| Spaceless match fallback in matcher | Catches p u t o and p.u.t.o evasions after symbol stripping |
| Dice coefficient for spam | O(n) time, no dynamic programming, works well on short chat messages |
| Aligned replace in replacer | Preserves original casing and punctuation around the substituted word |
| clearPlayer() on leave | Bounds memory — history never grows for disconnected players |
🎯 Example: HaxBall Room Integration
import { HaxFilter } from 'haxball-message-filter';
const filter = new HaxFilter({
mode: 'replace',
antiFlood: true,
antiSpam: true,
maxMessages: 4,
floodInterval: 3000,
});
room.onPlayerChat = (player, message) => {
const result = filter.process(player.id, message);
if (result.blocked) {
const warnings = {
flood: '⚠️ You are sending messages too fast.',
spam: '⚠️ Stop repeating the same message.',
banned_word: '⚠️ Your message was blocked.',
};
room.sendAnnouncement(warnings[result.reason], player.id, 0xFF4444);
return false;
}
if (result.modified) {
room.sendAnnouncement(`${player.name}: ${result.message}`, null, null);
return false;
}
return true;
};
room.onPlayerLeave = (player) => {
filter.clearPlayer(player.id);
};🗺️ Roadmap
- [x] v1 — Core: banned word detection, normalization, replace / censor / block modes, anti-spam, anti-flood, standalone room script
- [ ] v2 — Control: progressive warnings, temporary mute, admin commands, multi-language support, filter profiles
- [ ] v2.1 — Intelligence: caps lock detection, URL blocking, toxicity score, slow mode
- [ ] v3 — Platform: plugin system, persistent infractions, web dashboard
⚠️ Status
v1.0.0 — stable. Published on npm. The v1 API contract is frozen and won't break.
🧠 Design Philosophy
- modular by default, monolithic by never
- every module has one job and one job only
- no dependencies in production
- easy to extend without touching the core
📄 License
MIT
