feat: add async task container

This commit is contained in:
Project_IO 2024-09-24 21:22:41 +09:00
parent 867a7c4ff7
commit 74fb7c7e04
14 changed files with 206 additions and 131 deletions

View file

@ -3,11 +3,13 @@ package net.projecttl.p.x32.api
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import net.dv8tion.jda.api.hooks.ListenerAdapter import net.dv8tion.jda.api.hooks.ListenerAdapter
import net.projecttl.p.x32.api.model.PluginConfig import net.projecttl.p.x32.api.model.PluginConfig
import net.projecttl.p.x32.api.util.AsyncTaskContainer
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
abstract class Plugin { abstract class Plugin {
private val handlerContainer = mutableListOf<ListenerAdapter>() private val handlerContainer = mutableListOf<ListenerAdapter>()
val taskContainer = AsyncTaskContainer()
val config = this.javaClass.getResourceAsStream("/plugin.json")!!.let { val config = this.javaClass.getResourceAsStream("/plugin.json")!!.let {
val raw = it.bufferedReader().readText() val raw = it.bufferedReader().readText()

View file

@ -3,7 +3,6 @@ package net.projecttl.p.x32.api.command
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import net.dv8tion.jda.api.JDA import net.dv8tion.jda.api.JDA
import net.dv8tion.jda.api.events.interaction.command.MessageContextInteractionEvent import net.dv8tion.jda.api.events.interaction.command.MessageContextInteractionEvent
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent
@ -11,8 +10,8 @@ import net.dv8tion.jda.api.events.interaction.command.UserContextInteractionEven
import net.dv8tion.jda.api.hooks.ListenerAdapter import net.dv8tion.jda.api.hooks.ListenerAdapter
import net.dv8tion.jda.api.interactions.commands.build.Commands import net.dv8tion.jda.api.interactions.commands.build.Commands
fun commandHandler(block: (CommandHandler) -> Unit): CommandHandler { fun commandHandler(guildId: Long = 0L, block: (CommandHandler) -> Unit): CommandHandler {
val handler = CommandHandler() val handler = CommandHandler(guildId)
block.invoke(handler) block.invoke(handler)
return handler return handler
@ -25,15 +24,14 @@ class CommandHandler(val guildId: Long = 0L) : ListenerAdapter() {
override fun onSlashCommandInteraction(ev: SlashCommandInteractionEvent) { override fun onSlashCommandInteraction(ev: SlashCommandInteractionEvent) {
val name = ev.interaction.name val name = ev.interaction.name
commands.forEach { command -> commands.singleOrNull { it.data.name == name }?.also { command ->
if (command.data.name == name) { if (command is GlobalCommand) {
if (command is GlobalCommand) { GlobalScope.launch {
GlobalScope.launch { command.execute(ev)
command.execute(ev) println("${ev.user.id} is use command for: ${ev.interaction.name}")
}
return@forEach
} }
return
} }
} }
} }
@ -41,15 +39,14 @@ class CommandHandler(val guildId: Long = 0L) : ListenerAdapter() {
override fun onUserContextInteraction(ev: UserContextInteractionEvent) { override fun onUserContextInteraction(ev: UserContextInteractionEvent) {
val name = ev.interaction.name val name = ev.interaction.name
commands.forEach { command -> commands.singleOrNull { it.data.name == name }?.also { command ->
if (command.data.name == name) { if (command is UserContext) {
if (command is UserContext) { GlobalScope.launch {
GlobalScope.launch { command.execute(ev)
command.execute(ev) println("${ev.user.id} is use user context for: ${ev.interaction.name}")
}
return@forEach
} }
return
} }
} }
} }
@ -57,15 +54,14 @@ class CommandHandler(val guildId: Long = 0L) : ListenerAdapter() {
override fun onMessageContextInteraction(ev: MessageContextInteractionEvent) { override fun onMessageContextInteraction(ev: MessageContextInteractionEvent) {
val name = ev.interaction.name val name = ev.interaction.name
commands.forEach { command -> commands.singleOrNull { it.data.name == name }?.also { command ->
if (command.data.name == name) { if (command is MessageContext) {
if (command is MessageContext) { GlobalScope.launch {
GlobalScope.launch { command.execute(ev)
command.execute(ev) println("${ev.user.id} is use message context for: ${ev.interaction.name}")
}
return@forEach
} }
return
} }
} }
} }

View file

@ -38,16 +38,24 @@ class AsyncTask(val loop: Boolean = true, val block: suspend () -> Unit) {
* This feature is Experimental * This feature is Experimental
*/ */
class AsyncTaskContainer { class AsyncTaskContainer {
private val tasks = mutableMapOf<Long, AsyncTask>() val tasks = mutableMapOf<Long, AsyncTask>()
private var tid = 0L private var tid = 0L
fun getTask(tid: Long): AsyncTask? { fun getTask(tid: Long): AsyncTask? {
return tasks[tid] return tasks[tid]
} }
fun createTask(task: AsyncTask) { suspend fun createTask(task: AsyncTask) {
tasks[tid] = task tasks[tid] = task
println("created task with id: $tid") println("created task with id: $tid")
try {
task.run()
} catch (ex: Exception) {
ex.printStackTrace()
tasks.remove(tid)
return
}
tid++ tid++
} }

View file

@ -2,7 +2,6 @@ package net.projecttl.p.x32.api.util
import net.dv8tion.jda.api.EmbedBuilder import net.dv8tion.jda.api.EmbedBuilder
import net.dv8tion.jda.api.entities.User import net.dv8tion.jda.api.entities.User
import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
import kotlin.random.Random import kotlin.random.Random

View file

@ -6,7 +6,7 @@ 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.DefaultConfig import net.projecttl.p.x32.config.DefaultConfig
import net.projecttl.p.x32.kernel.PluginLoader import net.projecttl.p.x32.kernel.CoreKernel.PluginLoader
import java.lang.management.ManagementFactory import java.lang.management.ManagementFactory
object Info : GlobalCommand { object Info : GlobalCommand {

View file

@ -8,7 +8,7 @@ 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.api.util.colour import net.projecttl.p.x32.api.util.colour
import net.projecttl.p.x32.api.util.footer import net.projecttl.p.x32.api.util.footer
import net.projecttl.p.x32.kernel.PluginLoader import net.projecttl.p.x32.kernel.CoreKernel.PluginLoader
object PluginCommand : GlobalCommand { object PluginCommand : GlobalCommand {
override val data = CommandData.fromData(CommandDataImpl("plugin", "봇에 불러온 플러그인을 확인 합니다").toData()) override val data = CommandData.fromData(CommandDataImpl("plugin", "봇에 불러온 플러그인을 확인 합니다").toData())

View file

@ -1,14 +1,22 @@
package net.projecttl.p.x32.kernel package net.projecttl.p.x32.kernel
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.serialization.json.Json
import net.dv8tion.jda.api.JDA import net.dv8tion.jda.api.JDA
import net.dv8tion.jda.api.JDABuilder import net.dv8tion.jda.api.JDABuilder
import net.dv8tion.jda.api.hooks.ListenerAdapter import net.dv8tion.jda.api.hooks.ListenerAdapter
import net.dv8tion.jda.api.requests.GatewayIntent import net.dv8tion.jda.api.requests.GatewayIntent
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.api.model.PluginConfig
import net.projecttl.p.x32.api.util.AsyncTaskContainer
import net.projecttl.p.x32.config.Config import net.projecttl.p.x32.config.Config
import net.projecttl.p.x32.func.BundleModule import net.projecttl.p.x32.func.BundleModule
import net.projecttl.p.x32.logger import net.projecttl.p.x32.logger
import java.io.File
import java.net.URLClassLoader
import java.nio.charset.Charset
import java.util.jar.JarFile
class CoreKernel(token: String) { class CoreKernel(token: String) {
private val builder = JDABuilder.createDefault(token, listOf( private val builder = JDABuilder.createDefault(token, listOf(
@ -116,4 +124,88 @@ class CoreKernel(token: String) {
memLock.unlock() memLock.unlock()
} }
object PluginLoader {
private val plugins = mutableMapOf<PluginConfig, Plugin>()
private val parentDir = File("./plugins").apply {
if (!exists()) {
mkdirs()
}
}
fun getPlugins(): Map<PluginConfig, Plugin> {
return plugins.toMap()
}
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
}
plugins[config] = plugin
}
fun load() {
parentDir.listFiles()?.forEach { file ->
loadPlugin(file)
}
logger.info("Loaded ${plugins.size} plugins")
}
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()
}
}
}
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<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

@ -1,94 +0,0 @@
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<PluginConfig, Plugin>()
private val parentDir = File("./plugins").apply {
if (!exists()) {
mkdirs()
}
}
fun getPlugins(): Map<PluginConfig, Plugin> {
return plugins.toMap()
}
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
}
plugins[config] = plugin
}
fun load() {
parentDir.listFiles()?.forEach { file ->
loadPlugin(file)
}
logger.info("Loaded ${plugins.size} plugins")
}
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()
}
}
}
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<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

@ -1,4 +0,0 @@
package net.projecttl.p.x32.kernel
object TaskManager {
}

View file

@ -1,8 +1,11 @@
token=<discord_bot_token> token=<discord_bot_token>
owner= owner=
# Px32's Default module activation
# setting value is 1 or 0
bundle_func=1 bundle_func=1
# Database connection settings
db_driver=org.sqlite.JDBC db_driver=org.sqlite.JDBC
db_url=jdbc:sqlite:data.db db_url=jdbc:sqlite:data.db
db_username= db_username=

View file

@ -17,6 +17,8 @@ class BundleModule : Plugin() {
addHandler(Ready) addHandler(Ready)
addHandler(commandHandler { handler -> addHandler(commandHandler { handler ->
handler.addCommand(Avatar) handler.addCommand(Avatar)
handler.addCommand(Bmi)
handler.addCommand(Dice)
handler.addCommand(MsgLength) handler.addCommand(MsgLength)
handler.addCommand(MsgPurge) handler.addCommand(MsgPurge)
handler.addCommand(Ping) handler.addCommand(Ping)

View file

@ -0,0 +1,54 @@
package net.projecttl.p.x32.func.command
import net.dv8tion.jda.api.EmbedBuilder
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent
import net.dv8tion.jda.api.interactions.commands.OptionType
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.api.util.footer
import java.math.RoundingMode
import java.text.DecimalFormat
import kotlin.math.pow
object Bmi : GlobalCommand {
override val data = CommandData.fromData(CommandDataImpl("bmi", "키와 몸무게 기반으로 bmi지수를 계산할 수 있어요").apply {
addOption(OptionType.NUMBER, "height", "신장 길이를 적어 주세요 (cm)", true)
addOption(OptionType.NUMBER, "weight", "몸무게를 적어 주세요 (kg)", true)
}.toData())
override suspend fun execute(ev: SlashCommandInteractionEvent) {
val height = ev.getOption("height")!!.asDouble
val weight = ev.getOption("weight")!!.asDouble
if (height <= 0.0 || weight <= 0.0) {
ev.reply(":warning: 키 또는 몸무게가 0 또는 음수일 수 없어요").setEphemeral(true).queue()
return
}
val bmi = weight / (height / 100).pow(2)
fun result(bmi: Double): String {
return when {
bmi < 18.5 -> "**저체중**"
bmi in 18.5..24.9 -> "**정상 체중**"
bmi in 25.0.. 29.9 -> "**과체중**"
else -> "**비만**"
}
}
val df = DecimalFormat("#.##")
df.roundingMode = RoundingMode.HALF_UP
ev.replyEmbeds(EmbedBuilder().apply {
setTitle(":pencil: BMI 지수가 나왔어요")
setDescription("${result(bmi)} 이에요")
addField(":straight_ruler: **신장 길이**", "`${height}cm`", true)
addField(":scales: **몸무게**", "`${weight}kg`", true)
addField(":chart_with_downwards_trend: **BMI 지수**", "`${df.format(bmi)}` ", true)
colour()
footer(ev.user)
}.build()).queue()
}
}

View file

@ -0,0 +1,17 @@
package net.projecttl.p.x32.func.command
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 kotlin.random.Random
import kotlin.random.nextInt
object Dice : GlobalCommand {
override val data = CommandData.fromData(CommandDataImpl("dice", "랜덤으로 주사위를 굴립니다").toData())
override suspend fun execute(ev: SlashCommandInteractionEvent) {
val rand = Random.nextInt(1..6)
ev.reply(":game_die: **${rand}**").queue()
}
}

View file

@ -1,11 +1,11 @@
package net.projecttl.p.x32.func.command package net.projecttl.p.x32.func.command
import net.projecttl.p.x32.func.Conf
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent
import net.dv8tion.jda.api.interactions.commands.OptionType import net.dv8tion.jda.api.interactions.commands.OptionType
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.func.Conf
object MsgPurge : GlobalCommand { object MsgPurge : GlobalCommand {
override val data = CommandData.fromData( override val data = CommandData.fromData(