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 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.Bot
import io.github.null2264.tsukumogami.bot.core.module.generalModule
import io.github.null2264.tsukumogami.core.bot
import org.koin.core.context.GlobalContext.startKoin
suspend fun main() {
@ -13,13 +12,13 @@ suspend fun main() {
modules(appModule)
}
Bot {
bot {
Logger.setTag("Tsukumogami")
token = System.getenv("DISCORD_TOKEN")
prefixes("src!", "mm!") // mm! for backwards compatibility
extensions(::DeveloperModule, ::GeneralModule)
modules(generalModule)
}.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
import io.github.null2264.tsukumogami.core.commands.annotation.Command
import io.github.null2264.tsukumogami.core.Context
import io.github.null2264.tsukumogami.core.module.BotModule
class DeveloperModule : BotModule("Developer", "Only for developers") {
@Command(
name="poweroff",
description="Turn the bot off",
)
private suspend fun shutdown(ctx: Context) {
ctx.reply("Shutting Down...", mentionsAuthor = true)
bot?.stop()
}
}
//class DeveloperModule : BotModule("Developer", "Only for developers") {
//
// @Command(
// name="poweroff",
// description="Turn the bot off",
// )
// 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
import dev.kord.core.entity.effectiveName
import io.github.null2264.tsukumogami.core.commands.annotation.Command
import io.github.null2264.tsukumogami.core.Context
import io.github.null2264.tsukumogami.core.module.BotModule
import io.github.null2264.tsukumogami.bot.core.module.arguments.TestArguments
import io.github.null2264.tsukumogami.core.module.api.botModules
import kotlinx.datetime.Clock
class GeneralModule : BotModule("General", "idk") {
@Command(description = "Ping the bot!")
private suspend fun ping(ctx: Context) {
val generalModule = botModules("General") {
commands("ping", description = "Ping the bot!") { ctx ->
val startTime = Clock.System.now()
ctx.typing()
val endTime = Clock.System.now()
ctx.send("Pong! ${endTime.toEpochMilliseconds() - startTime.toEpochMilliseconds()}ms")
}
@Command("test")
private suspend fun differentName(ctx: Context) {
ctx.send("Hello World! ${ctx.author?.effectiveName}")
groups("group") {
commands("test", arguments = ::TestArguments) { ctx, args -> ctx.send("Hello world ${args.test}") }
}
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.gateway.Intent
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.CommandNotFound
import io.github.null2264.tsukumogami.core.module.BotModule
import io.github.null2264.tsukumogami.core.commands.CommandHolder
import io.github.null2264.tsukumogami.core.utils.parseCommandAndArguments
import io.github.null2264.tsukumogami.core.commands.IGroup
import io.github.null2264.tsukumogami.core.ext.parseCommandAndArguments
import kotlin.reflect.full.callSuspend
import kotlinx.coroutines.runBlocking
abstract class AbstractBot(configurator: BotConfigurator.() -> Unit) {
open class Bot internal constructor(): IGroup {
private val commands: Map<String, CommandHolder>
private val extensions: Map<String, BotModule>
private val prefixes: List<String>
private val client: Kord
lateinit var client: Kord
private val modules = mutableMapOf<String, BotModule>()
private val _prefixes = mutableListOf<String>()
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
init {
val currentConfig = BotConfigurator()
currentConfig.apply(configurator)
fun addModule(module: BotModule) {
modules[module.name] = module.install(this)
}
extensions = mutableMapOf()
currentConfig.extensions.forEach { module ->
module.setup()
module.install(this, currentConfig)
extensions[module.name] = module
fun addPrefix(prefix: String) {
_prefixes.add(prefix)
}
fun addCommand(command: Command) {
if (allCommands.containsKey(command.name)) {
throw IllegalStateException("Duplicate command: '${command.name}'")
}
commands = currentConfig.commands
prefixes = currentConfig.prefixes
allCommands[command.name] = command
}
client = runBlocking {
Kord(currentConfig.token).apply {
on<ReadyEvent> { onReady() }
on<MessageCreateEvent> { onMessage(this.message, this) }
}
fun removeCommand(command: Command) {
if (!allCommands.containsKey(command.name)) {
throw IllegalStateException("Command not found: '${command.name}'")
}
allCommands.remove(command.name)
}
open suspend fun start() {
client = Kord(token).apply {
on<ReadyEvent> { onReady() }
on<MessageCreateEvent> { onMessage(this.message, this) }
}
client.login {
@OptIn(PrivilegedIntent::class)
intents += Intent.MessageContent
@ -57,11 +64,13 @@ abstract class AbstractBot(configurator: BotConfigurator.() -> Unit) {
client.shutdown()
}
fun getCommand(name: String) = commands[name]
fun getCommand(name: String?) = allCommands[name]
private fun getContext(message: Message): Context {
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) {
@ -72,9 +81,7 @@ abstract class AbstractBot(configurator: BotConfigurator.() -> Unit) {
val ctx = getContext(message)
try {
ctx.command?.let {
it.callback.callSuspend(extensions[it.extension], ctx)
} ?: throw CommandNotFound()
ctx.command?.invoke(ctx) ?: throw CommandNotFound()
} catch (e: CommandException) {
onCommandError(ctx, e)
} 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.rest.builder.message.AllowedMentionsBuilder
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(
private val bot: AbstractBot,
private val bot: Bot,
private val message: Message,
/**
* The prefix that used to invoke the command
*/
val prefix: String?,
commandAndArguments: List<String>?,
/**
* Potential command name and/or arguments
*/
val candidate: List<String>?,
) {
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)

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
open class Arguments {
abstract class Arguments {
val args = mutableListOf<Argument<*>>()
fun <R : Any> args(
@ -13,4 +13,10 @@ open class Arguments {
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> {
if (isBlank()) {

View file

@ -1,48 +1,20 @@
package io.github.null2264.tsukumogami.core.module
import co.touchlab.kermit.Logger
import io.github.null2264.tsukumogami.core.AbstractBot
import io.github.null2264.tsukumogami.core.commands.annotation.Command
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
import io.github.null2264.tsukumogami.core.Bot
import io.github.null2264.tsukumogami.core.commands.Command
import io.github.null2264.tsukumogami.core.commands.IGroup
abstract class BotModule(val name: String, val description: String? = null) {
class BotModule internal constructor(val name: String) : IGroup {
var bot: AbstractBot? = null
internal set
override val allCommands: MutableMap<String, Command> = mutableMapOf()
open fun setup() {}
internal fun install(bot: Bot): BotModule {
allCommands.values.forEach(bot::addCommand)
return this
}
internal fun install(bot: AbstractBot, configurator: BotConfigurator) {
this.bot = bot
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,
)
)
}
}
}
}
internal fun uninstall(bot: Bot): BotModule {
allCommands.values.forEach(bot::removeCommand)
return this
}
}

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