-
Notifications
You must be signed in to change notification settings - Fork 12
feat: Chat resend detection based on blank lines and invisible unicode #27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,7 @@ | |
| import to.itsme.itsmyconfig.util.IMCSerializer; | ||
| import to.itsme.itsmyconfig.util.Strings; | ||
| import to.itsme.itsmyconfig.util.Utilities; | ||
| import to.itsme.itsmyconfig.util.ChatResendDetector; | ||
|
|
||
| import java.util.HashMap; | ||
| import java.util.Map; | ||
|
|
@@ -53,24 +54,38 @@ public void load() { | |
| public void onPacketSending(final PacketEvent event) { | ||
| final PacketContainer container = event.getPacket(); | ||
| final PacketType type = container.getType(); | ||
| Utilities.debug(() -> "################# CHAT PACKET #################\nProccessing packet " + type.name()); | ||
|
|
||
| Utilities.debug(() -> "################# CHAT PACKET #################\nProcessing packet " + type.name()); | ||
|
|
||
| final PacketContent<PacketContainer> packet = this.processPacket(container); | ||
| if (packet == null || packet.isEmpty()) { | ||
| Utilities.debug(() -> "Packet is null or empty\n" + Strings.DEBUG_HYPHEN); | ||
| return; | ||
| } | ||
|
|
||
| final String message = packet.message(); | ||
| Utilities.debug(() -> "Found message: " + message); | ||
| final Player player = event.getPlayer(); | ||
| final String playerIdentifier = player.getUniqueId().toString(); | ||
|
|
||
| // Check message for resend patterns (blank lines, invisible unicode) | ||
| // This also updates burst state for this player | ||
| final boolean isInBurst = ChatResendDetector.checkMessage(playerIdentifier, message); | ||
|
|
||
| Utilities.debug(() -> "Found message: " + message + (isInBurst ? " [RESEND DETECTED]" : "")); | ||
|
|
||
| final Optional<String> parsed = Strings.parsePrefixedMessage(message); | ||
| if (parsed.isEmpty()) { | ||
| Utilities.debug(() -> "Message doesn't start w/ the symbol-prefix: " + message + "\n" + Strings.DEBUG_HYPHEN); | ||
|
|
||
| // Also check if message contains ItsMyConfig placeholders even without prefix | ||
| final boolean hasPlaceholders = message.contains("<p:"); | ||
|
|
||
| if (parsed.isEmpty() && !hasPlaceholders) { | ||
| Utilities.debug(() -> "Message doesn't start w/ the symbol-prefix and has no <p: placeholders: " + message + "\n" + Strings.DEBUG_HYPHEN); | ||
| return; | ||
| } | ||
|
|
||
| final Player player = event.getPlayer(); | ||
| final Component translated = Utilities.translate(parsed.get(), player); | ||
| // Use the parsed message if available, otherwise use original message | ||
| final String messageToProcess = parsed.isPresent() ? parsed.get() : message; | ||
| final Component translated = Utilities.translate(messageToProcess, player); | ||
|
Comment on lines
+86
to
+88
|
||
| if (translated.equals(Component.empty())) { | ||
| event.setCancelled(true); | ||
| Utilities.debug(() -> "Component is empty, cancelling...\n" + Strings.DEBUG_HYPHEN); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,7 @@ | |
| import java.util.concurrent.atomic.AtomicInteger; | ||
| import java.util.regex.Matcher; | ||
| import java.util.regex.Pattern; | ||
| import to.itsme.itsmyconfig.util.ChatResendDetector; | ||
|
|
||
| public final class TagManager { | ||
|
|
||
|
|
@@ -51,6 +52,10 @@ public static String processArgumentTags( | |
| final Player player, | ||
| @NotNull String text | ||
| ) { | ||
| // Check if player is currently in burst mode (chat resend detected) | ||
| final String playerIdentifier = player.getUniqueId().toString(); | ||
| final boolean isChatResendBurst = ChatResendDetector.isInBurst(playerIdentifier); | ||
|
|
||
| Matcher matcher = ARG_TAG_PATTERN.matcher(text); | ||
| while (matcher.find()) { | ||
| final int start = matcher.start(); | ||
|
|
@@ -67,6 +72,21 @@ public static String processArgumentTags( | |
| continue; // unknown tag — skip safely, do NOT replace | ||
| } | ||
|
|
||
| // During chat resend bursts, filter out sound and actionbar tags | ||
| if (isChatResendBurst) { | ||
| if (tagName.equals("actionbar")) { | ||
| // Actionbar during burst: cancel entire message by returning empty | ||
| // This causes packet listeners to cancel the packet | ||
| return ""; | ||
| } | ||
| if (tagName.equals("sound")) { | ||
| // Sound during burst: strip tag but keep rest of message | ||
| text = text.substring(0, start) + text.substring(end); | ||
| matcher = ARG_TAG_PATTERN.matcher(text); | ||
| continue; | ||
| } | ||
|
Comment on lines
+77
to
+87
|
||
| } | ||
|
Comment on lines
+75
to
+88
|
||
|
|
||
| final String arguments = matcher.group(2); | ||
| final String[] args = extractArguments(arguments); | ||
| if (args.length == 1 && "cancel".equals(args[0])) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,193 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| package to.itsme.itsmyconfig.util; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.Map; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.concurrent.ConcurrentHashMap; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.concurrent.atomic.AtomicInteger; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.concurrent.atomic.AtomicLong; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.regex.Pattern; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Detects chat resend patterns used by chat deletion/moderation plugins. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * When plugins delete messages from chat, they typically flood blank lines | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * (often with invisible unicode characters) to clear the chat window, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * then resend the chat history. This detector identifies that pattern | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * to prevent actionbar/sound effects from firing during resends. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Detection triggers on: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * - 20+ blank lines (empty or whitespace-only messages) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * - Messages containing invisible unicode characters (like Allium plugin uses) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Once triggered, burst mode lasts for 150ms to filter subsequent messages. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| public final class ChatResendDetector { | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| private static final long BURST_DURATION_MS = 150; // How long burst mode lasts after detection | ||||||||||||||||||||||||||||||||||||||||||||||||||
| private static final int BLANK_LINE_THRESHOLD = 20; // 20+ blank lines triggers burst | ||||||||||||||||||||||||||||||||||||||||||||||||||
| private static final long TRACKER_EXPIRY_MS = 5000; // Remove stale trackers after 5 seconds | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| private static final Map<String, BurstTracker> trackers = new ConcurrentHashMap<>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // Invisible unicode characters commonly used by chat clear plugins to prevent | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // chat mods from condensing repeated blank lines. Includes (but not limited to): | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // Zero Width Space (U+200B), Zero Width Non-Joiner (U+200C), Zero Width Joiner (U+200D), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // Word Joiner (U+2060), Zero Width No-Break Space/BOM (U+FEFF), Mongolian Vowel Separator (U+180E), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // Soft Hyphen (U+00AD), Combining Grapheme Joiner (U+034F), Arabic Letter Mark (U+061C), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // Hangul Filler chars (U+115F, U+1160), Khmer Inherent Vowels (U+17B4, U+17B5), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // Invisible math operators (U+2061-U+2064), and various format characters (U+206A-U+206F). | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+30
to
+36
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // Invisible unicode characters commonly used by chat clear plugins to prevent | |
| // chat mods from condensing repeated blank lines. Includes (but not limited to): | |
| // Zero Width Space (U+200B), Zero Width Non-Joiner (U+200C), Zero Width Joiner (U+200D), | |
| // Word Joiner (U+2060), Zero Width No-Break Space/BOM (U+FEFF), Mongolian Vowel Separator (U+180E), | |
| // Soft Hyphen (U+00AD), Combining Grapheme Joiner (U+034F), Arabic Letter Mark (U+061C), | |
| // Hangul Filler chars (U+115F, U+1160), Khmer Inherent Vowels (U+17B4, U+17B5), | |
| // Invisible math operators (U+2061-U+2064), and various format characters (U+206A-U+206F). | |
| // Invisible Unicode characters commonly used by chat clear plugins to prevent | |
| // chat mods from condensing repeated blank lines. This pattern matches: | |
| // - Zero Width Space (U+200B) | |
| // - Zero Width Non-Joiner (U+200C) | |
| // - Zero Width Joiner (U+200D) | |
| // - Word Joiner (U+2060) | |
| // - Zero Width No-Break Space/BOM (U+FEFF) | |
| // - Mongolian Vowel Separator (U+180E) | |
| // - Soft Hyphen (U+00AD) | |
| // - Combining Grapheme Joiner (U+034F) | |
| // - Arabic Letter Mark (U+061C) | |
| // - Hangul Choseong Filler (U+115F) | |
| // - Hangul Jungseong Filler (U+1160) | |
| // - Khmer Vowel Inherent Aq (U+17B4) | |
| // - Khmer Vowel Inherent Aa (U+17B5) | |
| // - Invisible math operators (U+2061–U+2064) | |
| // - Word/segment formatting controls (U+206A–U+206F) |
Copilot
AI
Dec 14, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing reset of blank line counter for non-blank messages. When a non-blank message is received during the tracking window, the counter is not reset. This means if a player sends 10 blank lines, then a normal message, then 10 more blank lines within 500ms, the counter continues from 10 and could trigger burst at the 20th total blank line even though they were interrupted by a normal message.
Consider resetting the counter when a non-blank, non-invisible-unicode message is received, as this indicates the blank line flood has ended and a new potential flood should be tracked separately.
Copilot
AI
Dec 14, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing documentation for the endBurst method. While the method name is self-explanatory, it's a public API method and should have JavaDoc documentation explaining when and why it should be called, especially since the comment mentions "testing or manual intervention" which suggests it has specific use cases.
Add a JavaDoc comment explaining that this method forcefully clears burst tracking state, is primarily intended for testing, and should generally not be called in production code as burst mode expires automatically after 150ms.
Copilot
AI
Dec 14, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Race condition in blank line counting logic. The window check and counter reset are not atomic operations. Between checking the time condition and resetting the counter, another thread could increment the counter, causing the reset to discard valid blank line counts.
This can lead to burst mode not triggering when it should, as rapid blank lines from multiple threads could be incorrectly reset. Consider using compareAndSet pattern or moving the reset logic into a synchronized block.
Copilot
AI
Dec 14, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Race condition between incrementing counter and checking threshold. Multiple threads could increment the counter past the threshold before any of them call triggerBurst, potentially causing the burst to be triggered multiple times with different counter values. While not critically harmful, this creates inconsistent state.
Consider using getAndIncrement() and checking if the returned value equals exactly BLANK_LINE_THRESHOLD - 1 to ensure only one thread triggers the burst.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inconsistent comment style. The comment uses "Check message for resend patterns" while other comments in the file use different phrasing. The comment spans 3 lines where 2 would suffice.
Consider consolidating to: "Check message for resend patterns and update burst state" on a single line for better readability.