init: Initial commit

This commit is contained in:
Ahmad Ansori Palembani 2025-05-19 19:21:45 +07:00
commit 0fa9a44640
25 changed files with 767 additions and 0 deletions

12
core/build.gradle.kts Normal file
View file

@ -0,0 +1,12 @@
plugins {
kotlin("jvm") version "2.0.0"
}
group = "io.github.null2264.tsukumogami.core"
dependencies {
api("dev.kord:kord-core:0.15.0")
api("io.insert-koin:koin-core:4.0.3")
api("co.touchlab:kermit:2.0.4")
api(kotlin("reflect"))
}

View file

@ -0,0 +1,105 @@
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(this, currentConfig)
extensions[module.name] = module
}
commands = currentConfig.commands
prefixes = currentConfig.prefixes
client = runBlocking {
Kord(currentConfig.token).apply {
on<ReadyEvent> {
Logger.i { "Online! ${self.username}" }
}
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)
}
suspend fun onCommandError(context: Context, error: CommandException) {
context.send(error.message!!)
}
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}'" }
}
}
suspend fun onMessage(message: Message, event: MessageCreateEvent) {
if (message.author?.isBot != false) return
processCommand(message)
}
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))
return@forEach
}
}
return ret
}
}

View file

@ -0,0 +1,39 @@
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

@ -0,0 +1,25 @@
package io.github.null2264.tsukumogami.core
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
class Context(private val bot: AbstractBot, private val message: Message, val prefix: String?, private val commandName: String?) {
val author get() = message.author
val command get() = commandName?.let { bot.getCommand(it) }
suspend fun send(content: String) = message.channel.createMessage(content)
suspend fun reply(content: String, mentionsAuthor: Boolean = false) = message.channel.createMessage {
this.content = content
messageReference = message.id
if (!mentionsAuthor)
allowedMentions {
AllowedMentionsBuilder().repliedUser
}
}
suspend fun typing() = message.channel.type()
}

View file

@ -0,0 +1,3 @@
package io.github.null2264.tsukumogami.core.exceptions
open class CommandException(message: String = "Something went wrong while executing the command") : Exception(message)

View file

@ -0,0 +1,3 @@
package io.github.null2264.tsukumogami.core.exceptions
class CommandNotFound : CommandException("Command not found")

View file

@ -0,0 +1,44 @@
package io.github.null2264.tsukumogami.core.module
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
abstract class BotModule(val name: String, val description: String? = null) {
var bot: AbstractBot? = null
internal set
internal fun setup(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 CommandAnnotation)
continue
configurator.apply {
val kMethod = method.kotlinFunction
if (isExists(kMethod?.name)) {
println("Command already exists")
return
}
kMethod?.let {
it.isAccessible = true
commands(
Command(
annotation.name.ifEmpty { it.name },
name,
it,
annotation.description.ifEmpty { description },
)
)
}
}
}
}
}
}

View file

@ -0,0 +1,13 @@
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,13 @@
package io.github.null2264.tsukumogami.core.module.annotation
/**
* Annotation to tag a function as command
*
* @param name the command's name
* @param description the command's description
* @param help the command's extended description (a more detailed description)
*/
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class Command(val name: String = "", val description: String = "", val help: String = "")