Skip to content

Commit c67e3a9

Browse files
authored
Feature: AntiSpam module (#202)
AntiSpam module to filter, remove or ignore certain categories of words. In the near future it will also filter out regular spam using possible a bucket token approach.
1 parent 1510a62 commit c67e3a9

File tree

7 files changed

+506
-6
lines changed

7 files changed

+506
-6
lines changed

src/main/java/com/lambda/mixin/render/ChatHudMixin.java

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,15 @@
1717

1818
package com.lambda.mixin.render;
1919

20+
import com.lambda.event.EventFlow;
21+
import com.lambda.event.events.ChatEvent;
22+
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
2023
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
21-
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
22-
import net.minecraft.client.font.TextRenderer;
23-
import net.minecraft.client.gui.DrawContext;
2424
import net.minecraft.client.gui.hud.ChatHud;
25-
import net.minecraft.text.OrderedText;
25+
import net.minecraft.client.gui.hud.MessageIndicator;
26+
import net.minecraft.network.message.MessageSignatureData;
27+
import net.minecraft.text.Text;
2628
import org.spongepowered.asm.mixin.Mixin;
27-
import org.spongepowered.asm.mixin.injection.At;
2829

2930
@Mixin(ChatHud.class)
3031
public class ChatHudMixin {
@@ -40,4 +41,12 @@ public class ChatHudMixin {
4041
// int wrapRenderCall(DrawContext instance, TextRenderer textRenderer, OrderedText text, int x, int y, int color, Operation<Integer> original) {
4142
// return original.call(instance, textRenderer, LambdaMoji.INSTANCE.parse(text, x, y, color), 0, y, 16777215 + (color << 24));
4243
// }
44+
45+
@WrapMethod(method = "addMessage(Lnet/minecraft/text/Text;Lnet/minecraft/network/message/MessageSignatureData;Lnet/minecraft/client/gui/hud/MessageIndicator;)V")
46+
void wrapAddMessage(Text message, MessageSignatureData signatureData, MessageIndicator indicator, Operation<Void> original) {
47+
var event = new ChatEvent.Message(message, signatureData, indicator);
48+
49+
if (!EventFlow.post(event).isCanceled())
50+
original.call(event.getMessage(), event.getSignature(), event.getIndicator());
51+
}
4352
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2025 Lambda
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
package com.lambda.config.groups
19+
20+
import com.lambda.util.Describable
21+
22+
interface ReplaceConfig {
23+
val action: ActionStrategy
24+
val replace: ReplaceStrategy
25+
26+
val enabled: Boolean get() = action != ActionStrategy.None
27+
28+
enum class ActionStrategy(override val description: String) : Describable {
29+
Hide("Hides the message. Will override other strategies."),
30+
Delete("Deletes the matching part off the message."),
31+
Replace("Replace the matching string in the message with one of the following replace strategy."),
32+
None("Don't do anything."),
33+
}
34+
35+
enum class ReplaceStrategy(val block: (String) -> String) {
36+
CensorAll({ it.replaceRange(0..<it.length, "*".repeat(it.length))}),
37+
CensorHalf({ it.foldIndexed("") { i, acc, char -> if (i % 2 == 0) acc + char else "$acc*" } }),
38+
KeepFirst({ if (it.length <= 1) it else it.replaceRange(1, it.length, "*".repeat(it.length - 1)) }),
39+
}
40+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2025 Lambda
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
package com.lambda.event.events
19+
20+
import com.lambda.event.Event
21+
import com.lambda.event.callback.Cancellable
22+
import net.minecraft.client.gui.hud.MessageIndicator
23+
import net.minecraft.network.message.MessageSignatureData
24+
import net.minecraft.text.Text
25+
26+
sealed class ChatEvent {
27+
class Message(
28+
var message: Text,
29+
var signature: MessageSignatureData?,
30+
var indicator: MessageIndicator?,
31+
) : Event, Cancellable()
32+
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/*
2+
* Copyright 2025 Lambda
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
package com.lambda.module.modules.chat
19+
20+
import com.lambda.config.Configurable
21+
import com.lambda.config.SettingGroup
22+
import com.lambda.config.applyEdits
23+
import com.lambda.config.groups.ReplaceConfig
24+
import com.lambda.event.events.ChatEvent
25+
import com.lambda.event.listener.SafeListener.Companion.listen
26+
import com.lambda.friend.FriendManager
27+
import com.lambda.module.Module
28+
import com.lambda.module.tag.ModuleTag
29+
import com.lambda.util.ChatUtils.addresses
30+
import com.lambda.util.ChatUtils.colors
31+
import com.lambda.util.ChatUtils.discord
32+
import com.lambda.util.ChatUtils.hex
33+
import com.lambda.util.ChatUtils.sexual
34+
import com.lambda.util.ChatUtils.slurs
35+
import com.lambda.util.ChatUtils.swears
36+
import com.lambda.util.ChatUtils.toAscii
37+
import com.lambda.util.NamedEnum
38+
import com.lambda.util.text.MessageDirection
39+
import com.lambda.util.text.MessageParser
40+
import com.lambda.util.text.MessageType
41+
import net.minecraft.text.Text
42+
43+
object AntiSpam : Module(
44+
name = "AntiSpam",
45+
description = "Keeps your chat clean",
46+
tag = ModuleTag.CHAT,
47+
) {
48+
private val fancyChats by setting("Replace Fancy Chat", false)
49+
50+
private val filterSelf by setting("Ignore Self", true)
51+
private val filterFriends by setting("Ignore Friends", true)
52+
private val filterSystem by setting("Filter System Messages", false)
53+
private val filterDms by setting("Filter DMs", true)
54+
55+
private val ignoreSystem by setting("Ignore System", false)
56+
private val ignoreDms by setting("Ignore DMs", false)
57+
58+
private val detectSlurs = ReplaceSettings("Slurs", this, Group.Slurs)
59+
private val detectSwears = ReplaceSettings("Swears", this, Group.Swears)
60+
private val detectSexual = ReplaceSettings("Sexual", this, Group.Sexual)
61+
private val detectDiscord = ReplaceSettings("Discord", this, Group.Discord)
62+
.apply { applyEdits { editTyped(::action) { defaultValue(ReplaceConfig.ActionStrategy.Hide) } } }
63+
private val detectAddresses = ReplaceSettings("Addresses", this, Group.Addresses)
64+
.apply { applyEdits { editTyped(::action) { defaultValue(ReplaceConfig.ActionStrategy.Hide) } } }
65+
private val detectHexBypass = ReplaceSettings("Hex", this, Group.Hex)
66+
.apply { applyEdits { editTyped(::action) { defaultValue(ReplaceConfig.ActionStrategy.Hide) } } }
67+
private val detectColors = ReplaceSettings("Colors", this, Group.Colors)
68+
.apply { applyEdits { editTyped(::action) { defaultValue(ReplaceConfig.ActionStrategy.None) } } }
69+
70+
enum class Group(override val displayName: String) : NamedEnum {
71+
General("General"),
72+
Slurs("Slurs"),
73+
Swears("Swears"),
74+
Sexual("Sexual"),
75+
Discord("Discord Invites"),
76+
Addresses("IPs and Addresses"),
77+
Hex("Hex Bypass"),
78+
Colors("Color Prefixes")
79+
}
80+
81+
init {
82+
listen<ChatEvent.Message> { event ->
83+
var raw = event.message.string
84+
val author = MessageParser.playerName(raw)
85+
86+
if (
87+
ignoreSystem && !MessageType.Both.matches(raw) && !MessageDirection.Both.matches(raw) ||
88+
ignoreDms && MessageDirection.Receive.matches(raw)
89+
) return@listen
90+
91+
val slurMatches = slurs.takeIf { detectSlurs.enabled }.orEmpty().flatMap { it.findAll(raw).toList().reversed() }
92+
val swearMatches = swears.takeIf { detectSwears.enabled }.orEmpty().flatMap { it.findAll(raw).toList().reversed() }
93+
val sexualMatches = sexual.takeIf { detectSexual.enabled }.orEmpty().flatMap { it.findAll(raw).toList().reversed() }
94+
val discordMatches = discord.takeIf { detectDiscord.enabled }.orEmpty().flatMap { it.findAll(raw).toList().reversed() }
95+
val addressMatches = addresses.takeIf { detectAddresses.enabled }.orEmpty().flatMap { it.findAll(raw).toList().reversed() }
96+
val hexMatches = hex.takeIf { detectHexBypass.enabled }.orEmpty().flatMap { it.findAll(raw).toList().reversed() }
97+
val colorMatches = colors.takeIf { detectColors.enabled }.orEmpty().flatMap { it.findAll(raw).toList().reversed() }
98+
99+
var cancelled = false
100+
var hasMatches = false
101+
102+
fun doMatch(replace: ReplaceConfig, matches: Sequence<MatchResult>) {
103+
if (
104+
cancelled ||
105+
filterSystem && !MessageType.Both.matches(raw) && !MessageDirection.Both.matches(raw) ||
106+
filterDms && MessageDirection.Receive.matches(raw) ||
107+
filterFriends && author?.let { FriendManager.isFriend(it) } == true ||
108+
filterSelf && MessageType.Self.matches(raw)
109+
) return
110+
111+
when (replace.action) {
112+
ReplaceConfig.ActionStrategy.Hide -> matches.firstOrNull()?.let { event.cancel(); cancelled = true } // If there's one detection, nuke the whole damn thang
113+
ReplaceConfig.ActionStrategy.Delete -> matches
114+
.forEach { raw = raw.replaceRange(it.range, ""); hasMatches = true }
115+
ReplaceConfig.ActionStrategy.Replace -> matches
116+
.forEach { raw = raw.replaceRange(it.range, replace.replace.block(it.value)); hasMatches = true }
117+
ReplaceConfig.ActionStrategy.None -> {}
118+
}
119+
}
120+
121+
doMatch(detectSlurs, slurMatches)
122+
doMatch(detectSwears, swearMatches)
123+
doMatch(detectSexual, sexualMatches)
124+
doMatch(detectDiscord, discordMatches)
125+
doMatch(detectAddresses, addressMatches)
126+
doMatch(detectHexBypass, hexMatches)
127+
doMatch(detectColors, colorMatches)
128+
129+
if (cancelled) return@listen event.cancel()
130+
if (!hasMatches) return@listen
131+
132+
event.message = Text.of(if (fancyChats) raw.toAscii else raw)
133+
}
134+
}
135+
136+
class ReplaceSettings(
137+
name: String,
138+
c: Configurable,
139+
baseGroup: NamedEnum,
140+
) : ReplaceConfig, SettingGroup(c) {
141+
override val action by setting("$name Action Strategy", ReplaceConfig.ActionStrategy.Replace).group(baseGroup)
142+
override val replace by setting("$name Replace Strategy", ReplaceConfig.ReplaceStrategy.CensorAll) { action == ReplaceConfig.ActionStrategy.Replace }.group(baseGroup)
143+
}
144+
}

src/main/kotlin/com/lambda/module/tag/ModuleTag.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,13 @@ data class ModuleTag(override val name: String) : Nameable {
4040
val MOVEMENT = ModuleTag("Movement")
4141
val RENDER = ModuleTag("Render")
4242
val PLAYER = ModuleTag("Player")
43+
val CHAT = ModuleTag("Chat")
4344
val CLIENT = ModuleTag("Client")
4445
val NETWORK = ModuleTag("Network")
4546
val DEBUG = ModuleTag("Debug")
4647
val HUD = ModuleTag("Hud")
4748

48-
val defaults = setOf(COMBAT, MOVEMENT, RENDER, PLAYER, NETWORK, CLIENT, HUD)
49+
val defaults = setOf(COMBAT, MOVEMENT, RENDER, PLAYER, NETWORK, CHAT, CLIENT, HUD)
4950

5051
val shownTags = defaults.toMutableSet()
5152

0 commit comments

Comments
 (0)