Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion chattore/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ plugins {
dependencies {
implementation(project(":common"))
implementation(libs.acf)
implementation(libs.javacord)
implementation(libs.kord)
implementation(libs.exposed.core)
implementation(libs.exposed.jdbc)
implementation(libs.exposed.javaTime)
Expand Down
9 changes: 5 additions & 4 deletions chattore/src/main/kotlin/feature/Alias.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ import org.slf4j.Logger

private val IDENTIFIER: MinecraftChannelIdentifier = MinecraftChannelIdentifier.from(ALIAS_CHANNEL)

private val argumentsRegex = Regex("\\\$(args|[0-9]+)")
private val argumentsRegex = Regex("""\$(args|[0-9]+)""")

@Serializable
data class AliasConfig(
val alias: String = "alias",
val commands: List<String> = listOf("some", "commands"),
val alias: String,
val commands: List<String>,
)

fun PluginScope.createAliasFeature() {
Expand Down Expand Up @@ -53,7 +53,8 @@ private class AliasCommand(

private val requiredArgs = commands.flatMap {
argumentsRegex.findAll(it).map { match ->
match.groupValues[1].toIntOrNull()?.plus(1) ?: 1
// require 0 for "args"
match.groupValues[1].toIntOrNull()?.plus(1) ?: 0
}
}.maxOrNull() ?: 0

Expand Down
145 changes: 90 additions & 55 deletions chattore/src/main/kotlin/feature/Discord.kt
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
package org.openredstone.chattore.feature

import com.velocitypowered.api.event.Subscribe
import com.velocitypowered.api.event.proxy.ProxyShutdownEvent
import com.velocitypowered.api.proxy.ProxyServer
import org.javacord.api.DiscordApi
import org.javacord.api.DiscordApiBuilder
import org.javacord.api.entity.channel.TextChannel
import org.javacord.api.entity.intent.Intent
import org.javacord.api.entity.message.MessageBuilder
import org.javacord.api.event.message.MessageCreateEvent
import org.javacord.api.listener.message.MessageCreateListener
import dev.kord.common.annotation.KordPreview
import dev.kord.common.entity.Snowflake
import dev.kord.core.Kord
import dev.kord.core.entity.channel.TextChannel
import dev.kord.core.event.message.MessageCreateEvent
import dev.kord.core.live.channel.live
import dev.kord.core.live.channel.onMessageCreate
import dev.kord.gateway.Intent
import dev.kord.gateway.Intents
import dev.kord.gateway.PrivilegedIntent
import kotlinx.coroutines.*
import org.openredstone.chattore.*
import org.slf4j.Logger
import kotlin.jvm.optionals.getOrNull

fun String.discordEscape() = this.replace("""_""", "\\_")

Expand Down Expand Up @@ -50,49 +54,70 @@ fun PluginScope.createDiscordFeature(
config: DiscordConfig,
) {
if (!config.enable) return
val discordNetwork = DiscordApiBuilder()
.setToken(config.networkToken)
.addIntents(Intent.MESSAGE_CONTENT)
.login()
.join()
val discordMap = loadDiscordTokens(proxy, logger, config.serverTokens)
discordMap.forEach { (_, discordApi) -> discordApi.updateActivity(config.playingMessage) }
val textChannel = discordNetwork.getTextChannelById(config.channelId).getOrNull()
?: throw ChattoreException("Cannot find Discord channel")
textChannel.addMessageCreateListener(
DiscordListener(logger, messenger, proxy, emojis, config)
)
registerListeners(DiscordBroadcastListener(config, discordMap, discordNetwork))

@OptIn(DelicateCoroutinesApi::class)
GlobalScope.launch(Dispatchers.Default) {
coroutineScope {
val discordNetwork = Kord(config.networkToken)
// login blocks until the bot shuts down, so we launch it in its own coroutine
launch {
discordNetwork.login {
@OptIn(PrivilegedIntent::class)
intents += Intent.MessageContent
presence {
playing(config.playingMessage)
}
}
}
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, proxy, emojis, config)
@OptIn(KordPreview::class)
mainBotChannel.live().onMessageCreate(block = listener::onMessageCreate)
registerListeners(DiscordBroadcastListener(config, serverChannels, mainBotChannel, this))
onEvent<ProxyShutdownEvent> {
// block so that velocity waits before shutting down
// future considerations:
// - can this use async velocity events?
// - should we do the shutdowns concurrently?
runBlocking {
discordNetwork.shutdown()
discordMap.forEach { (_, kord) -> kord.shutdown() }
}
}
}
}
}

private suspend fun getGameChat(api: Kord, id: Long): TextChannel = api.getChannelOf(Snowflake(id))
?: throw IllegalArgumentException("Cannot find game-chat channel")

private class DiscordBroadcastListener(
private val config: DiscordConfig,
discordMap: Map<String, DiscordApi>,
discordApi: DiscordApi,
private val serverChannelMapping: Map<String, TextChannel>,
private val mainBotChannel: TextChannel,
private val scope: CoroutineScope,
) {
private val serverChannelMapping: Map<String, TextChannel> = discordMap.entries.associate { (server, api) ->
server to (api.getTextChannelById(config.channelId).getOrNull()
?: throw IllegalArgumentException("Could not get specified channel"))
}

private val mainBotChannel: TextChannel = discordApi.getTextChannelById(config.channelId).getOrNull()
?: throw IllegalArgumentException("Could not get specified channel")

@Subscribe
fun onBroadcastEvent(event: DiscordBroadcastEvent) {
val channel = serverChannelMapping[event.server] ?: return
val content = config.discordFormat
.replace("%prefix%", event.prefix)
.replace("%sender%", event.sender.discordEscape())
.replace("%message%", event.message)
MessageBuilder().setContent(content).send(channel)
scope.launch {
val channel = serverChannelMapping[event.server] ?: return@launch
val content = config.discordFormat
.replace("%prefix%", event.prefix)
.replace("%sender%", event.sender.discordEscape())
.replace("%message%", event.message)
channel.createMessage(content)
}
}

@Subscribe
fun onBroadcastEventRaw(event: DiscordBroadcastEventMain) {
val message = event.format
.replace("%player%", event.player.discordEscape())
MessageBuilder().setContent(message).send(mainBotChannel)
scope.launch {
val message = event.format
.replace("%player%", event.player.discordEscape())
mainBotChannel.createMessage(message)
}
}
}

Expand All @@ -102,8 +127,7 @@ private class DiscordListener(
private val proxy: ProxyServer,
private val emojis: Emojis,
private val config: DiscordConfig,
) : MessageCreateListener {

) {
private val emojiPattern = emojis.emojiToName.keys.joinToString("|", "(", ")") { Regex.escape(it) }
private val emojiRegex = Regex(emojiPattern)
private val urlMarkdownRegex = """\[([^]]*)]\(\s?(\S+)\s?\)""".toRegex()
Expand All @@ -114,29 +138,33 @@ private class DiscordListener(
if (emojiName != null) ":$emojiName:" else emoji
}

override fun onMessageCreate(event: MessageCreateEvent) {
if (event.messageAuthor.isBotUser && event.messageAuthor.id != config.chadId) return
val attachments = event.messageAttachments.joinToString(" ", " ") { it.url.toString() }
val toSend = replaceEmojis(event.message.readableContent) + attachments
logger.info("[Discord] ${event.messageAuthor.displayName} (${event.messageAuthor.id}): $toSend")
fun onMessageCreate(event: MessageCreateEvent) {
// guaranteed to not happen because events are filtered beforehand
val sender = event.member ?: throw IllegalStateException("onMessageCreate: event.member is null")
if (sender.isBot && sender.id != Snowflake(config.chadId)) return
val attachments = event.message.attachments.joinToString(" ", " ") { it.url }
val toSend = replaceEmojis(event.message.content) + attachments
val displayName = sender.effectiveName
logger.info("[Discord] $displayName (${sender.id}): $toSend")
val transformedMessage = toSend.replace(urlMarkdownRegex) { matchResult ->
val text = matchResult.groupValues[1].trim()
val url = matchResult.groupValues[2].trim()
"$text: $url"
}.replace("""\s+""".toRegex(), " ")
proxy.all.sendRichMessage(
config.ingameFormat,
"sender" toS event.messageAuthor.displayName,
"sender" toS displayName,
"message" toC messenger.prepareChatMessage(transformedMessage, null),
)
}
}

private fun loadDiscordTokens(
private suspend fun CoroutineScope.spawnServerBots(
proxy: ProxyServer,
logger: Logger,
serverTokens: Map<String, String>,
): Map<String, DiscordApi> {
config: DiscordConfig,
): Map<String, Kord> {
val serverTokens = config.serverTokens
val availableServers = proxy.allServers.map { it.serverInfo.name.lowercase() }.sorted()
val configServers = serverTokens.map { it.key.lowercase() }.sorted()
if (availableServers != configServers) {
Expand All @@ -149,9 +177,16 @@ private fun loadDiscordTokens(
)
}
return serverTokens.mapValues { (_, token) ->
DiscordApiBuilder()
.setToken(token)
.login()
.join()
val kord = Kord(token)
launch {
kord.login {
// server bots don't need any intents
intents = Intents()
presence {
playing(config.playingMessage)
}
}
}
kord
}
}
44 changes: 26 additions & 18 deletions chattore/src/main/resources/aliases.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
{
"alias": "nv",
"commands": [
"effect give $1 minecraft:night_vision infinite"
"effect give $0 minecraft:night_vision infinite"
]
},
{
Expand Down Expand Up @@ -83,14 +83,14 @@
"alias": "/hs",
"commands": [
"/hpos2",
"/rs $1 1"
"/rs $0 1"
]
},
{
"alias": "/me",
"commands": [
"/hpos2",
"/replace $1 air"
"/replace $0 air"
]
},
{
Expand Down Expand Up @@ -127,41 +127,41 @@
{
"alias": "/s",
"commands": [
"/stack $1"
"/stack $args"
]
},
{
"alias": "/sa",
"commands": [
"/stack -a $1"
"/stack -a $args"
]
},
{
"alias": "/su",
"commands": [
"/hpos2",
"/stack $1 u"
"/stack $0 u"
]
},
{
"alias": "/sd",
"commands": [
"/hpos2",
"/stack $1 d"
"/stack $0 d"
]
},
{
"alias": "/sl",
"commands": [
"/hpos2",
"/stack $1 l"
"/stack $0 l"
]
},
{
"alias": "/sr",
"commands": [
"/hpos2",
"/stack $1 r"
"/stack $0 r"
]
},
{
Expand All @@ -173,49 +173,49 @@
{
"alias": "/r",
"commands": [
"/rotate $1"
"/rotate $args"
]
},
{
"alias": "/u",
"commands": [
"/undo $1"
"/undo $args"
]
},
{
"alias": "/z",
"commands": [
"/undo"
"/undo $args"
]
},
{
"alias": "/y",
"commands": [
"/redo"
"/redo $args"
]
},
{
"alias": "/m",
"commands": [
"/move $1"
"/move $args"
]
},
{
"alias": "/md",
"commands": [
"/move $1 $2"
"/move $0 d"
]
},
{
"alias": "/ma",
"commands": [
"/move $1 -a"
"/move -a $args"
]
},
{
"alias": "/mas",
"commands": [
"/move $1 -a -s"
"/move -as $args"
]
},
{
Expand All @@ -230,7 +230,7 @@
{
"alias": "/e",
"commands": [
"/expand $1"
"/expand $args"
]
},
{
Expand All @@ -247,5 +247,13 @@
"/flip",
"/paste -a"
]
},
{
"alias": "/xfv",
"commands": [
"/cut",
"/flip",
"/paste -a"
]
}
]
Loading