Compare commits
10 commits
135cbd31a5
...
26e5ba9e85
Author | SHA1 | Date | |
---|---|---|---|
26e5ba9e85 | |||
b22cf439ad | |||
513959d4a7 | |||
a62d86563a | |||
04472f8bfa | |||
fce8afa2b2 | |||
411e183e04 | |||
690812b628 | |||
c901eb2f66 | |||
1c20e05066 |
30 changed files with 744 additions and 243 deletions
|
@ -2,24 +2,19 @@ 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() {
|
||||
|
||||
startKoin {
|
||||
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.module.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,26 @@
|
|||
package io.github.null2264.tsukumogami.bot.core.module
|
||||
|
||||
import dev.kord.core.entity.effectiveName
|
||||
import io.github.null2264.tsukumogami.core.module.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.Test2Arguments
|
||||
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", alias = setOf("p"), 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 = ::Test2Arguments) { ctx, args -> ctx.reply("Hello world ${args.user}") }
|
||||
groups("group") {
|
||||
commands("test", alias = setOf("t")) { ctx ->
|
||||
ctx.reply("Hello world ${ctx.command?.module?.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
commands("test", arguments = ::TestArguments) { ctx, args -> ctx.send("Hello world ${args.test}") }
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
package io.github.null2264.tsukumogami.bot.core.module.arguments
|
||||
|
||||
import io.github.null2264.tsukumogami.core.commands.Arguments
|
||||
import io.github.null2264.tsukumogami.core.commands.ext.string
|
||||
import io.github.null2264.tsukumogami.core.commands.ext.user
|
||||
|
||||
class TestArguments : Arguments() {
|
||||
val test by string("Test") {
|
||||
default("Lmao")
|
||||
}
|
||||
}
|
||||
|
||||
class Test2Arguments : Arguments() {
|
||||
val user by user("User")
|
||||
}
|
|
@ -1,108 +0,0 @@
|
|||
package io.github.null2264.tsukumogami.core
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import dev.kord.core.Kord
|
||||
import dev.kord.core.entity.Message
|
||||
import dev.kord.core.event.gateway.ReadyEvent
|
||||
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.exceptions.CommandException
|
||||
import io.github.null2264.tsukumogami.core.exceptions.CommandNotFound
|
||||
import io.github.null2264.tsukumogami.core.module.BotModule
|
||||
import io.github.null2264.tsukumogami.core.module.Command
|
||||
import kotlin.reflect.full.callSuspend
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
abstract class AbstractBot(configurator: BotConfigurator.() -> Unit) {
|
||||
|
||||
private val commands: Map<String, Command>
|
||||
private val extensions: Map<String, BotModule>
|
||||
private val prefixes: List<String>
|
||||
private val client: Kord
|
||||
|
||||
init {
|
||||
val currentConfig = BotConfigurator()
|
||||
currentConfig.apply(configurator)
|
||||
|
||||
extensions = mutableMapOf()
|
||||
currentConfig.extensions.forEach {
|
||||
val module = it.call()
|
||||
module.setup()
|
||||
module.install(this, currentConfig)
|
||||
extensions[module.name] = module
|
||||
}
|
||||
commands = currentConfig.commands
|
||||
prefixes = currentConfig.prefixes
|
||||
|
||||
client = runBlocking {
|
||||
Kord(currentConfig.token).apply {
|
||||
on<ReadyEvent> { onReady() }
|
||||
|
||||
on<MessageCreateEvent> { onMessage(this.message, this) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open suspend fun start() {
|
||||
client.login {
|
||||
@OptIn(PrivilegedIntent::class)
|
||||
intents += Intent.MessageContent
|
||||
}
|
||||
}
|
||||
|
||||
open suspend fun stop() {
|
||||
client.shutdown()
|
||||
}
|
||||
|
||||
fun getCommand(name: String) = commands[name]
|
||||
|
||||
private fun getContext(message: Message): Context {
|
||||
val candidate = message.content.hasPrefix()
|
||||
return Context(this, message, candidate?.first, candidate?.second)
|
||||
}
|
||||
|
||||
open suspend fun onCommandError(context: Context, error: CommandException) {
|
||||
context.send(error.message!!)
|
||||
}
|
||||
|
||||
open suspend fun processCommand(message: Message) {
|
||||
val ctx = getContext(message)
|
||||
|
||||
try {
|
||||
ctx.command?.let {
|
||||
it.callback.callSuspend(extensions[it.extension], ctx)
|
||||
} ?: throw CommandNotFound()
|
||||
} catch (e: CommandException) {
|
||||
onCommandError(ctx, e)
|
||||
} catch (e: Exception) {
|
||||
Logger.e(e) { "Something went wrong while trying to process command '${ctx.command?.name}'" }
|
||||
}
|
||||
}
|
||||
|
||||
open suspend fun onMessage(message: Message, event: MessageCreateEvent) {
|
||||
if (message.author?.isBot != false) return
|
||||
|
||||
processCommand(message)
|
||||
}
|
||||
|
||||
open suspend fun onReady() {
|
||||
Logger.i { "Online! ${client.getSelf().username}" }
|
||||
}
|
||||
|
||||
fun String.hasPrefix(): Pair<String, String>? {
|
||||
if (this.isBlank()) return null
|
||||
|
||||
var ret: Pair<String, String>? = null
|
||||
|
||||
prefixes.forEach {
|
||||
if (this.substring(0, it.length) == it) {
|
||||
ret = Pair(this.substring(0, it.length), this.substring(it.length).split(" ").first())
|
||||
return@forEach
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
}
|
127
core/src/main/kotlin/io/github/null2264/tsukumogami/core/Bot.kt
Normal file
127
core/src/main/kotlin/io/github/null2264/tsukumogami/core/Bot.kt
Normal file
|
@ -0,0 +1,127 @@
|
|||
package io.github.null2264.tsukumogami.core
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import dev.kord.core.Kord
|
||||
import dev.kord.core.entity.Message
|
||||
import dev.kord.core.event.gateway.ReadyEvent
|
||||
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.IGroup
|
||||
import io.github.null2264.tsukumogami.core.ext.parseCommandAndArguments
|
||||
import io.github.null2264.tsukumogami.core.koin.TsukumogamiKoinComponent
|
||||
import kotlin.reflect.full.callSuspend
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.koin.core.component.inject
|
||||
|
||||
open class Bot internal constructor(): IGroup, TsukumogamiKoinComponent {
|
||||
|
||||
val client: Kord by inject()
|
||||
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()
|
||||
|
||||
fun addModule(module: BotModule) {
|
||||
modules[module.name] = module.install(this)
|
||||
}
|
||||
|
||||
fun addPrefix(prefix: String) {
|
||||
_prefixes.add(prefix)
|
||||
}
|
||||
|
||||
private fun addCommand(name: String, command: Command) {
|
||||
if (allCommands.containsKey(name)) {
|
||||
throw IllegalStateException("Duplicate command: '${command.name}'")
|
||||
}
|
||||
allCommands[command.name] = command
|
||||
}
|
||||
|
||||
fun addCommand(command: Command) {
|
||||
addCommand(command.name, command)
|
||||
command.alias.forEach { addCommand(it, command) }
|
||||
}
|
||||
|
||||
private fun removeCommand(name: String) {
|
||||
if (!allCommands.containsKey(name)) {
|
||||
throw IllegalStateException("Command not found: '${name}'")
|
||||
}
|
||||
allCommands.remove(name)
|
||||
}
|
||||
|
||||
fun removeCommand(command: Command) {
|
||||
removeCommand(command.name)
|
||||
command.alias.forEach { removeCommand(it) }
|
||||
}
|
||||
|
||||
open suspend fun start() {
|
||||
client.login {
|
||||
@OptIn(PrivilegedIntent::class)
|
||||
intents += Intent.MessageContent
|
||||
}
|
||||
}
|
||||
|
||||
open suspend fun stop() {
|
||||
client.shutdown()
|
||||
}
|
||||
|
||||
fun getCommand(name: String?) = allCommands[name]
|
||||
|
||||
private fun getContext(message: Message): Context {
|
||||
val parsed = message.content.parsePrefixAndCommand()
|
||||
val context = Context(this, message, parsed?.first)
|
||||
context.command = getCommand(parsed?.second)
|
||||
if (context.command != null) context.invokedWith = parsed?.second
|
||||
return context
|
||||
}
|
||||
|
||||
open suspend fun onCommandError(context: Context, error: CommandException) {
|
||||
context.send(error.message!!)
|
||||
}
|
||||
|
||||
open suspend fun processCommand(message: Message) {
|
||||
val ctx = getContext(message)
|
||||
|
||||
try {
|
||||
ctx.command?.invoke(ctx) ?: throw CommandNotFound()
|
||||
} catch (e: CommandException) {
|
||||
onCommandError(ctx, e)
|
||||
} catch (e: Exception) {
|
||||
Logger.e(e) { "Something went wrong while trying to process command '${ctx.command?.name}'" }
|
||||
}
|
||||
}
|
||||
|
||||
open suspend fun onMessage(message: Message, event: MessageCreateEvent) {
|
||||
if (message.author?.isBot != false) return
|
||||
|
||||
processCommand(message)
|
||||
}
|
||||
|
||||
open suspend fun onReady() {
|
||||
Logger.i { "Online! ${client.getSelf().username}" }
|
||||
}
|
||||
|
||||
private fun String.parsePrefixAndCommand(): Pair<String, String>? {
|
||||
if (this.isBlank()) return null
|
||||
|
||||
var ret: Pair<String, String>? = null
|
||||
|
||||
run {
|
||||
prefixes.forEach { candidate ->
|
||||
val prefix = this.substring(0, candidate.length)
|
||||
if (prefix != candidate) { return@forEach }
|
||||
val command = this.drop(candidate.length).substringBefore(' ')
|
||||
ret = prefix to command
|
||||
return@run
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package io.github.null2264.tsukumogami.core
|
||||
|
||||
import dev.kord.core.Kord
|
||||
import dev.kord.core.event.gateway.ReadyEvent
|
||||
import dev.kord.core.event.message.MessageCreateEvent
|
||||
import dev.kord.core.on
|
||||
import io.github.null2264.tsukumogami.core.ext.loadModule
|
||||
import io.github.null2264.tsukumogami.core.koin.TsukumogamiKoinContext
|
||||
import io.github.null2264.tsukumogami.core.module.BotModule
|
||||
import kotlin.reflect.KFunction
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.koin.dsl.bind
|
||||
|
||||
class BotBuilder internal constructor(
|
||||
val bot: Bot
|
||||
) {
|
||||
|
||||
var token: String = ""
|
||||
|
||||
var kordBuilder: suspend (String) -> Kord = { token ->
|
||||
Kord(token).apply {
|
||||
on<ReadyEvent> { bot.onReady() }
|
||||
on<MessageCreateEvent> { bot.onMessage(this.message, this) }
|
||||
}
|
||||
}
|
||||
|
||||
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: BotBuilder.() -> Unit): Bot {
|
||||
if (TsukumogamiKoinContext.getOrNull() == null)
|
||||
TsukumogamiKoinContext.startKoin {
|
||||
}
|
||||
|
||||
val bot = clazz.call()
|
||||
val holder = BotBuilder(bot)
|
||||
declaration(holder)
|
||||
|
||||
val kord = runBlocking {
|
||||
holder.kordBuilder(holder.token)
|
||||
}
|
||||
loadModule { single { kord } bind Kord::class }
|
||||
loadModule { single { bot } bind Bot::class }
|
||||
|
||||
return holder.bot
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
package io.github.null2264.tsukumogami.core
|
||||
|
||||
import io.github.null2264.tsukumogami.core.module.BotModule
|
||||
import io.github.null2264.tsukumogami.core.module.Command
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KFunction
|
||||
import kotlin.reflect.full.isSubclassOf
|
||||
|
||||
class BotConfigurator internal constructor() {
|
||||
|
||||
internal val commands = mutableMapOf<String, Command>()
|
||||
internal val extensions = mutableListOf<KFunction<BotModule>>()
|
||||
internal val prefixes = mutableListOf<String>()
|
||||
var token: String = ""
|
||||
|
||||
internal fun isExists(name: String?) = this.commands.containsKey(name)
|
||||
|
||||
fun commands(command: Command, name: String? = null) {
|
||||
this.commands[if (name.isNullOrEmpty()) command.name else name] = command
|
||||
}
|
||||
|
||||
fun extensions(vararg extensions: KFunction<BotModule>) {
|
||||
extensions.forEach {
|
||||
val kClass = it.returnType.classifier as KClass<*>
|
||||
if (!kClass.isSubclassOf(BotModule::class))
|
||||
return
|
||||
|
||||
this.extensions.add(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun prefixes(vararg prefixes: String) {
|
||||
prefixes(prefixes.toList())
|
||||
}
|
||||
|
||||
fun prefixes(prefixes: List<String>) {
|
||||
this.prefixes.addAll(prefixes)
|
||||
}
|
||||
}
|
|
@ -4,11 +4,33 @@ 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.Command
|
||||
|
||||
class Context(private val bot: AbstractBot, private val message: Message, val prefix: String?, private val commandName: String?) {
|
||||
class Context(
|
||||
val bot: Bot,
|
||||
val message: Message,
|
||||
/**
|
||||
* The prefix that used to invoke the command
|
||||
*/
|
||||
val prefix: String?,
|
||||
) {
|
||||
|
||||
/**
|
||||
* The user that invoked the command
|
||||
*
|
||||
* Alias to [Message.author]
|
||||
*/
|
||||
val author get() = message.author
|
||||
val command get() = commandName?.let { bot.getCommand(it) }
|
||||
|
||||
/**
|
||||
* The text that invoked the command
|
||||
*/
|
||||
var invokedWith: String? = null
|
||||
|
||||
/**
|
||||
* The command that currently being invoked
|
||||
*/
|
||||
var command: Command? = null
|
||||
|
||||
suspend fun send(content: String) = message.channel.createMessage(content)
|
||||
|
||||
|
@ -22,4 +44,15 @@ class Context(private val bot: AbstractBot, private val message: Message, val pr
|
|||
}
|
||||
|
||||
suspend fun typing() = message.channel.type()
|
||||
|
||||
fun parseArguments(): MutableList<String>? =
|
||||
if (prefix != null && invokedWith != null) {
|
||||
message.content
|
||||
.substringAfter("$prefix$invokedWith")
|
||||
.trim()
|
||||
.split(' ')
|
||||
.toMutableList()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1,12 @@
|
|||
package io.github.null2264.tsukumogami.core.commands
|
||||
|
||||
import io.github.null2264.tsukumogami.core.commands.converters.Converter
|
||||
|
||||
data class Argument<T : Any?>(
|
||||
val name: String,
|
||||
val converter: Converter<T>,
|
||||
) {
|
||||
init {
|
||||
converter.argumentObj = this
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package io.github.null2264.tsukumogami.core.commands
|
||||
|
||||
import io.github.null2264.tsukumogami.core.Context
|
||||
import io.github.null2264.tsukumogami.core.commands.converters.Converter
|
||||
|
||||
abstract class Arguments {
|
||||
|
||||
val args = mutableListOf<Argument<*>>()
|
||||
|
||||
fun <R : Any> args(
|
||||
name: String,
|
||||
converter: Converter<R>
|
||||
): Converter<R> {
|
||||
args.add(Argument(name, converter))
|
||||
|
||||
return converter
|
||||
}
|
||||
|
||||
suspend fun parse(context: Context) {
|
||||
val currentValues = context.parseArguments()
|
||||
|
||||
run {
|
||||
args.forEach { arg ->
|
||||
val value = currentValues?.removeFirstOrNull() ?: return@run
|
||||
arg.converter.parse(context, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package io.github.null2264.tsukumogami.core.commands
|
||||
|
||||
import io.github.null2264.tsukumogami.core.Context
|
||||
import io.github.null2264.tsukumogami.core.module.BotModule
|
||||
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,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Return parent command if this command is a subcommand otherwise it returns null
|
||||
*/
|
||||
var parent: Command? = null
|
||||
|
||||
/**
|
||||
* The module this command belong to
|
||||
*/
|
||||
var module: BotModule? = null
|
||||
get() {
|
||||
if (parent != null) {
|
||||
field = parent?.module
|
||||
}
|
||||
return field
|
||||
}
|
||||
|
||||
open suspend fun invoke(context: Context) {
|
||||
val parsedArguments = arguments.call()
|
||||
parsedArguments.parse(context)
|
||||
handler.invoke(context, parsedArguments)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
package io.github.null2264.tsukumogami.core.commands
|
||||
|
||||
class EmptyArguments : Arguments()
|
|
@ -0,0 +1,38 @@
|
|||
package io.github.null2264.tsukumogami.core.commands
|
||||
|
||||
import io.github.null2264.tsukumogami.core.Context
|
||||
import io.github.null2264.tsukumogami.core.annotation.TsukumogamiInternalApi
|
||||
|
||||
@OptIn(TsukumogamiInternalApi::class)
|
||||
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 fun _addCommand(command: Command) {
|
||||
command.parent = this
|
||||
super._addCommand(command)
|
||||
}
|
||||
|
||||
override suspend fun invoke(context: Context) {
|
||||
val subcommandName = context.message.content
|
||||
.substringAfter("${context.prefix}${context.invokedWith}")
|
||||
.trim()
|
||||
.substringBefore(' ')
|
||||
val command = allCommands[subcommandName] ?: return
|
||||
context.invokedWith += " $subcommandName"
|
||||
context.command = command
|
||||
command.invoke(context)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package io.github.null2264.tsukumogami.core.commands
|
||||
|
||||
import io.github.null2264.tsukumogami.core.Context
|
||||
import io.github.null2264.tsukumogami.core.annotation.TsukumogamiInternalApi
|
||||
import kotlin.reflect.KFunction
|
||||
|
||||
@OptIn(TsukumogamiInternalApi::class)
|
||||
interface IGroup {
|
||||
|
||||
val allCommands: MutableMap<String, Command>
|
||||
|
||||
@TsukumogamiInternalApi
|
||||
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)
|
||||
declaration(group)
|
||||
_addCommand(group)
|
||||
}
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
package io.github.null2264.tsukumogami.core.module.annotation
|
||||
|
||||
package io.github.null2264.tsukumogami.core.commands.annotation
|
||||
|
||||
/**
|
||||
* Annotation to tag a function as command
|
|
@ -0,0 +1,24 @@
|
|||
package io.github.null2264.tsukumogami.core.commands.converters
|
||||
|
||||
import io.github.null2264.tsukumogami.core.Context
|
||||
import io.github.null2264.tsukumogami.core.commands.Argument
|
||||
import io.github.null2264.tsukumogami.core.commands.Arguments
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
abstract class Converter<OutputType: Any?> {
|
||||
|
||||
lateinit var argumentObj: Argument<OutputType>
|
||||
|
||||
abstract var parsed: OutputType
|
||||
|
||||
abstract suspend fun parse(context: Context, input: String): OutputType
|
||||
|
||||
operator fun getValue(thisRef: Arguments, property: KProperty<*>): OutputType {
|
||||
return this.parsed
|
||||
}
|
||||
|
||||
fun default(defaultValue: OutputType): OutputType {
|
||||
this.parsed = defaultValue
|
||||
return this.parsed
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package io.github.null2264.tsukumogami.core.commands.converters.impl
|
||||
|
||||
import io.github.null2264.tsukumogami.core.Context
|
||||
import io.github.null2264.tsukumogami.core.commands.converters.Converter
|
||||
|
||||
class StringConverter : Converter<String>() {
|
||||
|
||||
override var parsed: String = ""
|
||||
|
||||
override suspend fun parse(context: Context, input: String): String {
|
||||
this.parsed = input
|
||||
return input
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package io.github.null2264.tsukumogami.core.commands.converters.impl
|
||||
|
||||
import dev.kord.common.entity.Snowflake
|
||||
import dev.kord.core.entity.User
|
||||
import io.github.null2264.tsukumogami.core.Context
|
||||
import io.github.null2264.tsukumogami.core.annotation.TsukumogamiInternalApi
|
||||
import io.github.null2264.tsukumogami.core.commands.converters.Converter
|
||||
import io.github.null2264.tsukumogami.core.exceptions.CommandException
|
||||
import io.github.null2264.tsukumogami.core.ext.users
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
|
||||
class UserConverter : Converter<User>() {
|
||||
|
||||
override lateinit var parsed: User
|
||||
|
||||
override suspend fun parse(context: Context, input: String): User {
|
||||
if (input.equals("me", true)) {
|
||||
val user = context.author
|
||||
if (user != null) {
|
||||
this.parsed = user
|
||||
return this.parsed
|
||||
}
|
||||
}
|
||||
if (input.equals("you", true)) {
|
||||
this.parsed = context.bot.client.getSelf()
|
||||
return this.parsed
|
||||
}
|
||||
|
||||
this.parsed = context.findUser(input) ?:
|
||||
throw CommandException("User not found")
|
||||
|
||||
return this.parsed
|
||||
}
|
||||
|
||||
private suspend fun Context.findUser(arg: String): User? =
|
||||
if (arg.startsWith("<@") && arg.endsWith(">")) {
|
||||
val id: String = arg.substring(2, arg.length - 1).replace("!", "")
|
||||
|
||||
try {
|
||||
bot.client.getUser(Snowflake(id))
|
||||
} catch (_: NumberFormatException) {
|
||||
throw CommandException("Invalid user ID")
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
bot.client.getUser(Snowflake(arg))
|
||||
} catch (_: NumberFormatException) {
|
||||
if (!arg.contains("#")) {
|
||||
null
|
||||
} else {
|
||||
bot.client.users.firstOrNull { user ->
|
||||
user.tag.equals(arg, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package io.github.null2264.tsukumogami.core.commands.ext
|
||||
|
||||
import dev.kord.core.entity.User
|
||||
import io.github.null2264.tsukumogami.core.commands.Arguments
|
||||
import io.github.null2264.tsukumogami.core.commands.converters.Converter
|
||||
import io.github.null2264.tsukumogami.core.commands.converters.impl.StringConverter
|
||||
import io.github.null2264.tsukumogami.core.commands.converters.impl.UserConverter
|
||||
|
||||
fun Arguments.string(name: String, declaration: Converter<String>.() -> Unit = {}): Converter<String> {
|
||||
val converter = StringConverter()
|
||||
declaration(converter)
|
||||
return args(name, converter)
|
||||
}
|
||||
|
||||
fun Arguments.user(name: String, declaration: Converter<User>.() -> Unit = {}): Converter<User> {
|
||||
val converter = UserConverter()
|
||||
declaration(converter)
|
||||
return args(name, converter)
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package io.github.null2264.tsukumogami.core.ext
|
||||
|
||||
import io.github.null2264.tsukumogami.core.koin.TsukumogamiKoinContext
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.dsl.ModuleDeclaration
|
||||
import org.koin.dsl.module
|
||||
|
||||
fun loadModule(
|
||||
createdAtStart: Boolean = false,
|
||||
moduleDeclaration: ModuleDeclaration,
|
||||
): Module {
|
||||
val moduleObj = module(createdAtStart, moduleDeclaration)
|
||||
|
||||
TsukumogamiKoinContext.loadKoinModules(moduleObj)
|
||||
|
||||
return moduleObj
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package io.github.null2264.tsukumogami.core.ext
|
||||
|
||||
import dev.kord.core.Kord
|
||||
import dev.kord.core.supplier.EntitySupplyStrategy
|
||||
|
||||
val Kord.users get() = with(EntitySupplyStrategy.cache).users
|
|
@ -0,0 +1,56 @@
|
|||
package io.github.null2264.tsukumogami.core.ext
|
||||
|
||||
fun String.parseCommandAndArguments(): List<String> {
|
||||
if (isBlank()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val result = mutableListOf<String>()
|
||||
val currentToken = StringBuilder()
|
||||
var inQuotes = false
|
||||
|
||||
for (char in this) {
|
||||
when (char) {
|
||||
'"' -> {
|
||||
if (inQuotes) {
|
||||
// Closing quote: add the accumulated token
|
||||
result.add(currentToken.toString())
|
||||
currentToken.clear()
|
||||
inQuotes = false
|
||||
} else {
|
||||
// Opening quote:
|
||||
// If there's an existing token (e.g., word"another"), add it first
|
||||
if (currentToken.isNotEmpty()) {
|
||||
result.add(currentToken.toString())
|
||||
currentToken.clear()
|
||||
}
|
||||
inQuotes = true
|
||||
}
|
||||
}
|
||||
' ' -> {
|
||||
if (inQuotes) {
|
||||
// Space inside quotes: append it
|
||||
currentToken.append(char)
|
||||
} else {
|
||||
// Space outside quotes: separator
|
||||
if (currentToken.isNotEmpty()) {
|
||||
result.add(currentToken.toString())
|
||||
currentToken.clear()
|
||||
}
|
||||
// Ignore multiple spaces between tokens
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// Any other character: append it
|
||||
currentToken.append(char)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add any remaining token after the loop (e.g., if the string doesn't end with a quote or space)
|
||||
if (currentToken.isNotEmpty()) {
|
||||
result.add(currentToken.toString())
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package io.github.null2264.tsukumogami.core.koin
|
||||
|
||||
import org.koin.core.Koin
|
||||
import org.koin.core.component.KoinComponent
|
||||
|
||||
interface TsukumogamiKoinComponent : KoinComponent {
|
||||
override fun getKoin(): Koin = TsukumogamiKoinContext.get()
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
package io.github.null2264.tsukumogami.core.koin
|
||||
|
||||
import org.koin.core.Koin
|
||||
import org.koin.core.KoinApplication
|
||||
import org.koin.core.context.KoinContext
|
||||
import org.koin.core.error.KoinApplicationAlreadyStartedException
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.dsl.KoinAppDeclaration
|
||||
|
||||
/**
|
||||
* The [KoinContext] for Tsukumogami.
|
||||
*
|
||||
* To use this context, implement [TsukumogamiKoinComponent].
|
||||
*
|
||||
* @see org.koin.core.context.GlobalContext
|
||||
*/
|
||||
object TsukumogamiKoinContext : KoinContext {
|
||||
/** The current [Koin] instance. */
|
||||
private var koin: Koin? = null
|
||||
|
||||
/** The current [KoinApplication]. */
|
||||
private var koinApp: KoinApplication? = null
|
||||
|
||||
override fun get(): Koin = koin ?: error("KoinApplication has not been started")
|
||||
override fun getOrNull(): Koin? = koin
|
||||
public fun getKoinApplicationOrNull(): KoinApplication? = koinApp
|
||||
|
||||
private fun register(koinApplication: KoinApplication) {
|
||||
if (koin != null) {
|
||||
throw KoinApplicationAlreadyStartedException("KoinApplication has already been started")
|
||||
}
|
||||
|
||||
koinApp = koinApplication
|
||||
koin = koinApplication.koin
|
||||
}
|
||||
|
||||
override fun loadKoinModules(module: Module, createEagerInstances: Boolean) {
|
||||
get().loadModules(listOf(module), createEagerInstances = createEagerInstances)
|
||||
}
|
||||
|
||||
override fun loadKoinModules(modules: List<Module>, createEagerInstances: Boolean) {
|
||||
get().loadModules(modules, createEagerInstances = createEagerInstances)
|
||||
}
|
||||
|
||||
override fun startKoin(koinApplication: KoinApplication): KoinApplication = synchronized(this) {
|
||||
register(koinApplication)
|
||||
koinApplication.createEagerInstances()
|
||||
koinApplication.allowOverride(true)
|
||||
|
||||
return koinApplication
|
||||
}
|
||||
|
||||
override fun startKoin(appDeclaration: KoinAppDeclaration): KoinApplication = synchronized(this) {
|
||||
val koinApplication = KoinApplication.init()
|
||||
|
||||
register(koinApplication)
|
||||
appDeclaration(koinApplication)
|
||||
koinApplication.createEagerInstances()
|
||||
|
||||
return koinApplication
|
||||
}
|
||||
|
||||
override fun stopKoin() {
|
||||
koin?.close()
|
||||
koin = null
|
||||
}
|
||||
|
||||
override fun unloadKoinModules(module: Module) {
|
||||
get().unloadModules(listOf(module))
|
||||
}
|
||||
|
||||
override fun unloadKoinModules(modules: List<Module>) {
|
||||
get().unloadModules(modules)
|
||||
}
|
||||
}
|
|
@ -1,47 +1,27 @@
|
|||
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.module.annotation.Command as CommandAnnotation
|
||||
import io.github.null2264.tsukumogami.core.BotConfigurator
|
||||
import kotlin.reflect.jvm.isAccessible
|
||||
import kotlin.reflect.jvm.kotlinFunction
|
||||
import io.github.null2264.tsukumogami.core.Bot
|
||||
import io.github.null2264.tsukumogami.core.annotation.TsukumogamiInternalApi
|
||||
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) {
|
||||
@OptIn(TsukumogamiInternalApi::class)
|
||||
open class BotModule constructor(val name: String) : IGroup {
|
||||
|
||||
var bot: AbstractBot? = null
|
||||
internal set
|
||||
override val allCommands: MutableMap<String, Command> = mutableMapOf()
|
||||
|
||||
open fun setup() {}
|
||||
override fun _addCommand(command: Command) {
|
||||
command.module = this
|
||||
super._addCommand(command)
|
||||
}
|
||||
|
||||
internal fun install(bot: AbstractBot, configurator: BotConfigurator) {
|
||||
this.bot = bot
|
||||
internal fun install(bot: Bot): BotModule {
|
||||
allCommands.values.distinctBy { it.name }.forEach(bot::addCommand)
|
||||
return this
|
||||
}
|
||||
|
||||
val methods = this::class.java.declaredMethods
|
||||
for (method in methods) {
|
||||
for (annotation in method.annotations) {
|
||||
if (annotation !is CommandAnnotation)
|
||||
continue
|
||||
|
||||
configurator.apply {
|
||||
val kMethod = method.kotlinFunction
|
||||
if (isExists(kMethod?.name)) {
|
||||
Logger.e { "Command already exists" }
|
||||
return
|
||||
}
|
||||
kMethod?.let {
|
||||
it.isAccessible = true
|
||||
commands(
|
||||
Command(
|
||||
annotation.name.ifEmpty { it.name },
|
||||
name,
|
||||
it,
|
||||
annotation.description.ifEmpty { description },
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
internal fun uninstall(bot: Bot): BotModule {
|
||||
allCommands.values.distinctBy { it.name }.forEach(bot::removeCommand)
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
package io.github.null2264.tsukumogami.core.module
|
||||
|
||||
import kotlin.reflect.KFunction
|
||||
|
||||
/**
|
||||
* Class holding information about a command
|
||||
*/
|
||||
data class Command(
|
||||
val name: String,
|
||||
val extension: String,
|
||||
val callback: KFunction<*>,
|
||||
val description: String? = null,
|
||||
)
|
|
@ -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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue