feat: plugin command

This commit is contained in:
Project_IO 2024-09-21 23:13:23 +09:00
parent 01a003a001
commit 1536bebf7a
12 changed files with 184 additions and 51 deletions

6
.gitignore vendored
View file

@ -7,8 +7,10 @@ build/
.fleet/ .fleet/
.kotlin/ .kotlin/
.env config.properties
!.env.example !config.sample.properties
data.db
deploy.sh deploy.sh
plugins/ plugins/

View file

@ -5,11 +5,14 @@ plugins {
group = property("group")!! group = property("group")!!
version = property("version")!! version = property("version")!!
plugins
val ktor_version: String by project val ktor_version: String by project
val log4j_version: String by project val log4j_version: String by project
val exposed_version: String by project val exposed_version: String by project
val sqlite_version: String by project
val postgres_version: String by project
allprojects { allprojects {
apply(plugin = "org.jetbrains.kotlin.jvm") apply(plugin = "org.jetbrains.kotlin.jvm")
apply(plugin = "org.jetbrains.kotlin.plugin.serialization") apply(plugin = "org.jetbrains.kotlin.plugin.serialization")
@ -35,12 +38,13 @@ subprojects {
implementation("net.dv8tion:JDA:5.1.0") implementation("net.dv8tion:JDA:5.1.0")
implementation("io.ktor:ktor-client-cio:$ktor_version") implementation("io.ktor:ktor-client-cio:$ktor_version")
implementation("io.ktor:ktor-client-core:$ktor_version") implementation("io.ktor:ktor-client-core:$ktor_version")
implementation("org.xerial:sqlite-jdbc:$sqlite_version")
implementation("org.postgresql:postgresql:$postgres_version")
implementation("org.apache.logging.log4j:log4j-api:$log4j_version") implementation("org.apache.logging.log4j:log4j-api:$log4j_version")
implementation("org.apache.logging.log4j:log4j-core:$log4j_version") implementation("org.apache.logging.log4j:log4j-core:$log4j_version")
implementation("org.jetbrains.exposed:exposed-core:$exposed_version") implementation("org.jetbrains.exposed:exposed-core:$exposed_version")
implementation("org.jetbrains.exposed:exposed-jdbc:$exposed_version") implementation("org.jetbrains.exposed:exposed-jdbc:$exposed_version")
implementation("org.apache.logging.log4j:log4j-slf4j2-impl:$log4j_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-serialization-json:1.7.2")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
testImplementation(platform("org.junit:junit-bom:5.10.0")) testImplementation(platform("org.junit:junit-bom:5.10.0"))

View file

@ -7,3 +7,7 @@ version=0.1.0-SNAPSHOT
ktor_version=2.3.12 ktor_version=2.3.12
log4j_version=2.23.1 log4j_version=2.23.1
exposed_version=0.54.0 exposed_version=0.54.0
# Database Driver
sqlite_version=3.46.1.0
postgres_version=42.7.4

View file

@ -33,7 +33,7 @@ tasks {
} }
shadowJar { shadowJar {
archiveBaseName.set(project.name) archiveBaseName.set(rootProject.name)
archiveClassifier.set("") archiveClassifier.set("")
archiveVersion.set("") archiveVersion.set("")

View file

@ -1,25 +1,35 @@
package net.projecttl.p.x32 package net.projecttl.p.x32
import net.dv8tion.jda.api.JDA import net.dv8tion.jda.api.JDA
import net.projecttl.p.x32.command.PluginCommand
import net.projecttl.p.x32.command.Reload import net.projecttl.p.x32.command.Reload
import net.projecttl.p.x32.config.Config
import net.projecttl.p.x32.config.DefaultConfig import net.projecttl.p.x32.config.DefaultConfig
import net.projecttl.p.x32.func.loadDefault import net.projecttl.p.x32.func.loadDefault
import net.projecttl.p.x32.func.handler.Ready import net.projecttl.p.x32.func.handler.Ready
import net.projecttl.p.x32.kernel.CoreKernel import net.projecttl.p.x32.kernel.CoreKernel
import org.jetbrains.exposed.sql.Database
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
lateinit var jda: JDA lateinit var jda: JDA
lateinit var kernel: CoreKernel lateinit var kernel: CoreKernel
lateinit var database: Database
val logger: Logger = LoggerFactory.getLogger(Px32::class.java) val logger: Logger = LoggerFactory.getLogger(Px32::class.java)
fun main() { fun main() {
println("Px32 version v${DefaultConfig.version}") println("Px32 version v${DefaultConfig.version}")
kernel = CoreKernel(System.getenv("TOKEN")) if (Config.owner.isBlank() || Config.owner.isEmpty()) {
val handler = kernel.getGlobalCommandHandler() logger.warn("owner option is blank or empty!")
}
kernel = CoreKernel(Config.token)
val handler = kernel.getCommandContainer()
kernel.addHandler(Ready) kernel.addHandler(Ready)
handler.addCommand(Reload) handler.addCommand(Reload)
handler.addCommand(PluginCommand)
loadDefault(handler) loadDefault(handler)
jda = kernel.build() jda = kernel.build()

View file

@ -0,0 +1,34 @@
package net.projecttl.p.x32.command
import net.dv8tion.jda.api.EmbedBuilder
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.api.command.GlobalCommand
import net.projecttl.p.x32.api.util.colour
import net.projecttl.p.x32.kernel.PluginLoader
object PluginCommand : GlobalCommand {
override val data = CommandData.fromData(CommandDataImpl("plugin", "봇에 불러온 플러그인을 확인 합니다").toData())
override suspend fun execute(ev: SlashCommandInteractionEvent) {
val embed = EmbedBuilder().apply {
setTitle(":sparkles: **${ev.jda.selfUser.name}**봇 플러그인 리스트")
setThumbnail(ev.jda.selfUser.avatarUrl)
colour()
}
val loader = PluginLoader.getPlugins()
val fields = loader.map { (c, _) ->
MessageEmbed.Field(":electric_plug: **${c.name}**", "`${c.version}`", true)
}
fields.forEach { field ->
embed.addField(field)
}
ev.replyEmbeds(embed.build()).queue()
}
}

View file

@ -4,12 +4,17 @@ import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEve
import net.dv8tion.jda.api.interactions.commands.build.CommandData import net.dv8tion.jda.api.interactions.commands.build.CommandData
import net.dv8tion.jda.internal.interactions.CommandDataImpl import net.dv8tion.jda.internal.interactions.CommandDataImpl
import net.projecttl.p.x32.api.command.GlobalCommand import net.projecttl.p.x32.api.command.GlobalCommand
import net.projecttl.p.x32.config.Config
import net.projecttl.p.x32.kernel import net.projecttl.p.x32.kernel
object Reload : GlobalCommand { object Reload : GlobalCommand {
override val data = CommandData.fromData(CommandDataImpl("reload", "플러그인을 다시 불러 옵니다").toData()) override val data = CommandData.fromData(CommandDataImpl("reload", "플러그인을 다시 불러 옵니다").toData())
override suspend fun execute(ev: SlashCommandInteractionEvent) { override suspend fun execute(ev: SlashCommandInteractionEvent) {
if (ev.user.id != Config.owner) {
return ev.reply(":warning: 권한을 가지고 있지 않아요").queue()
}
try { try {
kernel.reload() kernel.reload()
} catch (ex: Exception) { } catch (ex: Exception) {

View file

@ -1,7 +1,45 @@
package net.projecttl.p.x32.config 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 { object Config {
private fun useConfig(): ConfigDelegate {
return ConfigDelegate()
}
val token: String by useConfig()
val owner: String by useConfig()
val db_driver: String by useConfig()
val db_url: String by useConfig()
val db_username: String by useConfig()
val db_password: String by useConfig()
} }
class ConfigDelegate { 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()
}
}

View file

@ -11,7 +11,7 @@ object DefaultConfig {
val version: String by useConfig() val version: String by useConfig()
} }
private class DefaultConfigDelegate { class DefaultConfigDelegate {
private val props = Properties() private val props = Properties()
init { init {

View file

@ -5,15 +5,16 @@ import net.dv8tion.jda.api.JDABuilder
import net.dv8tion.jda.api.hooks.ListenerAdapter import net.dv8tion.jda.api.hooks.ListenerAdapter
import net.projecttl.p.x32.api.Plugin import net.projecttl.p.x32.api.Plugin
import net.projecttl.p.x32.api.command.CommandHandler import net.projecttl.p.x32.api.command.CommandHandler
import net.projecttl.p.x32.jda
import net.projecttl.p.x32.logger import net.projecttl.p.x32.logger
class CoreKernel(token: String) { class CoreKernel(token: String) {
private val builder = JDABuilder.createDefault(token) private val builder = JDABuilder.createDefault(token)
private val handlers = mutableListOf<ListenerAdapter>() private val handlers = mutableListOf<ListenerAdapter>()
private val handler = CommandHandler() private val commandContainer = CommandHandler()
fun getGlobalCommandHandler(): CommandHandler { fun getCommandContainer(): CommandHandler {
return handler return commandContainer
} }
fun addHandler(handler: ListenerAdapter) { fun addHandler(handler: ListenerAdapter) {
@ -31,24 +32,19 @@ class CoreKernel(token: String) {
fun build(): JDA { fun build(): JDA {
PluginLoader.load() PluginLoader.load()
val plugins = PluginLoader.getPlugins() plugins().forEach { plugin ->
plugins.forEach { (c, p) -> plugin.getHandlers().forEach { handler ->
logger.info("Load plugin ${c.name} v${c.version}")
p.onLoad()
p.getHandlers().map { handler ->
handlers.add(handler) handlers.add(handler)
} }
} }
handlers.map { handlers.map {
println("test $it")
builder.addEventListeners(it) builder.addEventListeners(it)
} }
builder.addEventListeners(handler) builder.addEventListeners(commandContainer)
val jda = builder.build() val jda = builder.build()
handler.register(jda) commandContainer.register(jda)
handlers.forEach { h -> handlers.forEach { h ->
if (h is CommandHandler) { if (h is CommandHandler) {
h.register(jda) h.register(jda)
@ -63,15 +59,36 @@ class CoreKernel(token: String) {
} }
fun reload() { fun reload() {
val plugins = PluginLoader.getPlugins() val newHandlers = mutableListOf<ListenerAdapter>()
plugins.forEach { (c, p) -> PluginLoader.destroy()
logger.info("Reload plugin ${c.name} v${c.version}") plugins().forEach { plugin ->
p.destroy() plugin.getHandlers().forEach { handler ->
if (handlers.contains(handler)) {
jda.removeEventListener(handler)
handlers.remove(handler)
}
}
} }
PluginLoader.load() PluginLoader.load()
plugins.forEach { (_, p) ->
p.onLoad() plugins().forEach { plugin ->
plugin.getHandlers().forEach { handler ->
if (!handlers.contains(handler)) {
handlers.add(handler)
newHandlers.add(handler)
}
}
}
handlers.map {
builder.addEventListeners(it)
}
newHandlers.forEach { h ->
if (h is CommandHandler) {
h.register(jda)
}
} }
} }
} }

View file

@ -1,6 +1,7 @@
package net.projecttl.p.x32.kernel package net.projecttl.p.x32.kernel
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import net.dv8tion.jda.api.hooks.ListenerAdapter
import net.projecttl.p.x32.api.Plugin import net.projecttl.p.x32.api.Plugin
import net.projecttl.p.x32.api.model.PluginConfig import net.projecttl.p.x32.api.model.PluginConfig
import net.projecttl.p.x32.logger import net.projecttl.p.x32.logger
@ -23,29 +24,7 @@ object PluginLoader {
fun load() { fun load() {
parentDir.listFiles()?.forEach { file -> parentDir.listFiles()?.forEach { file ->
if (file.name.endsWith(".jar")) { loadPlugin(file)
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<PluginConfig>(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")
}
}
} }
} }
@ -61,4 +40,37 @@ object PluginLoader {
} }
} }
} }
private fun loadPlugin(file: File) {
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<PluginConfig>(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
}
} }

View file

@ -0,0 +1,7 @@
token=<discord_bot_token>
owner=
db_driver=org.sqlite.JDBC
db_url=jdbc:sqlite:data.db
db_username=
db_password=