ChatTrust

object ChatTrust

Whether a captured chat line is something run_command target=client may hand back by default.

The signal is the translation key: command feedback is translatable (commands.time.query, Brigadier's command.unknown.command, an i18n'd mod's own key), while player-influenced chat either carries a chat key or none at all — a formatting or cross-server plugin builds its lines with MiniMessage / legacy codes, whose output is literal text. That last case is what earns the machinery: forwarded chat arrives as a SYSTEM message on every version, so no structural signal sees it.

Best effort, deliberately: the point is keeping unrelated chatter out of a command's result, and a hostile server can wrap anything in any key anyway — allow_untrusted_chat is the boundary, this is the noise filter. What still gets through is a player name in a join or death line, a dozen [A-Za-z0-9_] characters, too small to carry an instruction; anything long enough (a book, a sign) has to be reached for and never arrives unbidden.

Reflection because no typed call spans the range: getContents is the only structural accessor and its return type moved (String ->ComponentContents) at 1.19. Mappings is already what run_command refuses to run without, for this same by-name reason.

Types

Link copied to clipboard
private class Reader(val getContents: Method?, val translatable: Class<*>, val getKey: Method)

getContents is null on 1.18.2, where the component IS the translatable rather than holding one.

Properties

Link copied to clipboard
private const val COMPONENT: String
Link copied to clipboard
private val DENY: List<String>
Link copied to clipboard
private const val MAX_DEPTH: Int = 16

The component is the server's to shape, and this walk runs on the client thread.

Link copied to clipboard
Link copied to clipboard
private const val TRANSLATABLE_COMPONENT: String
Link copied to clipboard
private const val TRANSLATABLE_CONTENTS: String

Functions

Link copied to clipboard
fun init()

Call once the mapping table is as loaded as it will get: a by-name lookup before that misses for a reason that goes away on its own, so resolving on first use would cache the wrong verdict.

Link copied to clipboard
fun isUntrusted(c: Component): Boolean

Fail-closed: an unresolved runtime, an unreadable component and a plugin's literal line all answer the same, and allow_untrusted_chat is the way back.

Link copied to clipboard
private fun resolve(): ChatTrust.Reader?
Link copied to clipboard
private fun scan(c: Component, r: ChatTrust.Reader, depth: Int): Int

-1 denied, 0 no key anywhere, 1 keyed and clean. A denied key ANYWHERE wins, so this cannot return on the first clean one.