> ## Documentation Index
> Fetch the complete documentation index at: https://docs.awokenlabs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Antispam & moderation

> Rate limiting, duplicate detection, blocked-word filter, and the profanity escalation ladder.

Three layers run server-side on every outbound message, in this order:

1. **Rate limit** (per-player rolling window).
2. **Duplicate detection** (same message twice within a short window).
3. **Blocked-word filter** (normalised substring match, leet-aware).

A fourth layer - the **profanity escalation ladder** - sits on top of the blocked-word filter and tracks repeat offenders.

All four are configured in `config.lua` under the `antispam` and `profanityEscalation` blocks. The defaults are reasonable for a roleplay server; the rest of this page covers the tuning knobs.

## Rate limit + duplicate detection

```lua config.lua theme={"dark"}
antispam = {
    enabled           = true,
    maxMessages       = 6,       -- per windowMs
    windowMs          = 5000,    -- rolling 5-second window
    duplicateWindowMs = 4000,    -- "same text twice in 4s" guard
    minLength         = 1,       -- empty messages dropped
    maxLength         = 220,     -- hard cap (also enforced client-side)
    bypassPermission  = 'awoken_chat.bypass',
},
```

| Key                 | Default                | What it does                                                                                                               |
| ------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `enabled`           | `true`                 | Master switch. `false` disables the rate-limit and duplicate checks (NOT the blocked-word filter - that has its own gate). |
| `maxMessages`       | `6`                    | Max messages a player can send within `windowMs` before the next message is dropped with a `ratelimited` notify.           |
| `windowMs`          | `5000`                 | Rolling window for the rate limit, in milliseconds.                                                                        |
| `duplicateWindowMs` | `4000`                 | If a player sends the **exact same text** twice within this window, the second send is dropped. Set to `0` to disable.     |
| `minLength`         | `1`                    | Minimum message length. Empty / whitespace-only sends are dropped silently.                                                |
| `maxLength`         | `220`                  | Hard cap. Messages longer than this are dropped. The client-side counter shows this limit.                                 |
| `bypassPermission`  | `'awoken_chat.bypass'` | ACE permission that exempts a player from antispam (and the blocked-word filter).                                          |

### Per-channel exemption

Some channels - typically `/me` - benefit from repeated text. Players legitimately type "`*sighs*`", "`*nods*`", "`*looks around*`" in quick succession, and the duplicate detection would flag them as flood.

Tick **Bypass antispam** in the Category Manager for that channel. The flag stores as `bypassAntispam = true` on the category row. When set:

* Rate limit + duplicate checks are skipped for messages in that channel.
* Length cap **still applies** (structural safety, not behavioural).
* Blocked-word filter **still applies** (slurs are slurs regardless of channel).
* Profanity escalation **still applies** on blocked-word hits.

See [**Categories**](/resources/awoken-chat/configuration/categories) for the full per-channel flag reference.

## Blocked-word filter

Substring-matched against a **normalised** version of the message, so common evasion tricks (leet, spacing, padding) collapse to the same canonical form as the bare word.

```lua config.lua theme={"dark"}
antispam = {
    -- ...
    blockedWords = {
        -- ...80+ entries...
    },
},
```

### Normalisation pipeline

Both the message body and each blocked-word entry are normalised the same way before comparison:

1. **Lowercased** - `"SLUR"` → `"slur"`
2. **Leet substitutions** - `1→i`, `3→e`, `4→a`, `5→s`, `7→t`, `8→b`, `9→g`, `@→a`, `$→s`, `!→i`, `|→i`, `+→t`, `0→o`, `2→z`, `6→g`. `"61ur"` → `"slur"`.
3. **Strip non-letters** - punctuation, spaces, digits not in the leet map. `"s l u r"` → `"slur"`. `"s.l.u.r"` → `"slur"`.
4. **Collapse runs** - `(.)\1+` → `\1`. Any run of 2+ identical letters collapses to one. `"slurrrr"` → `"slu"`. **The blocked-word list is normalised the same way**, so `'slur'` becomes `'slu'` too and the haystack `"slu..."` contains the needle `"slu"`. Match.

<Info>
  The collapse-to-one rule has one known false positive: the country / place name "Niger" (single g) normalises to the same canonical form as the slur. If your server can't accept that collision, remove the slur from `blockedWords` and live with the evasion gap.
</Info>

### Customising the list

Add or remove entries freely. The shipped list is intentionally **conservative** - a handful of very short / ambiguous terms (e.g. `'gay'`, `'fag'` outside `'faggot'`, `'fat'`, `'ass'`, `'rape'`) are deliberately omitted because they collide with legitimate English words at substring level. Add them yourself if you want stricter matching and accept the false positives.

```lua theme={"dark"}
blockedWords = {
    -- shipped list…
    'yournewword',
    'anotherone',
},
```

## Profanity escalation

Counts blocked-word hits per-license and runs an action when the count crosses a rung.

```lua config.lua theme={"dark"}
profanityEscalation = {
    enabled       = true,
    resetAfterMs  = 3600000,   -- 1 hour of clean behaviour resets the counter
    rungs = {
        { hits = 1, action = 'warn' },
        { hits = 3, action = 'mute', durationMs = 60000 },
        { hits = 6, action = 'kick' },
    },
},
```

### How the ladder works

* A player trips the blocked-word filter → counter increments → highest matching rung fires.
* Counter is **per-license**, persists across reconnects within the session.
* Counter **resets to zero** after `resetAfterMs` of clean behaviour (no blocked-word hits). Default: 1 hour.
* Players with `awoken_chat.bypass` ACE never increment the counter.

### Rung actions

| Action | What happens                                                                                                                   |
| ------ | ------------------------------------------------------------------------------------------------------------------------------ |
| `warn` | Yellow system toast: `"Warning: this is offense #N. Continued offenses will be muted or kicked."`                              |
| `mute` | Player can't send chat for `durationMs`. Tries to send → red toast: `"You've been muted for N seconds for repeated offenses."` |
| `kick` | `DropPlayer(source, 'Removed from chat for repeated offenses.')`                                                               |

<Tip>
  The shipped ladder (warn → mute(60s) → kick at 6 hits) is intentionally gentle for first-offence cases. Tune the `hits` thresholds, `durationMs`, and `resetAfterMs` to match your server's culture.
</Tip>

### Customising the ladder

Rungs are evaluated bottom-to-top - the highest rung whose `hits` threshold is matched fires. So you can add intermediate steps:

```lua theme={"dark"}
rungs = {
    { hits = 1,  action = 'warn' },
    { hits = 3,  action = 'mute', durationMs = 60000 },     -- 1 minute
    { hits = 5,  action = 'mute', durationMs = 300000 },    -- 5 minutes
    { hits = 8,  action = 'kick' },
    -- Combine with a Discord webhook + your ban resource for permaban at hit 12
},
```

## Bypass permission

```text theme={"dark"}
add_ace group.admin awoken_chat.bypass allow
```

<Warning>
  `awoken_chat.bypass` is the only ACE that's NOT implied by `awoken_chat.admin`. Granting "admin" alone gives Category Manager access + admin-delete + clear, but does NOT skip the blocked-word filter. Bypass is intentional, opt-in only.
</Warning>

A player with bypass:

* Never trips the rate limit / duplicate / blocked-word filter.
* Doesn't increment the profanity escalation counter.
* Can send the exact same message back-to-back without throttling.

Reserve for trusted staff who need to discuss moderation cases ("the player said X, here's what they typed verbatim") without their own messages getting filtered.

## Audit trail

Every blocked-word hit, rate-limit hit, and duplicate flood posts a Discord embed to `webhooks.violations` if configured. See [**Webhooks**](/resources/awoken-chat/configuration/webhooks#violations-log) for the embed shape and what's included.
