diff --git a/gradle.properties b/gradle.properties index 2aa3ba2..f60dbd3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,7 +2,7 @@ kotlin.code.style=official org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 group=net.projecttl -version=0.2.3-SNAPSHOT +version=0.3.0-SNAPSHOT ktor_version=2.3.12 log4j_version=2.23.1 diff --git a/px32-bot-api/src/main/kotlin/net/projecttl/p/x32/api/BotConfig.kt b/px32-bot-api/src/main/kotlin/net/projecttl/p/x32/api/BotConfig.kt new file mode 100644 index 0000000..dbd9bb9 --- /dev/null +++ b/px32-bot-api/src/main/kotlin/net/projecttl/p/x32/api/BotConfig.kt @@ -0,0 +1,70 @@ +package net.projecttl.p.x32.api + +import org.jetbrains.exposed.sql.Database +import java.io.File +import java.io.FileInputStream +import java.util.* +import kotlin.reflect.KProperty +import kotlin.system.exitProcess + +object BotConfig { + private fun useBotConfig(): BotConfigDelegate { + return BotConfigDelegate() + } + + fun useDatabase(): Database { + if (db_username.isNotBlank() && db_password.isNotBlank()) { + return Database.connect( + driver = db_driver, + url = db_url, + user = db_username, + password = db_password + ) + } + + return Database.connect( + driver = db_driver, + url = db_url + ) + } + + val token: String by useBotConfig() + val owner: String by useBotConfig() + + private val bundle_func: String by useBotConfig() + val bundle = when (bundle_func) { + "1" -> true + "0" -> false + else -> { + throw IllegalArgumentException("bundle_func option must be 0 or 1") + } + } + + private val db_driver: String by useBotConfig() + private val db_url: String by useBotConfig() + private val db_username: String by useBotConfig() + private val db_password: String by useBotConfig() +} + +private class BotConfigDelegate { + private val props = Properties() + + init { + val file = File("config.properties") + if (!file.exists()) { + val default = this.javaClass.getResourceAsStream("/config.sample.properties")!!.readBytes() + file.outputStream().use { stream -> + stream.write(default) + } + + println("config.properties is not found, create new one...") + exitProcess(1) + } + + props.load(FileInputStream(file)) + } + + operator fun getValue(thisRef: Any?, property: KProperty<*>): String { + return props.getProperty(property.name).toString() + } +} 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 a747ae0..595c921 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 @@ -7,28 +7,28 @@ import net.dv8tion.jda.api.JDA import net.projecttl.p.x32.command.Info import net.projecttl.p.x32.command.PluginCommand import net.projecttl.p.x32.command.Reload -import net.projecttl.p.x32.config.Config +import net.projecttl.p.x32.api.BotConfig import net.projecttl.p.x32.config.DefaultConfig import net.projecttl.p.x32.kernel.CoreKernel -import org.jetbrains.exposed.sql.Database import org.slf4j.Logger import org.slf4j.LoggerFactory lateinit var jda: JDA + private set lateinit var kernel: CoreKernel -lateinit var database: Database + private set val logger: Logger = LoggerFactory.getLogger(Px32::class.java) @OptIn(DelicateCoroutinesApi::class) fun main() { println("Px32 version v${DefaultConfig.version}") - if (Config.owner.isBlank() || Config.owner.isEmpty()) { + if (BotConfig.owner.isBlank() || BotConfig.owner.isEmpty()) { logger.warn("owner option is blank or empty!") } - kernel = CoreKernel(Config.token) - val handler = kernel.getCommandContainer() + kernel = CoreKernel(BotConfig.token) + val handler = kernel.commandContainer handler.addCommand(Info) handler.addCommand(Reload) diff --git a/px32-bot-core/src/main/kotlin/net/projecttl/p/x32/command/Info.kt b/px32-bot-core/src/main/kotlin/net/projecttl/p/x32/command/Info.kt index b197a42..b921c48 100644 --- a/px32-bot-core/src/main/kotlin/net/projecttl/p/x32/command/Info.kt +++ b/px32-bot-core/src/main/kotlin/net/projecttl/p/x32/command/Info.kt @@ -6,7 +6,7 @@ import net.dv8tion.jda.api.interactions.commands.build.CommandData import net.dv8tion.jda.internal.interactions.CommandDataImpl import net.projecttl.p.x32.api.command.GlobalCommand import net.projecttl.p.x32.config.DefaultConfig -import net.projecttl.p.x32.kernel.CoreKernel.PluginLoader +import net.projecttl.p.x32.kernel import java.lang.management.ManagementFactory object Info : GlobalCommand { @@ -18,7 +18,8 @@ object Info : GlobalCommand { val rb = ManagementFactory.getRuntimeMXBean() val r = Runtime.getRuntime() - val size = PluginLoader.getPlugins().size + val size = kernel.plugins.size + val hSize = kernel.handlers.size val info = """ Px32Bot v${DefaultConfig.version}, JDA `v${JDAInfo.VERSION}`, @@ -30,7 +31,8 @@ object Info : GlobalCommand { Assigned `${r.maxMemory() / 1048576}MB` of Max Memories at this Bot Using `${(r.totalMemory() - r.freeMemory()) / 1048576}MB` at this Bot - Total $size plugin${if (size > 1) "s" else ""} loaded + Total $size plugin${if (size > 1) "s" else ""} loaded + Total $hSize handler${if (hSize > 1) "s" else ""} used """.trimIndent() ev.reply(info).queue() diff --git a/px32-bot-core/src/main/kotlin/net/projecttl/p/x32/command/PluginCommand.kt b/px32-bot-core/src/main/kotlin/net/projecttl/p/x32/command/PluginCommand.kt index 370f57b..2af47c7 100644 --- a/px32-bot-core/src/main/kotlin/net/projecttl/p/x32/command/PluginCommand.kt +++ b/px32-bot-core/src/main/kotlin/net/projecttl/p/x32/command/PluginCommand.kt @@ -8,10 +8,10 @@ import net.dv8tion.jda.internal.interactions.CommandDataImpl import net.projecttl.p.x32.api.command.GlobalCommand import net.projecttl.p.x32.api.util.colour import net.projecttl.p.x32.api.util.footer -import net.projecttl.p.x32.kernel.CoreKernel.PluginLoader +import net.projecttl.p.x32.kernel object PluginCommand : GlobalCommand { - override val data = CommandData.fromData(CommandDataImpl("plugin", "봇에 불러온 플러그인을 확인 합니다").toData()) + override val data = CommandData.fromData(CommandDataImpl("plugins", "봇에 불러온 플러그인을 확인 합니다").toData()) override suspend fun execute(ev: SlashCommandInteractionEvent) { val embed = EmbedBuilder().apply { @@ -22,7 +22,7 @@ object PluginCommand : GlobalCommand { footer(ev.user) } - val loader = PluginLoader.getPlugins() + val loader = kernel.plugins val fields = loader.map { (c, _) -> MessageEmbed.Field(":electric_plug: **${c.name}**", "`${c.version}`", true) } diff --git a/px32-bot-core/src/main/kotlin/net/projecttl/p/x32/command/Reload.kt b/px32-bot-core/src/main/kotlin/net/projecttl/p/x32/command/Reload.kt index 2d82c1f..991f5cc 100644 --- a/px32-bot-core/src/main/kotlin/net/projecttl/p/x32/command/Reload.kt +++ b/px32-bot-core/src/main/kotlin/net/projecttl/p/x32/command/Reload.kt @@ -4,7 +4,7 @@ import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEve import net.dv8tion.jda.api.interactions.commands.build.CommandData import net.dv8tion.jda.internal.interactions.CommandDataImpl import net.projecttl.p.x32.api.command.GlobalCommand -import net.projecttl.p.x32.config.Config +import net.projecttl.p.x32.api.BotConfig import net.projecttl.p.x32.kernel object Reload : GlobalCommand { @@ -15,7 +15,7 @@ object Reload : GlobalCommand { return } - if (ev.user.id != Config.owner) { + if (ev.user.id != BotConfig.owner) { return ev.reply(":warning: 권한을 가지고 있지 않아요").queue() } 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 deleted file mode 100644 index 5cacf94..0000000 --- a/px32-bot-core/src/main/kotlin/net/projecttl/p/x32/config/Config.kt +++ /dev/null @@ -1,48 +0,0 @@ -package net.projecttl.p.x32.config - -import net.projecttl.p.x32.logger -import java.io.File -import java.io.FileInputStream -import java.util.* -import kotlin.reflect.KProperty -import kotlin.system.exitProcess - -object Config { - private fun useConfig(): ConfigDelegate { - return ConfigDelegate() - } - - val token: String by useConfig() - val owner: String by useConfig() - - private val bundle_func: String by useConfig() - val bundle = if (bundle_func == "1") true else if (bundle_func == "0") false else throw IllegalArgumentException("bundle_func option must be 0 or 1") - - val db_driver: String by useConfig() - val db_url: String by useConfig() - val db_username: String by useConfig() - val db_password: String by useConfig() -} - -private class ConfigDelegate { - private val props = Properties() - - init { - val file = File("config.properties") - if (!file.exists()) { - val default = this.javaClass.getResourceAsStream("/config.sample.properties")!!.readBytes() - file.outputStream().use { stream -> - stream.write(default) - } - - logger.error("config.properties is not found, create new one...") - exitProcess(1) - } - - props.load(FileInputStream(file)) - } - - operator fun getValue(thisRef: Any?, property: KProperty<*>): String { - return props.getProperty(property.name).toString() - } -} \ 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 40ce0bc..d6f1d30 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 @@ -7,10 +7,11 @@ import net.dv8tion.jda.api.JDABuilder import net.dv8tion.jda.api.hooks.ListenerAdapter import net.dv8tion.jda.api.requests.GatewayIntent import net.dv8tion.jda.api.utils.MemberCachePolicy +import net.projecttl.p.x32.api.BotConfig import net.projecttl.p.x32.api.Plugin import net.projecttl.p.x32.api.command.CommandHandler import net.projecttl.p.x32.api.model.PluginConfig -import net.projecttl.p.x32.config.Config +import net.projecttl.p.x32.config.DefaultConfig import net.projecttl.p.x32.func.BundleModule import net.projecttl.p.x32.logger import java.io.File @@ -19,6 +20,9 @@ import java.nio.charset.Charset import java.util.jar.JarFile class CoreKernel(token: String) { + lateinit var jda: JDA + private set + private val builder = JDABuilder.createDefault(token, listOf( GatewayIntent.GUILD_PRESENCES, GatewayIntent.GUILD_MEMBERS, @@ -28,189 +32,202 @@ class CoreKernel(token: String) { GatewayIntent.GUILD_EMOJIS_AND_STICKERS, GatewayIntent.SCHEDULED_EVENTS )).setMemberCachePolicy(MemberCachePolicy.ALL) - private val handlers = mutableListOf() - private val commandContainer = CommandHandler() val memLock = Mutex() - val plugins get() = PluginLoader.getPlugins().map { it.value } + val commandContainer = CommandHandler() + val plugins = mutableMapOf() + var isActive = false + private set - private fun include() { - if (Config.bundle) { - val b = BundleModule() - PluginLoader.putModule(b.config, b) + private val parentDir = File("./plugins").apply { + if (!exists()) { + mkdirs() } } - fun getCommandContainer(): CommandHandler { - return commandContainer + val handlers: List + get() { + if (!isActive) { + return listOf() + } + + return jda.eventManager.registeredListeners.map { it as ListenerAdapter } } fun addHandler(handler: ListenerAdapter) { - handlers.add(handler) + if (isActive) { + jda.addEventListener(handler) + return + } + + builder.addEventListeners(handler) } fun delHandler(handler: ListenerAdapter) { - handlers.remove(handler) - } - - fun build(): JDA { - include() - - PluginLoader.load() - plugins.forEach { plugin -> - plugin.handlers.forEach { handler -> - handlers.add(handler) - } + if (isActive) { + jda.removeEventListener(handler) + return } - handlers.map { - logger.info("Load event listener: ${it::class.simpleName}") - builder.addEventListeners(it) - } - builder.addEventListeners(commandContainer) - - return builder.build() + builder.removeEventListeners(handler) } fun register(jda: JDA) { commandContainer.register(jda) - handlers.forEach { h -> - if (h is CommandHandler) { - h.register(jda) - } + jda.eventManager.registeredListeners.filterIsInstance().forEach { h -> + h.register(jda) } Runtime.getRuntime().addShutdownHook(Thread { - PluginLoader.destroy() + destroy() }) } + private fun include() { + if (BotConfig.bundle) { + val b = BundleModule() + loadModule(b.config, b) + } + } + + private fun load() { + parentDir.listFiles()?.forEach { file -> + try { + loadPlugin(file) + } catch (ex: Exception) { + logger.error("error occurred while to plugin loading: ${ex.message}") + } + } + + logger.info("Loaded ${plugins.size} plugins") + } + + private fun destroy() { + val unloaded = mutableListOf() + + 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() + } + } + + unloaded.forEach { + plugins.remove(it) + } + } + suspend fun reload(jda: JDA) { if (!memLock.isLocked) { memLock.lock() } - PluginLoader.destroy() - plugins.forEach { plugin -> - plugin.handlers.filter { handlers.contains(it) }.map { - handlers.remove(it) + plugins.forEach { (_, plugin) -> + plugin.handlers.forEach { + delHandler(it) } } - include() - PluginLoader.load() + destroy() + logger.info(jda.eventManager.registeredListeners.size.toString()) - plugins.forEach { plugin -> - plugin.handlers.forEach { handler -> - if (!handlers.contains(handler)) { - handlers.add(handler) - jda.addEventListener(handler) - } + load() + logger.info(jda.eventManager.registeredListeners.size.toString()) + + plugins.forEach { (_, plugin) -> + plugin.handlers.forEach { + addHandler(it) } } - handlers.forEach { h -> - if (h is CommandHandler) { - h.register(jda) - } + logger.info(jda.eventManager.registeredListeners.size.toString()) + + handlers.filterIsInstance().forEach { h -> + h.register(jda) } memLock.unlock() } - object PluginLoader { - private val plugins = mutableMapOf() - private val parentDir = File("./plugins").apply { - if (!exists()) { - mkdirs() - } + private fun loadPlugin(file: File) { + if (file.name == "px32-bot-module") { + return } - fun getPlugins(): Map { - return plugins.toMap() + if (!file.name.endsWith(".jar")) { + return } - fun putModule(config: PluginConfig, plugin: Plugin) { - try { - logger.info("Load module ${config.name} v${config.version}") - plugin.onLoad() - } catch (ex: Exception) { - ex.printStackTrace() - plugin.destroy() - return - } + val jar = JarFile(file) + val cnf = jar.entries().toList().singleOrNull { jarEntry -> jarEntry.name == "plugin.json" } + if (cnf == null) + throw IllegalAccessException("${file.name} is not a plugin. aborted") - plugins[config] = plugin + val stream = jar.getInputStream(cnf) + val raw = stream.use { + return@use it.readBytes().toString(Charset.forName("UTF-8")) } - fun load() { - parentDir.listFiles()?.forEach { file -> - try { - loadPlugin(file) - } catch (ex: Exception) { - logger.error("error occurred while to plugin loading: ${ex.message}") - } - } + val config = Json.decodeFromString(raw) + val cl = URLClassLoader(arrayOf(file.toPath().toUri().toURL())) + val obj = cl.loadClass(config.main).getDeclaredConstructor().newInstance() - logger.info("Loaded ${plugins.size} plugins") - } + if (obj !is Plugin) + throw IllegalAccessException("${config.name} is not valid plugin class. aborted") - fun destroy() { - val unloaded = mutableListOf() - - 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() - } - - unloaded += config - } - - unloaded.forEach { - plugins.remove(it) - } - } - - private fun loadPlugin(file: File) { - if (file.name == "px32-bot-module") { - return - } - - if (!file.name.endsWith(".jar")) { - return - } - - val jar = JarFile(file) - val cnf = jar.entries().toList().singleOrNull { jarEntry -> jarEntry.name == "plugin.json" } - if (cnf == null) - return logger.error("${file.name} is not a plugin. aborted") - - 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 obj = cl.loadClass(config.main).getDeclaredConstructor().newInstance() - - if (obj !is Plugin) - return logger.error("${config.name} is not valid main class. aborted") - - try { - logger.info("Load plugin ${config.name} v${config.version}") - obj.onLoad() - } catch (ex: Exception) { - ex.printStackTrace() - return logger.error("Failed to load plugin ${config.name}") - } - - plugins[config] = obj + try { + loadModule(config, obj) + } catch (ex: Exception) { + throw ex } } + + private fun loadModule(config: PluginConfig, plugin: Plugin) { + try { + logger.info("Load plugin ${config.name} v${config.version}") + plugin.onLoad() + } catch (ex: Exception) { + try { + plugin.destroy() + } catch (ex: Exception) { + throw ex + } + + throw ex + } + + plugins[config] = plugin + } + + fun build(): JDA { + if (isActive) { + logger.error("core kernel is already loaded! you cannot rebuild this kernel.") + return jda + } + + include() + plugins.forEach { (_, plugin) -> + plugin.handlers.forEach { handler -> + logger.info("Load event listener: ${handler::class.simpleName}") + addHandler(handler) + } + } + + builder.addEventListeners(commandContainer) + jda = builder.build() + isActive = true + + Runtime.getRuntime().addShutdownHook(Thread { + isActive = false + + logger.info("shutdown now Px32 kernel v${DefaultConfig.version}") + jda.shutdownNow() + }) + + return jda + } }