Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <emoji_names>+` | `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 <emoji_names>+` | `chattore.emoji` | View multiple emojis | No aliases |
| `/chatreply <id> <message>` | `chattore.chat` | Reply to a chat message | No aliases |
1 change: 1 addition & 0 deletions chattore/src/main/kotlin/ChattORE.kt
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class ChattORE @Inject constructor(
val bubbleManager = createBubbleFeature(messenger, database, chatConfirmations, config.format, userCache)
createAliasFeature()
createChatFeature(messenger, chatConfirmations, bubbleManager)
createChatReplyFeature(messenger)
createChattoreFeature()
createDiscordFeature(messenger, emojis, config.discord)
createFunCommandsFeature(chatConfirmations)
Expand Down
2 changes: 1 addition & 1 deletion chattore/src/main/kotlin/ChattOREConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ data class ChattOREConfig(
)

data class FormatConfig(
val chatMessage: String = "<prefix> <gray>|</gray> <sender><gray>:</gray> <message>",
val chatMessage: String = "<prefix> <gray>|</gray> <sender><reply><gray>:</gray> <message>",
val join: String = "<yellow><player> has joined the network",
val leave: String = "<yellow><player> has left the network",
val bubblePrefix: String = "<bubble_info>\uD83D\uDCAC</bubble_info>",
Expand Down
46 changes: 42 additions & 4 deletions chattore/src/main/kotlin/Messenger.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Comment thread
Wueffi marked this conversation as resolved.
import kotlin.jvm.optionals.getOrNull

fun PluginScope.createMessenger(
Expand Down Expand Up @@ -90,24 +92,54 @@ class Messenger(
.renderSimpleC(name.render(player.username))
}

fun formatReply(replyAuthor: String?, replyContent: String?): Component {
if (replyAuthor == null || replyContent == null) return "".render()
Comment thread
Wueffi marked this conversation as resolved.
Outdated
val originalMessage = replyContent.replace("'", "\\'")
return " <hover:show_text:'<aqua>$replyAuthor</aqua><gray>:</gray> $originalMessage'><gray>↪ $replyAuthor</gray></hover>"
.render()
}

fun formatChatMessage(
message: String,
player: Player,
sender: Component = formatSender(player),
prefix: Component = formatPrefix(player),
messageID: Int? = null,
replyAuthor: String? = null,
replyContent: String? = null,
reply: Component = formatReply(replyAuthor, replyContent)
) = formatConfig.chatMessage.render(
"message" toC prepareChatMessage(message, player),
"message" toC prepareChatMessage(message, player, messageID),
"sender" toC sender,
"prefix" toC prefix,
"reply" toC reply,
)

val globalChat = proxy.all { it.uniqueId !in excludedFromGlobalChat }

fun broadcastChatMessage(player: Player, message: String) {
fun broadcastChatMessage(
player: Player,
message: String,
replyAuthor: String? = null,
replyContent: String? = 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 = ChatReply.nextMessageId()

ChatReply.saveMessage(messageID, player.username, message)

globalChat.sendMessage(
formatChatMessage(
message,
player,
prefix = compoPrefix,
messageID = messageID,
replyAuthor = replyAuthor,
replyContent = replyContent
)
Comment thread
Wueffi marked this conversation as resolved.
)

val plainPrefix = PlainTextComponentSerializer.plainText().serialize(compoPrefix)
val discordBroadcast = DiscordBroadcastEvent(
Expand All @@ -134,6 +166,7 @@ class Messenger(
fun prepareChatMessage(
message: String,
player: Player?,
messageId: Int? = null,
): Component {
val canObfuscate = player?.hasPermission("chattore.chat.obfuscate") ?: false
val parts = urlRegex.split(message)
Expand All @@ -146,7 +179,12 @@ class Messenger(
builder.append(formatLink(nextMatch.groupValues[1]))
}
}
return builder.build().performReplacements(chatReplacements)
val content = builder.build().performReplacements(chatReplacements)
return if (messageId != null) {
content.clickEvent(ClickEvent.suggestCommand("/chatreply $messageId "))
Comment thread
Wueffi marked this conversation as resolved.
Outdated
} else {
content
}
}

private fun formatLink(str: String): Component {
Expand Down
36 changes: 18 additions & 18 deletions chattore/src/main/kotlin/Pride.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
package org.openredstone.chattore

fun weighted(vararg colors: Pair<String, Int>): Array<String> =
colors.flatMap { (value, weight) -> List(weight) { value } }.toTypedArray()
colors.flatMap { (value, weight) -> List(weight) { value } }.toTypedArray()

val prideColors = mapOf(
"rainbow" to arrayOf(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -520,5 +520,5 @@ val prideColors = mapOf(
)

val pridePresets = prideColors.mapValues { (_, colors) ->
"<gradient:${colors.joinToString(':'.toString())}><username></gradient>"
}.toSortedMap()
"<gradient:${colors.joinToString(':'.toString())}><username></gradient>"
}.toSortedMap()
61 changes: 61 additions & 0 deletions chattore/src/main/kotlin/feature/ChatReply.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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.proxy.Player
import org.openredstone.chattore.ChattoreException
import org.openredstone.chattore.Messenger
import org.openredstone.chattore.PluginScope
import java.util.Collections
import java.util.concurrent.atomic.AtomicInteger
import java.util.LinkedHashMap

data class StoredMessage(
val author: String,
val content: String,
)

object ChatReply {
Comment thread
Wueffi marked this conversation as resolved.
Outdated
private const val maxMessages = 100

private val messageIdCounter = AtomicInteger(0)

private val messages: MutableMap<Int, StoredMessage> = Collections.synchronizedMap(
Comment thread
Wueffi marked this conversation as resolved.
Outdated
object : LinkedHashMap<Int, StoredMessage>(maxMessages, 0.75f, false) {
Comment thread
Wueffi marked this conversation as resolved.
Outdated
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<Int, StoredMessage>?): Boolean =
size > maxMessages
}
)

fun nextMessageId(): Int = messageIdCounter.incrementAndGet()
Comment thread
Wueffi marked this conversation as resolved.
Outdated

fun saveMessage(id: Int, author: String, content: String) {
messages[id] = StoredMessage(author, content)
}

fun getMessage(id: Int): StoredMessage? = messages[id]
}

fun PluginScope.createChatReplyFeature(messenger: Messenger) {
@CommandAlias("chatreply")
@CommandPermission("chattore.chat")
class ChatReplyCommand : BaseCommand() {
@Default
@Syntax("<id> <message>")
fun default(sender: Player, id: Int, message: String) {
Comment thread
Wueffi marked this conversation as resolved.
Outdated
val original = ChatReply.getMessage(id)
?: throw ChattoreException("That message is too old!")
messenger.broadcastChatMessage(
sender,
message,
replyAuthor = original.author,
replyContent = original.content,
)
}
}

registerCommands(ChatReplyCommand())
}
Comment thread
Wueffi marked this conversation as resolved.
Outdated
29 changes: 26 additions & 3 deletions chattore/src/main/kotlin/feature/Discord.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.velocitypowered.api.proxy.ProxyServer
import dev.kord.common.annotation.KordPreview
import dev.kord.common.entity.Snowflake
import dev.kord.core.Kord
import dev.kord.core.entity.Message
import dev.kord.core.entity.channel.TextChannel
import dev.kord.core.event.message.MessageCreateEvent
import dev.kord.core.live.channel.live
Expand Down Expand Up @@ -34,7 +35,7 @@ data class DiscordConfig(
val senderSpecificFormats: Map<ULong, String> = mapOf(
1234567890UL to "<red>SomeUser <gray>»<reset> <message>"
),
val ingameFormat: String = "<dark_aqua>Discord</dark_aqua> <gray>|</gray> <dark_purple><sender></dark_purple><gray>:</gray> <message>",
val ingameFormat: String = "<dark_aqua>Discord</dark_aqua> <gray>|</gray> <dark_purple><sender></dark_purple><gray><reply>:</gray> <message>",
)

// TO Discord
Expand Down Expand Up @@ -75,7 +76,8 @@ fun PluginScope.createDiscordFeature(
val discordMap = spawnServerBots(proxy, logger, config)
val serverChannels = discordMap.mapValues { (_, api) -> getGameChat(api, config.channelId) }
val mainBotChannel = getGameChat(discordNetwork, config.channelId)
val listener = DiscordListener(logger, messenger, emojis, config)
val serverBotIds = discordMap.values.map { it.selfId }.toSet()
val listener = DiscordListener(logger, messenger, emojis, config, serverBotIds)
@OptIn(KordPreview::class)
mainBotChannel.live().onMessageCreate(block = listener::onMessageCreate)
registerListeners(DiscordBroadcastListener(config, serverChannels, mainBotChannel, this))
Expand Down Expand Up @@ -129,17 +131,28 @@ private class DiscordListener(
private val messenger: Messenger,
private val emojis: Emojis,
private val config: DiscordConfig,
private val serverBotIds: Set<Snowflake>,
) {
private val emojiPattern = emojis.emojiToName.keys.joinToString("|", "(", ")") { Regex.escape(it) }
private val emojiRegex = Regex(emojiPattern)
private val urlMarkdownRegex = """\[([^]]*)]\(\s?(\S+)\s?\)""".toRegex()
private val relayedMessageRegex = """^`.*?`\s*\*\*(.+?)\*\*:\s?(.*)$""".toRegex(RegexOption.DOT_MATCHES_ALL)

private fun replaceEmojis(input: String) = emojiRegex.replace(input) { matchResult ->
val emoji = matchResult.value
val emojiName = emojis.emojiToName[emoji]
if (emojiName != null) ":$emojiName:" else emoji
}

private fun extractReplyInfo(referenced: Message): Pair<String, String>? {
Comment thread
Wueffi marked this conversation as resolved.
Outdated
val author = referenced.author
if (author != null && author.id in serverBotIds) {
val match = relayedMessageRegex.find(referenced.content) ?: return null
Comment thread
Wueffi marked this conversation as resolved.
Outdated
return match.groupValues[1] to match.groupValues[2]
}
return author?.username?.let { it to referenced.content }
}

fun onMessageCreate(event: MessageCreateEvent) {
// guaranteed to not happen because events are filtered beforehand
val sender = event.member ?: throw IllegalStateException("onMessageCreate: event.member is null")
Expand All @@ -153,10 +166,20 @@ private class DiscordListener(
val url = matchResult.groupValues[2].trim()
"$text: $url"
}.replace("""\s+""".toRegex(), " ")

val referenced = event.message.referencedMessage
val replyInfo = referenced?.let { extractReplyInfo(it) }
val replyAuthor = replyInfo?.first
val replyContent = replyInfo?.second?.let { replaceEmojis(it) }
Comment thread
Wueffi marked this conversation as resolved.
Outdated

val messageID = ChatReply.nextMessageId()
Comment thread
Wueffi marked this conversation as resolved.
Outdated
ChatReply.saveMessage(messageID, event.message.author?.username ?: "unknown", transformedMessage)
Comment thread
Wueffi marked this conversation as resolved.
Outdated

messenger.globalChat.sendRichMessage(
config.senderSpecificFormats[sender.id.value] ?: config.ingameFormat,
"sender" toS displayName,
"message" toC messenger.prepareChatMessage(transformedMessage, null),
"message" toC messenger.prepareChatMessage(transformedMessage, null, messageID),
"reply" toC messenger.formatReply(replyAuthor, replyContent),
)
}
}
Expand Down
Loading