Skip to content

Commit 5a5f59c

Browse files
committed
better detection
1 parent 0c7d909 commit 5a5f59c

File tree

3 files changed

+138
-33
lines changed

3 files changed

+138
-33
lines changed

src/main/kotlin/com/lambda/module/modules/chat/AntiSpam.kt

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,15 @@ import com.lambda.module.tag.ModuleTag
2929
import com.lambda.util.ChatUtils.addresses
3030
import com.lambda.util.ChatUtils.colors
3131
import com.lambda.util.ChatUtils.discord
32+
import com.lambda.util.ChatUtils.hex
3233
import com.lambda.util.ChatUtils.sexual
3334
import com.lambda.util.ChatUtils.slurs
3435
import com.lambda.util.ChatUtils.swears
3536
import com.lambda.util.ChatUtils.toAscii
3637
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
3741
import net.minecraft.text.Text
3842

3943
object AntiSpam : Module(
@@ -44,6 +48,8 @@ object AntiSpam : Module(
4448
private val ignoreSelf by setting("Ignore Self", true)
4549
private val ignoreFriends by setting("Ignore Friends", true)
4650
private val fancyChats by setting("Replace Fancy Chat", false)
51+
private val ignoreSystem by setting("Ignore System", false)
52+
private val ignoreDms by setting("Ignore DMs", false)
4753

4854
private val detectSlurs = ReplaceSettings("Slurs", this, Group.Slurs)
4955
private val detectSwears = ReplaceSettings("Swears", this, Group.Swears)
@@ -52,6 +58,8 @@ object AntiSpam : Module(
5258
.apply { applyEdits { editTyped(::action) { defaultValue(ReplaceConfig.ActionStrategy.Hide) } } }
5359
private val detectAddresses = ReplaceSettings("Addresses", this, Group.Addresses)
5460
.apply { applyEdits { editTyped(::action) { defaultValue(ReplaceConfig.ActionStrategy.Hide) } } }
61+
private val detectHexBypass = ReplaceSettings("Hex", this, Group.Hex)
62+
.apply { applyEdits { editTyped(::action) { defaultValue(ReplaceConfig.ActionStrategy.Hide) } } }
5563
private val detectColors = ReplaceSettings("Colors", this, Group.Colors)
5664
.apply { applyEdits { editTyped(::action) { defaultValue(ReplaceConfig.ActionStrategy.None) } } }
5765

@@ -62,34 +70,29 @@ object AntiSpam : Module(
6270
Sexual("Sexual"),
6371
Discord("Discord Invites"),
6472
Addresses("IPs and Addresses"),
73+
Hex("Hex Bypass"),
6574
Colors("Color Prefixes")
6675
}
6776

6877
init {
6978
listen<ChatEvent.Message> { event ->
70-
val author = event.message.string.substringAfter('<').substringBefore('>')
71-
var content = event.message.string.substringAfter(' ')
72-
73-
if (!ignoreFriends && FriendManager.isFriend(author) ||
74-
!ignoreSelf && player.gameProfile.name == author) return@listen
75-
76-
val slurMatches = slurs.takeIf { detectSlurs.enabled }.orEmpty()
77-
.flatMap { it.findAll(content).toList().reversed() }
78-
79-
val swearMatches = swears.takeIf { detectSwears.enabled }.orEmpty()
80-
.flatMap { it.findAll(content).toList().reversed() }
81-
82-
val sexualMatches = sexual.takeIf { detectSexual.enabled }.orEmpty()
83-
.flatMap { it.findAll(content).toList().reversed() }
84-
85-
val discordMatches = discord.takeIf { detectDiscord.enabled }.orEmpty()
86-
.flatMap { it.findAll(content).toList().reversed() }
87-
88-
val addressMatches = addresses.takeIf { detectAddresses.enabled }.orEmpty()
89-
.flatMap { it.findAll(content).toList().reversed() }
90-
91-
val colorMatches = colors.takeIf { detectColors.enabled }.orEmpty()
92-
.flatMap { it.findAll(content).toList().reversed() }
79+
var raw = event.message.string
80+
val author = MessageParser.playerName(raw)
81+
82+
if (
83+
ignoreSystem && !MessageType.Both.matches(raw) && !MessageDirection.Both.matches(raw) ||
84+
ignoreDms && MessageDirection.Receive.matches(raw) ||
85+
ignoreFriends && author?.let { FriendManager.isFriend(it) } == true ||
86+
ignoreSelf && MessageType.Self.matches(raw)
87+
) return@listen
88+
89+
val slurMatches = slurs.takeIf { detectSlurs.enabled }.orEmpty().flatMap { it.findAll(raw).toList().reversed() }
90+
val swearMatches = swears.takeIf { detectSwears.enabled }.orEmpty().flatMap { it.findAll(raw).toList().reversed() }
91+
val sexualMatches = sexual.takeIf { detectSexual.enabled }.orEmpty().flatMap { it.findAll(raw).toList().reversed() }
92+
val discordMatches = discord.takeIf { detectDiscord.enabled }.orEmpty().flatMap { it.findAll(raw).toList().reversed() }
93+
val addressMatches = addresses.takeIf { detectAddresses.enabled }.orEmpty().flatMap { it.findAll(raw).toList().reversed() }
94+
val hexMatches = hex.takeIf { detectHexBypass.enabled }.orEmpty().flatMap { it.findAll(raw).toList().reversed() }
95+
val colorMatches = colors.takeIf { detectColors.enabled }.orEmpty().flatMap { it.findAll(raw).toList().reversed() }
9396

9497
var cancelled = false
9598
var hasMatches = false
@@ -100,9 +103,9 @@ object AntiSpam : Module(
100103
when (replace.action) {
101104
ReplaceConfig.ActionStrategy.Hide -> matches.firstOrNull()?.let { event.cancel(); cancelled = true } // If there's one detection, nuke the whole damn thang
102105
ReplaceConfig.ActionStrategy.Delete -> matches
103-
.forEach { content = content.replaceRange(it.range, ""); hasMatches = true }
106+
.forEach { raw = raw.replaceRange(it.range, ""); hasMatches = true }
104107
ReplaceConfig.ActionStrategy.Replace -> matches
105-
.forEach { content = content.replaceRange(it.range, replace.replace.block(it.value)); hasMatches = true }
108+
.forEach { raw = raw.replaceRange(it.range, replace.replace.block(it.value)); hasMatches = true }
106109
ReplaceConfig.ActionStrategy.None -> {}
107110
}
108111
}
@@ -112,12 +115,13 @@ object AntiSpam : Module(
112115
doMatch(detectSexual, sexualMatches)
113116
doMatch(detectDiscord, discordMatches)
114117
doMatch(detectAddresses, addressMatches)
118+
doMatch(detectHexBypass, hexMatches)
115119
doMatch(detectColors, colorMatches)
116120

121+
if (cancelled) return@listen event.cancel()
117122
if (!hasMatches) return@listen
118123

119-
val postprocessed = if (fancyChats) content.toAscii else content
120-
event.message = Text.of("<$author> $postprocessed")
124+
event.message = Text.of(if (fancyChats) raw.toAscii else raw)
121125
}
122126
}
123127

src/main/kotlin/com/lambda/util/ChatUtils.kt

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,15 @@
1818
package com.lambda.util
1919

2020
object ChatUtils {
21-
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) }
22-
val swears = sequenceOf("fuck(er)?", "shit", "cunt", "puss(ie|y)", "bitch", "twat").map { Regex(it) }
23-
val sexual = sequenceOf("^cum[s\\$]?$", "cumm?[i1]ng", "h[o0]rny", "mast(e|ur)b(8|ait|ate)").map { Regex(it) }
24-
val discord = sequenceOf("(http(s)?:\\/\\/)?(discord)?(\\.)?gg(\\/| ).\\S{1,25}", "(http(s)?:\\/\\/)?(discord)?(\\.)?com\\/invite(\\/| ).\\S{1,25}", "(dsc)?(\\.)?gg(\\/| ).\\S{1,25}").map { Regex(it) }
25-
val addresses = sequenceOf("^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}(:\\d{1,5}$)?", "^(\\[?)(\\:\\:)?[0-9a-fA-F]{1,4}(\\:\\:?[0-9a-fA-F]{1,4}){0,7}(\\:\\:)?(\\])?(:\\d{1,5}$)?$").map { Regex(it) }
21+
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, RegexOption.IGNORE_CASE) }
22+
val swears = sequenceOf("fuck(er)?", "shit", "cunt", "puss(ie|y)", "bitch", "twat").map { Regex(it, RegexOption.IGNORE_CASE) }
23+
val sexual = sequenceOf("^cum[s\\$]?$", "cumm?[i1]ng", "h[o0]rny", "mast(e|ur)b(8|ait|ate)").map { Regex(it, RegexOption.IGNORE_CASE) }
24+
val discord = sequenceOf("(http(s)?:\\/\\/)?(discord)?(\\.)?gg(\\/| ).\\S{1,25}", "(http(s)?:\\/\\/)?(discord)?(\\.)?com\\/invite(\\/| ).\\S{1,25}", "(dsc)?(\\.)?gg(\\/| ).\\S{1,25}").map { Regex(it, RegexOption.IGNORE_CASE) }
25+
val addresses = sequenceOf("^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}(:\\d{1,5}$)?", "^(\\[?)(\\:\\:)?[0-9a-fA-F]{1,4}(\\:\\:?[0-9a-fA-F]{1,4}){0,7}(\\:\\:)?(\\])?(:\\d{1,5}$)?$").map { Regex(it, RegexOption.IGNORE_CASE) }
26+
val hex = sequenceOf("\\s([A-Fa-f0-9]+){5,10}$").map { Regex(it) }
2627
val colors = sequenceOf(">", "`").map { Regex(it) }
2728

2829
val fancyToAscii = mapOf('' to 'a', 'ʙ' to 'b', 'c' to 'c', '' to 'd', '' to 'e', '' to 'f', 'ɢ' to 'g', 'ʜ' to 'h', 'ɪ' to 'i', '' to 'j', '' to 'k', 'ʟ' to 'l', '' to 'm', 'ɴ' to 'n', '' to 'o', '' to 'p', 'q' to 'q', 'ʀ' to 'r', '' to 's', '' to 't', '' to 'u', '' to 'v', '' to 'w', 'x' to 'x', 'y' to 'y', '' to 'z',)
2930

30-
val String.toAscii get() = map { fancyToAscii.getOrDefault(it, it) }
31+
val String.toAscii get() = buildString { this@toAscii.forEach { append(fancyToAscii.getOrDefault(it, it)) } }
3132
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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.util.text
19+
20+
import baritone.api.BaritoneAPI
21+
import com.lambda.Lambda.mc
22+
import com.lambda.command.CommandRegistry
23+
import com.lambda.command.commands.PrefixCommand
24+
import kotlin.math.max
25+
import kotlin.text.substring
26+
27+
val playerRegex = "^<(.+)>".toRegex()
28+
29+
interface Detector {
30+
fun matches(input: CharSequence): Boolean
31+
}
32+
33+
interface RemovableDetector {
34+
fun removedOrNull(input: CharSequence): CharSequence?
35+
}
36+
37+
interface PlayerDetector {
38+
fun playerName(input: CharSequence): String?
39+
}
40+
41+
interface RegexDetector : Detector, RemovableDetector {
42+
val regexes: Array<out Regex>
43+
44+
fun result(input: CharSequence) = regexes.find { it.containsMatchIn(input) }
45+
46+
override fun matches(input: CharSequence) = regexes.any { it.containsMatchIn(input) }
47+
48+
override fun removedOrNull(input: CharSequence) =
49+
result(input)
50+
?.replace(input, "")
51+
?.takeIf { it.isNotBlank() }
52+
}
53+
54+
object MessageParser : PlayerDetector, RemovableDetector {
55+
override fun playerName(input: CharSequence) =
56+
playerRegex.find(input)?.value?.drop(1)?.dropLast(1)
57+
58+
override fun removedOrNull(input: CharSequence) =
59+
input.replace(playerRegex, "")
60+
}
61+
62+
63+
enum class MessageType : Detector, PlayerDetector, RemovableDetector {
64+
Self {
65+
override fun matches(input: CharSequence) =
66+
input.startsWith("<${mc.gameProfile.name}>")
67+
68+
override fun playerName(input: CharSequence): String? =
69+
mc.gameProfile.name
70+
},
71+
Others {
72+
override fun matches(input: CharSequence) = playerName(input) != null
73+
74+
override fun playerName(input: CharSequence) =
75+
playerRegex.find(input)?.groupValues?.getOrNull(1)?.takeIf { it.isNotBlank() && it != name }?.drop(1)?.dropLast(1)
76+
},
77+
Both {
78+
override fun matches(input: CharSequence) = input.contains(playerRegex)
79+
80+
override fun playerName(input: CharSequence) =
81+
playerRegex.find(input)?.groupValues?.getOrNull(1)?.takeIf { it.isNotBlank() }?.drop(1)?.dropLast(1)
82+
};
83+
84+
override fun removedOrNull(input: CharSequence) =
85+
playerName(input)?.let { input.removePrefix("<$it>") }
86+
}
87+
88+
enum class MessageDirection(override vararg val regexes: Regex) : RegexDetector, PlayerDetector {
89+
Sent("^To (.+?): ".toRegex(RegexOption.IGNORE_CASE)),
90+
Receive(
91+
"^(.+?) whispers( to you)?: ".toRegex(),
92+
"^\\[?(.+?)( )?->( )?.+?]?( )?:? ".toRegex(),
93+
"^From (.+?): ".toRegex(RegexOption.IGNORE_CASE),
94+
"^. (.+?) » .w+? » ".toRegex()
95+
),
96+
Both(*Sent.regexes, *Receive.regexes);
97+
98+
override fun playerName(input: CharSequence) =
99+
result(input)?.find(input)?.groupValues?.getOrNull(1)?.takeIf { it.isNotBlank() }
100+
}

0 commit comments

Comments
 (0)