Compare commits

...

10 commits

30 changed files with 744 additions and 243 deletions

View file

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

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.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()
// }
//}

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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
}