Skip to content

12. Chat Filter & Moderation ​

The cross-server chat pipeline is filtered and moderated before delivery. The chat filter lives in common/.../Types/RegexFilter.java. The moderation queue lives in common/.../Queue/SignedQueue.java.

12.1 RegexFilter ​

java
public class RegexFilter {
    public boolean checkAdvertising(String message);
    public boolean checkCurseWords(String message);
    public List<Reason> checkAll(String message);

    public enum Reason {
        IP, URL, CURSE_WORD, REPEATED_CHARS, SPAM, CAPS_LOCK
    }
}

IP Detection ​

The IP regex (configurable) matches dotted-quad IPv4 with optional port:

\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}(?::[0-9]{1,5})?\b

A typical match: 192.168.1.5:25565.

URL Detection ​

The URL regex matches http://, https://, and bare www.:

\b((?:https?:\/\/|www\.)[^\s]+)

Curse Words ​

Config.ChatFilter.CURSE_WORDS is a String[]. Each is compiled into a case-insensitive whole-word boundary regex:

java
Pattern.compile("\\b" + Pattern.quote(word) + "\\b", Pattern.CASE_INSENSITIVE);

Repeated Characters ​

A message containing 8+ identical characters in a row is flagged as REPEATED_CHARS.

Spam Heuristic ​

If a player sends the same message 3+ times within 30 seconds, the third message is flagged as SPAM.

Caps Lock ​

If a message has 12+ characters and 80%+ uppercase, it is flagged as CAPS_LOCK.

12.2 SignedQueue Moderation Pipeline ​

When a player sends a chat message on the Velocity proxy, the message is wrapped in QueuedData and registered with SignedQueue. A staff player (any online with StaffToggled=true) is prompted to /allow or /deny the message; if no staff acts within the timer, it auto-resolves to ALLOWED so the pipeline never stalls.

QueuedData<T> ​

java
public class QueuedData<T> {
    String id;                   // random UUID
    T payload;
    CompletableFuture<SignedResult> future;
}

Lifecycle ​

java
// 1) Player sends a chat message:
String id = SignedQueue.enqueue(MCPlayer sender, String message);

// 2) Velocity sends a clickable embed to all staff:
/allow <id>  /  /deny <id>

// 3) Staff uses /chatfilter allow/deny (admin-only) or staff chat command:
//    SignedQueue.resolve(id, ALLOWED or DENIED);

// 4) If no action within 50ms, ScheduledTask auto-resolves to ALLOWED.

Auto-Resolve ​

java
SchedulerAdapter.runLater(() -> SignedQueue.resolve(id, SignedResult.ALLOWED),
                         Duration.ofMillis(50));

This 50 ms is intentionally short: it's a debounce, not a moderation window. Real moderation uses the staff-side command prompt that times out after a separate timer (configurable, default 60 s).

12.3 Chat Channel Routing ​

Each ATPlayer carries a currentChannel: ChatChannel:

java
public enum ChatChannel {
    GLOBAL,    // default — broadcast to all servers
    PARTY,     // only to party members
    MESSAGE,   // private message routing
    STAFF      // staff chat only
}

/chat <channel> switches the channel.

When a player sends a chat message:

PlayerChatEvent
 ├─ Read player.currentChannel
 ├─ If GLOBAL:
 │   ├─ Apply RegexFilter.checkAll(...)
 │   ├─ If clean → broadcast to all servers (including via plugin message)
 │   └─ If curse → broadcast + staff notification
 ├─ If PARTY → only to party members (in-memory set)
 ├─ If STAFF → only to staff with StaffChatToggled
 └─ If MESSAGE → routed as private message (handled by MessageCommand)

12.4 Advertising Detection ​

advertising is defined as either an IP or a URL in the message body. If checkAdvertising returns true:

  • The message is dropped (not broadcast).
  • The sender is warned with <red>Advertising is not allowed.
  • The staff are notified with Embeds.createCurseEmbed (yellow border).

12.5 Curse Words ​

If checkCurseWords returns true:

  • The message is broadcast, but the matched word is replaced with ****.
  • A staff notification is posted with the original.

12.6 Anti-Spam ​

Spam detection is done by checking the player's last 5 messages. If 3+ are identical, the third and subsequent are dropped and the player is warned.

12.7 Configuration ​

yaml
chat-filter:
  enabled: true
  curse-words: ["badword1", "badword2"]
  block-advertising: true

The addresses and the links are filtered by the patterns that are built into the plugin; only the curse words can be changed.

12.8 Per-Message Flow ​

1. Player types "Hello www.example.com 1.2.3.4".
2. PlayerChatEvent fires on Velocity.
3. ChatListener reads player.currentChannel = GLOBAL.
4. RegexFilter.checkAll(...) returns [IP, URL].
5. SignedQueue.enqueue(...) → id = abc123.
6. Velocity sends ALL staff a clickable component:
     <yellow>[abc123]</yellow> <gray>Player:</gray> <red>Hello www.example.com 1.2.3.4</red>
     <click:run_command:/chatfilter allow id><green>[Allow]</green></click>
     <click:run_command:/chatfilter deny id><red>[Deny]</red></click>
7. Auto-resolve scheduled in 50ms.
8. Either a staff acts or the message is allowed.
9. If denied: player gets "<red>Your message was blocked by staff."
   If allowed: broadcast to all servers via "appeltools:chat".

12.9 ChatFilterCommand (/chatfilter) ​

Permission: Permissions.Command.Admin.CHAT_FILTER.

Subcommands:

  • allow <id> — resolves the queued message as ALLOWED.
  • deny <id> — resolves as DENIED.
  • list — shows recent queued items.
  • addcurse <word> / removecurse <word> — mutates the curse list at runtime.
  • reload — re-reads regexes from config.

12.10 Paper-side Enforcement ​

On Paper, the AsyncPlayerChatEvent is also subject to:

  • RegexFilter.checkAll(...) — applied locally before Velocity receives the message.
  • Local mute enforcement: if Mute punishment is active for the player's UUID or IP, the chat event is cancelled and the player sees a mute message.

This is intentional defense-in-depth: if the velocity is down or a player has their chat bypassed somehow, paper will still block.

Built with VitePress.