npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

haxball-message-filter

v1.0.0

Published

Lightweight, modular chat filter for HaxBall — and any Node.js project.

Downloads

170

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-filter
import { 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): FilterResult

Allowed 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