refactor: Completely changed the API and a working argument

This commit is contained in:
Ahmad Ansori Palembani 2025-05-22 16:13:06 +07:00
parent 690812b628
commit 411e183e04
19 changed files with 244 additions and 199 deletions

View file

@ -2,9 +2,8 @@ package io.github.null2264.tsukumogami.bot
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
import io.github.null2264.tsukumogami.bot.core.di.appModule import io.github.null2264.tsukumogami.bot.core.di.appModule
import io.github.null2264.tsukumogami.bot.core.module.DeveloperModule import io.github.null2264.tsukumogami.bot.core.module.generalModule
import io.github.null2264.tsukumogami.bot.core.module.GeneralModule import io.github.null2264.tsukumogami.core.bot
import io.github.null2264.tsukumogami.bot.core.Bot
import org.koin.core.context.GlobalContext.startKoin import org.koin.core.context.GlobalContext.startKoin
suspend fun main() { suspend fun main() {
@ -13,13 +12,13 @@ suspend fun main() {
modules(appModule) modules(appModule)
} }
Bot { bot {
Logger.setTag("Tsukumogami") Logger.setTag("Tsukumogami")
token = System.getenv("DISCORD_TOKEN") token = System.getenv("DISCORD_TOKEN")
prefixes("src!", "mm!") // mm! for backwards compatibility prefixes("src!", "mm!") // mm! for backwards compatibility
extensions(::DeveloperModule, ::GeneralModule) modules(generalModule)
}.start() }.start()
} }

View file

@ -1,6 +0,0 @@
package io.github.null2264.tsukumogami.bot.core
import io.github.null2264.tsukumogami.core.AbstractBot
import io.github.null2264.tsukumogami.core.BotConfigurator
class Bot(block: BotConfigurator.() -> Unit = {}) : AbstractBot(block)

View file

@ -1,17 +1,13 @@
package io.github.null2264.tsukumogami.bot.core.module package io.github.null2264.tsukumogami.bot.core.module
import io.github.null2264.tsukumogami.core.commands.annotation.Command //class DeveloperModule : BotModule("Developer", "Only for developers") {
import io.github.null2264.tsukumogami.core.Context //
import io.github.null2264.tsukumogami.core.module.BotModule // @Command(
// name="poweroff",
class DeveloperModule : BotModule("Developer", "Only for developers") { // description="Turn the bot off",
// )
@Command( // private suspend fun shutdown(ctx: Context) {
name="poweroff", // ctx.reply("Shutting Down...", mentionsAuthor = true)
description="Turn the bot off", // bot?.stop()
) // }
private suspend fun shutdown(ctx: Context) { //}
ctx.reply("Shutting Down...", mentionsAuthor = true)
bot?.stop()
}
}

View file

@ -1,23 +1,20 @@
package io.github.null2264.tsukumogami.bot.core.module package io.github.null2264.tsukumogami.bot.core.module
import dev.kord.core.entity.effectiveName import io.github.null2264.tsukumogami.bot.core.module.arguments.TestArguments
import io.github.null2264.tsukumogami.core.commands.annotation.Command import io.github.null2264.tsukumogami.core.module.api.botModules
import io.github.null2264.tsukumogami.core.Context
import io.github.null2264.tsukumogami.core.module.BotModule
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
class GeneralModule : BotModule("General", "idk") { val generalModule = botModules("General") {
commands("ping", description = "Ping the bot!") { ctx ->
@Command(description = "Ping the bot!")
private suspend fun ping(ctx: Context) {
val startTime = Clock.System.now() val startTime = Clock.System.now()
ctx.typing() ctx.typing()
val endTime = Clock.System.now() val endTime = Clock.System.now()
ctx.send("Pong! ${endTime.toEpochMilliseconds() - startTime.toEpochMilliseconds()}ms") ctx.send("Pong! ${endTime.toEpochMilliseconds() - startTime.toEpochMilliseconds()}ms")
} }
@Command("test") groups("group") {
private suspend fun differentName(ctx: Context) { commands("test", arguments = ::TestArguments) { ctx, args -> ctx.send("Hello world ${args.test}") }
ctx.send("Hello World! ${ctx.author?.effectiveName}")
} }
commands("test", arguments = ::TestArguments) { ctx, args -> ctx.send("Hello world ${args.test}") }
} }

View file

@ -8,45 +8,52 @@ import dev.kord.core.event.message.MessageCreateEvent
import dev.kord.core.on import dev.kord.core.on
import dev.kord.gateway.Intent import dev.kord.gateway.Intent
import dev.kord.gateway.PrivilegedIntent import dev.kord.gateway.PrivilegedIntent
import io.github.null2264.tsukumogami.core.commands.Command
import io.github.null2264.tsukumogami.core.exceptions.CommandException import io.github.null2264.tsukumogami.core.exceptions.CommandException
import io.github.null2264.tsukumogami.core.exceptions.CommandNotFound import io.github.null2264.tsukumogami.core.exceptions.CommandNotFound
import io.github.null2264.tsukumogami.core.module.BotModule import io.github.null2264.tsukumogami.core.module.BotModule
import io.github.null2264.tsukumogami.core.commands.CommandHolder import io.github.null2264.tsukumogami.core.commands.IGroup
import io.github.null2264.tsukumogami.core.utils.parseCommandAndArguments import io.github.null2264.tsukumogami.core.ext.parseCommandAndArguments
import kotlin.reflect.full.callSuspend import kotlin.reflect.full.callSuspend
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
abstract class AbstractBot(configurator: BotConfigurator.() -> Unit) { open class Bot internal constructor(): IGroup {
private val commands: Map<String, CommandHolder> lateinit var client: Kord
private val extensions: Map<String, BotModule> private val modules = mutableMapOf<String, BotModule>()
private val prefixes: List<String> private val _prefixes = mutableListOf<String>()
private val client: Kord val prefixes: List<String> get() = _prefixes.toList()
internal lateinit var token: String
override val allCommands: MutableMap<String, Command> = mutableMapOf()
// TODO: Bind Bot and Kord to Koin fun addModule(module: BotModule) {
init { modules[module.name] = module.install(this)
val currentConfig = BotConfigurator() }
currentConfig.apply(configurator)
extensions = mutableMapOf() fun addPrefix(prefix: String) {
currentConfig.extensions.forEach { module -> _prefixes.add(prefix)
module.setup() }
module.install(this, currentConfig)
extensions[module.name] = module fun addCommand(command: Command) {
if (allCommands.containsKey(command.name)) {
throw IllegalStateException("Duplicate command: '${command.name}'")
} }
commands = currentConfig.commands allCommands[command.name] = command
prefixes = currentConfig.prefixes }
client = runBlocking { fun removeCommand(command: Command) {
Kord(currentConfig.token).apply { if (!allCommands.containsKey(command.name)) {
on<ReadyEvent> { onReady() } throw IllegalStateException("Command not found: '${command.name}'")
on<MessageCreateEvent> { onMessage(this.message, this) }
}
} }
allCommands.remove(command.name)
} }
open suspend fun start() { open suspend fun start() {
client = Kord(token).apply {
on<ReadyEvent> { onReady() }
on<MessageCreateEvent> { onMessage(this.message, this) }
}
client.login { client.login {
@OptIn(PrivilegedIntent::class) @OptIn(PrivilegedIntent::class)
intents += Intent.MessageContent intents += Intent.MessageContent
@ -57,11 +64,13 @@ abstract class AbstractBot(configurator: BotConfigurator.() -> Unit) {
client.shutdown() client.shutdown()
} }
fun getCommand(name: String) = commands[name] fun getCommand(name: String?) = allCommands[name]
private fun getContext(message: Message): Context { private fun getContext(message: Message): Context {
val candidate = message.content.parsePrefixCommandAndArguments() val candidate = message.content.parsePrefixCommandAndArguments()
return Context(this, message, candidate?.first, candidate?.second) val context = Context(this, message, candidate?.first, candidate?.second)
context.command = getCommand(candidate?.second?.get(0))
return context
} }
open suspend fun onCommandError(context: Context, error: CommandException) { open suspend fun onCommandError(context: Context, error: CommandException) {
@ -72,9 +81,7 @@ abstract class AbstractBot(configurator: BotConfigurator.() -> Unit) {
val ctx = getContext(message) val ctx = getContext(message)
try { try {
ctx.command?.let { ctx.command?.invoke(ctx) ?: throw CommandNotFound()
it.callback.callSuspend(extensions[it.extension], ctx)
} ?: throw CommandNotFound()
} catch (e: CommandException) { } catch (e: CommandException) {
onCommandError(ctx, e) onCommandError(ctx, e)
} catch (e: Exception) { } catch (e: Exception) {

View file

@ -1,32 +0,0 @@
package io.github.null2264.tsukumogami.core
import io.github.null2264.tsukumogami.core.module.internal.Modules
import io.github.null2264.tsukumogami.core.module.BotModule
import io.github.null2264.tsukumogami.core.commands.CommandHolder
import kotlin.reflect.KFunction
class BotConfigurator internal constructor() {
internal val commands = mutableMapOf<String, CommandHolder>()
internal val extensions = Modules()
internal val prefixes = mutableListOf<String>()
var token: String = ""
internal fun isExists(name: String?) = this.commands.containsKey(name)
internal fun commands(command: CommandHolder, name: String? = null) {
this.commands[if (name.isNullOrEmpty()) command.name else name] = command
}
fun extensions(vararg extensions: KFunction<BotModule>) {
this.extensions.initializeAndAddAll(extensions.toList())
}
fun prefixes(vararg prefixes: String) {
prefixes(prefixes.toList())
}
fun prefixes(prefixes: List<String>) {
this.prefixes.addAll(prefixes)
}
}

View file

@ -0,0 +1,32 @@
package io.github.null2264.tsukumogami.core
import dev.kord.core.Kord
import io.github.null2264.tsukumogami.core.module.BotModule
import kotlin.reflect.KFunction
class BotHolder internal constructor(
val bot: Bot
) {
internal val prefixes = mutableListOf<String>()
var token: String
get() = bot.token
set(value) {
bot.token = value
}
fun modules(vararg modules: BotModule) {
modules.forEach(bot::addModule)
}
fun prefixes(vararg prefixes: String) {
prefixes.forEach(bot::addPrefix)
}
}
fun bot(clazz: KFunction<Bot> = ::Bot, declaration: BotHolder.() -> Unit): Bot {
val bot = clazz.call()
val holder = BotHolder(bot)
declaration(holder)
return holder.bot
}

View file

@ -4,18 +4,26 @@ import dev.kord.core.behavior.channel.createMessage
import dev.kord.core.entity.Message import dev.kord.core.entity.Message
import dev.kord.rest.builder.message.AllowedMentionsBuilder import dev.kord.rest.builder.message.AllowedMentionsBuilder
import dev.kord.rest.builder.message.allowedMentions import dev.kord.rest.builder.message.allowedMentions
import io.github.null2264.tsukumogami.core.commands.CommandHolder import io.github.null2264.tsukumogami.core.commands.Command
class Context( class Context(
private val bot: AbstractBot, private val bot: Bot,
private val message: Message, private val message: Message,
/**
* The prefix that used to invoke the command
*/
val prefix: String?, val prefix: String?,
commandAndArguments: List<String>?, /**
* Potential command name and/or arguments
*/
val candidate: List<String>?,
) { ) {
val author get() = message.author val author get() = message.author
val commandAndArguments: MutableList<String>? = commandAndArguments?.toMutableList()
val command: CommandHolder? = this.commandAndArguments?.removeAt(0)?.let { bot.getCommand(it) } /**
* The command that currently being invoked
*/
var command: Command? = null
suspend fun send(content: String) = message.channel.createMessage(content) suspend fun send(content: String) = message.channel.createMessage(content)

View file

@ -0,0 +1,13 @@
package io.github.null2264.tsukumogami.core.annotation
@RequiresOptIn(message = "Intended for internal usage. External usage is strongly discouraged.", level = RequiresOptIn.Level.ERROR)
@Retention(AnnotationRetention.BINARY)
@Target(
AnnotationTarget.CLASS,
AnnotationTarget.TYPEALIAS,
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY,
AnnotationTarget.FIELD,
AnnotationTarget.CONSTRUCTOR,
)
annotation class TsukumogamiInternalApi

View file

@ -2,7 +2,7 @@ package io.github.null2264.tsukumogami.core.commands
import io.github.null2264.tsukumogami.core.commands.converters.Converter import io.github.null2264.tsukumogami.core.commands.converters.Converter
open class Arguments { abstract class Arguments {
val args = mutableListOf<Argument<*>>() val args = mutableListOf<Argument<*>>()
fun <R : Any> args( fun <R : Any> args(
@ -13,4 +13,10 @@ open class Arguments {
return converter return converter
} }
suspend fun parse(value: String) {
args.forEach { arg ->
arg.converter.parse(value)
}
}
} }

View file

@ -0,0 +1,20 @@
package io.github.null2264.tsukumogami.core.commands
import io.github.null2264.tsukumogami.core.Context
import kotlin.reflect.KFunction
open class Command(
val name: String,
val alias: Set<String>,
val description: String,
private val arguments: KFunction<Arguments>,
private val handler: suspend (Context, Arguments) -> Unit,
) {
open suspend fun invoke(context: Context) {
val parsedArguments = arguments.call()
// TODO: Don't hardcode this
parsedArguments.parse(context.candidate?.get(1) ?: "test")
handler.invoke(context, parsedArguments)
}
}

View file

@ -1,13 +0,0 @@
package io.github.null2264.tsukumogami.core.commands
import kotlin.reflect.KFunction
/**
* Class holding information about a command
*/
data class CommandHolder internal constructor(
val name: String,
val extension: String,
val description: String? = null,
val callback: KFunction<*>,
)

View file

@ -0,0 +1,4 @@
package io.github.null2264.tsukumogami.core.commands
class EmptyArguments : Arguments() {
}

View file

@ -0,0 +1,26 @@
package io.github.null2264.tsukumogami.core.commands
import io.github.null2264.tsukumogami.core.Context
class Group(
name: String,
alias: Set<String>,
description: String,
) :
IGroup,
Command(
name,
alias,
description,
::EmptyArguments,
{ _, _ -> /* No handler for group to match the behaviour of Discord's slash command */ },
) {
override val allCommands: MutableMap<String, Command> = mutableMapOf()
override suspend fun invoke(context: Context) {
val command = allCommands["TODO"] ?: return
context.command = command
command.invoke(context)
}
}

View file

@ -0,0 +1,45 @@
package io.github.null2264.tsukumogami.core.commands
import io.github.null2264.tsukumogami.core.Context
import kotlin.reflect.KFunction
interface IGroup {
val allCommands: MutableMap<String, Command>
private fun addCommand(command: Command) {
allCommands[command.name] = command
command.alias.forEach { allCommands[it] = command }
}
fun commands(
name: String,
alias: Set<String> = setOf(),
description: String = "",
handler: suspend (Context) -> Unit,
) {
val command = Command(name, alias, description, ::EmptyArguments) { ctx, _ -> handler(ctx) }
addCommand(command)
}
fun <Args : Arguments> commands(
name: String,
alias: Set<String> = setOf(),
description: String = "",
arguments: KFunction<Args>,
handler: suspend (Context, Args) -> Unit,
) {
val command = Command(name, alias, description, arguments) { ctx, args -> handler(ctx, args as Args) }
addCommand(command)
}
fun groups(
name: String,
alias: Set<String> = setOf(),
description: String = "",
declaration: IGroup.() -> Unit,
) {
val group = Group(name, alias, description)
addCommand(group)
}
}

View file

@ -1,4 +1,4 @@
package io.github.null2264.tsukumogami.core.utils package io.github.null2264.tsukumogami.core.ext
fun String.parseCommandAndArguments(): List<String> { fun String.parseCommandAndArguments(): List<String> {
if (isBlank()) { if (isBlank()) {

View file

@ -1,48 +1,20 @@
package io.github.null2264.tsukumogami.core.module package io.github.null2264.tsukumogami.core.module
import co.touchlab.kermit.Logger import io.github.null2264.tsukumogami.core.Bot
import io.github.null2264.tsukumogami.core.AbstractBot import io.github.null2264.tsukumogami.core.commands.Command
import io.github.null2264.tsukumogami.core.commands.annotation.Command import io.github.null2264.tsukumogami.core.commands.IGroup
import io.github.null2264.tsukumogami.core.BotConfigurator
import io.github.null2264.tsukumogami.core.commands.CommandHolder
import kotlin.reflect.jvm.isAccessible
import kotlin.reflect.jvm.kotlinFunction
abstract class BotModule(val name: String, val description: String? = null) { class BotModule internal constructor(val name: String) : IGroup {
var bot: AbstractBot? = null override val allCommands: MutableMap<String, Command> = mutableMapOf()
internal set
open fun setup() {} internal fun install(bot: Bot): BotModule {
allCommands.values.forEach(bot::addCommand)
return this
}
internal fun install(bot: AbstractBot, configurator: BotConfigurator) { internal fun uninstall(bot: Bot): BotModule {
this.bot = bot allCommands.values.forEach(bot::removeCommand)
return this
val methods = this::class.java.declaredMethods
for (method in methods) {
for (annotation in method.annotations) {
if (annotation !is Command)
continue
configurator.apply {
val kMethod = method.kotlinFunction
if (isExists(kMethod?.name)) {
Logger.e { "Command already exists" }
return
}
kMethod?.let {
it.isAccessible = true
commands(
CommandHolder(
annotation.name.ifEmpty { it.name },
name,
annotation.description.ifEmpty { description },
it,
)
)
}
}
}
}
} }
} }

View file

@ -0,0 +1,11 @@
package io.github.null2264.tsukumogami.core.module.api
import io.github.null2264.tsukumogami.core.module.BotModule
typealias BotModuleDeclaration = BotModule.() -> Unit
fun botModules(name: String, declaration: BotModuleDeclaration): BotModule {
val module = BotModule(name)
declaration(module)
return module
}

View file

@ -1,40 +0,0 @@
package io.github.null2264.tsukumogami.core.module.internal
import io.github.null2264.tsukumogami.core.module.BotModule
import java.util.function.Consumer
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.full.isSubclassOf
class Modules {
private val list = mutableListOf<BotModule>()
fun get(index: Int) = list[index]
private fun KFunction<BotModule>.tryInitialize(): BotModule? {
val kClass = this.returnType.classifier as KClass<*>
if (!kClass.isSubclassOf(BotModule::class))
return null
return this.call()
}
fun initializeAndAddAll(modules: List<KFunction<BotModule>>) {
addAll(modules.mapNotNull { it.tryInitialize() })
}
fun addAll(modules: List<BotModule>) {
list.addAll(modules)
}
fun add(module: KFunction<BotModule>) {
module.tryInitialize()?.let { add(it) }
}
fun add(module: BotModule) {
list.add(module)
}
fun forEach(consumer: Consumer<in BotModule>) = list.forEach(consumer)
}