Skip to content
Open
Show file tree
Hide file tree
Changes from all 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()
}
144 changes: 144 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,144 @@
/*
* 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 fancyChats by setting("Replace Fancy Chat", false)

private val filterSelf by setting("Ignore Self", true)
private val filterFriends by setting("Ignore Friends", true)
private val filterSystem by setting("Filter System Messages", false)
private val filterDms by setting("Filter DMs", true)

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)
) 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 ||
filterSystem && !MessageType.Both.matches(raw) && !MessageDirection.Both.matches(raw) ||
filterDms && MessageDirection.Receive.matches(raw) ||
filterFriends && author?.let { FriendManager.isFriend(it) } == true ||
filterSelf && MessageType.Self.matches(raw)
) 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
Loading