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+ }
0 commit comments