From 647459f01168c9c7ac266e4b59450e0863ae6b76 Mon Sep 17 00:00:00 2001 From: phinner <62483793+Phinner@users.noreply.github.com> Date: Tue, 7 Nov 2023 14:33:47 +0100 Subject: [PATCH 1/5] chore: Smol changes in the PlaceholderPipeline --- .../com/xpdustry/imperium/mindustry/MindustryModule.kt | 4 ++-- .../imperium/mindustry/placeholder/PlaceholderPipeline.kt | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/MindustryModule.kt b/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/MindustryModule.kt index 92a25d9c..f92a7937 100644 --- a/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/MindustryModule.kt +++ b/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/MindustryModule.kt @@ -37,7 +37,7 @@ import com.xpdustry.imperium.mindustry.history.BlockHistory import com.xpdustry.imperium.mindustry.history.SimpleBlockHistory import com.xpdustry.imperium.mindustry.misc.Entities import com.xpdustry.imperium.mindustry.placeholder.PlaceholderPipeline -import com.xpdustry.imperium.mindustry.placeholder.SimplePlaceholderManager +import com.xpdustry.imperium.mindustry.placeholder.SimplePlaceholderPipeline import com.xpdustry.imperium.mindustry.security.GatekeeperPipeline import com.xpdustry.imperium.mindustry.security.SimpleGatekeeperPipeline import fr.xpdustry.distributor.api.plugin.MindustryPlugin @@ -62,7 +62,7 @@ fun mindustryModule(plugin: ImperiumPlugin) = single { SimpleChatMessagePipeline() } - single { SimplePlaceholderManager() } + single { SimplePlaceholderPipeline() } single("directory") { plugin.directory } diff --git a/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/placeholder/PlaceholderPipeline.kt b/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/placeholder/PlaceholderPipeline.kt index acb203c3..72965179 100644 --- a/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/placeholder/PlaceholderPipeline.kt +++ b/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/placeholder/PlaceholderPipeline.kt @@ -26,15 +26,15 @@ import kotlinx.coroutines.awaitAll import kotlinx.coroutines.withContext import mindustry.gen.Player -data class PlaceholderContext(val player: Player, val message: String) +data class PlaceholderContext(val player: Player?, val query: String) interface PlaceholderPipeline : ProcessorPipeline -class SimplePlaceholderManager : +class SimplePlaceholderPipeline : PlaceholderPipeline, AbstractProcessorPipeline() { override suspend fun pump(context: PlaceholderContext): String = withContext(ImperiumScope.MAIN.coroutineContext) { - PLACEHOLDER_REGEX.findAll(context.message) + PLACEHOLDER_REGEX.findAll(context.query) .map { it.groupValues[1] } .toSet() .map { placeholder -> @@ -53,7 +53,7 @@ class SimplePlaceholderManager : } .awaitAll() .filterNotNull() - .fold(context.message) { message, result -> + .fold(context.query) { message, result -> message.replace("%${result.first}%", result.second) } } From 9cff1735e057c46485a31b6edc5ed0c8ca21cfcd Mon Sep 17 00:00:00 2001 From: phinner <62483793+Phinner@users.noreply.github.com> Date: Tue, 7 Nov 2023 14:39:56 +0100 Subject: [PATCH 2/5] feat: Added placeholder pipeline to chat messages --- .../mindustry/chat/ChatMessageListener.kt | 15 ++++++++------- .../mindustry/placeholder/PlaceholderPipeline.kt | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/chat/ChatMessageListener.kt b/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/chat/ChatMessageListener.kt index 05faad3d..168d8329 100644 --- a/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/chat/ChatMessageListener.kt +++ b/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/chat/ChatMessageListener.kt @@ -52,26 +52,26 @@ import mindustry.net.ValidateException private val logger = logger("ROOT") class ChatMessageListener(instances: InstanceManager) : ImperiumApplication.Listener { - private val pipeline = instances.get() + private val chatMessagePipeline = instances.get() private val punishments = instances.get() override fun onImperiumInit() { // Intercept chat messages, so they go through the async processing pipeline Vars.net.handleServer(SendChatMessageCallPacket::class.java) { con, packet -> if (con.player == null || packet.message == null) return@handleServer - interceptChatMessage(con.player, packet.message, pipeline) + interceptChatMessage(con.player, packet.message, chatMessagePipeline) } // I don't know why but Foo client appends invisible characters to the end of messages, // this is very annoying for the discord bridge. - pipeline.register("anti-foo-sign", Priority.HIGHEST) { context -> + chatMessagePipeline.register("anti-foo-sign", Priority.HIGHEST) { context -> val msg = context.message // https://github.com/mindustry-antigrief/mindustry-client/blob/23025185c20d102f3fbb9d9a4c20196cc871d94b/core/src/mindustry/client/communication/InvisibleCharCoder.kt#L14 if (msg.takeLast(2).all { (0xF80 until 0x107F).contains(it.code) }) msg.dropLast(2) else msg } - pipeline.register("mute", Priority.HIGH) { ctx -> + chatMessagePipeline.register("mute", Priority.HIGH) { ctx -> if (ctx.sender == null) { return@register ctx.message } @@ -108,7 +108,8 @@ class ChatMessageListener(instances: InstanceManager) : ImperiumApplication.List if (sender.player.team() != target.team()) continue ImperiumScope.MAIN.launch { val filtered2 = - pipeline.pump(ChatMessageContext(sender.player, sender.player, message)) + chatMessagePipeline.pump( + ChatMessageContext(sender.player, sender.player, message)) if (filtered2.isBlank()) return@launch target.sendMessage( @@ -131,7 +132,7 @@ class ChatMessageListener(instances: InstanceManager) : ImperiumApplication.List Vars.netServer.admins.filterMessage(sender.player, message) } if (filtered1.isNullOrBlank()) return - val filtered2 = pipeline.pump(ChatMessageContext(sender.player, target, message)) + val filtered2 = chatMessagePipeline.pump(ChatMessageContext(sender.player, target, message)) if (filtered2.isBlank()) return val formatted = @@ -151,7 +152,7 @@ class ChatMessageListener(instances: InstanceManager) : ImperiumApplication.List // The null target represents the server, for logging purposes (Entities.PLAYERS + listOf(null)).forEach { target -> ImperiumScope.MAIN.launch { - val processed = pipeline.pump(ChatMessageContext(null, target, message)) + val processed = chatMessagePipeline.pump(ChatMessageContext(null, target, message)) if (processed.isBlank()) return@launch target?.sendMessage("[scarlet][[Server]:[] $processed") if (target == null) { diff --git a/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/placeholder/PlaceholderPipeline.kt b/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/placeholder/PlaceholderPipeline.kt index 72965179..8b91a29c 100644 --- a/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/placeholder/PlaceholderPipeline.kt +++ b/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/placeholder/PlaceholderPipeline.kt @@ -39,7 +39,7 @@ class SimplePlaceholderPipeline : .toSet() .map { placeholder -> async { - val parts = placeholder.split("_", limit = 2) + val parts = placeholder.split(":", limit = 2) val processor = processor(parts[0]) ?: return@async null val query = parts.getOrNull(1) ?: "" try { From bd8e79cd110a86481088e5932a554a9b11a4603b Mon Sep 17 00:00:00 2001 From: phinner <62483793+phinner@users.noreply.github.com> Date: Wed, 8 Nov 2023 11:05:17 +0100 Subject: [PATCH 3/5] feat: Actually added placeholder pipeline to chat messages --- .../common/bridge/BridgeChatMessage.kt | 3 +- .../imperium/common/config/ImperiumConfig.kt | 6 + .../imperium/discord/bridge/BridgeListener.kt | 5 +- .../discord/commands/ModerationCommand.kt | 7 +- .../imperium/discord/misc/UserExtensions.kt | 24 +++ .../chat/BridgeChatMessageListener.kt | 32 ++-- .../mindustry/chat/ChatMessageListener.kt | 178 ++++++++++++------ .../placeholder/PlaceholderPipeline.kt | 41 +++- 8 files changed, 212 insertions(+), 84 deletions(-) create mode 100644 imperium-discord/src/main/kotlin/com/xpdustry/imperium/discord/misc/UserExtensions.kt diff --git a/imperium-common/src/main/kotlin/com/xpdustry/imperium/common/bridge/BridgeChatMessage.kt b/imperium-common/src/main/kotlin/com/xpdustry/imperium/common/bridge/BridgeChatMessage.kt index 79241ad1..09aec2dd 100644 --- a/imperium-common/src/main/kotlin/com/xpdustry/imperium/common/bridge/BridgeChatMessage.kt +++ b/imperium-common/src/main/kotlin/com/xpdustry/imperium/common/bridge/BridgeChatMessage.kt @@ -18,8 +18,9 @@ package com.xpdustry.imperium.common.bridge import com.xpdustry.imperium.common.message.Message +import com.xpdustry.imperium.common.security.Identity import kotlinx.serialization.Serializable @Serializable -data class BridgeChatMessage(val serverName: String, val senderName: String, val message: String) : +data class BridgeChatMessage(val serverName: String, val sender: Identity, val message: String) : Message diff --git a/imperium-common/src/main/kotlin/com/xpdustry/imperium/common/config/ImperiumConfig.kt b/imperium-common/src/main/kotlin/com/xpdustry/imperium/common/config/ImperiumConfig.kt index f1362483..af41ec33 100644 --- a/imperium-common/src/main/kotlin/com/xpdustry/imperium/common/config/ImperiumConfig.kt +++ b/imperium-common/src/main/kotlin/com/xpdustry/imperium/common/config/ImperiumConfig.kt @@ -98,6 +98,7 @@ sealed interface ServerConfig { val color: Color = Color.WHITE, val world: World = World(), val security: Security = Security(), + val templates: Templates = Templates(), ) : ServerConfig { init { require(name != "discord") { "Mindustry Server name cannot be discord" } @@ -121,6 +122,11 @@ sealed interface ServerConfig { val imageProcessingDelay: Duration = 3.seconds, ) + data class Templates( + val chatMessage: String = + "[cyan]<%subject_playtime:hours%[cyan]> [%subject_color:hex%]%subject_name:display% [cyan]> [white]", + ) + companion object { private val NAME_REGEX = Regex("^[a-z0-9](-?[a-z0-9])+\$") } diff --git a/imperium-discord/src/main/kotlin/com/xpdustry/imperium/discord/bridge/BridgeListener.kt b/imperium-discord/src/main/kotlin/com/xpdustry/imperium/discord/bridge/BridgeListener.kt index 0bd161d7..dc7dcfdf 100644 --- a/imperium-discord/src/main/kotlin/com/xpdustry/imperium/discord/bridge/BridgeListener.kt +++ b/imperium-discord/src/main/kotlin/com/xpdustry/imperium/discord/bridge/BridgeListener.kt @@ -27,6 +27,7 @@ import com.xpdustry.imperium.common.inject.get import com.xpdustry.imperium.common.message.Messenger import com.xpdustry.imperium.common.message.consumer import com.xpdustry.imperium.common.misc.LoggerDelegate +import com.xpdustry.imperium.discord.misc.identity import com.xpdustry.imperium.discord.service.DiscordService import kotlin.jvm.optionals.getOrNull import kotlinx.coroutines.future.await @@ -50,7 +51,9 @@ class BridgeListener(instances: InstanceManager) : ImperiumApplication.Listener ImperiumScope.MAIN.launch { messenger.publish( BridgeChatMessage( - channel.name, event.message.author.name, event.message.content)) + channel.name, + event.message.author.asUser().get().identity, + event.message.content)) } } } diff --git a/imperium-discord/src/main/kotlin/com/xpdustry/imperium/discord/commands/ModerationCommand.kt b/imperium-discord/src/main/kotlin/com/xpdustry/imperium/discord/commands/ModerationCommand.kt index d126c6c8..de4d90fd 100644 --- a/imperium-discord/src/main/kotlin/com/xpdustry/imperium/discord/commands/ModerationCommand.kt +++ b/imperium-discord/src/main/kotlin/com/xpdustry/imperium/discord/commands/ModerationCommand.kt @@ -26,10 +26,10 @@ import com.xpdustry.imperium.common.image.LogicImageAnalysis import com.xpdustry.imperium.common.inject.InstanceManager import com.xpdustry.imperium.common.inject.get import com.xpdustry.imperium.common.misc.toInetAddress -import com.xpdustry.imperium.common.security.Identity import com.xpdustry.imperium.common.security.Punishment import com.xpdustry.imperium.common.security.PunishmentManager import com.xpdustry.imperium.discord.command.InteractionSender +import com.xpdustry.imperium.discord.misc.identity import java.time.Duration import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.take @@ -137,8 +137,7 @@ class ModerationCommand(instances: InstanceManager) : ImperiumApplication.Listen Punishment.Target(user.lastAddress!!, user._id) } - punishments.punish( - Identity.Discord(actor.user.name, actor.user.id), lookup, reason, type, duration) + punishments.punish(actor.user.identity, lookup, reason, type, duration) actor.respond("$verb user $target.") } @@ -161,7 +160,7 @@ class ModerationCommand(instances: InstanceManager) : ImperiumApplication.Listen return } - punishments.pardon(Identity.Discord(actor.user.name, actor.user.id), snowflake, reason) + punishments.pardon(actor.user.identity, snowflake, reason) actor.respond("Pardoned user.") } diff --git a/imperium-discord/src/main/kotlin/com/xpdustry/imperium/discord/misc/UserExtensions.kt b/imperium-discord/src/main/kotlin/com/xpdustry/imperium/discord/misc/UserExtensions.kt new file mode 100644 index 00000000..9af7228c --- /dev/null +++ b/imperium-discord/src/main/kotlin/com/xpdustry/imperium/discord/misc/UserExtensions.kt @@ -0,0 +1,24 @@ +/* + * Imperium, the software collection powering the Xpdustry network. + * Copyright (C) 2023 Xpdustry + * + * 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 . + */ +package com.xpdustry.imperium.discord.misc + +import com.xpdustry.imperium.common.security.Identity +import org.javacord.api.entity.user.User + +val User.identity: Identity + get() = Identity.Discord(name, id) diff --git a/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/chat/BridgeChatMessageListener.kt b/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/chat/BridgeChatMessageListener.kt index 1abf185e..db1540d5 100644 --- a/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/chat/BridgeChatMessageListener.kt +++ b/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/chat/BridgeChatMessageListener.kt @@ -22,41 +22,44 @@ import com.xpdustry.imperium.common.application.ImperiumApplication import com.xpdustry.imperium.common.async.ImperiumScope import com.xpdustry.imperium.common.bridge.BridgeChatMessage import com.xpdustry.imperium.common.bridge.MindustryPlayerMessage -import com.xpdustry.imperium.common.config.ImperiumConfig +import com.xpdustry.imperium.common.config.ServerConfig import com.xpdustry.imperium.common.inject.InstanceManager import com.xpdustry.imperium.common.inject.get import com.xpdustry.imperium.common.message.Messenger import com.xpdustry.imperium.common.message.consumer import com.xpdustry.imperium.common.misc.logger import com.xpdustry.imperium.common.misc.stripMindustryColors +import com.xpdustry.imperium.common.security.Identity import com.xpdustry.imperium.mindustry.misc.Entities import com.xpdustry.imperium.mindustry.misc.identity +import com.xpdustry.imperium.mindustry.placeholder.PlaceholderContext +import com.xpdustry.imperium.mindustry.placeholder.PlaceholderPipeline import fr.xpdustry.distributor.api.event.EventHandler import kotlinx.coroutines.launch import mindustry.game.EventType -import mindustry.gen.Iconc private val logger = logger("ROOT") class BridgeChatMessageListener(instances: InstanceManager) : ImperiumApplication.Listener { - private val config = instances.get() + private val config = instances.get() private val messenger = instances.get() - private val pipeline = instances.get() + private val chatMessagePipeline = instances.get() + private val placeholderPipeline = instances.get() override fun onImperiumInit() { messenger.consumer { - if (it.serverName != config.server.name) return@consumer + if (it.serverName != config.name) return@consumer // The null target represents the server, for logging purposes (Entities.PLAYERS + listOf(null)).forEach { target -> ImperiumScope.MAIN.launch { - val processed = pipeline.pump(ChatMessageContext(null, target, it.message)) + val processed = + chatMessagePipeline.pump(ChatMessageContext(null, target, it.message)) if (processed.isBlank()) return@launch - target?.sendMessage( - "[coral][[[white]${Iconc.discord}[]][[[orange]${it.senderName}[coral]]:[white] $processed") + target?.sendMessage(formatChatMessage(it.sender, processed)) if (target == null) { logger.info( "&fi&lcDiscord ({}): &fr&lw${processed.stripMindustryColors()}", - it.senderName) + it.sender.name) } } } @@ -68,7 +71,7 @@ class BridgeChatMessageListener(instances: InstanceManager) : ImperiumApplicatio ImperiumScope.MAIN.launch { messenger.publish( MindustryPlayerMessage( - config.server.name, event.player.identity, MindustryPlayerMessage.Action.Join)) + config.name, event.player.identity, MindustryPlayerMessage.Action.Join)) } @EventHandler @@ -76,7 +79,7 @@ class BridgeChatMessageListener(instances: InstanceManager) : ImperiumApplicatio ImperiumScope.MAIN.launch { messenger.publish( MindustryPlayerMessage( - config.server.name, event.player.identity, MindustryPlayerMessage.Action.Quit)) + config.name, event.player.identity, MindustryPlayerMessage.Action.Quit)) } @EventHandler @@ -84,9 +87,14 @@ class BridgeChatMessageListener(instances: InstanceManager) : ImperiumApplicatio ImperiumScope.MAIN.launch { messenger.publish( MindustryPlayerMessage( - config.server.name, + config.name, event.player.identity, MindustryPlayerMessage.Action.Chat(Strings.stripColors(event.message))), ) } + + private suspend fun formatChatMessage(subject: Identity, message: String): String { + return placeholderPipeline.pump(PlaceholderContext(subject, config.templates.chatMessage)) + + message + } } diff --git a/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/chat/ChatMessageListener.kt b/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/chat/ChatMessageListener.kt index 168d8329..1dbaa9bc 100644 --- a/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/chat/ChatMessageListener.kt +++ b/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/chat/ChatMessageListener.kt @@ -19,25 +19,35 @@ package com.xpdustry.imperium.mindustry.chat import arc.util.CommandHandler.ResponseType import arc.util.Time +import com.xpdustry.imperium.common.account.AccountManager import com.xpdustry.imperium.common.application.ImperiumApplication import com.xpdustry.imperium.common.async.ImperiumScope import com.xpdustry.imperium.common.command.Command import com.xpdustry.imperium.common.command.annotation.Greedy +import com.xpdustry.imperium.common.config.ServerConfig import com.xpdustry.imperium.common.inject.InstanceManager import com.xpdustry.imperium.common.inject.get +import com.xpdustry.imperium.common.misc.MINDUSTRY_ACCENT_COLOR import com.xpdustry.imperium.common.misc.logger import com.xpdustry.imperium.common.misc.stripMindustryColors +import com.xpdustry.imperium.common.misc.toHexString import com.xpdustry.imperium.common.misc.toInetAddress +import com.xpdustry.imperium.common.security.Identity import com.xpdustry.imperium.common.security.Punishment import com.xpdustry.imperium.common.security.PunishmentManager import com.xpdustry.imperium.mindustry.command.annotation.ClientSide import com.xpdustry.imperium.mindustry.command.annotation.ServerSide import com.xpdustry.imperium.mindustry.misc.Entities +import com.xpdustry.imperium.mindustry.misc.identity import com.xpdustry.imperium.mindustry.misc.runMindustryThread import com.xpdustry.imperium.mindustry.misc.showInfoMessage +import com.xpdustry.imperium.mindustry.placeholder.PlaceholderContext +import com.xpdustry.imperium.mindustry.placeholder.PlaceholderPipeline +import com.xpdustry.imperium.mindustry.placeholder.invalidQueryError import fr.xpdustry.distributor.api.DistributorProvider import fr.xpdustry.distributor.api.command.sender.CommandSender import fr.xpdustry.distributor.api.util.Priority +import java.awt.Color import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch @@ -49,17 +59,22 @@ import mindustry.net.Administration import mindustry.net.Packets.KickReason import mindustry.net.ValidateException -private val logger = logger("ROOT") +private val BLURPLE = Color(0x5865F2) +private val rootLogger = logger("ROOT") class ChatMessageListener(instances: InstanceManager) : ImperiumApplication.Listener { + private val chatMessagePipeline = instances.get() + private val placeholderPipeline = instances.get() + private val accounts = instances.get() private val punishments = instances.get() + private val config = instances.get() override fun onImperiumInit() { // Intercept chat messages, so they go through the async processing pipeline Vars.net.handleServer(SendChatMessageCallPacket::class.java) { con, packet -> if (con.player == null || packet.message == null) return@handleServer - interceptChatMessage(con.player, packet.message, chatMessagePipeline) + interceptChatMessage(con.player, packet.message) } // I don't know why but Foo client appends invisible characters to the end of messages, @@ -94,6 +109,50 @@ class ChatMessageListener(instances: InstanceManager) : ImperiumApplication.List } ctx.message } + + placeholderPipeline.register("subject_playtime") { (subject, query) -> + val playtime = + when (subject) { + is Identity.Mindustry -> accounts.findByIdentity(subject)?.playtime + is Identity.Discord -> accounts.findByDiscordId(subject.id)?.playtime + else -> null + } + ?: return@register "" + when (query) { + "hours" -> playtime.toHours().toString() + else -> invalidQueryError(query) + } + } + + placeholderPipeline.register("subject_name") { (subject, query) -> + when (query) { + "plain" -> subject.name + "display" -> + when (subject) { + is Identity.Mindustry -> + Entities.PLAYERS.find { it.uuid() == subject.uuid }?.name + ?: subject.name + else -> subject.name + } + else -> invalidQueryError(query) + } + } + + placeholderPipeline.register("subject_color") { (subject, query) -> + if (query != "hex") { + invalidQueryError(query) + } + when (subject) { + is Identity.Mindustry -> + Entities.PLAYERS.find { it.uuid() == subject.uuid } + ?.color + ?.toString() + ?.let { "#$it" } + ?: MINDUSTRY_ACCENT_COLOR.toHexString() + is Identity.Discord -> BLURPLE.toHexString() + else -> Color.RED.toHexString() + } + } } @Command(["t"]) @@ -113,7 +172,7 @@ class ChatMessageListener(instances: InstanceManager) : ImperiumApplication.List if (filtered2.isBlank()) return@launch target.sendMessage( - "[#${sender.player.team().color}] ${Vars.netServer.chatFormatter.format(sender.player, filtered2)}", + "[#${sender.player.team().color}] ${formatChatMessage(sender.player.identity, filtered2)}", sender.player, filtered2, ) @@ -135,8 +194,7 @@ class ChatMessageListener(instances: InstanceManager) : ImperiumApplication.List val filtered2 = chatMessagePipeline.pump(ChatMessageContext(sender.player, target, message)) if (filtered2.isBlank()) return - val formatted = - "[gray][] ${Vars.netServer.chatFormatter.format(sender.player, filtered2)}" + val formatted = "[gray][] ${formatChatMessage(sender.player.identity, filtered2)}" sender.player.sendMessage(formatted, sender.player, filtered2) target.sendMessage(formatted, sender.player, filtered2) } @@ -154,75 +212,81 @@ class ChatMessageListener(instances: InstanceManager) : ImperiumApplication.List ImperiumScope.MAIN.launch { val processed = chatMessagePipeline.pump(ChatMessageContext(null, target, message)) if (processed.isBlank()) return@launch - target?.sendMessage("[scarlet][[Server]:[] $processed") + target?.sendMessage(formatChatMessage(config.identity, processed)) if (target == null) { sender.sendMessage("&fi&lcServer: &fr&lw${processed.stripMindustryColors()}") } } } } -} -private fun interceptChatMessage(sender: Player, message: String, pipeline: ChatMessagePipeline) { - // do not receive chat messages from clients that are too young or not registered - if (Time.timeSinceMillis(sender.con.connectTime) < 500 || - !sender.con.hasConnected || - !sender.isAdded) - return - - // detect and kick for foul play - if (!sender.con.chatRate.allow(2000, Administration.Config.chatSpamLimit.num())) { - sender.con.kick(KickReason.kick) - Vars.netServer.admins.blacklistDos(sender.con.address) - return + private suspend fun formatChatMessage(subject: Identity, message: String): String { + return placeholderPipeline.pump(PlaceholderContext(subject, config.templates.chatMessage)) + + message } - if (message.length > Vars.maxTextLength) { - throw ValidateException(sender, "Player has sent a message above the text limit.") - } + private fun interceptChatMessage(sender: Player, message: String) { + // do not receive chat messages from clients that are too young or not registered + if (Time.timeSinceMillis(sender.con.connectTime) < 500 || + !sender.con.hasConnected || + !sender.isAdded) + return + + // detect and kick for foul play + if (!sender.con.chatRate.allow(2000, Administration.Config.chatSpamLimit.num())) { + sender.con.kick(KickReason.kick) + Vars.netServer.admins.blacklistDos(sender.con.address) + return + } - val escaped = message.replace("\n", "") + if (message.length > Vars.maxTextLength) { + throw ValidateException(sender, "Player has sent a message above the text limit.") + } - DistributorProvider.get().eventBus.post(PlayerChatEvent(sender, escaped)) + val escaped = message.replace("\n", "") - // log commands before they are handled - if (escaped.startsWith(Vars.netServer.clientCommands.getPrefix())) { - // log with brackets - logger.info("<&fi{}: {}&fr>", "&lk${sender.plainName()}", "&lw$escaped") - } + DistributorProvider.get().eventBus.post(PlayerChatEvent(sender, escaped)) - // check if it's a command - val response = Vars.netServer.clientCommands.handleMessage(escaped, sender) + // log commands before they are handled + if (escaped.startsWith(Vars.netServer.clientCommands.getPrefix())) { + // log with brackets + rootLogger.info("<&fi{}: {}&fr>", "&lk${sender.plainName()}", "&lw$escaped") + } + + // check if it's a command + val response = Vars.netServer.clientCommands.handleMessage(escaped, sender) - if (response.type != ResponseType.noCommand) { - // a command was sent, now get the output - if (response.type != ResponseType.valid) { - val text = Vars.netServer.invalidHandler.handle(sender, response) - if (text != null) { - sender.sendMessage(text) + if (response.type != ResponseType.noCommand) { + // a command was sent, now get the output + if (response.type != ResponseType.valid) { + val text = Vars.netServer.invalidHandler.handle(sender, response) + if (text != null) { + sender.sendMessage(text) + } } + return } - return - } - val filtered1 = Vars.netServer.admins.filterMessage(sender, escaped) - if (filtered1.isNullOrBlank()) return - - // The null target represents the server, for logging and event purposes - (Entities.PLAYERS + listOf(null)).forEach { target -> - ImperiumScope.MAIN.launch { - val filtered2 = pipeline.pump(ChatMessageContext(sender, target, filtered1)) - if (filtered2.isBlank()) return@launch - target?.sendMessage(Vars.netServer.chatFormatter.format(sender, filtered2)) - if (target == null) { - logger.info( - "&fi{}: {}", - "&lc${sender.name().stripMindustryColors()}", - "&lw${filtered2.stripMindustryColors()}") - runMindustryThread { - DistributorProvider.get() - .eventBus - .post(ProcessedPlayerChatEvent(sender, filtered2)) + val filtered1 = Vars.netServer.admins.filterMessage(sender, escaped) + if (filtered1.isNullOrBlank()) return + + // The null target represents the server, for logging and event purposes + (Entities.PLAYERS + listOf(null)).forEach { target -> + ImperiumScope.MAIN.launch { + val filtered2 = + chatMessagePipeline.pump(ChatMessageContext(sender, target, filtered1)) + if (filtered2.isBlank()) return@launch + target?.sendMessage(formatChatMessage(sender.identity, filtered2)) + if (target == null) { + rootLogger.info( + "&fi{}: {}", + "&lc${sender.name().stripMindustryColors()}", + "&lw${filtered2.stripMindustryColors()}") + runMindustryThread { + DistributorProvider.get() + .eventBus + .post(ProcessedPlayerChatEvent(sender, filtered2)) + } } } } diff --git a/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/placeholder/PlaceholderPipeline.kt b/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/placeholder/PlaceholderPipeline.kt index 8b91a29c..6b9f8ed2 100644 --- a/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/placeholder/PlaceholderPipeline.kt +++ b/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/placeholder/PlaceholderPipeline.kt @@ -19,24 +19,25 @@ package com.xpdustry.imperium.mindustry.placeholder import com.xpdustry.imperium.common.async.ImperiumScope import com.xpdustry.imperium.common.misc.LoggerDelegate +import com.xpdustry.imperium.common.security.Identity import com.xpdustry.imperium.mindustry.processing.AbstractProcessorPipeline import com.xpdustry.imperium.mindustry.processing.ProcessorPipeline import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.withContext -import mindustry.gen.Player -data class PlaceholderContext(val player: Player?, val query: String) +data class PlaceholderContext(val subject: Identity, val query: String) interface PlaceholderPipeline : ProcessorPipeline +fun invalidQueryError(query: String): Nothing = + throw IllegalArgumentException("Invalid query: $query") + class SimplePlaceholderPipeline : PlaceholderPipeline, AbstractProcessorPipeline() { override suspend fun pump(context: PlaceholderContext): String = withContext(ImperiumScope.MAIN.coroutineContext) { - PLACEHOLDER_REGEX.findAll(context.query) - .map { it.groupValues[1] } - .toSet() + extractPlaceholders(context.query) .map { placeholder -> async { val parts = placeholder.split(":", limit = 2) @@ -44,7 +45,7 @@ class SimplePlaceholderPipeline : val query = parts.getOrNull(1) ?: "" try { placeholder to - processor.process(PlaceholderContext(context.player, query)) + processor.process(PlaceholderContext(context.subject, query)) } catch (error: Exception) { logger.error("Failed to process placeholder '{}'", placeholder, error) null @@ -53,13 +54,35 @@ class SimplePlaceholderPipeline : } .awaitAll() .filterNotNull() - .fold(context.query) { message, result -> - message.replace("%${result.first}%", result.second) + .fold(context.query) { message, (placeholder, result) -> + message.replace("%$placeholder%", result) } } + private fun extractPlaceholders(query: String): Set { + val placeholders = mutableSetOf() + var index = 0 + while (index < query.length) { + if (query[index] == '%') { + val start = index + do { + index++ + } while (index < query.length && query[index] != '%') + if (index == query.length) { + logger.warn("Invalid placeholder query: {}", query) + break + } else if (index == start + 1) { + index++ + continue + } + placeholders.add(query.substring(start + 1, index)) + } + index++ + } + return placeholders + } + companion object { private val logger by LoggerDelegate() - private val PLACEHOLDER_REGEX = Regex("%([^%]+)%") } } From 1dafa5eb5481021b7a0aa1fe35ac3fa2cecf986c Mon Sep 17 00:00:00 2001 From: phinner <62483793+phinner@users.noreply.github.com> Date: Wed, 8 Nov 2023 14:26:03 +0100 Subject: [PATCH 4/5] fix: Fix whisper command allowing sending message to sender --- .../xpdustry/imperium/mindustry/chat/ChatMessageListener.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/chat/ChatMessageListener.kt b/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/chat/ChatMessageListener.kt index 1dbaa9bc..df011f08 100644 --- a/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/chat/ChatMessageListener.kt +++ b/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/chat/ChatMessageListener.kt @@ -187,6 +187,10 @@ class ChatMessageListener(instances: InstanceManager) : ImperiumApplication.List target: Player, @Greedy message: String ) { + if (sender.player == target) { + sender.sendWarning("You can't whisper to yourself.") + return + } val filtered1 = runMindustryThread { Vars.netServer.admins.filterMessage(sender.player, message) } From 9174f3d3482b61c6eefcb1aab684f7a40a6067d6 Mon Sep 17 00:00:00 2001 From: phinner <62483793+phinner@users.noreply.github.com> Date: Wed, 8 Nov 2023 14:32:16 +0100 Subject: [PATCH 5/5] fix: Better formatting --- .../imperium/common/config/ImperiumConfig.kt | 5 ++-- .../common/misc/MindustryExtensions.kt | 2 ++ .../chat/BridgeChatMessageListener.kt | 14 +++++++-- .../mindustry/chat/ChatMessageListener.kt | 30 ++++++++++++------- 4 files changed, 36 insertions(+), 15 deletions(-) diff --git a/imperium-common/src/main/kotlin/com/xpdustry/imperium/common/config/ImperiumConfig.kt b/imperium-common/src/main/kotlin/com/xpdustry/imperium/common/config/ImperiumConfig.kt index af41ec33..fe5a11dd 100644 --- a/imperium-common/src/main/kotlin/com/xpdustry/imperium/common/config/ImperiumConfig.kt +++ b/imperium-common/src/main/kotlin/com/xpdustry/imperium/common/config/ImperiumConfig.kt @@ -123,8 +123,9 @@ sealed interface ServerConfig { ) data class Templates( - val chatMessage: String = - "[cyan]<%subject_playtime:hours%[cyan]> [%subject_color:hex%]%subject_name:display% [cyan]> [white]", + val chatPrefix: String = "<%prefix%>", + val chatFormat: String = + "[cyan]<[white]%subject_playtime:chaotic%[cyan]> [%subject_color:hex%]%subject_name:display% [cyan]>[white]", ) companion object { diff --git a/imperium-common/src/main/kotlin/com/xpdustry/imperium/common/misc/MindustryExtensions.kt b/imperium-common/src/main/kotlin/com/xpdustry/imperium/common/misc/MindustryExtensions.kt index 31349c4e..47ba7184 100644 --- a/imperium-common/src/main/kotlin/com/xpdustry/imperium/common/misc/MindustryExtensions.kt +++ b/imperium-common/src/main/kotlin/com/xpdustry/imperium/common/misc/MindustryExtensions.kt @@ -21,6 +21,8 @@ import java.awt.Color val MINDUSTRY_ACCENT_COLOR = Color(0xffd37f) +val MINDUSTRY_ORANGE_COLOR = Color(0xffa108) + fun CharSequence.stripMindustryColors(): String { val out = StringBuilder(length) var index = 0 diff --git a/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/chat/BridgeChatMessageListener.kt b/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/chat/BridgeChatMessageListener.kt index db1540d5..7e54c039 100644 --- a/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/chat/BridgeChatMessageListener.kt +++ b/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/chat/BridgeChatMessageListener.kt @@ -29,15 +29,19 @@ import com.xpdustry.imperium.common.message.Messenger import com.xpdustry.imperium.common.message.consumer import com.xpdustry.imperium.common.misc.logger import com.xpdustry.imperium.common.misc.stripMindustryColors +import com.xpdustry.imperium.common.misc.toHexString import com.xpdustry.imperium.common.security.Identity import com.xpdustry.imperium.mindustry.misc.Entities import com.xpdustry.imperium.mindustry.misc.identity import com.xpdustry.imperium.mindustry.placeholder.PlaceholderContext import com.xpdustry.imperium.mindustry.placeholder.PlaceholderPipeline import fr.xpdustry.distributor.api.event.EventHandler +import java.awt.Color import kotlinx.coroutines.launch import mindustry.game.EventType +import mindustry.gen.Iconc +private val BLURPLE = Color(0x5865F2) private val logger = logger("ROOT") class BridgeChatMessageListener(instances: InstanceManager) : ImperiumApplication.Listener { @@ -55,7 +59,8 @@ class BridgeChatMessageListener(instances: InstanceManager) : ImperiumApplicatio val processed = chatMessagePipeline.pump(ChatMessageContext(null, target, it.message)) if (processed.isBlank()) return@launch - target?.sendMessage(formatChatMessage(it.sender, processed)) + target?.sendMessage( + "[${BLURPLE.toHexString()}]${getDiscordChatPrefix()} ${formatChatMessage(it.sender, processed)}") if (target == null) { logger.info( "&fi&lcDiscord ({}): &fr&lw${processed.stripMindustryColors()}", @@ -94,7 +99,12 @@ class BridgeChatMessageListener(instances: InstanceManager) : ImperiumApplicatio } private suspend fun formatChatMessage(subject: Identity, message: String): String { - return placeholderPipeline.pump(PlaceholderContext(subject, config.templates.chatMessage)) + + return placeholderPipeline.pump(PlaceholderContext(subject, config.templates.chatFormat)) + + " " + message } + + private fun getDiscordChatPrefix(): String { + return config.templates.chatPrefix.replace("%prefix%", Iconc.discord.toString()) + } } diff --git a/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/chat/ChatMessageListener.kt b/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/chat/ChatMessageListener.kt index df011f08..0ab6ebf5 100644 --- a/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/chat/ChatMessageListener.kt +++ b/imperium-mindustry/src/main/kotlin/com/xpdustry/imperium/mindustry/chat/ChatMessageListener.kt @@ -27,7 +27,7 @@ import com.xpdustry.imperium.common.command.annotation.Greedy import com.xpdustry.imperium.common.config.ServerConfig import com.xpdustry.imperium.common.inject.InstanceManager import com.xpdustry.imperium.common.inject.get -import com.xpdustry.imperium.common.misc.MINDUSTRY_ACCENT_COLOR +import com.xpdustry.imperium.common.misc.MINDUSTRY_ORANGE_COLOR import com.xpdustry.imperium.common.misc.logger import com.xpdustry.imperium.common.misc.stripMindustryColors import com.xpdustry.imperium.common.misc.toHexString @@ -48,22 +48,22 @@ import fr.xpdustry.distributor.api.DistributorProvider import fr.xpdustry.distributor.api.command.sender.CommandSender import fr.xpdustry.distributor.api.util.Priority import java.awt.Color +import java.text.DecimalFormat import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import mindustry.Vars import mindustry.game.EventType.PlayerChatEvent +import mindustry.gen.Iconc import mindustry.gen.Player import mindustry.gen.SendChatMessageCallPacket import mindustry.net.Administration import mindustry.net.Packets.KickReason import mindustry.net.ValidateException -private val BLURPLE = Color(0x5865F2) private val rootLogger = logger("ROOT") class ChatMessageListener(instances: InstanceManager) : ImperiumApplication.Listener { - private val chatMessagePipeline = instances.get() private val placeholderPipeline = instances.get() private val accounts = instances.get() @@ -120,6 +120,7 @@ class ChatMessageListener(instances: InstanceManager) : ImperiumApplication.List ?: return@register "" when (query) { "hours" -> playtime.toHours().toString() + "chaotic" -> DecimalFormat("000").format(playtime.toHours()) else -> invalidQueryError(query) } } @@ -148,8 +149,8 @@ class ChatMessageListener(instances: InstanceManager) : ImperiumApplication.List ?.color ?.toString() ?.let { "#$it" } - ?: MINDUSTRY_ACCENT_COLOR.toHexString() - is Identity.Discord -> BLURPLE.toHexString() + ?: MINDUSTRY_ORANGE_COLOR.toHexString() + is Identity.Discord -> MINDUSTRY_ORANGE_COLOR.toHexString() else -> Color.RED.toHexString() } } @@ -172,7 +173,7 @@ class ChatMessageListener(instances: InstanceManager) : ImperiumApplication.List if (filtered2.isBlank()) return@launch target.sendMessage( - "[#${sender.player.team().color}] ${formatChatMessage(sender.player.identity, filtered2)}", + "[#${sender.player.team().color}]${getChatPrefix("T")} ${getChatFormat(sender.player.identity, filtered2)}", sender.player, filtered2, ) @@ -198,7 +199,8 @@ class ChatMessageListener(instances: InstanceManager) : ImperiumApplication.List val filtered2 = chatMessagePipeline.pump(ChatMessageContext(sender.player, target, message)) if (filtered2.isBlank()) return - val formatted = "[gray][] ${formatChatMessage(sender.player.identity, filtered2)}" + val formatted = + "[gray]${getChatPrefix("W")}[] ${getChatFormat(sender.player.identity, filtered2)}" sender.player.sendMessage(formatted, sender.player, filtered2) target.sendMessage(formatted, sender.player, filtered2) } @@ -216,7 +218,8 @@ class ChatMessageListener(instances: InstanceManager) : ImperiumApplication.List ImperiumScope.MAIN.launch { val processed = chatMessagePipeline.pump(ChatMessageContext(null, target, message)) if (processed.isBlank()) return@launch - target?.sendMessage(formatChatMessage(config.identity, processed)) + target?.sendMessage( + "[${Color.RED}]${getChatPrefix(Iconc.infoCircle.toString())} ${getChatFormat(config.identity, processed)}") if (target == null) { sender.sendMessage("&fi&lcServer: &fr&lw${processed.stripMindustryColors()}") } @@ -224,11 +227,16 @@ class ChatMessageListener(instances: InstanceManager) : ImperiumApplication.List } } - private suspend fun formatChatMessage(subject: Identity, message: String): String { - return placeholderPipeline.pump(PlaceholderContext(subject, config.templates.chatMessage)) + + private suspend fun getChatFormat(subject: Identity, message: String): String { + return placeholderPipeline.pump(PlaceholderContext(subject, config.templates.chatFormat)) + + " " + message } + private fun getChatPrefix(prefix: String): String { + return config.templates.chatPrefix.replace("%prefix%", prefix) + } + private fun interceptChatMessage(sender: Player, message: String) { // do not receive chat messages from clients that are too young or not registered if (Time.timeSinceMillis(sender.con.connectTime) < 500 || @@ -280,7 +288,7 @@ class ChatMessageListener(instances: InstanceManager) : ImperiumApplication.List val filtered2 = chatMessagePipeline.pump(ChatMessageContext(sender, target, filtered1)) if (filtered2.isBlank()) return@launch - target?.sendMessage(formatChatMessage(sender.identity, filtered2)) + target?.sendMessage(getChatFormat(sender.identity, filtered2)) if (target == null) { rootLogger.info( "&fi{}: {}",