diff --git a/.gitignore b/.gitignore index b0cafb2..2a386e0 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,9 @@ build/ .env !.env.example +deploy.sh +plugins/ + ### IntelliJ IDEA ### .idea/ *.iws diff --git a/build.gradle.kts b/build.gradle.kts index 9600a43..3d8f226 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,6 @@ plugins { kotlin("jvm") version "2.0.20" + kotlin("plugin.serialization") version "2.0.20" } group = property("group")!! @@ -11,6 +12,7 @@ val exposed_version: String by project allprojects { apply(plugin = "org.jetbrains.kotlin.jvm") + apply(plugin = "org.jetbrains.kotlin.plugin.serialization") java { toolchain { @@ -39,6 +41,8 @@ subprojects { implementation("org.jetbrains.exposed:exposed-jdbc:$exposed_version") implementation("org.apache.logging.log4j:log4j-slf4j2-impl:$log4j_version") implementation("io.ktor:ktor-client-okhttp-jvm:2.3.12") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.2") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0") testImplementation(platform("org.junit:junit-bom:5.10.0")) testImplementation("org.junit.jupiter:junit-jupiter") } diff --git a/px32-bot-api/build.gradle.kts b/px32-bot-api/build.gradle.kts new file mode 100644 index 0000000..3e805f6 --- /dev/null +++ b/px32-bot-api/build.gradle.kts @@ -0,0 +1,99 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + `maven-publish` + signing +} + +group = rootProject.group +version = rootProject.version + +repositories { + mavenCentral() +} + +dependencies { + testImplementation(platform("org.junit:junit-bom:5.10.0")) + testImplementation("org.junit.jupiter:junit-jupiter") +} + +tasks { + withType { + options.encoding = "UTF-8" + } + + withType { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_21) + } + } + + create("sourcesJar") { + archiveClassifier.set("sources") + from(sourceSets["main"].allSource) + } + + create("javadocJar") { + archiveClassifier.set("javadoc") + dependsOn("dokkaHtml") + from("${projectDir}/build/dokka/html") + } + + test { + useJUnitPlatform() + } +} + +publishing { + publications { + create("${rootProject.name}-api") { + from(components["java"]) + artifacts { + tasks["sourcesJar"] + tasks["javadocJar"] + } + + repositories { + maven { + name = "ProjectCentral" + val releasesRepoUrl = "https://repo.wh64.net/repository/maven-releases" + val snapshotsRepoUrl = "https://repo.wh64.net/repository/maven-snapshots" + url = uri(if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl) + + credentials.runCatching { + val nexusUsername: String by project + val nexusPassword: String by project + + username = nexusUsername + password = nexusPassword + } + } + } + + pom { + name.set(rootProject.name) + description.set("Px32 Discord Bot framework api for Kotlin") + + licenses { + license { + name.set("MIT License") + } + } + + developers { + developer { + id.set("devproje") + name.set("Project_IO") + email.set("me@projecttl.net") + } + } + } + } + } +} + +signing { + isRequired = true + sign(publishing.publications["${rootProject.name}-api"]) +} diff --git a/px32-bot-api/src/main/kotlin/net/projecttl/p/x32/api/Plugin.kt b/px32-bot-api/src/main/kotlin/net/projecttl/p/x32/api/Plugin.kt new file mode 100644 index 0000000..bdc2d67 --- /dev/null +++ b/px32-bot-api/src/main/kotlin/net/projecttl/p/x32/api/Plugin.kt @@ -0,0 +1,42 @@ +package net.projecttl.p.x32.api + +import kotlinx.serialization.json.Json +import net.dv8tion.jda.api.hooks.ListenerAdapter +import net.projecttl.p.x32.api.command.CommandHandler +import net.projecttl.p.x32.api.model.PluginConfig +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +abstract class Plugin { + private val globalCommandHandler = CommandHandler() + private val handlerContainer = mutableListOf(globalCommandHandler) + private val config = this.javaClass.getResourceAsStream("/plugin.json")?.let { + val raw = it.bufferedReader().readText() + val obj = Json.decodeFromString(raw) + + return@let obj + } + + fun getLogger(): Logger { + return LoggerFactory.getLogger(config?.name) + } + + fun getHandlers(): List { + return handlerContainer + } + + fun addHandler(listener: ListenerAdapter) { + handlerContainer.add(listener) + } + + fun delHandler(listener: ListenerAdapter) { + handlerContainer.remove(listener) + } + + fun getCommandContainer(): CommandHandler { + return globalCommandHandler + } + + abstract fun onLoad() + abstract fun destroy() +} diff --git a/px32-bot-core/src/main/kotlin/net/projecttl/p/x32/handler/CommandExecutor.kt b/px32-bot-api/src/main/kotlin/net/projecttl/p/x32/api/command/CommandExecutor.kt similarity index 95% rename from px32-bot-core/src/main/kotlin/net/projecttl/p/x32/handler/CommandExecutor.kt rename to px32-bot-api/src/main/kotlin/net/projecttl/p/x32/api/command/CommandExecutor.kt index b9de5e8..2efbfb0 100644 --- a/px32-bot-core/src/main/kotlin/net/projecttl/p/x32/handler/CommandExecutor.kt +++ b/px32-bot-api/src/main/kotlin/net/projecttl/p/x32/api/command/CommandExecutor.kt @@ -1,4 +1,4 @@ -package net.projecttl.p.x32.handler +package net.projecttl.p.x32.api.command import net.dv8tion.jda.api.events.interaction.command.MessageContextInteractionEvent import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent diff --git a/px32-bot-core/src/main/kotlin/net/projecttl/p/x32/handler/CommandHandler.kt b/px32-bot-api/src/main/kotlin/net/projecttl/p/x32/api/command/CommandHandler.kt similarity index 83% rename from px32-bot-core/src/main/kotlin/net/projecttl/p/x32/handler/CommandHandler.kt rename to px32-bot-api/src/main/kotlin/net/projecttl/p/x32/api/command/CommandHandler.kt index 51dd23d..39480b5 100644 --- a/px32-bot-core/src/main/kotlin/net/projecttl/p/x32/handler/CommandHandler.kt +++ b/px32-bot-api/src/main/kotlin/net/projecttl/p/x32/api/command/CommandHandler.kt @@ -1,4 +1,4 @@ -package net.projecttl.p.x32.handler +package net.projecttl.p.x32.api.command import kotlinx.coroutines.runBlocking import net.dv8tion.jda.api.JDA @@ -8,7 +8,6 @@ import net.dv8tion.jda.api.events.interaction.command.UserContextInteractionEven import net.dv8tion.jda.api.hooks.ListenerAdapter import net.dv8tion.jda.api.interactions.commands.Command import net.dv8tion.jda.api.interactions.commands.build.Commands -import net.projecttl.p.x32.logger class CommandHandler(val guildId: Long = 0L) : ListenerAdapter() { private val commands = mutableListOf() @@ -73,7 +72,7 @@ class CommandHandler(val guildId: Long = 0L) : ListenerAdapter() { val guild = jda.getGuildById(guildId) if (guildId != 0L) { if (guild == null) { - logger.info("'${guildId}' guild is not exists!") + println("'${guildId}' guild is not exists!") return } } @@ -84,10 +83,10 @@ class CommandHandler(val guildId: Long = 0L) : ListenerAdapter() { if (command is GlobalCommand) { if (guild == null) { jda.upsertCommand(data).queue() - logger.info("Register Global Command: /${data.name}") + println("Register Global Command: /${data.name}") } else { guild.upsertCommand(data).queue() - logger.info("Register '${guild.id}' Guild's Command: /${data.name}") + println("Register '${guild.id}' Guild's Command: /${data.name}") } } @@ -98,13 +97,13 @@ class CommandHandler(val guildId: Long = 0L) : ListenerAdapter() { Commands.message(data.name) ).queue() - logger.info("Register User Context Command: /${data.name}") + println("Register User Context Command: /${data.name}") } else { guild.updateCommands().addCommands( Commands.context(Command.Type.USER, data.name), Commands.message(data.name) ).queue() - logger.info("Register '${guild.id}' Guild's User Context Command: /${data.name}") + println("Register '${guild.id}' Guild's User Context Command: /${data.name}") } } @@ -115,14 +114,14 @@ class CommandHandler(val guildId: Long = 0L) : ListenerAdapter() { Commands.message(data.name) ) - logger.info("Register Message Context Command: /${data.name}") + println("Register Message Context Command: /${data.name}") } else { guild.updateCommands().addCommands( Commands.context(Command.Type.MESSAGE, data.name), Commands.message(data.name) ) - logger.info("Register '${guild.id}' Guild's Message Context Command: /${data.name}") + println("Register '${guild.id}' Guild's Message Context Command: /${data.name}") } } } diff --git a/px32-bot-api/src/main/kotlin/net/projecttl/p/x32/api/model/PluginConfig.kt b/px32-bot-api/src/main/kotlin/net/projecttl/p/x32/api/model/PluginConfig.kt new file mode 100644 index 0000000..d3b168f --- /dev/null +++ b/px32-bot-api/src/main/kotlin/net/projecttl/p/x32/api/model/PluginConfig.kt @@ -0,0 +1,10 @@ +package net.projecttl.p.x32.api.model + +import kotlinx.serialization.Serializable + +@Serializable +data class PluginConfig( + val name: String, + val version: String, + val main: String +) diff --git a/px32-bot-core/src/main/kotlin/net/projecttl/p/x32/util/Color.kt b/px32-bot-api/src/main/kotlin/net/projecttl/p/x32/api/util/Color.kt similarity index 83% rename from px32-bot-core/src/main/kotlin/net/projecttl/p/x32/util/Color.kt rename to px32-bot-api/src/main/kotlin/net/projecttl/p/x32/api/util/Color.kt index 537821a..e1a7578 100644 --- a/px32-bot-core/src/main/kotlin/net/projecttl/p/x32/util/Color.kt +++ b/px32-bot-api/src/main/kotlin/net/projecttl/p/x32/api/util/Color.kt @@ -1,4 +1,4 @@ -package net.projecttl.p.x32.util +package net.projecttl.p.x32.api.util import net.dv8tion.jda.api.EmbedBuilder import kotlin.random.Random diff --git a/px32-bot-core/build.gradle.kts b/px32-bot-core/build.gradle.kts index e880c5e..10e3830 100644 --- a/px32-bot-core/build.gradle.kts +++ b/px32-bot-core/build.gradle.kts @@ -5,14 +5,16 @@ plugins { id("com.gradleup.shadow") version "8.3.0" } -group = "net.projecttl" -version = "0.1.0-SNAPSHOT" +group = rootProject.group +version = rootProject.version repositories { mavenCentral() } dependencies { + implementation(project(":${rootProject.name}-api")) + implementation(project(":${rootProject.name}-func")) testImplementation(platform("org.junit:junit-bom:5.10.0")) testImplementation("org.junit.jupiter:junit-jupiter") } diff --git a/px32-bot-core/src/main/kotlin/net/projecttl/p/x32/Px32.kt b/px32-bot-core/src/main/kotlin/net/projecttl/p/x32/Px32.kt index 007bdf8..a0518a3 100644 --- a/px32-bot-core/src/main/kotlin/net/projecttl/p/x32/Px32.kt +++ b/px32-bot-core/src/main/kotlin/net/projecttl/p/x32/Px32.kt @@ -1,9 +1,8 @@ package net.projecttl.p.x32 import net.dv8tion.jda.api.JDA -import net.projecttl.p.x32.command.Avatar -import net.projecttl.p.x32.command.Ping import net.projecttl.p.x32.config.DefaultConfig +import net.projecttl.p.x32.func.loadDefault import net.projecttl.p.x32.handler.Ready import net.projecttl.p.x32.kernel.CoreKernel import org.slf4j.Logger @@ -18,10 +17,9 @@ fun main() { kernel.addHandler(Ready) val handler = kernel.getGlobalCommandHandler() - handler.addCommand(Avatar) - handler.addCommand(Ping) + loadDefault(handler) jda = kernel.build() } -class Px32 +object Px32 diff --git a/px32-bot-core/src/main/kotlin/net/projecttl/p/x32/config/Config.kt b/px32-bot-core/src/main/kotlin/net/projecttl/p/x32/config/Config.kt index 6565d54..cba3f9a 100644 --- a/px32-bot-core/src/main/kotlin/net/projecttl/p/x32/config/Config.kt +++ b/px32-bot-core/src/main/kotlin/net/projecttl/p/x32/config/Config.kt @@ -4,4 +4,4 @@ object Config { } class ConfigDelegate { -} \ No newline at end of file +} diff --git a/px32-bot-core/src/main/kotlin/net/projecttl/p/x32/kernel/CoreKernel.kt b/px32-bot-core/src/main/kotlin/net/projecttl/p/x32/kernel/CoreKernel.kt index 8e4a18d..de16fee 100644 --- a/px32-bot-core/src/main/kotlin/net/projecttl/p/x32/kernel/CoreKernel.kt +++ b/px32-bot-core/src/main/kotlin/net/projecttl/p/x32/kernel/CoreKernel.kt @@ -3,12 +3,12 @@ package net.projecttl.p.x32.kernel import net.dv8tion.jda.api.JDA import net.dv8tion.jda.api.JDABuilder import net.dv8tion.jda.api.hooks.ListenerAdapter -import net.projecttl.p.x32.handler.CommandHandler +import net.projecttl.p.x32.api.command.CommandHandler +import net.projecttl.p.x32.logger class CoreKernel(token: String) { private val builder = JDABuilder.createDefault(token) private val handlers = mutableListOf() - private val handler = CommandHandler() fun getGlobalCommandHandler(): CommandHandler { @@ -28,6 +28,17 @@ class CoreKernel(token: String) { builder.addEventListeners(it) } builder.addEventListeners(handler) + PluginLoader.load() + + val plugins = PluginLoader.getPlugins() + plugins.forEach { (c, p) -> + logger.info("Load plugin ${c.name} v${c.version}") + p.onLoad() + + p.getHandlers().map { handler -> + builder.addEventListeners(handler) + } + } val jda = builder.build() handler.register(jda) @@ -37,6 +48,10 @@ class CoreKernel(token: String) { } } + Runtime.getRuntime().addShutdownHook(Thread { + PluginLoader.destroy() + }) + return jda } } \ No newline at end of file diff --git a/px32-bot-core/src/main/kotlin/net/projecttl/p/x32/kernel/PluginLoader.kt b/px32-bot-core/src/main/kotlin/net/projecttl/p/x32/kernel/PluginLoader.kt new file mode 100644 index 0000000..7d79648 --- /dev/null +++ b/px32-bot-core/src/main/kotlin/net/projecttl/p/x32/kernel/PluginLoader.kt @@ -0,0 +1,64 @@ +package net.projecttl.p.x32.kernel + +import kotlinx.serialization.json.Json +import net.projecttl.p.x32.api.Plugin +import net.projecttl.p.x32.api.model.PluginConfig +import net.projecttl.p.x32.logger +import java.io.File +import java.net.URLClassLoader +import java.nio.charset.Charset +import java.util.jar.JarFile + +object PluginLoader { + private val plugins = mutableMapOf() + private val parentDir = File("./plugins").apply { + if (!exists()) { + mkdirs() + } + } + + fun load() { + parentDir.listFiles()?.forEach { file -> + if (file.name.endsWith(".jar")) { + val jar = JarFile(file) + val cnf = jar.entries().toList().singleOrNull { jarEntry -> jarEntry.name == "plugin.json" } + if (cnf != null) { + val stream = jar.getInputStream(cnf) + val raw = stream.use { + return@use it.readBytes().toString(Charset.forName("UTF-8")) + } + + val config = Json.decodeFromString(raw) + val cl = URLClassLoader(arrayOf(file.toPath().toUri().toURL())) + val clazz = cl.loadClass(config.main) + val obj = clazz.getDeclaredConstructor().newInstance() + + if (obj is Plugin) { + plugins[config] = obj + } else { + logger.error("${config.name} is not valid main class. aborted") + } + } else { + logger.error("${file.name} is not a plugin. aborted") + } + } + } + } + + fun getPlugins(): Map { + return plugins.toMap() + } + + fun destroy() { + plugins.forEach { (config, plugin) -> + logger.info("disable ${config.name} plugin...") + + try { + plugin.destroy() + } catch (ex: Exception) { + logger.error("failed to destroy ${config.name} plugin") + ex.printStackTrace() + } + } + } +} \ No newline at end of file diff --git a/px32-bot-func/build.gradle.kts b/px32-bot-func/build.gradle.kts new file mode 100644 index 0000000..6bdf016 --- /dev/null +++ b/px32-bot-func/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + id("java") +} + +group = "net.projecttl" +version = "0.1.0-SNAPSHOT" + +repositories { + mavenCentral() +} + +dependencies { + compileOnly(project(":px32-bot-api")) + testImplementation(platform("org.junit:junit-bom:5.10.0")) + testImplementation("org.junit.jupiter:junit-jupiter") +} + +tasks.test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/px32-bot-func/src/main/kotlin/net/projecttl/p/x32/func/Loader.kt b/px32-bot-func/src/main/kotlin/net/projecttl/p/x32/func/Loader.kt new file mode 100644 index 0000000..96fc751 --- /dev/null +++ b/px32-bot-func/src/main/kotlin/net/projecttl/p/x32/func/Loader.kt @@ -0,0 +1,10 @@ +package net.projecttl.p.x32.func + +import net.projecttl.p.x32.api.command.CommandHandler +import net.projecttl.p.x32.func.command.Avatar +import net.projecttl.p.x32.func.command.Ping + +fun loadDefault(handler: CommandHandler) = with(handler) { + addCommand(Avatar) + addCommand(Ping) +} diff --git a/px32-bot-core/src/main/kotlin/net/projecttl/p/x32/command/Avatar.kt b/px32-bot-func/src/main/kotlin/net/projecttl/p/x32/func/command/Avatar.kt similarity index 83% rename from px32-bot-core/src/main/kotlin/net/projecttl/p/x32/command/Avatar.kt rename to px32-bot-func/src/main/kotlin/net/projecttl/p/x32/func/command/Avatar.kt index 3bf335d..ea70935 100644 --- a/px32-bot-core/src/main/kotlin/net/projecttl/p/x32/command/Avatar.kt +++ b/px32-bot-func/src/main/kotlin/net/projecttl/p/x32/func/command/Avatar.kt @@ -1,11 +1,11 @@ -package net.projecttl.p.x32.command +package net.projecttl.p.x32.func.command import net.dv8tion.jda.api.EmbedBuilder import net.dv8tion.jda.api.events.interaction.command.UserContextInteractionEvent import net.dv8tion.jda.api.interactions.commands.build.CommandData import net.dv8tion.jda.internal.interactions.CommandDataImpl -import net.projecttl.p.x32.handler.UserContext -import net.projecttl.p.x32.util.colour +import net.projecttl.p.x32.api.command.UserContext +import net.projecttl.p.x32.api.util.colour object Avatar : UserContext { override val data = CommandData.fromData(CommandDataImpl("avatar", "유저의 프로필 이미지를 가져 옵니다").toData()) diff --git a/px32-bot-core/src/main/kotlin/net/projecttl/p/x32/command/Ping.kt b/px32-bot-func/src/main/kotlin/net/projecttl/p/x32/func/command/Ping.kt similarity index 57% rename from px32-bot-core/src/main/kotlin/net/projecttl/p/x32/command/Ping.kt rename to px32-bot-func/src/main/kotlin/net/projecttl/p/x32/func/command/Ping.kt index cdd75e2..e617f04 100644 --- a/px32-bot-core/src/main/kotlin/net/projecttl/p/x32/command/Ping.kt +++ b/px32-bot-func/src/main/kotlin/net/projecttl/p/x32/func/command/Ping.kt @@ -1,4 +1,4 @@ -package net.projecttl.p.x32.command +package net.projecttl.p.x32.func.command import net.dv8tion.jda.api.EmbedBuilder import net.dv8tion.jda.api.JDA @@ -6,9 +6,8 @@ import net.dv8tion.jda.api.entities.MessageEmbed import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent import net.dv8tion.jda.api.interactions.commands.build.CommandData import net.dv8tion.jda.internal.interactions.CommandDataImpl -import net.projecttl.p.x32.handler.GlobalCommand -import net.projecttl.p.x32.util.colour -import kotlin.random.Random +import net.projecttl.p.x32.api.command.GlobalCommand +import net.projecttl.p.x32.api.util.colour object Ping : GlobalCommand { override val data: CommandData = CommandData.fromData(CommandDataImpl( @@ -17,14 +16,25 @@ object Ping : GlobalCommand { ).toData()) override suspend fun execute(ev: SlashCommandInteractionEvent) { - val embed = measure(ev.jda) - ev.replyEmbeds(embed).queue() + val started = System.currentTimeMillis() + var embed = EmbedBuilder().let { + it.setTitle(":hourglass: Just wait a sec...") + it.colour() + + it + }.build() + + ev.replyEmbeds(embed).queue { + embed = measure(started, ev.jda) + it.editOriginalEmbeds(embed).queue() + } } - private fun measure(jda: JDA): MessageEmbed { + private fun measure(started: Long, jda: JDA): MessageEmbed { val embed = EmbedBuilder() embed.setTitle(":ping_pong: **Pong!**") embed.addField(":electric_plug: **API**", "`${jda.gatewayPing}ms`", true) + embed.addField(":robot: **BOT**", "`${System.currentTimeMillis() - started}ms`", true) embed.colour() return embed.build() diff --git a/settings.gradle.kts b/settings.gradle.kts index cd76ddb..8c27b04 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,2 +1,4 @@ rootProject.name = "px32-bot" include("px32-bot-core") +include("px32-bot-api") +include("px32-bot-func")