diff --git a/build.gradle.kts b/build.gradle.kts index 034bb81..f851350 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,6 +3,7 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar plugins { java id("org.jetbrains.kotlin.jvm") version "1.6.10" apply false + id("org.jetbrains.kotlin.plugin.serialization") version "1.6.10" apply false id("com.github.johnrengelman.shadow") version "7.1.1" apply false } @@ -25,6 +26,7 @@ allprojects { subprojects { plugins.apply("org.jetbrains.kotlin.jvm") + plugins.apply("org.jetbrains.kotlin.plugin.serialization") plugins.apply("com.github.johnrengelman.shadow") group = "io.gorence" @@ -35,6 +37,9 @@ subprojects { implementation(platform("org.jetbrains.kotlin:kotlin-bom")) implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + // Serialization + implementation("com.charleskorn.kaml:kaml:0.38.0") + // Paper API compileOnly("io.papermc.paper:paper-api:1.18.1-R0.1-SNAPSHOT") } diff --git a/foundation-bifrost/build.gradle.kts b/foundation-bifrost/build.gradle.kts index 7bd630d..67598be 100644 --- a/foundation-bifrost/build.gradle.kts +++ b/foundation-bifrost/build.gradle.kts @@ -1,5 +1,7 @@ dependencies { - implementation("net.dv8tion:JDA:5.0.0-alpha.2") + implementation("net.dv8tion:JDA:5.0.0-alpha.2") { + exclude(module = "opus-java") + } compileOnly(project(":foundation-core")) } diff --git a/foundation-bifrost/src/main/kotlin/cloud/kubelet/foundation/bifrost/FoundationBifrostPlugin.kt b/foundation-bifrost/src/main/kotlin/cloud/kubelet/foundation/bifrost/FoundationBifrostPlugin.kt index db0fb42..d0bceb3 100644 --- a/foundation-bifrost/src/main/kotlin/cloud/kubelet/foundation/bifrost/FoundationBifrostPlugin.kt +++ b/foundation-bifrost/src/main/kotlin/cloud/kubelet/foundation/bifrost/FoundationBifrostPlugin.kt @@ -1,13 +1,68 @@ package cloud.kubelet.foundation.bifrost +import cloud.kubelet.foundation.bifrost.model.BifrostConfig import cloud.kubelet.foundation.core.FoundationCorePlugin +import cloud.kubelet.foundation.core.Util +import com.charleskorn.kaml.Yaml +import io.papermc.paper.event.player.AsyncChatEvent +import net.dv8tion.jda.api.JDA +import net.dv8tion.jda.api.JDABuilder +import net.dv8tion.jda.api.events.GenericEvent +import net.dv8tion.jda.api.events.message.MessageReceivedEvent +import net.dv8tion.jda.api.hooks.EventListener +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.TextComponent +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener import org.bukkit.plugin.java.JavaPlugin +import kotlin.io.path.inputStream + +class FoundationBifrostPlugin : JavaPlugin(), EventListener, Listener { + private lateinit var config: BifrostConfig + private lateinit var jda: JDA -class FoundationBifrostPlugin : JavaPlugin() { override fun onEnable() { - slF4JLogger.info("Enabling!") - val foundation = server.pluginManager.getPlugin("Foundation") as FoundationCorePlugin slF4JLogger.info("Plugin data path: ${foundation.pluginDataPath}") + + val configPath = Util.copyDefaultConfig(slF4JLogger, foundation.pluginDataPath, "bifrost.yaml") + config = Yaml.default.decodeFromStream(BifrostConfig.serializer(), configPath.inputStream()) + + server.pluginManager.registerEvents(this, this) + + jda = JDABuilder + .createDefault(config.authentication.token) + .addEventListeners(this) + .build() + } + + override fun onEvent(e: GenericEvent) { + when (e) { + is MessageReceivedEvent -> { + // Prevent this bot from receiving its own messages and creating a feedback loop. + if (e.author.id == jda.selfUser.id) return + + slF4JLogger.debug( + "${e.guild.name} - ${e.channel.name} - ${e.author.name}: ${e.message.contentDisplay}" + ) + server.sendMessage(Component.text("${e.author.name} - ${e.message.contentDisplay}")) + } + } + } + + @EventHandler + private fun onPlayerChat(e: AsyncChatEvent) { + val channel = jda.getTextChannelById(config.channel.id) + if (channel == null) { + slF4JLogger.error("Failed to retrieve channel ${config.channel.id}") + return + } + + val message = e.message() + if (message is TextComponent) { + channel.sendMessage("${e.player.name}: ${message.content()}").queue() + } else { + slF4JLogger.error("Not sure what to do here, message != TextComponent: ${message.javaClass}") + } } } diff --git a/foundation-bifrost/src/main/kotlin/cloud/kubelet/foundation/bifrost/model/BifrostConfig.kt b/foundation-bifrost/src/main/kotlin/cloud/kubelet/foundation/bifrost/model/BifrostConfig.kt new file mode 100644 index 0000000..8b9f6b9 --- /dev/null +++ b/foundation-bifrost/src/main/kotlin/cloud/kubelet/foundation/bifrost/model/BifrostConfig.kt @@ -0,0 +1,19 @@ +package cloud.kubelet.foundation.bifrost.model + +import kotlinx.serialization.Serializable + +@Serializable +data class BifrostConfig( + val authentication: BifrostAuthentication, + val channel: BifrostChannel, +) + +@Serializable +data class BifrostAuthentication( + val token: String, +) + +@Serializable +data class BifrostChannel( + val id: String, +) diff --git a/foundation-core/src/main/kotlin/cloud/kubelet/foundation/core/FoundationCorePlugin.kt b/foundation-core/src/main/kotlin/cloud/kubelet/foundation/core/FoundationCorePlugin.kt index 0856168..7dda33f 100644 --- a/foundation-core/src/main/kotlin/cloud/kubelet/foundation/core/FoundationCorePlugin.kt +++ b/foundation-core/src/main/kotlin/cloud/kubelet/foundation/core/FoundationCorePlugin.kt @@ -1,10 +1,8 @@ package cloud.kubelet.foundation.core import cloud.kubelet.foundation.core.command.BackupCommand -import io.papermc.paper.event.player.ChatEvent import net.kyori.adventure.text.Component import org.bukkit.command.CommandExecutor -import org.bukkit.event.EventHandler import org.bukkit.event.Listener import org.bukkit.plugin.java.JavaPlugin import java.nio.file.Path @@ -23,7 +21,9 @@ class FoundationCorePlugin : JavaPlugin(), Listener { } return _pluginDataPath } - private set(value) { _pluginDataPath = value } + private set(value) { + _pluginDataPath = value + } override fun onEnable() { pluginDataPath = dataFolder.toPath() @@ -41,7 +41,7 @@ class FoundationCorePlugin : JavaPlugin(), Listener { val log = slF4JLogger log.info("Features:") - Util.printFeatureStatus(log, "Backup: ", BACKUP_ENABLED) + Util.printFeatureStatus(log, "Backup", BACKUP_ENABLED) } private fun registerCommandExecutor(name: String, executor: CommandExecutor) { @@ -49,8 +49,11 @@ class FoundationCorePlugin : JavaPlugin(), Listener { command.setExecutor(executor) } - @EventHandler + // TODO: Disabling chat reformatting until I do something with it and figure out how to make it + // be less disruptive. + /*@EventHandler private fun onChatMessage(e: ChatEvent) { + return e.isCancelled = true val name = e.player.displayName() val component = Component.empty() @@ -60,7 +63,7 @@ class FoundationCorePlugin : JavaPlugin(), Listener { .append(Component.text(' ')) .append(e.message()) server.sendMessage(component) - } + }*/ companion object { private const val BACKUPS_DIRECTORY = "backups" diff --git a/foundation-core/src/main/kotlin/cloud/kubelet/foundation/core/TextColors.kt b/foundation-core/src/main/kotlin/cloud/kubelet/foundation/core/TextColors.kt index 039172d..49d8c0c 100644 --- a/foundation-core/src/main/kotlin/cloud/kubelet/foundation/core/TextColors.kt +++ b/foundation-core/src/main/kotlin/cloud/kubelet/foundation/core/TextColors.kt @@ -3,5 +3,5 @@ package cloud.kubelet.foundation.core import net.kyori.adventure.text.format.TextColor object TextColors { - val AMARANTH_PINK = TextColor.fromHexString("#F7A8B8") + val AMARANTH_PINK = TextColor.fromHexString("#F7A8B8")!! } \ No newline at end of file diff --git a/foundation-core/src/main/kotlin/cloud/kubelet/foundation/core/Util.kt b/foundation-core/src/main/kotlin/cloud/kubelet/foundation/core/Util.kt index 0413b77..00c7233 100644 --- a/foundation-core/src/main/kotlin/cloud/kubelet/foundation/core/Util.kt +++ b/foundation-core/src/main/kotlin/cloud/kubelet/foundation/core/Util.kt @@ -3,6 +3,7 @@ package cloud.kubelet.foundation.core import net.kyori.adventure.text.Component import net.kyori.adventure.text.format.TextColor import org.slf4j.Logger +import java.nio.file.Path object Util { private val leftBracket: Component = Component.text('[') @@ -14,15 +15,50 @@ object Util { logger.info("{}: {}", feature, if (state) "Enabled" else "Disabled") } - fun formatSystemMessage(message: String?): Component { + fun formatSystemMessage(message: String): Component { return formatSystemMessage(TextColors.AMARANTH_PINK, message) } - fun formatSystemMessage(prefixColor: TextColor?, message: String?): Component { + fun formatSystemMessage(prefixColor: TextColor, message: String): Component { return leftBracket .append(foundationName.color(prefixColor)) .append(rightBracket) .append(whitespace) - .append(Component.text(message!!)) + .append(Component.text(message)) + } + + /** + * Copy the default configuration from the resource [resourceName] into the directory [targetPath]. + * @param targetPath The output directory as a path, it must exist before calling this. + * @param resourceName Path to resource, it should be in the root of the `resources` directory, + * without the leading slash. + */ + fun copyDefaultConfig(log: Logger, targetPath: Path, resourceName: String): Path { + if (resourceName.startsWith("/")) { + throw IllegalArgumentException("resourceName starts with slash") + } + + if (!targetPath.toFile().exists()) { + throw Exception("Configuration output path does not exist!") + } + val outPath = targetPath.resolve(resourceName) + val outFile = outPath.toFile() + if (outFile.exists()) { + log.debug("Configuration file already exists.") + return outPath + } + + val resourceStream = javaClass.getResourceAsStream("/$resourceName") + ?: throw Exception("Configuration resource does not exist!") + val outputStream = outFile.outputStream() + + resourceStream.use { + outputStream.use { + log.info("Copied default configuration to $outPath") + resourceStream.copyTo(outputStream) + } + } + + return outPath } } \ No newline at end of file diff --git a/foundation-core/src/main/resources/bifrost.yaml b/foundation-core/src/main/resources/bifrost.yaml new file mode 100644 index 0000000..63338cb --- /dev/null +++ b/foundation-core/src/main/resources/bifrost.yaml @@ -0,0 +1,10 @@ +# Authentication configuration for the bridge. +authentication: + # Token from the Discord Bot developer's page. + token: abc123 + +# Channel configuration for the bridge. +channel: + # Channel ID, can be copied by turning on Developer Mode in User Settings -> Advanced. The ID can + # then be copied by right-clicking the channel and selecting "Copy ID". + id: 123456789