From 93a32bbbd183cf45af10a7fbe9a5238585eb442e Mon Sep 17 00:00:00 2001 From: Kendell R Date: Sun, 29 May 2022 12:39:36 -0700 Subject: spam block v2 --- src/main/kotlin/cc/woverflow/chatting/Chatting.kt | 137 ++++++++++++++++----- .../cc/woverflow/chatting/chat/ChatSpamBlock.kt | 124 +++++++++++++++++++ .../cc/woverflow/chatting/config/ChattingConfig.kt | 101 ++++++++++----- 3 files changed, 300 insertions(+), 62 deletions(-) create mode 100644 src/main/kotlin/cc/woverflow/chatting/chat/ChatSpamBlock.kt (limited to 'src/main/kotlin/cc/woverflow/chatting') diff --git a/src/main/kotlin/cc/woverflow/chatting/Chatting.kt b/src/main/kotlin/cc/woverflow/chatting/Chatting.kt index d012ab3..ce18f13 100644 --- a/src/main/kotlin/cc/woverflow/chatting/Chatting.kt +++ b/src/main/kotlin/cc/woverflow/chatting/Chatting.kt @@ -2,6 +2,7 @@ package cc.woverflow.chatting import cc.woverflow.chatting.chat.ChatSearchingManager import cc.woverflow.chatting.chat.ChatShortcuts +import cc.woverflow.chatting.chat.ChatSpamBlock import cc.woverflow.chatting.chat.ChatTabs import cc.woverflow.chatting.config.ChattingConfig import cc.woverflow.chatting.hook.GuiNewChatHook @@ -13,6 +14,10 @@ import cc.woverflow.chatting.utils.screenshot import cc.woverflow.onecore.utils.* import gg.essential.universal.UDesktop import gg.essential.universal.UResolution +import java.awt.image.BufferedImage +import java.io.File +import java.text.SimpleDateFormat +import java.util.* import net.minecraft.client.Minecraft import net.minecraft.client.gui.* import net.minecraft.client.renderer.GlStateManager @@ -36,6 +41,7 @@ import java.io.File import java.text.SimpleDateFormat import java.util.* +import skytils.skytilsmod.core.Config @Mod( modid = Chatting.ID, @@ -72,13 +78,10 @@ object Chatting { @Mod.EventHandler fun onInitialization(event: FMLInitializationEvent) { ChattingConfig.preload() - command("chatting") { - main { - ChattingConfig.openScreen() - } - } + command("chatting") { main { ChattingConfig.openScreen() } } ClientRegistry.registerKeyBinding(keybind) EVENT_BUS.register(this) + EVENT_BUS.register(ChatSpamBlock) ChatTabs.initialize() ChatShortcuts.initialize() } @@ -95,20 +98,42 @@ object Chatting { fun onForgeLoad(event: FMLLoadCompleteEvent) { if (ChattingConfig.informForAlternatives) { if (isHychat) { - sendBrandedNotification(NAME, "Hychat can be removed at it is replaced by Chatting. Click here for more information.", action = { - UDesktop.browseURL("https://github.com/MicrocontrollersDev/Alternatives/blob/main/Hychat.md") - }) + sendBrandedNotification( + NAME, + "Hychat can be removed at it is replaced by Chatting. Click here for more information.", + action = { + UDesktop.browseURL( + "https://github.com/MicrocontrollersDev/Alternatives/blob/main/Hychat.md" + ) + } + ) } if (isSkytils) { - try { - skytilsCompat(Class.forName("gg.skytils.skytilsmod.core.Config")) - } catch (e: Exception) { - e.printStackTrace() - try { - skytilsCompat(Class.forName("skytils.skytilsmod.core.Config")) - } catch (e: Exception) { - e.printStackTrace() - } + if (Config.chatTabs) { + sendBrandedNotification( + NAME, + "Skytils' chat tabs can be disabled as it is replace by Chatting.\nClick here to automatically do this.", + 6F, + action = { + Config.chatTabs = false + ChattingConfig.chatTabs = true + ChattingConfig.hypixelOnlyChatTabs = true + Config.markDirty() + Config.writeData() + } + ) + } + if (Config.copyChat) { + sendBrandedNotification( + NAME, + "Skytils' copy chat messages can be disabled as it is replace by Chatting.\nClick here to automatically do this.", + 6F, + action = { + Config.copyChat = false + Config.markDirty() + Config.writeData() + } + ) } } } @@ -142,7 +167,12 @@ object Chatting { @SubscribeEvent fun onTickEvent(event: TickEvent.ClientTickEvent) { - if (event.phase == TickEvent.Phase.START && Minecraft.getMinecraft().theWorld != null && Minecraft.getMinecraft().thePlayer != null && (Minecraft.getMinecraft().currentScreen == null || Minecraft.getMinecraft().currentScreen is GuiChat)) { + if (event.phase == TickEvent.Phase.START && + Minecraft.getMinecraft().theWorld != null && + Minecraft.getMinecraft().thePlayer != null && + (Minecraft.getMinecraft().currentScreen == null || + Minecraft.getMinecraft().currentScreen is GuiChat) + ) { if (doTheThing) { screenshotChat() doTheThing = false @@ -154,8 +184,13 @@ object Chatting { var height = if (opened) ChattingConfig.focusedHeight else ChattingConfig.unfocusedHeight height = (height * Minecraft.getMinecraft().gameSettings.chatScale).toInt() val chatY = ModCompatHooks.yOffset + ModCompatHooks.chatPosition - if (height + chatY + 27 > (UResolution.scaledHeight / Minecraft.getMinecraft().gameSettings.chatScale).toInt() - 27 - chatY) { - height = (UResolution.scaledHeight / Minecraft.getMinecraft().gameSettings.chatScale).toInt() - 27 - chatY + if (height + chatY + 27 > + (UResolution.scaledHeight / Minecraft.getMinecraft().gameSettings.chatScale) + .toInt() - 27 - chatY + ) { + height = + (UResolution.scaledHeight / Minecraft.getMinecraft().gameSettings.chatScale) + .toInt() - 27 - chatY } return height } @@ -164,7 +199,17 @@ object Chatting { val hud = Minecraft.getMinecraft().ingameGUI val chat = hud.chatGUI val i = MathHelper.floor_float(chat.chatWidth / chat.chatScale) - return screenshot(GuiUtilRenderComponents.splitText(line.chatComponent, i, Minecraft.getMinecraft().fontRendererObj, false, false).map { it.formattedText }.reversed()) + return screenshot( + GuiUtilRenderComponents.splitText( + line.chatComponent, + i, + Minecraft.getMinecraft().fontRendererObj, + false, + false + ) + .map { it.formattedText } + .reversed() + ) } private fun screenshotChat() { @@ -175,14 +220,23 @@ object Chatting { val hud = Minecraft.getMinecraft().ingameGUI val chat = hud.chatGUI val chatLines = ArrayList() - ChatSearchingManager.filterMessages((chat as GuiNewChatHook).prevText, (chat as GuiNewChatAccessor).drawnChatLines)?.let { drawnLines -> - val chatHeight = if (ChattingConfig.customChatHeight) getChatHeight(true) / 9 else GuiNewChat.calculateChatboxHeight(Minecraft.getMinecraft().gameSettings.chatHeightFocused / 9) - for (i in scrollPos until drawnLines.size.coerceAtMost(scrollPos + chatHeight)) { - chatLines.add(drawnLines[i].chatComponent.formattedText) - } + ChatSearchingManager.filterMessages( + (chat as GuiNewChatHook).prevText, + (chat as GuiNewChatAccessor).drawnChatLines + ) + ?.let { drawnLines -> + val chatHeight = + if (ChattingConfig.customChatHeight) getChatHeight(true) / 9 + else + GuiNewChat.calculateChatboxHeight( + Minecraft.getMinecraft().gameSettings.chatHeightFocused / 9 + ) + for (i in scrollPos until drawnLines.size.coerceAtMost(scrollPos + chatHeight)) { + chatLines.add(drawnLines[i].chatComponent.formattedText) + } - screenshot(chatLines)?.copyToClipboard() - } + screenshot(chatLines)?.copyToClipboard() + } } private fun screenshot(messages: List): BufferedImage? { @@ -191,30 +245,47 @@ object Chatting { return null } if (!OpenGlHelper.isFramebufferEnabled()) { - sendBrandedNotification("Chatting", "Screenshot failed, please disable “Fast Render” in OptiFine’s “Performance” tab.") + sendBrandedNotification( + "Chatting", + "Screenshot failed, please disable “Fast Render” in OptiFine’s “Performance” tab." + ) return null } val fr: FontRenderer = ModCompatHooks.fontRenderer val width = messages.maxOf { fr.getStringWidth(it) } + 4 val fb: Framebuffer = createBindFramebuffer(width * 2, (messages.size * 9) * 2) - val file = File(Minecraft.getMinecraft().mcDataDir, "screenshots/chat/" + fileFormatter.format(Date())) + val file = + File( + Minecraft.getMinecraft().mcDataDir, + "screenshots/chat/" + fileFormatter.format(Date()) + ) GlStateManager.scale(2f, 2f, 1f) val scale = Minecraft.getMinecraft().gameSettings.chatScale GlStateManager.scale(scale, scale, 1f) for (i in messages.indices) { - ModCompatHooks.redirectDrawString(messages[i], 0f, (messages.size - 1 - i) * 9f, 0xffffff) + ModCompatHooks.redirectDrawString( + messages[i], + 0f, + (messages.size - 1 - i) * 9f, + 0xffffff + ) } val image = fb.screenshot(file) Minecraft.getMinecraft().entityRenderer.setupOverlayRendering() Minecraft.getMinecraft().framebuffer.bindFramebuffer(true) - sendBrandedNotification("Chatting", "Chat screenshotted successfully." + (if (ChattingConfig.copyMode != 1) "\nClick to open." else ""), action = { + sendBrandedNotification( + "Chatting", + "Chat screenshotted successfully." + + (if (ChattingConfig.copyMode != 1) "\nClick to open." else ""), + action = { if (!UDesktop.open(file)) { sendBrandedNotification("Chatting", "Could not browse!") } - }) + } + ) return image } } diff --git a/src/main/kotlin/cc/woverflow/chatting/chat/ChatSpamBlock.kt b/src/main/kotlin/cc/woverflow/chatting/chat/ChatSpamBlock.kt new file mode 100644 index 0000000..91ea6b5 --- /dev/null +++ b/src/main/kotlin/cc/woverflow/chatting/chat/ChatSpamBlock.kt @@ -0,0 +1,124 @@ +package cc.woverflow.chatting.chat + +import cc.woverflow.chatting.config.ChattingConfig +import com.google.gson.JsonObject +import com.google.gson.JsonParser +import java.text.Normalizer +import net.minecraft.util.ChatComponentText +import net.minecraft.util.EnumChatFormatting +import net.minecraftforge.client.event.ClientChatReceivedEvent +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent + +object ChatSpamBlock { + /* + Made by @KTibow + Based off of Unspam (also by @KTibow) + Algorithm based off of https://paulgraham.com/spam.html + */ + private val PLAYER_MESSAGE = Regex("^(\\[VIP\\+?\\] |\\[MVP\\+?\\+?\\] |)(\\w{2,16}): (.*)$") + + @SubscribeEvent + fun onChat(event: ClientChatReceivedEvent) { + val message = event.message.unformattedText.replace(Regex("\u00A7."), "") + if (!PLAYER_MESSAGE.matches(message)) return + + val (rank, player, content) = PLAYER_MESSAGE.matchEntire(message)!!.destructured + + if (ChattingConfig.spamThreshold != 100) { + val tokens = tokenize(content) + val spamProb = findSpamProbability(tokens) + if (spamProb * 100 > ChattingConfig.spamThreshold) { + if (ChattingConfig.hideSpam) { + event.isCanceled = true + } else { + var newMessage = + EnumChatFormatting.DARK_GRAY.toString() + + EnumChatFormatting.STRIKETHROUGH.toString() + if (!ChattingConfig.customChatFormatting) { + newMessage += rank + } + newMessage += "$player${EnumChatFormatting.DARK_GRAY.toString()}: $content" + event.message = ChatComponentText(newMessage) + } + return + } + } + if (ChattingConfig.customChatFormatting) { + val coloredPlayer = findRankColor(rank) + player + EnumChatFormatting.RESET.toString() + event.message = ChatComponentText("$coloredPlayer: $content") + } + } + + private fun tokenize(message: String): MutableList { + val strippedMessage = + Normalizer.normalize(message, Normalizer.Form.NFKC) + .replace(Regex("[^\\w\\s/]"), " ") + .lowercase() + .trim() + val tokens = strippedMessage.split(Regex("\\s+")).toMutableList() + if (tokens.size <= 2) { + tokens.add("TINY_LENGTH") + } else if (tokens.size <= 4) { + tokens.add("SMALL_LENGTH") + } else if (tokens.size <= 7) { + tokens.add("MEDIUM_LENGTH") + } else { + tokens.add("LONG_LENGTH") + } + if (message.replace(Regex("[\\w\\s]"), "").length > 2) { + tokens.add("SPECIAL_CHARS") + } else if (message.replace(Regex("[\\w\\s]"), "").length > 0) { + tokens.add("SPECIAL_CHAR") + } else { + tokens.add("LOW_SPECIAL_CHARS") + } + if (message.replace(Regex("[^A-Z]"), "").length >= message.length / 4) { + tokens.add("HIGH_CAPS") + } else { + tokens.add("LOW_CAPS") + } + return tokens + } + + private fun findSpamProbability(tokens: MutableList): Double { + val tokenProbs = mutableMapOf() + for (token in tokens) { + if (!spamInfoJson.has(token)) continue + val spamInToken = spamInfoJson.get(token).asJsonObject.get("spam").asDouble + val fineInToken = spamInfoJson.get(token).asJsonObject.get("fine").asDouble + tokenProbs[token] = + ((spamInToken / messageCountsJson.get("spam").asInt) / + (fineInToken / messageCountsJson.get("fine").asInt + + spamInToken / messageCountsJson.get("spam").asInt)) + } + val spamProbs = tokenProbs.values.toMutableList() + val fineProbs = tokenProbs.values.map { 1 - it }.toMutableList() + val spamProbability = spamProbs.reduce { a, b -> a * b } + val fineProbability = fineProbs.reduce { a, b -> a * b } + return spamProbability / (spamProbability + fineProbability) + } + + private fun findRankColor(rank: String): String { + println(rank) + return when (rank) { + "[VIP] ", + "[VIP+] " -> EnumChatFormatting.GREEN.toString() + "[MVP] ", + "[MVP+] " -> EnumChatFormatting.AQUA.toString() + "[MVP++] " -> EnumChatFormatting.GOLD.toString() + else -> EnumChatFormatting.GRAY.toString() + } + } + + private fun getResourceAsText(path: String): String? = + object {}.javaClass.getResource(path)?.readText() + private val spamInfoJson: JsonObject + private val messageCountsJson: JsonObject + + init { + // Load the file spamInfo.json from resources/ + val spamInfo = getResourceAsText("/spamInfo.json") + spamInfoJson = JsonParser().parse(spamInfo).asJsonObject + messageCountsJson = JsonParser().parse(" { \"fine\": 668, \"spam\": 230 }").asJsonObject + } +} diff --git a/src/main/kotlin/cc/woverflow/chatting/config/ChattingConfig.kt b/src/main/kotlin/cc/woverflow/chatting/config/ChattingConfig.kt index 1a936c9..3041af8 100644 --- a/src/main/kotlin/cc/woverflow/chatting/config/ChattingConfig.kt +++ b/src/main/kotlin/cc/woverflow/chatting/config/ChattingConfig.kt @@ -17,7 +17,11 @@ import java.awt.Color import java.io.File object ChattingConfig : - Vigilant(File(Chatting.modDir, "${Chatting.ID}.toml"), Chatting.NAME, sortingBehavior = ConfigSorting) { + Vigilant( + File(Chatting.modDir, "${Chatting.ID}.toml"), + Chatting.NAME, + sortingBehavior = ConfigSorting + ) { @Property( type = PropertyType.SELECTOR, @@ -73,7 +77,8 @@ object ChattingConfig : @Property( type = PropertyType.SWITCH, name = "Inform for Alternatives", - description = "Inform the user if a mod they are using can be replaced by a feature in Chatting.", + description = + "Inform the user if a mod they are using can be replaced by a feature in Chatting.", category = "General" ) var informForAlternatives = true @@ -97,6 +102,40 @@ object ChattingConfig : */ + @Property( + type = PropertyType.SLIDER, + min = 80, + max = 100, + name = "Spam Blocker Threshold", + description = + "If Chatting detects a public chat message that seems like spam, and the probability is higher than this, it will hide it.\n" + + "Made for Hypixel Skyblock. Set to 100% to disable. 95% is a reasonable threshold to use it at.\n" + + "Note that this is not and never will be 100% accurate; however, it's pretty much guaranteed to block most spam.", + category = "Player Chats" + ) + var spamThreshold = 100 + + @Property( + type = PropertyType.SWITCH, + name = "Custom Chat Formatting", + description = + "Reformat all Skyblock chat messages. Example:\n" + + "§a[VIP] Person§f: Message\n§7Person2: Message\n" + + "§eBecomes:\n" + + "§aPerson§f: Message\n§7Person2§f: Message", + category = "Player Chats" + ) + var customChatFormatting = false + + @Property( + type = PropertyType.SWITCH, + name = "Hide Spam", + description = + "When Chatting detects spam (if it's enabled), hide it instead of just graying it out.", + category = "Player Chats" + ) + var hideSpam = false + @Property( type = PropertyType.SWITCH, name = "Custom Chat Height", @@ -143,7 +182,10 @@ object ChattingConfig : var chatSearch = true @Property( - type = PropertyType.SWITCH, name = "Chat Tabs", description = "Add chat tabs.", category = "Tabs" + type = PropertyType.SWITCH, + name = "Chat Tabs", + description = "Add chat tabs.", + category = "Tabs" ) var chatTabs = true get() { @@ -164,7 +206,10 @@ object ChattingConfig : var hypixelOnlyChatTabs = true @Property( - type = PropertyType.SWITCH, name = "Chat Shortcuts", description = "Add chat shortcuts.", category = "Shortcuts" + type = PropertyType.SWITCH, + name = "Chat Shortcuts", + description = "Add chat shortcuts.", + category = "Shortcuts" ) var chatShortcuts = false get() { @@ -198,26 +243,27 @@ object ChattingConfig : chatTabs = funny ChatTabs.initialize() if (!funny) { - val dummy = ChatTab( - true, - "ALL", - false, - false, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - TabButton.color, - TabButton.hoveredColor, - TabButton.selectedColor, - "" - ) + val dummy = + ChatTab( + true, + "ALL", + false, + false, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + TabButton.color, + TabButton.hoveredColor, + TabButton.selectedColor, + "" + ) dummy.initialize() ChatTabs.currentTab = dummy } else { @@ -228,16 +274,13 @@ object ChattingConfig : chatShortcuts = funny ChatShortcuts.initialize() } - //addDependency("showTimestampHover", "showTimestamp") + // addDependency("showTimestampHover", "showTimestamp") } private object ConfigSorting : SortingBehavior() { override fun getCategoryComparator(): Comparator = Comparator { o1, o2 -> if (o1.name == "General") return@Comparator -1 - if (o2.name == "General") return@Comparator 1 - else compareValuesBy(o1, o2) { - it.name - } + if (o2.name == "General") return@Comparator 1 else compareValuesBy(o1, o2) { it.name } } } } -- cgit