refactor: Completely changed the API and a working argument
This commit is contained in:
parent
690812b628
commit
411e183e04
19 changed files with 244 additions and 199 deletions
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
|
@ -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()
|
||||
// }
|
||||
//}
|
||||
|
|
|
@ -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}") }
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
extensions = mutableMapOf()
|
||||
currentConfig.extensions.forEach { module ->
|
||||
module.setup()
|
||||
module.install(this, currentConfig)
|
||||
extensions[module.name] = module
|
||||
fun addModule(module: BotModule) {
|
||||
modules[module.name] = module.install(this)
|
||||
}
|
||||
commands = currentConfig.commands
|
||||
prefixes = currentConfig.prefixes
|
||||
|
||||
client = runBlocking {
|
||||
Kord(currentConfig.token).apply {
|
||||
on<ReadyEvent> { onReady() }
|
||||
fun addPrefix(prefix: String) {
|
||||
_prefixes.add(prefix)
|
||||
}
|
||||
|
||||
on<MessageCreateEvent> { onMessage(this.message, this) }
|
||||
fun addCommand(command: Command) {
|
||||
if (allCommands.containsKey(command.name)) {
|
||||
throw IllegalStateException("Duplicate command: '${command.name}'")
|
||||
}
|
||||
allCommands[command.name] = command
|
||||
}
|
||||
|
||||
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) {
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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<*>,
|
||||
)
|
|
@ -0,0 +1,4 @@
|
|||
package io.github.null2264.tsukumogami.core.commands
|
||||
|
||||
class EmptyArguments : Arguments() {
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()) {
|
|
@ -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: 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 install(bot: Bot): BotModule {
|
||||
allCommands.values.forEach(bot::addCommand)
|
||||
return this
|
||||
}
|
||||
|
||||
internal fun uninstall(bot: Bot): BotModule {
|
||||
allCommands.values.forEach(bot::removeCommand)
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue