package at.hannibal2.skyhanni.data import at.hannibal2.skyhanni.SkyHanniMod import at.hannibal2.skyhanni.events.LorenzChatEvent import at.hannibal2.skyhanni.events.MessageSendToServerEvent import at.hannibal2.skyhanni.events.PacketEvent import at.hannibal2.skyhanni.features.chat.ChatFilterGui import at.hannibal2.skyhanni.utils.ChatUtils import at.hannibal2.skyhanni.utils.IdentityCharacteristics import at.hannibal2.skyhanni.utils.LorenzLogger import at.hannibal2.skyhanni.utils.LorenzUtils import at.hannibal2.skyhanni.utils.ReflectionUtils.getClassInstance import at.hannibal2.skyhanni.utils.ReflectionUtils.getModContainer import at.hannibal2.skyhanni.utils.ReflectionUtils.makeAccessible import at.hannibal2.skyhanni.utils.StringUtils.removeColor import net.minecraft.client.Minecraft import net.minecraft.client.gui.ChatLine import net.minecraft.client.gui.GuiNewChat import net.minecraft.event.HoverEvent import net.minecraft.network.play.client.C01PacketChatMessage import net.minecraft.util.ChatComponentText import net.minecraft.util.EnumChatFormatting import net.minecraft.util.IChatComponent import net.minecraftforge.client.event.ClientChatReceivedEvent import net.minecraftforge.fml.common.eventhandler.SubscribeEvent import net.minecraftforge.fml.relauncher.ReflectionHelper import java.lang.invoke.MethodHandles object ChatManager { private val loggerAll = LorenzLogger("chat/all") private val loggerFiltered = LorenzLogger("chat/blocked") private val loggerAllowed = LorenzLogger("chat/allowed") private val loggerModified = LorenzLogger("chat/modified") private val loggerFilteredTypes = mutableMapOf() private val messageHistory = object : LinkedHashMap, MessageFilteringResult>() { override fun removeEldestEntry(eldest: MutableMap.MutableEntry, MessageFilteringResult>?): Boolean { return size > 100 } } private fun getRecentMessageHistory(): List = messageHistory.toList().map { it.second } private fun getRecentMessageHistoryWithSearch(searchTerm: String): List = messageHistory.toList().map { it.second } .filter { it.message.formattedText.removeColor().contains(searchTerm, ignoreCase = true) } enum class ActionKind(format: Any) { BLOCKED(EnumChatFormatting.RED.toString() + EnumChatFormatting.BOLD), RETRACTED(EnumChatFormatting.DARK_PURPLE.toString() + EnumChatFormatting.BOLD), MODIFIED(EnumChatFormatting.YELLOW.toString() + EnumChatFormatting.BOLD), ALLOWED(EnumChatFormatting.GREEN), OUTGOING(EnumChatFormatting.BLUE), OUTGOING_BLOCKED(EnumChatFormatting.BLUE.toString() + EnumChatFormatting.BOLD), ; val renderedString = "$format$name" companion object { val maxLength by lazy { entries.maxOf { Minecraft.getMinecraft().fontRendererObj.getStringWidth(it.renderedString) } } } } data class MessageFilteringResult( val message: IChatComponent, var actionKind: ActionKind, var actionReason: String?, val modified: IChatComponent?, val hoverInfo: List = listOf(), val hoverExtraInfo: List = listOf(), ) @SubscribeEvent fun onSendMessageToServerPacket(event: PacketEvent.SendEvent) { val packet = event.packet as? C01PacketChatMessage ?: return val message = packet.message val component = ChatComponentText(message) val originatingModCall = event.findOriginatingModCall() val originatingModContainer = originatingModCall?.getClassInstance()?.getModContainer() val hoverInfo = listOf( "§7Message created by §a${originatingModCall?.toString() ?: "§cprobably minecraft"}", "§7Mod id: §a${originatingModContainer?.modId}", "§7Mod name: §a${originatingModContainer?.name}" ) val stackTrace = Thread.currentThread().stackTrace.map { "§7 §2${it.className}§7.§a${it.methodName}§7" + if (it.fileName == null) "" else "(§b${it.fileName}§7:§3${it.lineNumber}§7)" } val result = MessageFilteringResult( component, ActionKind.OUTGOING, null, null, hoverInfo = hoverInfo, hoverExtraInfo = hoverInfo + listOf("") + stackTrace ) messageHistory[IdentityCharacteristics(component)] = result val trimmedMessage = message.trimEnd() if (MessageSendToServerEvent( trimmedMessage, trimmedMessage.split(" "), originatingModContainer ).postAndCatch() ) { event.isCanceled = true messageHistory[IdentityCharacteristics(component)] = result.copy(actionKind = ActionKind.OUTGOING_BLOCKED) } } @SubscribeEvent(receiveCanceled = true) fun onChatReceive(event: ClientChatReceivedEvent) { if (event.type.toInt() == 2) return val original = event.message val message = LorenzUtils.stripVanillaMessage(original.formattedText) if (message.startsWith("§f{\"server\":\"")) { HypixelData.checkForLocraw(message) return } val key = IdentityCharacteristics(original) val chatEvent = LorenzChatEvent(message, original) if (!isSoopyMessage(event.message)) { chatEvent.postAndCatch() } val blockReason = chatEvent.blockedReason.uppercase() if (blockReason != "") { event.isCanceled = true loggerFiltered.log("[$blockReason] $message") loggerAll.log("[$blockReason] $message") loggerFilteredTypes.getOrPut(blockReason) { LorenzLogger("chat/filter_blocked/$blockReason") } .log(message) messageHistory[key] = MessageFilteringResult(original, ActionKind.BLOCKED, blockReason, null) return } val modified = chatEvent.chatComponent loggerAllowed.log("[allowed] $message") loggerAll.log("[allowed] $message") if (modified.formattedText != original.formattedText) { event.message = chatEvent.chatComponent loggerModified.log(" ") loggerModified.log("[original] " + original.formattedText) loggerModified.log("[modified] " + modified.formattedText) messageHistory[key] = MessageFilteringResult(original, ActionKind.MODIFIED, null, modified) } else { messageHistory[key] = MessageFilteringResult(original, ActionKind.ALLOWED, null, null) } // TODO: Handle this with ChatManager.retractMessage or some other way for logging and /shchathistory purposes? if (chatEvent.chatLineId != 0) { event.isCanceled = true Minecraft.getMinecraft().ingameGUI.chatGUI.printChatMessageWithOptionalDeletion( event.message, chatEvent.chatLineId ) } } private fun isSoopyMessage(message: IChatComponent): Boolean { for (sibling in message.siblings) { if (isSoopyMessage(sibling)) return true } val style = message.chatStyle ?: return false val hoverEvent = style.chatHoverEvent ?: return false if (hoverEvent.action != HoverEvent.Action.SHOW_TEXT) return false val text = hoverEvent.value?.formattedText ?: return false val lines = text.split("\n") if (lines.isEmpty()) return false val last = lines.last() if (last.startsWith("§f§lCOMMON")) return true if (last.startsWith("§a§lUNCOMMON")) return true if (last.startsWith("§9§lRARE")) return true if (last.startsWith("§5§lEPIC")) return true if (last.startsWith("§6§lLEGENDARY")) return true if (last.startsWith("§d§lMYTHIC")) return true if (last.startsWith("§c§lSPECIAL")) return true // TODO confirm this format is correct if (last.startsWith("§c§lVERY SPECIAL")) return true return false } fun openChatFilterGUI(args: Array) { SkyHanniMod.screenToOpen = if (args.isEmpty()) { ChatFilterGui(getRecentMessageHistory()) } else { val searchTerm = args.joinToString(" ") val history = getRecentMessageHistoryWithSearch(searchTerm) if (history.isEmpty()) { ChatUtils.chat("§eNot found in chat history! ($searchTerm)") return } ChatFilterGui(history) } } private val chatLinesField by lazy { MethodHandles.publicLookup().unreflectGetter( ReflectionHelper.findField(GuiNewChat::class.java, "chatLines", "field_146252_h", "h") .makeAccessible() ) } fun retractMessage(message: IChatComponent?, reason: String) { if (message == null) return val chatGUI = Minecraft.getMinecraft().ingameGUI.chatGUI @Suppress("UNCHECKED_CAST") val chatLines = chatLinesField.invokeExact(chatGUI) as MutableList? ?: return if (!chatLines.removeIf { it?.chatComponent === message }) return chatGUI.refreshChat() val history = messageHistory[IdentityCharacteristics(message)] ?: return history.actionKind = ActionKind.RETRACTED history.actionReason = reason.uppercase() } }