Appearance
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})?\bA 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: trueThe 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
Mutepunishment 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.