Compare commits

..

20 commits

Author SHA1 Message Date
5a13eb5376 feat: add choices 2024-10-14 16:05:37 +09:00
a48e25b33e fix: fixed remove command feature's exception 2024-10-07 15:38:49 +09:00
6cad37f299 docs: change release url 2024-10-07 15:03:40 +09:00
cf3089a097 feat: add repo 2024-10-07 14:25:09 +09:00
34854efe26 feat: README 2024-10-07 14:23:21 +09:00
67696cd3a2 Add LICENSE 2024-10-07 05:21:50 +00:00
dbeb750733 feat: change version name 2024-10-07 13:39:08 +09:00
4c525fcb4c Merge branch 'kernel-v0.3.0' into 'master'
Kernel v0.3.0-SNAPSHOT

See merge request devproje/px32-bot!2
2024-10-07 04:37:38 +00:00
83319facb2 feat: add application command dsl builder and migrate commands 2024-10-07 13:35:06 +09:00
b5aaae092b Delete wh64@192.168.10.12 2024-10-06 15:46:28 +00:00
208084b617 fix: inline handler loader to load(), destroy() method in kernel 2024-10-03 13:39:32 +09:00
b526a5ef57 fix: plugin unload issue fixed 2024-10-02 23:48:24 +09:00
e9f17fcd78 feat: middle save 2024-10-02 12:49:00 +09:00
a360e53d0f fix: handler bug fixed 2024-10-01 21:44:54 +09:00
60965ee92e HOTFIX: when plugin loading to occurred exception 2024-09-27 21:37:35 +09:00
75a3b73993 HOTFIX: message intent added 2024-09-27 21:27:40 +09:00
74fb7c7e04 feat: add async task container 2024-09-24 21:22:41 +09:00
867a7c4ff7 feat: async task container - not worked 2024-09-23 17:48:05 +09:00
8c9b4a1112 feat: middle save 2024-09-23 16:45:34 +09:00
bc4c985735 feat: middle save 2024-09-23 02:18:29 +09:00
34 changed files with 939 additions and 345 deletions

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Project_IO
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

71
README.md Normal file
View file

@ -0,0 +1,71 @@
# Px32 Bot
- [JDA](https://github.com/discord-jda/JDA)로 만든 Kotlin 디스코드 봇 구동기 입니다.
## Project Repo
- [Gitlab](https://gitlab.wh64.net/devproje/px32-bot.git)
- [Github](https://github.com/devproje/px32-bot.git)
## How to Use
봇 구동기는 [이곳](https://github.com/devproje/px32-bot/releases)에서 다운로드 받으실 수 있습니다.
## Developments
API를 사용하여 플러그인 개발을 원하신다면 아래의 코드로 라이브러리를 추가 해 주세요.
JDA의 버전은 [이곳](https://github.com/discord-jda/JDA/releases)에서 확인 하실 수 있습니다.
- Maven
```xml
<repositories>
<repository>
<id>ProjectCentral</id>
<url>https://repo.wh64.net/repository/maven-releases/</url>
</repository>
// If you want to snapshot version, please write this code.
<repository>
<id>ProjectCentral-SNAPSHOT</id>
<url>https://repo.wh64.net/repository/maven-snapshots/</url>
</repository>
</repositories>
```
```xml
<dependencies>
...
<!-- JDA is required -->
<dependency>
<groupId>net.dv8tion</groupId>
<artifactId>JDA</artifactId>
<version>VERSION</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.projecttl</groupId>
<artifactId>px32-bot-api</artifactId>
<version>VERSION</version>
<scope>provided</scope>
</dependency>
</dependencies>
```
- Gradle
```kts
repositories {
mavenCentral()
maven("https://repo.wh64.net/repository/maven-releases/")
// If you want to snapshot version, please write this code.
maven("https://repo.wh64.net/repository/maven-snapshots/")
}
```
```kts
dependencies {
...
// JDA is required
compileOnly("net.dv8tion:JDA:VERSION")
compileOnly("net.projecttl:px32-bot-api:VERSION")
}
```
## License
- 해당 프로젝트는 [MIT License](https://gitlab.wh64.net/devproje/px32-bot/-/blob/master/LICENSE)를 따릅니다.

View file

@ -1,5 +1,6 @@
plugins { plugins {
kotlin("jvm") version "2.0.20" kotlin("jvm") version "2.0.20"
id("org.jetbrains.dokka") version "1.9.20"
kotlin("plugin.serialization") version "2.0.20" kotlin("plugin.serialization") version "2.0.20"
} }
@ -14,6 +15,7 @@ val sqlite_version: String by project
val postgres_version: String by project val postgres_version: String by project
allprojects { allprojects {
apply(plugin = "org.jetbrains.dokka")
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")

View file

@ -2,7 +2,7 @@ kotlin.code.style=official
org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8
group=net.projecttl group=net.projecttl
version=0.1.0-SNAPSHOT version=1.0.0-beta.3
ktor_version=2.3.12 ktor_version=2.3.12
log4j_version=2.23.1 log4j_version=2.23.1

View file

@ -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()
}
}

View file

@ -3,11 +3,24 @@ 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
import java.io.File
abstract class Plugin { abstract class Plugin {
private val handlerContainer = mutableListOf<ListenerAdapter>() private val handlerContainer = mutableListOf<ListenerAdapter>()
val taskContainer = AsyncTaskContainer()
val pluginDataDir: File
get() {
val f = File("plugins/${config.name}")
if (!f.exists()) {
f.mkdirs()
}
return f
}
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()
val obj = Json.decodeFromString<PluginConfig>(raw) val obj = Json.decodeFromString<PluginConfig>(raw)

View file

@ -1,6 +1,8 @@
package net.projecttl.p.x32.api.command package net.projecttl.p.x32.api.command
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
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
@ -8,21 +10,28 @@ 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(guildId: Long = 0L, block: (CommandHandler) -> Unit): CommandHandler {
val handler = CommandHandler(guildId)
block.invoke(handler)
return handler
}
@OptIn(DelicateCoroutinesApi::class)
class CommandHandler(val guildId: Long = 0L) : ListenerAdapter() { class CommandHandler(val guildId: Long = 0L) : ListenerAdapter() {
private val commands = mutableListOf<CommandExecutor>() private val commands = mutableListOf<CommandExecutor>()
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 {
runBlocking { command.execute(ev)
command.execute(ev) println("${ev.user.id} is use command for: ${ev.interaction.name}")
}
return@forEach
} }
return
} }
} }
} }
@ -30,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 {
runBlocking { command.execute(ev)
command.execute(ev) println("${ev.user.id} is use user context for: ${ev.interaction.name}")
}
return@forEach
} }
return
} }
} }
} }
@ -46,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 {
runBlocking { command.execute(ev)
command.execute(ev) println("${ev.user.id} is use message context for: ${ev.interaction.name}")
}
return@forEach
} }
return
} }
} }
} }
@ -106,10 +113,10 @@ class CommandHandler(val guildId: Long = 0L) : ListenerAdapter() {
if (command is MessageContext) { if (command is MessageContext) {
if (guild == null) { if (guild == null) {
jda.upsertCommand(Commands.message(data.name)) jda.upsertCommand(Commands.message(data.name)).queue()
println("Register Message Context Command: /${data.name}") println("Register Message Context Command: /${data.name}")
} else { } else {
guild.upsertCommand(Commands.message(data.name)) guild.upsertCommand(Commands.message(data.name)).queue()
println("Register '${guild.id}' Guild's Message Context Command: /${data.name}") println("Register '${guild.id}' Guild's Message Context Command: /${data.name}")
} }
} }

View file

@ -0,0 +1,135 @@
package net.projecttl.p.x32.api.command
import net.dv8tion.jda.api.interactions.commands.Command
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.OptionData
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData
import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData
import net.dv8tion.jda.internal.interactions.CommandDataImpl
fun useCommand(init: CommandObj.() -> Unit): CommandData {
val obj = CommandObj()
init.invoke(obj)
return obj.build()
}
class CommandObj {
var name: String = ""
var description: String = ""
val subcommands = mutableListOf<SubcommandData>()
val subcommandGroups = mutableListOf<SubcommandGroupData>()
val options = mutableListOf<OptionData>()
fun subcommand(sub: SubcommandObj.() -> Unit) {
val obj = SubcommandObj()
sub.invoke(obj)
subcommands += obj.build()
}
fun subcommandGroup(group: SubcommandGroupObj.() -> Unit) {
val obj = SubcommandGroupObj()
group.invoke(obj)
subcommandGroups += obj.build()
}
fun option(opt: OptionObj.() -> Unit) {
val obj = OptionObj()
opt.invoke(obj)
options += obj.build()
}
fun build(): CommandData {
val obj = CommandDataImpl(name, description)
if (subcommands.isNotEmpty()) {
subcommands.forEach {
obj.addSubcommands(it)
}
}
if (subcommandGroups.isNotEmpty()) {
subcommandGroups.forEach {
obj.addSubcommandGroups(it)
}
}
if (options.isNotEmpty()) {
options.forEach {
obj.addOptions(it)
}
}
return CommandData.fromData(obj.toData())
}
}
class SubcommandObj {
var name: String = ""
var description: String = ""
val options = mutableListOf<OptionData>()
fun option(opt: OptionObj.() -> Unit) {
val obj = OptionObj()
opt.invoke(obj)
options += obj.build()
}
fun build(): SubcommandData {
val obj = SubcommandData(name, description)
if (options.isNotEmpty()) {
options.forEach {
obj.addOptions(it)
}
}
return obj
}
}
class SubcommandGroupObj {
var name: String = ""
var description: String = ""
val subcommands = mutableListOf<SubcommandData>()
fun subcommand(opt: SubcommandObj.() -> Unit) {
val obj = SubcommandObj()
opt.invoke(obj)
subcommands += obj.build()
}
fun build(): SubcommandGroupData {
val obj = SubcommandGroupData(name, description)
if (subcommands.isNotEmpty()) {
subcommands.forEach {
obj.addSubcommands(it)
}
}
return obj
}
}
class OptionObj {
lateinit var type: OptionType
var name: String = ""
var description: String = ""
var required: Boolean = false
var autoComplete: Boolean = false
var choices = mutableListOf<Command.Choice>()
fun build(): OptionData {
return OptionData(type, name, description, required, autoComplete).apply {
this@OptionObj.choices.forEach {
addChoices(it)
}
}
}
}

View file

@ -0,0 +1,78 @@
package net.projecttl.p.x32.api.util
import kotlinx.coroutines.sync.Mutex
/*
* This class only support by kotlin
* This feature is Experimental
*/
class AsyncTask(val loop: Boolean = true, val block: suspend () -> Unit) {
private val memLock = Mutex()
var isActive = true
private set
suspend fun run() {
do {
if (memLock.isLocked) {
if (!isActive) {
break
}
continue
}
block.invoke()
} while (loop)
}
suspend fun kill() {
memLock.lock()
isActive = false
memLock.unlock()
}
}
/*
* This class only support by kotlin
* This feature is Experimental
*/
class AsyncTaskContainer {
val tasks = mutableMapOf<Long, AsyncTask>()
private var tid = 0L
fun getTask(tid: Long): AsyncTask? {
return tasks[tid]
}
suspend fun createTask(task: AsyncTask) {
tasks[tid] = task
println("created task with id: $tid")
try {
task.run()
} catch (ex: Exception) {
ex.printStackTrace()
tasks.remove(tid)
return
}
tid++
}
suspend fun removeTask(tid: Long) {
val task = getTask(tid) ?: throw NullPointerException("task $tid is missing or not defined")
task.kill()
tasks.remove(tid)
println("removed task with id: $tid")
}
suspend fun killAll() {
tasks.forEach { (k, t) ->
t.kill()
tasks.remove(k)
println("removed task with id: $tid")
}
}
}

View file

@ -1,9 +0,0 @@
package net.projecttl.p.x32.api.util
import net.dv8tion.jda.api.EmbedBuilder
import kotlin.random.Random
fun EmbedBuilder.colour(): EmbedBuilder {
val rand = Random.nextInt(0x000001, 0xffffff)
return this.setColor(rand)
}

View file

@ -0,0 +1,23 @@
package net.projecttl.p.x32.api.util
import net.dv8tion.jda.api.EmbedBuilder
import net.dv8tion.jda.api.entities.User
import java.time.LocalDateTime
import kotlin.random.Random
fun EmbedBuilder.colour(): EmbedBuilder {
val rand = Random.nextInt(0x000001, 0xffffff)
return this.setColor(rand)
}
fun EmbedBuilder.footer(user: User): EmbedBuilder {
val date = LocalDateTime.now()
val mon = if (date.month.value > 9) {
date.month.value
} else {
"0${date.month.value}"
}
val str = "${user.name}${date.year}-${mon}-${date.dayOfMonth} ${date.hour}:${date.minute}"
return this.setFooter(str, "${user.avatarUrl}?size=1024")
}

View file

@ -14,9 +14,7 @@ repositories {
dependencies { dependencies {
implementation(project(":${rootProject.name}-api")) implementation(project(":${rootProject.name}-api"))
implementation(project(":px32-bot-module")) implementation(project(":${rootProject.name}-module"))
testImplementation(platform("org.junit:junit-bom:5.10.0"))
testImplementation("org.junit.jupiter:junit-jupiter")
} }
tasks { tasks {

View file

@ -1,34 +1,62 @@
package net.projecttl.p.x32 package net.projecttl.p.x32
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.dv8tion.jda.api.JDA import net.dv8tion.jda.api.JDA
import net.projecttl.p.x32.api.BotConfig
import net.projecttl.p.x32.command.Info
import net.projecttl.p.x32.command.PluginCommand 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.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
private set
lateinit var kernel: CoreKernel lateinit var kernel: CoreKernel
lateinit var database: Database private set
val logger: Logger = LoggerFactory.getLogger(Px32::class.java) val logger: Logger = LoggerFactory.getLogger(Px32::class.java)
fun main() { @OptIn(DelicateCoroutinesApi::class)
fun main(args: Array<out String>) {
println("Px32 version v${DefaultConfig.version}") 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!") logger.warn("owner option is blank or empty!")
} }
kernel = CoreKernel(Config.token) kernel = CoreKernel(BotConfig.token)
val handler = kernel.getCommandContainer() val handler = kernel.commandContainer
if (args.contains("--remove-cmd")) {
jda = kernel.build()
try {
val list = jda.retrieveCommands().complete()
if (list.isEmpty()) {
return
}
list.forEach { command ->
logger.info("unregister command: /${command.name}")
command.jda.deleteCommandById(command.id).complete()
}
} catch (ex: Exception) {
ex.printStackTrace()
}
return
}
handler.addCommand(Info)
handler.addCommand(Reload) handler.addCommand(Reload)
handler.addCommand(PluginCommand) handler.addCommand(PluginCommand)
jda = kernel.build() jda = kernel.build()
GlobalScope.launch {
kernel.register(jda)
}
} }
object Px32 object Px32

View file

@ -0,0 +1,38 @@
package net.projecttl.p.x32.command
import net.dv8tion.jda.api.JDAInfo
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent
import net.dv8tion.jda.api.interactions.commands.build.CommandData
import net.projecttl.p.x32.api.command.GlobalCommand
import net.projecttl.p.x32.api.command.useCommand
import net.projecttl.p.x32.config.DefaultConfig
import net.projecttl.p.x32.kernel
import java.lang.management.ManagementFactory
object Info : GlobalCommand {
override val data: CommandData = useCommand {
name = "info"
description = "봇의 정보를 표시 합니다"
}
override suspend fun execute(ev: SlashCommandInteractionEvent) {
val rb = ManagementFactory.getRuntimeMXBean()
val r = Runtime.getRuntime()
val size = kernel.plugins.size
val info = """
Px32Bot v${DefaultConfig.version}, JDA `v${JDAInfo.VERSION}`,
`Java ${System.getProperty("java.version")}` and `Kotlin ${KotlinVersion.CURRENT}` System on `${System.getProperty("os.name")}`
Process Started on <t:${(System.currentTimeMillis() - rb.uptime)/ 1000L}:R>
Bot Process Running on PID `${rb.pid}`
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
""".trimIndent()
ev.reply(info).queue()
}
}

View file

@ -3,14 +3,17 @@ package net.projecttl.p.x32.command
import net.dv8tion.jda.api.EmbedBuilder import net.dv8tion.jda.api.EmbedBuilder
import net.dv8tion.jda.api.entities.MessageEmbed import net.dv8tion.jda.api.entities.MessageEmbed
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.build.CommandData
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.command.useCommand
import net.projecttl.p.x32.api.util.colour import net.projecttl.p.x32.api.util.colour
import net.projecttl.p.x32.kernel.PluginLoader import net.projecttl.p.x32.api.util.footer
import net.projecttl.p.x32.kernel
object PluginCommand : GlobalCommand { object PluginCommand : GlobalCommand {
override val data = CommandData.fromData(CommandDataImpl("plugin", "봇에 불러온 플러그인을 확인 합니다").toData()) override val data = useCommand {
name = "plugins"
description = "봇에 불러온 플러그인을 확인 합니다"
}
override suspend fun execute(ev: SlashCommandInteractionEvent) { override suspend fun execute(ev: SlashCommandInteractionEvent) {
val embed = EmbedBuilder().apply { val embed = EmbedBuilder().apply {
@ -18,9 +21,10 @@ object PluginCommand : GlobalCommand {
setThumbnail(ev.jda.selfUser.avatarUrl) setThumbnail(ev.jda.selfUser.avatarUrl)
colour() colour()
footer(ev.user)
} }
val loader = PluginLoader.getPlugins() val loader = kernel.plugins
val fields = loader.map { (c, _) -> val fields = loader.map { (c, _) ->
MessageEmbed.Field(":electric_plug: **${c.name}**", "`${c.version}`", true) MessageEmbed.Field(":electric_plug: **${c.name}**", "`${c.version}`", true)
} }

View file

@ -1,32 +1,34 @@
package net.projecttl.p.x32.command package net.projecttl.p.x32.command
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.build.CommandData import net.projecttl.p.x32.api.BotConfig
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.api.command.useCommand
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 = useCommand {
name = "reload"
description = "플러그인을 다시 불러 옵니다"
}
override suspend fun execute(ev: SlashCommandInteractionEvent) { override suspend fun execute(ev: SlashCommandInteractionEvent) {
if (kernel.memLock) { if (kernel.memLock.isLocked) {
return return
} }
if (ev.user.id != Config.owner) { if (ev.user.id != BotConfig.owner) {
return ev.reply(":warning: 권한을 가지고 있지 않아요").queue() return ev.reply(":warning: 권한을 가지고 있지 않아요").queue()
} }
try { try {
kernel.reload() kernel.reload(ev.jda)
} catch (ex: Exception) { } catch (ex: Exception) {
ex.printStackTrace() ex.printStackTrace()
ev.reply(":warning: 플러그인을 다시 불러오는 도중에 오류가 발생 했어요. 자세한 내용은 콘솔을 확인해 주세요!").queue() ev.reply(":warning: 플러그인을 다시 불러오는 도중에 오류가 발생 했어요. 자세한 내용은 콘솔을 확인해 주세요!").queue()
return return
} }
ev.reply(":white_check_mark: 플러그인을 다시 불러 왔어요!\n불러온 플러그인 수: ${kernel.plugins().size}").queue() ev.reply(":white_check_mark: 플러그인을 다시 불러 왔어요!\n불러온 플러그인 수: ${kernel.plugins.size}").queue()
} }
} }

View file

@ -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()
}
}

View file

@ -1,112 +1,221 @@
package net.projecttl.p.x32.kernel package net.projecttl.p.x32.kernel
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.utils.MemberCachePolicy
import net.projecttl.p.x32.api.BotConfig
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.config.Config import net.projecttl.p.x32.api.model.PluginConfig
import net.projecttl.p.x32.func.General import net.projecttl.p.x32.config.DefaultConfig
import net.projecttl.p.x32.jda import net.projecttl.p.x32.func.BundleModule
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) {
var memLock = false lateinit var jda: JDA
private set private set
private val builder = JDABuilder.createDefault(token)
private val handlers = mutableListOf<ListenerAdapter>()
private val commandContainer = CommandHandler()
private fun include() { private val builder = JDABuilder.createDefault(token, listOf(
if (Config.bundle) { GatewayIntent.GUILD_MEMBERS,
val b = General() GatewayIntent.GUILD_MESSAGES,
PluginLoader.putModule(b.config, b) GatewayIntent.MESSAGE_CONTENT,
GatewayIntent.GUILD_PRESENCES,
GatewayIntent.SCHEDULED_EVENTS,
GatewayIntent.GUILD_VOICE_STATES,
GatewayIntent.GUILD_EMOJIS_AND_STICKERS
)).setMemberCachePolicy(MemberCachePolicy.ALL)
val memLock = Mutex()
val commandContainer = CommandHandler()
var plugins = mutableMapOf<PluginConfig, Plugin>()
private set
var isActive = false
private set
private val parentDir = File("./plugins").apply {
if (!exists()) {
mkdirs()
} }
} }
fun getCommandContainer(): CommandHandler { val handlers: List<ListenerAdapter>
return commandContainer get() {
} if (!isActive) {
return listOf()
}
return jda.eventManager.registeredListeners.map { it as ListenerAdapter }
}
fun addHandler(handler: ListenerAdapter) { fun addHandler(handler: ListenerAdapter) {
handlers.add(handler) if (isActive) {
jda.addEventListener(handler)
return
}
builder.addEventListeners(handler)
} }
fun delHandler(handler: ListenerAdapter) { fun delHandler(handler: ListenerAdapter) {
handlers.remove(handler) if (isActive) {
} jda.removeEventListener(handler)
return
fun plugins(): List<Plugin> {
return PluginLoader.getPlugins().map { it.value }
}
fun build(): JDA {
include()
PluginLoader.load()
plugins().forEach { plugin ->
plugin.handlers.forEach { handler ->
handlers.add(handler)
}
} }
handlers.map { builder.removeEventListeners(handler)
builder.addEventListeners(it) }
}
builder.addEventListeners(commandContainer)
val jda = builder.build() fun register(jda: JDA) {
commandContainer.register(jda) commandContainer.register(jda)
handlers.forEach { h -> jda.eventManager.registeredListeners.filterIsInstance<CommandHandler>().forEach { h ->
if (h is CommandHandler) { h.register(jda)
h.register(jda)
}
} }
Runtime.getRuntime().addShutdownHook(Thread { Runtime.getRuntime().addShutdownHook(Thread {
PluginLoader.destroy() destroy()
})
}
suspend fun reload(jda: JDA) {
if (!memLock.isLocked) {
memLock.lock()
}
destroy()
load()
handlers.filterIsInstance<CommandHandler>().forEach { h ->
h.register(jda)
}
memLock.unlock()
}
fun kill() {
isActive = false
destroy()
logger.info("shutdown now Px32 kernel v${DefaultConfig.version}")
jda.shutdownNow()
}
private fun load() {
if (BotConfig.bundle) {
val b = BundleModule()
loadModule(b.config, b)
}
parentDir.listFiles()?.forEach { file ->
try {
loadPlugin(file)
} catch (ex: Exception) {
logger.error("error occurred while to plugin loading: ${ex.message}")
}
}
var cnt = 0
plugins.forEach { (_, plugin) ->
plugin.handlers.forEach {
addHandler(it)
logger.info("Load event listener: ${it::class.simpleName}")
cnt++
}
}
logger.info("Loaded ${plugins.size} plugin${if (plugins.size > 1) "s" else ""} and $cnt handler${if (cnt > 1) "s" else ""}.")
}
private fun destroy() {
plugins.forEach { (config, plugin) ->
logger.info("disable ${config.name} plugin...")
plugin.handlers.forEach {
delHandler(it)
}
try {
plugin.destroy()
} catch (ex: Exception) {
logger.error("failed to destroy ${config.name} plugin")
ex.printStackTrace()
}
}
plugins = mutableMapOf()
}
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)
throw IllegalAccessException("${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)
throw IllegalAccessException("${config.name} is not valid plugin class. aborted")
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
}
load()
builder.addEventListeners(commandContainer)
jda = builder.build()
isActive = true
Runtime.getRuntime().addShutdownHook(Thread {
kill()
}) })
return jda return jda
} }
fun reload() {
if (!memLock) {
memLock = true
}
val newHandlers = mutableListOf<ListenerAdapter>()
PluginLoader.destroy()
plugins().forEach { plugin ->
plugin.handlers.forEach { handler ->
if (handlers.contains(handler)) {
jda.removeEventListener(handler)
handlers.remove(handler)
}
}
}
include()
PluginLoader.load()
plugins().forEach { plugin ->
plugin.handlers.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)
}
}
memLock = false
}
} }

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,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

@ -1 +1 @@
version=${project.version} version=${rootProject.version}

View file

@ -2,8 +2,8 @@ plugins {
id("java") id("java")
} }
group = "net.projecttl" group = rootProject.group
version = "0.1.0-SNAPSHOT" version = rootProject.version
repositories { repositories {
mavenCentral() mavenCentral()

View file

@ -0,0 +1,33 @@
package net.projecttl.p.x32.func
import net.projecttl.p.x32.api.Plugin
import net.projecttl.p.x32.api.command.commandHandler
import net.projecttl.p.x32.func.command.*
import net.projecttl.p.x32.func.handler.Ready
lateinit var instance: BundleModule
class BundleModule : Plugin() {
override fun onLoad() {
instance = this
logger.info("Created by Project_IO")
logger.info("Hello! This is Px32's general module!")
addHandler(Ready)
addHandler(commandHandler { handler ->
handler.addCommand(Avatar)
handler.addCommand(Bmi)
handler.addCommand(Dice)
handler.addCommand(MsgLength)
handler.addCommand(MsgPurge)
handler.addCommand(Ping)
})
}
override fun destroy() {
logger.info("bye!")
}
}

View file

@ -1,14 +1,21 @@
package net.projecttl.p.x32.func
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.util.* import java.util.*
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
object Conf { object Conf {
private fun useDef(): DefDel {
return DefDel()
}
private fun useConf(): ConfDel { private fun useConf(): ConfDel {
return ConfDel() return ConfDel()
} }
val owner: String by useConf() val owner: String by useConf()
val version: String by useDef()
} }
private class ConfDel { private class ConfDel {
@ -23,3 +30,15 @@ private class ConfDel {
return props.getProperty(property.name).toString() return props.getProperty(property.name).toString()
} }
} }
class DefDel {
private val props = Properties()
init {
props.load(this.javaClass.getResourceAsStream("/default.properties"))
}
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return props.getProperty(property.name).toString()
}
}

View file

@ -1,33 +0,0 @@
package net.projecttl.p.x32.func
import net.projecttl.p.x32.api.Plugin
import net.projecttl.p.x32.api.command.CommandHandler
import net.projecttl.p.x32.func.command.Avatar
import net.projecttl.p.x32.func.command.MsgLength
import net.projecttl.p.x32.func.command.MsgPurge
import net.projecttl.p.x32.func.command.Ping
import net.projecttl.p.x32.func.handler.Ready
class General : Plugin() {
override fun onLoad() {
logger.info("Created by Project_IO")
logger.info("Hello! This is Px32's general module!")
addHandler(Ready)
addHandler(with(CommandHandler()) {
addCommand(Avatar)
addCommand(MsgLength)
addCommand(Ping)
addCommand(MsgPurge)
this
})
}
override fun destroy() {
logger.info("bye!")
}
}

View file

@ -2,19 +2,23 @@ package net.projecttl.p.x32.func.command
import net.dv8tion.jda.api.EmbedBuilder import net.dv8tion.jda.api.EmbedBuilder
import net.dv8tion.jda.api.events.interaction.command.UserContextInteractionEvent 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.api.command.UserContext import net.projecttl.p.x32.api.command.UserContext
import net.projecttl.p.x32.api.command.useCommand
import net.projecttl.p.x32.api.util.colour import net.projecttl.p.x32.api.util.colour
import net.projecttl.p.x32.api.util.footer
object Avatar : UserContext { object Avatar : UserContext {
override val data = CommandData.fromData(CommandDataImpl("avatar", "유저의 프로필 이미지를 가져 옵니다").toData()) override val data = useCommand {
name = "avatar"
description = "유저의 프로필 이미지를 가져 옵니다"
}
override suspend fun execute(ev: UserContextInteractionEvent) { override suspend fun execute(ev: UserContextInteractionEvent) {
val embed = EmbedBuilder() val embed = EmbedBuilder()
embed.setTitle(":frame_photo: ${ev.target.name}'s Avatar") embed.setTitle(":frame_photo: ${ev.target.name}'s Avatar")
embed.setImage("${ev.target.effectiveAvatarUrl}?size=512") embed.setImage("${ev.target.effectiveAvatarUrl}?size=512")
embed.colour() embed.colour()
embed.footer(ev.user)
ev.replyEmbeds(embed.build()).queue() ev.replyEmbeds(embed.build()).queue()
} }

View file

@ -0,0 +1,67 @@
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.projecttl.p.x32.api.command.GlobalCommand
import net.projecttl.p.x32.api.command.useCommand
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 = useCommand {
name = "bmi"
description = "키와 몸무게 기반으로 bmi 지수를 계산해요"
option {
type = OptionType.NUMBER
name = "height"
description = "신장 길이를 적어 주세요 (cm)"
required = true
}
option {
type = OptionType.NUMBER
name = "weight"
description = "몸무게를 적어 주세요 (kg)"
required = true
}
}
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,19 @@
package net.projecttl.p.x32.func.command
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent
import net.projecttl.p.x32.api.command.GlobalCommand
import net.projecttl.p.x32.api.command.useCommand
import kotlin.random.Random
import kotlin.random.nextInt
object Dice : GlobalCommand {
override val data = useCommand {
name = "dice"
description = "랜덤으로 주사위를 굴립니다"
}
override suspend fun execute(ev: SlashCommandInteractionEvent) {
val rand = Random.nextInt(1..6)
ev.reply(":game_die: **${rand}**").queue()
}
}

View file

@ -1,15 +1,17 @@
package net.projecttl.p.x32.func.command package net.projecttl.p.x32.func.command
import net.dv8tion.jda.api.events.interaction.command.MessageContextInteractionEvent import net.dv8tion.jda.api.events.interaction.command.MessageContextInteractionEvent
import net.dv8tion.jda.api.interactions.commands.build.CommandData
import net.dv8tion.jda.internal.interactions.CommandDataImpl
import net.projecttl.p.x32.api.command.MessageContext import net.projecttl.p.x32.api.command.MessageContext
import net.projecttl.p.x32.api.command.useCommand
object MsgLength : MessageContext { object MsgLength : MessageContext {
override val data = CommandData.fromData(CommandDataImpl("length", "메시지의 길이를 확인 합니다.").toData()) override val data = useCommand {
name = "length"
description = "메시지의 길이를 확인 합니다."
}
override suspend fun execute(ev: MessageContextInteractionEvent) { override suspend fun execute(ev: MessageContextInteractionEvent) {
val target = ev.target val target = ev.target
ev.reply("${target.jumpUrl} 메시지의 길이:\n\t${target.contentRaw.split("\\s+").size}").queue() ev.reply("${target.jumpUrl} 메시지의 길이: ${target.contentRaw.length}").queue()
} }
} }

View file

@ -1,18 +1,22 @@
package net.projecttl.p.x32.func.command package net.projecttl.p.x32.func.command
import 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.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.command.useCommand
import net.projecttl.p.x32.func.Conf
object MsgPurge : GlobalCommand { object MsgPurge : GlobalCommand {
override val data = CommandData.fromData( override val data = useCommand {
CommandDataImpl("purge", "n개의 메시지를 채널에서 삭제해요").apply { name = "purge"
addOption(OptionType.INTEGER, "n", "n개만큼 메시지가 삭제 됩니다", true) description = "n개의 메시지를 채널에서 삭제해요"
}.toData() option {
) type = OptionType.INTEGER
name = "n"
description = "n개만큼 메시지가 삭제 됩니다"
required = true
}
}
override suspend fun execute(ev: SlashCommandInteractionEvent) { override suspend fun execute(ev: SlashCommandInteractionEvent) {
val n = ev.getOption("n")!!.asInt val n = ev.getOption("n")!!.asInt

View file

@ -3,17 +3,19 @@ package net.projecttl.p.x32.func.command
import net.dv8tion.jda.api.EmbedBuilder import net.dv8tion.jda.api.EmbedBuilder
import net.dv8tion.jda.api.JDA import net.dv8tion.jda.api.JDA
import net.dv8tion.jda.api.entities.MessageEmbed import net.dv8tion.jda.api.entities.MessageEmbed
import net.dv8tion.jda.api.entities.User
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.build.CommandData 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.command.GlobalCommand
import net.projecttl.p.x32.api.command.useCommand
import net.projecttl.p.x32.api.util.colour import net.projecttl.p.x32.api.util.colour
import net.projecttl.p.x32.api.util.footer
object Ping : GlobalCommand { object Ping : GlobalCommand {
override val data: CommandData = CommandData.fromData(CommandDataImpl( override val data: CommandData = useCommand {
"ping", name = "ping"
"Discord API 레이턴시 확인 합니다" description = "Discord API 레이턴시 확인 합니다"
).toData()) }
override suspend fun execute(ev: SlashCommandInteractionEvent) { override suspend fun execute(ev: SlashCommandInteractionEvent) {
val started = System.currentTimeMillis() val started = System.currentTimeMillis()
@ -25,17 +27,18 @@ object Ping : GlobalCommand {
}.build() }.build()
ev.replyEmbeds(embed).queue { ev.replyEmbeds(embed).queue {
embed = measure(started, ev.jda) embed = measure(started, ev.user, ev.jda)
it.editOriginalEmbeds(embed).queue() it.editOriginalEmbeds(embed).queue()
} }
} }
private fun measure(started: Long, jda: JDA): MessageEmbed { private fun measure(started: Long, user: User, jda: JDA): MessageEmbed {
val embed = EmbedBuilder() val embed = EmbedBuilder()
embed.setTitle(":ping_pong: **Pong!**") embed.setTitle(":ping_pong: **Pong!**")
embed.addField(":electric_plug: **API**", "`${jda.gatewayPing}ms`", true) embed.addField(":electric_plug: **API**", "`${jda.gatewayPing}ms`", true)
embed.addField(":robot: **BOT**", "`${System.currentTimeMillis() - started}ms`", true) embed.addField(":robot: **BOT**", "`${System.currentTimeMillis() - started}ms`", true)
embed.colour() embed.colour()
embed.footer(user)
return embed.build() return embed.build()
} }

View file

@ -1,10 +1,36 @@
package net.projecttl.p.x32.func.handler package net.projecttl.p.x32.func.handler
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import net.dv8tion.jda.api.OnlineStatus
import net.dv8tion.jda.api.entities.Activity
import net.dv8tion.jda.api.events.session.ReadyEvent import net.dv8tion.jda.api.events.session.ReadyEvent
import net.dv8tion.jda.api.hooks.ListenerAdapter import net.dv8tion.jda.api.hooks.ListenerAdapter
import net.projecttl.p.x32.func.Conf
import net.projecttl.p.x32.func.instance
object Ready : ListenerAdapter() { object Ready : ListenerAdapter() {
@OptIn(DelicateCoroutinesApi::class)
override fun onReady(ev: ReadyEvent) { override fun onReady(ev: ReadyEvent) {
val list = listOf(
Activity.playing("${ev.jda.selfUser.name} v${Conf.version}"),
Activity.listening("${ev.guildTotalCount}개의 서버와 함께 서비스 하는 중")
)
println("Logged in as ${ev.jda.selfUser.asTag}") println("Logged in as ${ev.jda.selfUser.asTag}")
GlobalScope.launch {
do {
list.forEach { act ->
try {
ev.jda.presence.setPresence(OnlineStatus.ONLINE, act)
} catch (ex: Exception) {
instance.logger.error(ex.message)
}
delay(1000 * 10L)
}
} while (true)
}
} }
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "${project.name}", "name": "bundle-module",
"version": "${project.version}", "version": "${rootProject.version}",
"main": "net.projecttl.p.x32.func.General" "main": "net.projecttl.p.x32.func.BundleModule"
} }

View file

@ -10,7 +10,6 @@ class CorePlugin extends Plugin {
logger.info "Hello, World!" logger.info "Hello, World!"
CommandHandler handler = new CommandHandler() CommandHandler handler = new CommandHandler()
handler.addCommand new Greeting() handler.addCommand new Greeting()
addHandler handler addHandler handler
} }