Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions src/main/java/com/lambda/mixin/render/ChatHudMixin.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@

package com.lambda.mixin.render;

import com.lambda.module.modules.client.LambdaMoji;
import com.lambda.event.EventFlow;
import com.lambda.event.events.ChatEvent;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.hud.ChatHud;
import net.minecraft.text.OrderedText;
import net.minecraft.client.gui.hud.MessageIndicator;
import net.minecraft.network.message.MessageSignatureData;
import net.minecraft.text.Text;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;

@Mixin(ChatHud.class)
public class ChatHudMixin {
Expand All @@ -41,4 +41,12 @@ public class ChatHudMixin {
// int wrapRenderCall(DrawContext instance, TextRenderer textRenderer, OrderedText text, int x, int y, int color, Operation<Integer> original) {
// return original.call(instance, textRenderer, LambdaMoji.INSTANCE.parse(text, x, y, color), 0, y, 16777215 + (color << 24));
// }

@WrapMethod(method = "addMessage(Lnet/minecraft/text/Text;Lnet/minecraft/network/message/MessageSignatureData;Lnet/minecraft/client/gui/hud/MessageIndicator;)V")
void wrapAddMessage(Text message, MessageSignatureData signatureData, MessageIndicator indicator, Operation<Void> original) {
var event = new ChatEvent.Message(message, signatureData, indicator);

if (!EventFlow.post(event).isCanceled())
original.call(event.getMessage(), event.getSignature(), event.getIndicator());
}
}
40 changes: 40 additions & 0 deletions src/main/kotlin/com/lambda/config/groups/ReplaceConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2025 Lambda
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package com.lambda.config.groups

import com.lambda.util.Describable

interface ReplaceConfig {
val action: ActionStrategy
val replace: ReplaceStrategy

val enabled: Boolean get() = action != ActionStrategy.None

enum class ActionStrategy(override val description: String) : Describable {
Hide("Hides the message. Will override other strategies."),
Delete("Deletes the matching part off the message."),
Replace("Replace the matching string in the message with one of the following replace strategy."),
None("Don't do anything."),
}

enum class ReplaceStrategy(val block: (String) -> String) {
CensorAll({ it.replaceRange(0..<it.length, "*".repeat(it.length))}),
CensorHalf({ it.foldIndexed("") { i, acc, char -> if (i % 2 == 0) acc + char else "$acc*" } }),
KeepFirst({ if (it.length <= 1) it else it.replaceRange(1, it.length, "*".repeat(it.length - 1)) }),
}
}
32 changes: 32 additions & 0 deletions src/main/kotlin/com/lambda/event/events/ChatEvent.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright 2025 Lambda
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package com.lambda.event.events

import com.lambda.event.Event
import com.lambda.event.callback.Cancellable
import net.minecraft.client.gui.hud.MessageIndicator
import net.minecraft.network.message.MessageSignatureData
import net.minecraft.text.Text

sealed class ChatEvent {
class Message(
var message: Text,
var signature: MessageSignatureData?,
var indicator: MessageIndicator?,
) : Event, Cancellable()
}
136 changes: 136 additions & 0 deletions src/main/kotlin/com/lambda/module/modules/chat/AntiSpam.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* Copyright 2025 Lambda
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package com.lambda.module.modules.chat

import com.lambda.config.Configurable
import com.lambda.config.SettingGroup
import com.lambda.config.applyEdits
import com.lambda.config.groups.ReplaceConfig
import com.lambda.event.events.ChatEvent
import com.lambda.event.listener.SafeListener.Companion.listen
import com.lambda.friend.FriendManager
import com.lambda.module.Module
import com.lambda.module.tag.ModuleTag
import com.lambda.util.ChatUtils.addresses
import com.lambda.util.ChatUtils.colors
import com.lambda.util.ChatUtils.discord
import com.lambda.util.ChatUtils.hex
import com.lambda.util.ChatUtils.sexual
import com.lambda.util.ChatUtils.slurs
import com.lambda.util.ChatUtils.swears
import com.lambda.util.ChatUtils.toAscii
import com.lambda.util.NamedEnum
import com.lambda.util.text.MessageDirection
import com.lambda.util.text.MessageParser
import com.lambda.util.text.MessageType
import net.minecraft.text.Text

object AntiSpam : Module(
name = "AntiSpam",
description = "Keeps your chat clean",
tag = ModuleTag.CHAT,
) {
private val ignoreSelf by setting("Ignore Self", true)
private val ignoreFriends by setting("Ignore Friends", true)
private val fancyChats by setting("Replace Fancy Chat", false)
private val ignoreSystem by setting("Ignore System", false)
private val ignoreDms by setting("Ignore DMs", false)

private val detectSlurs = ReplaceSettings("Slurs", this, Group.Slurs)
private val detectSwears = ReplaceSettings("Swears", this, Group.Swears)
private val detectSexual = ReplaceSettings("Sexual", this, Group.Sexual)
private val detectDiscord = ReplaceSettings("Discord", this, Group.Discord)
.apply { applyEdits { editTyped(::action) { defaultValue(ReplaceConfig.ActionStrategy.Hide) } } }
private val detectAddresses = ReplaceSettings("Addresses", this, Group.Addresses)
.apply { applyEdits { editTyped(::action) { defaultValue(ReplaceConfig.ActionStrategy.Hide) } } }
private val detectHexBypass = ReplaceSettings("Hex", this, Group.Hex)
.apply { applyEdits { editTyped(::action) { defaultValue(ReplaceConfig.ActionStrategy.Hide) } } }
private val detectColors = ReplaceSettings("Colors", this, Group.Colors)
.apply { applyEdits { editTyped(::action) { defaultValue(ReplaceConfig.ActionStrategy.None) } } }

enum class Group(override val displayName: String) : NamedEnum {
General("General"),
Slurs("Slurs"),
Swears("Swears"),
Sexual("Sexual"),
Discord("Discord Invites"),
Addresses("IPs and Addresses"),
Hex("Hex Bypass"),
Colors("Color Prefixes")
}

init {
listen<ChatEvent.Message> { event ->
var raw = event.message.string
val author = MessageParser.playerName(raw)

if (
ignoreSystem && !MessageType.Both.matches(raw) && !MessageDirection.Both.matches(raw) ||
ignoreDms && MessageDirection.Receive.matches(raw) ||
ignoreFriends && author?.let { FriendManager.isFriend(it) } == true ||
ignoreSelf && MessageType.Self.matches(raw)
) return@listen

val slurMatches = slurs.takeIf { detectSlurs.enabled }.orEmpty().flatMap { it.findAll(raw).toList().reversed() }
val swearMatches = swears.takeIf { detectSwears.enabled }.orEmpty().flatMap { it.findAll(raw).toList().reversed() }
val sexualMatches = sexual.takeIf { detectSexual.enabled }.orEmpty().flatMap { it.findAll(raw).toList().reversed() }
val discordMatches = discord.takeIf { detectDiscord.enabled }.orEmpty().flatMap { it.findAll(raw).toList().reversed() }
val addressMatches = addresses.takeIf { detectAddresses.enabled }.orEmpty().flatMap { it.findAll(raw).toList().reversed() }
val hexMatches = hex.takeIf { detectHexBypass.enabled }.orEmpty().flatMap { it.findAll(raw).toList().reversed() }
val colorMatches = colors.takeIf { detectColors.enabled }.orEmpty().flatMap { it.findAll(raw).toList().reversed() }

var cancelled = false
var hasMatches = false

fun doMatch(replace: ReplaceConfig, matches: Sequence<MatchResult>) {
if (cancelled) return

when (replace.action) {
ReplaceConfig.ActionStrategy.Hide -> matches.firstOrNull()?.let { event.cancel(); cancelled = true } // If there's one detection, nuke the whole damn thang
ReplaceConfig.ActionStrategy.Delete -> matches
.forEach { raw = raw.replaceRange(it.range, ""); hasMatches = true }
ReplaceConfig.ActionStrategy.Replace -> matches
.forEach { raw = raw.replaceRange(it.range, replace.replace.block(it.value)); hasMatches = true }
ReplaceConfig.ActionStrategy.None -> {}
}
}

doMatch(detectSlurs, slurMatches)
doMatch(detectSwears, swearMatches)
doMatch(detectSexual, sexualMatches)
doMatch(detectDiscord, discordMatches)
doMatch(detectAddresses, addressMatches)
doMatch(detectHexBypass, hexMatches)
doMatch(detectColors, colorMatches)

if (cancelled) return@listen event.cancel()
if (!hasMatches) return@listen

event.message = Text.of(if (fancyChats) raw.toAscii else raw)
}
}

class ReplaceSettings(
name: String,
c: Configurable,
baseGroup: NamedEnum,
) : ReplaceConfig, SettingGroup(c) {
override val action by setting("$name Action Strategy", ReplaceConfig.ActionStrategy.Replace).group(baseGroup)
override val replace by setting("$name Replace Strategy", ReplaceConfig.ReplaceStrategy.CensorAll) { action == ReplaceConfig.ActionStrategy.Replace }.group(baseGroup)
}
}
3 changes: 2 additions & 1 deletion src/main/kotlin/com/lambda/module/tag/ModuleTag.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,13 @@ data class ModuleTag(override val name: String) : Nameable {
val MOVEMENT = ModuleTag("Movement")
val RENDER = ModuleTag("Render")
val PLAYER = ModuleTag("Player")
val CHAT = ModuleTag("Chat")
val CLIENT = ModuleTag("Client")
val NETWORK = ModuleTag("Network")
val DEBUG = ModuleTag("Debug")
val HUD = ModuleTag("Hud")

val defaults = setOf(COMBAT, MOVEMENT, RENDER, PLAYER, NETWORK, CLIENT, HUD)
val defaults = setOf(COMBAT, MOVEMENT, RENDER, PLAYER, NETWORK, CHAT, CLIENT, HUD)

val shownTags = defaults.toMutableSet()

Expand Down
32 changes: 32 additions & 0 deletions src/main/kotlin/com/lambda/util/ChatUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright 2025 Lambda
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package com.lambda.util

object ChatUtils {
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) }
val swears = sequenceOf("fuck(er)?", "shit", "cunt", "puss(ie|y)", "bitch", "twat").map { Regex(it, RegexOption.IGNORE_CASE) }
val sexual = sequenceOf("^cum[s\\$]?$", "cumm?[i1]ng", "h[o0]rny", "mast(e|ur)b(8|ait|ate)").map { Regex(it, RegexOption.IGNORE_CASE) }
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) }
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) }
val hex = sequenceOf("\\s([A-Fa-f0-9]+){5,10}$").map { Regex(it) }
val colors = sequenceOf(">", "`").map { Regex(it) }

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',)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe make cyrillicToAscii to prevent bypassing those filters by replacing latin A with cyrillic А

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe make cyrillicToAscii to prevent bypassing those filters by replacing latin A with cyrillic А

It will cause russian messages to be incorrectly converted

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

theres no filters for russian language anyway or wdym


val String.toAscii get() = buildString { [email protected] { append(fancyToAscii.getOrDefault(it, it)) } }
}
100 changes: 100 additions & 0 deletions src/main/kotlin/com/lambda/util/text/Detections.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* Copyright 2025 Lambda
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package com.lambda.util.text

import baritone.api.BaritoneAPI
import com.lambda.Lambda.mc
import com.lambda.command.CommandRegistry
import com.lambda.command.commands.PrefixCommand
import kotlin.math.max
import kotlin.text.substring

val playerRegex = "^<(.+)>".toRegex()

interface Detector {
fun matches(input: CharSequence): Boolean
}

interface RemovableDetector {
fun removedOrNull(input: CharSequence): CharSequence?
}

interface PlayerDetector {
fun playerName(input: CharSequence): String?
}

interface RegexDetector : Detector, RemovableDetector {
val regexes: Array<out Regex>

fun result(input: CharSequence) = regexes.find { it.containsMatchIn(input) }

override fun matches(input: CharSequence) = regexes.any { it.containsMatchIn(input) }

override fun removedOrNull(input: CharSequence) =
result(input)
?.replace(input, "")
?.takeIf { it.isNotBlank() }
}

object MessageParser : PlayerDetector, RemovableDetector {
override fun playerName(input: CharSequence) =
playerRegex.find(input)?.value?.drop(1)?.dropLast(1)

override fun removedOrNull(input: CharSequence) =
input.replace(playerRegex, "")
}


enum class MessageType : Detector, PlayerDetector, RemovableDetector {
Self {
override fun matches(input: CharSequence) =
input.startsWith("<${mc.gameProfile.name}>")

override fun playerName(input: CharSequence): String? =
mc.gameProfile.name
},
Others {
override fun matches(input: CharSequence) = playerName(input) != null

override fun playerName(input: CharSequence) =
playerRegex.find(input)?.groupValues?.getOrNull(1)?.takeIf { it.isNotBlank() && it != name }?.drop(1)?.dropLast(1)
},
Both {
override fun matches(input: CharSequence) = input.contains(playerRegex)

override fun playerName(input: CharSequence) =
playerRegex.find(input)?.groupValues?.getOrNull(1)?.takeIf { it.isNotBlank() }?.drop(1)?.dropLast(1)
};

override fun removedOrNull(input: CharSequence) =
playerName(input)?.let { input.removePrefix("<$it>") }
}

enum class MessageDirection(override vararg val regexes: Regex) : RegexDetector, PlayerDetector {
Sent("^To (.+?): ".toRegex(RegexOption.IGNORE_CASE)),
Receive(
"^(.+?) whispers( to you)?: ".toRegex(),
"^\\[?(.+?)( )?->( )?.+?]?( )?:? ".toRegex(),
"^From (.+?): ".toRegex(RegexOption.IGNORE_CASE),
"^. (.+?) » .w+? » ".toRegex()
),
Both(*Sent.regexes, *Receive.regexes);

override fun playerName(input: CharSequence) =
result(input)?.find(input)?.groupValues?.getOrNull(1)?.takeIf { it.isNotBlank() }
}