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/
.kotlin/
.env
!.env.example
config.properties
!config.sample.properties
data.db
deploy.sh
plugins/

View file

@ -5,11 +5,14 @@ plugins {
group = property("group")!!
version = property("version")!!
plugins
val ktor_version: String by project
val log4j_version: String by project
val exposed_version: String by project
val sqlite_version: String by project
val postgres_version: String by project
allprojects {
apply(plugin = "org.jetbrains.kotlin.jvm")
apply(plugin = "org.jetbrains.kotlin.plugin.serialization")
@ -35,12 +38,13 @@ subprojects {
implementation("net.dv8tion:JDA:5.1.0")
implementation("io.ktor:ktor-client-cio:$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-core:$log4j_version")
implementation("org.jetbrains.exposed:exposed-core:$exposed_version")
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"))

View file

@ -7,3 +7,7 @@ version=0.1.0-SNAPSHOT
ktor_version=2.3.12
log4j_version=2.23.1
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 {
archiveBaseName.set(project.name)
archiveBaseName.set(rootProject.name)
archiveClassifier.set("")
archiveVersion.set("")

View file

@ -1,25 +1,35 @@
package net.projecttl.p.x32
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.config.Config
import net.projecttl.p.x32.config.DefaultConfig
import net.projecttl.p.x32.func.loadDefault
import net.projecttl.p.x32.func.handler.Ready
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
lateinit var kernel: CoreKernel
lateinit var database: Database
val logger: Logger = LoggerFactory.getLogger(Px32::class.java)
fun main() {
println("Px32 version v${DefaultConfig.version}")
kernel = CoreKernel(System.getenv("TOKEN"))
val handler = kernel.getGlobalCommandHandler()
if (Config.owner.isBlank() || Config.owner.isEmpty()) {
logger.warn("owner option is blank or empty!")
}
kernel = CoreKernel(Config.token)
val handler = kernel.getCommandContainer()
kernel.addHandler(Ready)
handler.addCommand(Reload)
handler.addCommand(PluginCommand)
loadDefault(handler)
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.internal.interactions.CommandDataImpl
import net.projecttl.p.x32.api.command.GlobalCommand
import net.projecttl.p.x32.config.Config
import net.projecttl.p.x32.kernel
object Reload : GlobalCommand {
override val data = CommandData.fromData(CommandDataImpl("reload", "플러그인을 다시 불러 옵니다").toData())
override suspend fun execute(ev: SlashCommandInteractionEvent) {
if (ev.user.id != Config.owner) {
return ev.reply(":warning: 권한을 가지고 있지 않아요").queue()
}
try {
kernel.reload()
} catch (ex: Exception) {

View file

@ -1,7 +1,45 @@
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()
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()
}
private class DefaultConfigDelegate {
class DefaultConfigDelegate {
private val props = Properties()
init {

View file

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