diff --git a/README.md b/README.md index ec1c594..fbd09d2 100644 --- a/README.md +++ b/README.md @@ -59,9 +59,10 @@ Because we want to have a chat system that actually wOREks for us. ## Other Commands -| Command | Permission | Description | Aliases | -|-------------------------|-----------------------|-------------------------------|------------| -| `/chattore reload` | `chattore.manage` | Reload Chattore configuration | No aliases | -| `/commandspy` | `chattore.commandspy` | Toggle spying on commands | No aliases | -| `/chattore version` | `chattore.manage` | View the version of Chattore | No aliases | -| `/emoji +` | `chattore.emoji` | View multiple emojis | No aliases | +| Command | Permission | Description | Aliases | +|---------------------------------|-----------------------|-------------------------------|------------| +| `/chattore reload` | `chattore.manage` | Reload Chattore configuration | No aliases | +| `/commandspy` | `chattore.commandspy` | Toggle spying on commands | No aliases | +| `/chattore version` | `chattore.manage` | View the version of Chattore | No aliases | +| `/emoji +` | `chattore.emoji` | View multiple emojis | No aliases | +| `/chatreply ` | `chattore.chat` | Reply to a chat message | No aliases | diff --git a/chattore/src/main/kotlin/ChattOREConfig.kt b/chattore/src/main/kotlin/ChattOREConfig.kt index cc9ee42..7570292 100644 --- a/chattore/src/main/kotlin/ChattOREConfig.kt +++ b/chattore/src/main/kotlin/ChattOREConfig.kt @@ -15,7 +15,7 @@ data class ChattOREConfig( ) data class FormatConfig( - val chatMessage: String = " | : ", + val chatMessage: String = " | : ", val join: String = " has joined the network", val leave: String = " has left the network", val bubblePrefix: String = "\uD83D\uDCAC", diff --git a/chattore/src/main/kotlin/Messenger.kt b/chattore/src/main/kotlin/Messenger.kt index 44efb2d..4fc1b28 100644 --- a/chattore/src/main/kotlin/Messenger.kt +++ b/chattore/src/main/kotlin/Messenger.kt @@ -9,6 +9,7 @@ import kotlinx.serialization.json.jsonPrimitive import net.kyori.adventure.text.Component import net.kyori.adventure.text.Component.space import net.kyori.adventure.text.TextReplacementConfig +import net.kyori.adventure.text.event.ClickEvent import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer import net.luckperms.api.LuckPerms @@ -17,6 +18,7 @@ import org.slf4j.Logger import java.net.URI import java.util.* import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicInteger import kotlin.jvm.optionals.getOrNull fun PluginScope.createMessenger( @@ -33,6 +35,11 @@ fun PluginScope.createMessenger( return Messenger(emojis, proxy, database, luckPerms, formatConfig, fileTypeMap, wiretap, logger, userCache) } +data class StoredMessage( + val author: String, + val content: String, +) + class Messenger( emojis: Emojis, private val proxy: ProxyServer, @@ -55,6 +62,31 @@ class Messenger( ) val excludedFromGlobalChat: MutableSet = ConcurrentHashMap.newKeySet() + private val maxMessages = 100 + private val messageIdCounter = AtomicInteger(0) + + private fun createMap(): MutableMap = Collections.synchronizedMap( + object : LinkedHashMap(maxMessages) { + override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean = + size > maxMessages + } + ) + + private val messages = createMap() + private val discordMessageIds = createMap() + + fun saveMessage(author: String, content: String, discordSnowflake: Long? = null): Int { + val messageId = messageIdCounter.incrementAndGet() + if (discordSnowflake != null) discordMessageIds[discordSnowflake] = messageId + messages[messageId] = StoredMessage(author, content) + return messageId + } + + fun getMessage(id: Int): StoredMessage? = messages[id] + + fun getMessageByDiscordSnowflake(discordSnowflake: Long): StoredMessage? = + discordMessageIds[discordSnowflake]?.let { getMessage(it) } + private fun formatReplacement(key: String, tag: String): TextReplacementConfig = TextReplacementConfig.builder() .match("""((\\?)(${Regex.escape(key)}(.*?)${Regex.escape(key)}))""") @@ -90,24 +122,55 @@ class Messenger( .renderSimpleC(name.render(player.username)) } + fun formatReply(reply: StoredMessage?): Component { + if (reply == null) return Component.empty() + val originalMessage = reply.content.replace("'", "\\'") + return " ${reply.author}: $originalMessage'>↪ ${reply.author}" + .render() + } + fun formatChatMessage( message: String, player: Player, - sender: Component = formatSender(player), prefix: Component = formatPrefix(player), + messageId: Int? = null, + reply: StoredMessage? = null, ) = formatConfig.chatMessage.render( - "message" toC prepareChatMessage(message, player), - "sender" toC sender, + "message" toC prepareChatMessage(message, player).withReplyClick(messageId), + "sender" toC formatSender(player), "prefix" toC prefix, + "reply" toC formatReply(reply), ) + private fun Component.withReplyClick(messageId: Int?): Component = + if (messageId != null) { + clickEvent(ClickEvent.suggestCommand("/chatreply $messageId ")) + } else { + this + } + val globalChat = proxy.all { it.uniqueId !in excludedFromGlobalChat } - fun broadcastChatMessage(player: Player, message: String) { + fun broadcastChatMessage( + player: Player, + message: String, + reply: StoredMessage? = null, + ) { logger.info("${player.username} (${player.uniqueId}): $message") val originServer = player.currentServer.getOrNull()?.serverInfo?.name ?: "VOID" val compoPrefix = formatPrefix(player) - globalChat.sendMessage(formatChatMessage(message, player, prefix = compoPrefix)) + + val messageId = saveMessage(player.username, message) + + globalChat.sendMessage( + formatChatMessage( + message, + player, + prefix = compoPrefix, + messageId = messageId, + reply = reply, + ) + ) val plainPrefix = PlainTextComponentSerializer.plainText().serialize(compoPrefix) val discordBroadcast = DiscordBroadcastEvent( diff --git a/chattore/src/main/kotlin/Pride.kt b/chattore/src/main/kotlin/Pride.kt index bd28ed2..85959f4 100644 --- a/chattore/src/main/kotlin/Pride.kt +++ b/chattore/src/main/kotlin/Pride.kt @@ -4,7 +4,7 @@ package org.openredstone.chattore fun weighted(vararg colors: Pair): Array = - colors.flatMap { (value, weight) -> List(weight) { value } }.toTypedArray() + colors.flatMap { (value, weight) -> List(weight) { value } }.toTypedArray() val prideColors = mapOf( "rainbow" to arrayOf( @@ -331,14 +331,14 @@ val prideColors = mapOf( ), "boyflux2" to weighted( - Pair("#E48AE4",1), - Pair("#9A81B4",1), - Pair("#55BFAB",1), - Pair("#FFFFFF",1), - Pair("#A8A8A8",1), - Pair("#81D5EF",5), - Pair("#69ABE5",5), - Pair("#5276D4",5), + Pair("#E48AE4", 1), + Pair("#9A81B4", 1), + Pair("#55BFAB", 1), + Pair("#FFFFFF", 1), + Pair("#A8A8A8", 1), + Pair("#81D5EF", 5), + Pair("#69ABE5", 5), + Pair("#5276D4", 5), ), "girlflux" to arrayOf( @@ -395,13 +395,13 @@ val prideColors = mapOf( ), "gendernonconforming1" to weighted( - Pair("#50284D",4), - Pair("#96467B",1), - Pair("#5C96F7",1), - Pair("#FFE6F7",1), - Pair("#5C96F7",1), - Pair("#96467B",1), - Pair("#50284D",4), + Pair("#50284D", 4), + Pair("#96467B", 1), + Pair("#5C96F7", 1), + Pair("#FFE6F7", 1), + Pair("#5C96F7", 1), + Pair("#96467B", 1), + Pair("#50284D", 4), ), "gendernonconforming2" to arrayOf( @@ -520,5 +520,5 @@ val prideColors = mapOf( ) val pridePresets = prideColors.mapValues { (_, colors) -> - "" - }.toSortedMap() + "" +}.toSortedMap() diff --git a/chattore/src/main/kotlin/feature/Chat.kt b/chattore/src/main/kotlin/feature/Chat.kt index 525996e..1d9be33 100644 --- a/chattore/src/main/kotlin/feature/Chat.kt +++ b/chattore/src/main/kotlin/feature/Chat.kt @@ -1,7 +1,14 @@ package org.openredstone.chattore.feature +import co.aikar.commands.BaseCommand +import co.aikar.commands.annotation.CommandAlias +import co.aikar.commands.annotation.CommandPermission +import co.aikar.commands.annotation.Default +import co.aikar.commands.annotation.Syntax import com.velocitypowered.api.event.Subscribe import com.velocitypowered.api.event.player.PlayerChatEvent +import com.velocitypowered.api.proxy.Player +import org.openredstone.chattore.ChattoreException import org.openredstone.chattore.Messenger import org.openredstone.chattore.PluginScope import org.openredstone.chattore.sendError @@ -12,6 +19,7 @@ fun PluginScope.createChatFeature( bubbleManager: BubbleManager, ) { registerListeners(ChatListener(confirmations, messenger, bubbleManager)) + registerCommands(ChatReplyCommand(confirmations, messenger)) } private class ChatListener( @@ -39,3 +47,28 @@ private class ChatListener( } } } + +@CommandAlias("chatreply") +@CommandPermission("chattore.chat") +private class ChatReplyCommand( + private val confirmations: ChatConfirmations, + private val messenger: Messenger, +) : BaseCommand() { + @Default + @Syntax(" ") + fun default( + sender: Player, + id: Int, + message: String, + ) { + val original = messenger.getMessage(id) ?: throw ChattoreException("That message is too old!") + + confirmations.submit(sender, message) { sender -> + messenger.broadcastChatMessage( + sender, + message, + reply = original, + ) + } + } +} diff --git a/chattore/src/main/kotlin/feature/Discord.kt b/chattore/src/main/kotlin/feature/Discord.kt index d61fac9..9a4d893 100644 --- a/chattore/src/main/kotlin/feature/Discord.kt +++ b/chattore/src/main/kotlin/feature/Discord.kt @@ -34,7 +34,7 @@ data class DiscordConfig( val senderSpecificFormats: Map = mapOf( 1234567890UL to "SomeUser » " ), - val ingameFormat: String = "Discord | : ", + val ingameFormat: String = "Discord | : ", ) // TO Discord @@ -153,10 +153,17 @@ private class DiscordListener( val url = matchResult.groupValues[2].trim() "$text: $url" }.replace("""\s+""".toRegex(), " ") + + val referencedId = event.message.data.messageReference.value?.id?.value + val reply = referencedId?.let { messenger.getMessageByDiscordSnowflake(it.value.toLong()) } + + val messageId = messenger.saveMessage(displayName, transformedMessage, event.message.id.value.toLong()) + messenger.globalChat.sendRichMessage( config.senderSpecificFormats[sender.id.value] ?: config.ingameFormat, "sender" toS displayName, "message" toC messenger.prepareChatMessage(transformedMessage, null), + "reply" toC messenger.formatReply(reply), ) } } diff --git a/chattore/src/main/resources/filetypes.json b/chattore/src/main/resources/filetypes.json index 2411929..01e24d2 100644 --- a/chattore/src/main/resources/filetypes.json +++ b/chattore/src/main/resources/filetypes.json @@ -1,6 +1,267 @@ { - "AUDIO": ["aac", "adt", "adts", "aa", "aax", "ac3", "aif", "aifc", "aiff", "au", "axa", "caf", "cdda", "flac", "gsm", "m3u", "m3u8", "m4a", "m4b", "m4p", "m4r", "mid", "midi", "mka", "mp3", "oga", "ogg", "opus", "pls", "ra", "ram", "rmi", "rpm", "sd2", "smd", "smx", "smz", "snd", "spx", "wav", "wave", "wax", "wma"], - "TEXT": ["323", "addin", "ssisdeploymentmanifest", "xoml", "asm", "bas", "c", "cc", "cd", "cfg", "cmd", "cnf", "contact", "cpp", "cs", "csdproj", "csproj", "css", "csv", "cxx", "dbproj", "def", "disco", "config", "dlm", "dsw", "dtd", "dtsconfig", "etx", "group", "h", "hdml", "hpp", "htc", "htm", "html", "htt", "hxt", "hxx", "i", "ical", "icalendar", "ics", "idl", "ifb", "inc", "ini", "inl", "ipproj", "iqy", "jsx", "jsxbin", "lst", "mak", "map", "md", "mk", "mno", "odh", "odl", "pkgdef", "pkgundef", "qht", "qhtm", "rc", "rc2", "rct", "reg", "rgs", "rqy", "rtx", "s", "scr", "sct", "sgml", "shtml", "sln", "sol", "sor", "srf", "tlh", "tli", "tsv", "txt", "uls", "user", "vb", "vbdproj", "vbproj", "vbs", "vcf", "vcs", "vddproj", "vdp", "vdproj", "vml", "vsct", "vsixlangpack", "vsixmanifest", "vspscc", "vsscc", "vssettings", "vssscc", "vstemplate", "vtt", "wml", "wmls", "wsc", "wsdl", "xdr", "xml", "xrm-ms", "xsd", "xsf", "xsl", "xslt"], - "IMAGE": ["apng", "art", "avci", "avcs", "avif", "avifs", "bmp", "cmx", "cod", "dib", "emf", "gif", "heic", "heics", "heif", "heifs", "ico", "ief", "jfif", "jpe", "jpeg", "jpg", "mac", "pbm", "pct", "pgm", "pic", "pict", "png", "pnm", "pnt", "pntg", "pnz", "ppm", "qti", "qtif", "ras", "rf", "rgb", "svg", "tif", "tiff", "wbmp", "wdp", "webp", "xbm", "xpm", "xwd"], - "VIDEO": ["3g2", "3gp", "3gp2", "3gpp", "ivf", "asf", "asr", "asx", "avi", "axv", "dif", "divx", "dv", "f4v", "flv", "lsf", "lsx", "m1v", "m2t", "m2ts", "m2v", "m4v", "mk3d", "mkv", "mod", "mov", "movie", "mp2", "mp2v", "mp4", "mp4v", "mpa", "mpe", "mpeg", "mpg", "mpv2", "mqv", "mts", "nsc", "ogv", "qt", "ts", "tts", "vbk", "webm", "wm", "wmp", "wmv", "wmx", "wvx"] + "AUDIO": [ + "aac", + "adt", + "adts", + "aa", + "aax", + "ac3", + "aif", + "aifc", + "aiff", + "au", + "axa", + "caf", + "cdda", + "flac", + "gsm", + "m3u", + "m3u8", + "m4a", + "m4b", + "m4p", + "m4r", + "mid", + "midi", + "mka", + "mp3", + "oga", + "ogg", + "opus", + "pls", + "ra", + "ram", + "rmi", + "rpm", + "sd2", + "smd", + "smx", + "smz", + "snd", + "spx", + "wav", + "wave", + "wax", + "wma" + ], + "TEXT": [ + "323", + "addin", + "ssisdeploymentmanifest", + "xoml", + "asm", + "bas", + "c", + "cc", + "cd", + "cfg", + "cmd", + "cnf", + "contact", + "cpp", + "cs", + "csdproj", + "csproj", + "css", + "csv", + "cxx", + "dbproj", + "def", + "disco", + "config", + "dlm", + "dsw", + "dtd", + "dtsconfig", + "etx", + "group", + "h", + "hdml", + "hpp", + "htc", + "htm", + "html", + "htt", + "hxt", + "hxx", + "i", + "ical", + "icalendar", + "ics", + "idl", + "ifb", + "inc", + "ini", + "inl", + "ipproj", + "iqy", + "jsx", + "jsxbin", + "lst", + "mak", + "map", + "md", + "mk", + "mno", + "odh", + "odl", + "pkgdef", + "pkgundef", + "qht", + "qhtm", + "rc", + "rc2", + "rct", + "reg", + "rgs", + "rqy", + "rtx", + "s", + "scr", + "sct", + "sgml", + "shtml", + "sln", + "sol", + "sor", + "srf", + "tlh", + "tli", + "tsv", + "txt", + "uls", + "user", + "vb", + "vbdproj", + "vbproj", + "vbs", + "vcf", + "vcs", + "vddproj", + "vdp", + "vdproj", + "vml", + "vsct", + "vsixlangpack", + "vsixmanifest", + "vspscc", + "vsscc", + "vssettings", + "vssscc", + "vstemplate", + "vtt", + "wml", + "wmls", + "wsc", + "wsdl", + "xdr", + "xml", + "xrm-ms", + "xsd", + "xsf", + "xsl", + "xslt" + ], + "IMAGE": [ + "apng", + "art", + "avci", + "avcs", + "avif", + "avifs", + "bmp", + "cmx", + "cod", + "dib", + "emf", + "gif", + "heic", + "heics", + "heif", + "heifs", + "ico", + "ief", + "jfif", + "jpe", + "jpeg", + "jpg", + "mac", + "pbm", + "pct", + "pgm", + "pic", + "pict", + "png", + "pnm", + "pnt", + "pntg", + "pnz", + "ppm", + "qti", + "qtif", + "ras", + "rf", + "rgb", + "svg", + "tif", + "tiff", + "wbmp", + "wdp", + "webp", + "xbm", + "xpm", + "xwd" + ], + "VIDEO": [ + "3g2", + "3gp", + "3gp2", + "3gpp", + "ivf", + "asf", + "asr", + "asx", + "avi", + "axv", + "dif", + "divx", + "dv", + "f4v", + "flv", + "lsf", + "lsx", + "m1v", + "m2t", + "m2ts", + "m2v", + "m4v", + "mk3d", + "mkv", + "mod", + "mov", + "movie", + "mp2", + "mp2v", + "mp4", + "mp4v", + "mpa", + "mpe", + "mpeg", + "mpg", + "mpv2", + "mqv", + "mts", + "nsc", + "ogv", + "qt", + "ts", + "tts", + "vbk", + "webm", + "wm", + "wmp", + "wmv", + "wmx", + "wvx" + ] } \ No newline at end of file