Skip to content

Commit 43ee927

Browse files
committed
draft: antispam
1 parent 51713b6 commit 43ee927

File tree

5 files changed

+192
-7
lines changed

5 files changed

+192
-7
lines changed

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

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

1818
package com.lambda.mixin.render;
1919

20-
import com.lambda.module.modules.client.LambdaMoji;
20+
import com.lambda.event.EventFlow;
21+
import com.lambda.event.events.ChatEvent;
22+
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
2123
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
22-
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
23-
import net.minecraft.client.font.TextRenderer;
24-
import net.minecraft.client.gui.DrawContext;
2524
import net.minecraft.client.gui.hud.ChatHud;
26-
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;
2728
import org.spongepowered.asm.mixin.Mixin;
28-
import org.spongepowered.asm.mixin.injection.At;
2929

3030
@Mixin(ChatHud.class)
3131
public class ChatHudMixin {
@@ -41,4 +41,12 @@ public class ChatHudMixin {
4141
// int wrapRenderCall(DrawContext instance, TextRenderer textRenderer, OrderedText text, int x, int y, int color, Operation<Integer> original) {
4242
// return original.call(instance, textRenderer, LambdaMoji.INSTANCE.parse(text, x, y, color), 0, y, 16777215 + (color << 24));
4343
// }
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+
}
4452
}
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({ 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: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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.groups.ReplaceConfig
23+
import com.lambda.event.events.ChatEvent
24+
import com.lambda.event.listener.SafeListener.Companion.listen
25+
import com.lambda.module.Module
26+
import com.lambda.module.tag.ModuleTag
27+
import com.lambda.util.NamedEnum
28+
import net.minecraft.text.Text
29+
30+
object AntiSpam : Module(
31+
name = "AntiSpam",
32+
description = "Keeps your chat clean",
33+
tag = ModuleTag.CHAT,
34+
) {
35+
private val detectSlurs = ReplaceSettings("Slurs", this, Group.Slurs)
36+
private val detectSwears = ReplaceSettings("Swears", this, Group.Swears)
37+
private val detectSexual = ReplaceSettings("Sexual", this, Group.Sexual)
38+
private val detectAddresses = ReplaceSettings("Addresses", this, Group.Addresses)
39+
40+
val slurs = sequenceOf("\\bch[i1l]nks?\\b", "\\bc[o0]{2}ns?\\b", "f[a@4](g{1,2}|qq)([e3il1o0]t{1,2}(ry|r[i1l]e)?)?\\b", "\\bk[il1y]k[e3](ry|r[i1l]e)?s?\\b", "\\b(s[a4]nd)?n[ila4o10][gq]{1,2}(l[e3]t|[e3]r|[a4]|n[o0]g)?s?\\b", "\\btr[a4]n{1,2}([il1][e3]|y|[e3]r)s?\\b").map { Regex(it) }
41+
val swears = sequenceOf("fuck(er)?", "shit", "cunt", "puss(ie|y)", "bitch", "twat").map { Regex(it) }
42+
val sexual = sequenceOf("^cum[s\\$]?$", "cumm?[i1]ng", "h[o0]rny", "mast(e|ur)b(8|ait|ate)").map { Regex(it) }
43+
val addresses = sequenceOf("^((25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]?\\d)(\\.)){3}(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]?\\d)$", "^(\\:\\:)?[0-9a-fA-F]{1,4}(\\:\\:?[0-9a-fA-F]{1,4}){0,7}(\\:\\:)?$", "[A-Za-z0-9-]{1,63}\\.[A-Za-z]{2,6}$").map { Regex(it) }
44+
45+
enum class Group(override val displayName: String) : NamedEnum {
46+
Slurs("Slurs"),
47+
Swears("Swears"),
48+
Sexual("Sexual"),
49+
Addresses("IPs and Addresses"),
50+
}
51+
52+
init {
53+
listen<ChatEvent.Message> { event ->
54+
val author = event.message.string.substringBefore(' ')
55+
var content = event.message.string.substringAfter(' ')
56+
57+
val slurMatches = slurs.takeIf { detectSlurs.enabled }.orEmpty()
58+
.flatMap { it.findAll(content).toList().reversed() }
59+
60+
val swearMatches = swears.takeIf { detectSwears.enabled }.orEmpty()
61+
.flatMap { it.findAll(content).toList().reversed() }
62+
63+
val sexualMatches = sexual.takeIf { detectSexual.enabled }.orEmpty()
64+
.flatMap { it.findAll(content).toList().reversed() }
65+
66+
val addressMatches = addresses.takeIf { detectAddresses.enabled }.orEmpty()
67+
.flatMap { it.findAll(content).toList().reversed() }
68+
69+
var cancelled = false
70+
var hasMatches = false
71+
72+
fun doMatch(replace: ReplaceConfig, matches: Sequence<MatchResult>) {
73+
if (cancelled) return
74+
75+
when (replace.action) {
76+
ReplaceConfig.ActionStrategy.Hide -> matches.firstOrNull()?.let { event.cancel(); cancelled = true } // If there's one detection, nuke the whole damn thang
77+
ReplaceConfig.ActionStrategy.Delete -> matches
78+
.forEach { content = content.replaceRange(it.range, ""); hasMatches = true }
79+
ReplaceConfig.ActionStrategy.Replace -> matches
80+
.forEach { content = content.replaceRange(it.range, replace.replace.block(it.value)); hasMatches = true }
81+
ReplaceConfig.ActionStrategy.None -> {}
82+
}
83+
}
84+
85+
doMatch(detectSlurs, slurMatches)
86+
doMatch(detectSwears, swearMatches)
87+
doMatch(detectSexual, sexualMatches)
88+
doMatch(detectAddresses, addressMatches)
89+
90+
if (!hasMatches) return@listen
91+
92+
event.message = Text.of("$author $content")
93+
}
94+
}
95+
96+
class ReplaceSettings(
97+
name: String,
98+
c: Configurable,
99+
baseGroup: NamedEnum,
100+
) : ReplaceConfig, SettingGroup(c) {
101+
override val action by setting("$name Action Strategy", ReplaceConfig.ActionStrategy.Replace).group(baseGroup)
102+
override val replace by setting("$name Replace Strategy", ReplaceConfig.ReplaceStrategy.CensorAll) { action == ReplaceConfig.ActionStrategy.Replace }.group(baseGroup)
103+
}
104+
}

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)