
com.freya02.botcommands.internal.commands.application.autobuilder.SlashCommandAutoBuilder.kt Maven / Gradle / Ivy
package com.freya02.botcommands.internal.commands.application.autobuilder
import com.freya02.botcommands.api.commands.CommandPath
import com.freya02.botcommands.api.commands.annotations.Command
import com.freya02.botcommands.api.commands.annotations.GeneratedOption
import com.freya02.botcommands.api.commands.application.*
import com.freya02.botcommands.api.commands.application.annotations.AppOption
import com.freya02.botcommands.api.commands.application.annotations.CommandId
import com.freya02.botcommands.api.commands.application.slash.GlobalSlashEvent
import com.freya02.botcommands.api.commands.application.slash.annotations.*
import com.freya02.botcommands.api.commands.application.slash.annotations.LongRange
import com.freya02.botcommands.api.commands.application.slash.builder.SlashCommandBuilder
import com.freya02.botcommands.api.commands.application.slash.builder.SlashCommandOptionBuilder
import com.freya02.botcommands.api.commands.application.slash.builder.TopLevelSlashCommandBuilder
import com.freya02.botcommands.api.core.service.annotations.BService
import com.freya02.botcommands.api.parameters.ParameterType
import com.freya02.botcommands.internal.*
import com.freya02.botcommands.internal.commands.application.autobuilder.metadata.SlashFunctionMetadata
import com.freya02.botcommands.internal.commands.autobuilder.*
import com.freya02.botcommands.internal.core.requiredFilter
import com.freya02.botcommands.internal.utils.FunctionFilter
import com.freya02.botcommands.internal.utils.LocalizationUtils
import com.freya02.botcommands.internal.utils.ReflectionUtils.nonInstanceParameters
import net.dv8tion.jda.api.entities.Guild
import net.dv8tion.jda.api.entities.channel.ChannelType
import kotlin.reflect.KParameter
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.jvm.jvmErasure
@BService
internal class SlashCommandAutoBuilder(private val context: BContextImpl) {
private val functions: List =
context.instantiableServiceAnnotationsMap
.getInstantiableFunctionsWithAnnotation()
.requiredFilter(FunctionFilter.nonStatic())
.requiredFilter(FunctionFilter.firstArg(GlobalSlashEvent::class))
.map {
val func = it.function
val annotation = func.findAnnotation() ?: throwInternal("@JDASlashCommand should be present")
val path = CommandPath.of(annotation.name, annotation.group.nullIfEmpty(), annotation.subcommand.nullIfEmpty()).also { path ->
if (path.group != null && path.nameCount == 2) {
throwUser(func, "Slash commands with groups need to have their subcommand name set")
}
}
val commandId = func.findAnnotation()?.value
SlashFunctionMetadata(it, annotation, path, commandId)
}
fun declareGlobal(manager: GlobalApplicationCommandManager) {
val subcommands: MutableMap> = hashMapOf()
val subcommandGroups: MutableMap = hashMapOf()
fillSubcommandsAndGroups(subcommands, subcommandGroups)
functions
.distinctBy { it.path.name } //Subcommands are handled by processCommand, only retain one metadata per top-level name
.forEachWithDelayedExceptions {
val annotation = it.annotation
if (!manager.isValidScope(annotation.scope)) return@forEachWithDelayedExceptions
processCommand(manager, it, subcommands, subcommandGroups)
}
}
fun declareGuild(manager: GuildApplicationCommandManager) {
val subcommands: MutableMap> = hashMapOf()
val subcommandGroups: MutableMap = hashMapOf()
fillSubcommandsAndGroups(subcommands, subcommandGroups)
functions
.distinctBy { it.path.name } //Subcommands are handled by processCommand, only retain one metadata per top-level name
.forEachWithDelayedExceptions { metadata ->
val annotation = metadata.annotation
if (!manager.isValidScope(annotation.scope)) return@forEachWithDelayedExceptions
val instance = metadata.instance
val path = metadata.path
//TODO test
metadata.commandId?.also { id ->
if (!checkCommandId(manager, instance, id, path)) {
return
}
}
if (!checkTestCommand(manager, metadata.func, annotation.scope, context)) {
return
}
processCommand(manager, metadata, subcommands, subcommandGroups)
}
}
private fun fillSubcommandsAndGroups(
subcommands: MutableMap>,
subcommandGroups: MutableMap
) {
functions.forEachWithDelayedExceptions { metadata ->
when (metadata.path.nameCount) {
2 -> subcommands.computeIfAbsent(metadata.path.name) { arrayListOf() }.add(metadata)
3 -> subcommandGroups
.computeIfAbsent(metadata.path.name) { SlashSubcommandGroupMetadata(metadata.path.group!!, metadata.annotation.description) }
.subcommands
.computeIfAbsent(metadata.path.subname!!) { arrayListOf() }
.add(metadata)
}
}
}
private fun processCommand(
manager: AbstractApplicationCommandManager,
metadata: SlashFunctionMetadata,
subcommands: Map>,
subcommandGroups: Map
) {
val annotation = metadata.annotation
val instance = metadata.instance
val path = metadata.path
val commandId = metadata.commandId
val name = path.name
val subcommandsMetadata = subcommands[name]
val subcommandGroupsMetadata = subcommandGroups[name]
val isTopLevel = subcommandsMetadata == null && subcommandGroupsMetadata == null
manager.slashCommand(name, annotation.scope, if (isTopLevel) metadata.func.castFunction() else null) {
defaultLocked = annotation.defaultLocked
description = getEffectiveDescription(annotation)
subcommandsMetadata?.let { metadataList ->
metadataList.forEach { subMetadata ->
subcommand(subMetadata.path.subname!!, subMetadata.func.castFunction()) {
//TODO replace with #subcommandDescription in annotation
[email protected] = subMetadata.annotation.description
[email protected](subMetadata)
[email protected]((manager as? GuildApplicationCommandManager)?.guild, subMetadata, instance, commandId)
}
}
}
subcommandGroupsMetadata?.let { groupMetadata ->
subcommandGroup(groupMetadata.name) {
[email protected] = groupMetadata.description
groupMetadata.subcommands.forEach { (subname, metadataList) ->
metadataList.forEach { subMetadata ->
subcommand(subname, subMetadata.func.castFunction()) {
[email protected](subMetadata)
[email protected]((manager as? GuildApplicationCommandManager)?.guild, subMetadata, instance, commandId)
}
}
}
}
}
configureBuilder(metadata)
if (isTopLevel) {
processOptions((manager as? GuildApplicationCommandManager)?.guild, metadata, instance, commandId)
}
}
}
private fun SlashCommandBuilder.configureBuilder(metadata: SlashFunctionMetadata) {
fillCommandBuilder(metadata.func)
fillApplicationCommandBuilder(metadata.func, metadata.annotation)
}
private fun SlashCommandBuilder.processOptions(
guild: Guild?,
metadata: SlashFunctionMetadata,
instance: ApplicationCommand,
commandId: String?
) {
val func = metadata.func
val path = metadata.path
func.nonInstanceParameters.drop(1).forEach { kParameter ->
val declaredName = kParameter.findDeclarationName()
when (val optionAnnotation = kParameter.findAnnotation()) {
null -> when (kParameter.findAnnotation()) {
null -> customOption(declaredName)
else -> generatedOption(
declaredName, instance.getGeneratedValueSupplier(
guild,
commandId,
path,
kParameter.findOptionName().asDiscordString(),
ParameterType.ofType(kParameter.type)
)
)
}
else -> {
val optionName = optionAnnotation.name.nullIfEmpty() ?: declaredName.asDiscordString()
if (kParameter.type.jvmErasure.isValue) {
val inlineClassType = kParameter.type.jvmErasure.java
when (val varArgs = kParameter.findAnnotation()) {
null -> inlineClassOption(declaredName, optionName, inlineClassType) {
configureOption(guild, instance, kParameter, optionAnnotation)
}
else -> inlineClassOptionVararg(declaredName, inlineClassType, varArgs.value, varArgs.numRequired, { i -> "${optionName}_$i" }) {
configureOption(guild, instance, kParameter, optionAnnotation)
}
}
} else {
when (val varArgs = kParameter.findAnnotation()) {
null -> option(declaredName, optionName) {
configureOption(guild, instance, kParameter, optionAnnotation)
}
else -> optionVararg(declaredName, varArgs.value, varArgs.numRequired, { i -> "${optionName}_$i" }) {
configureOption(guild, instance, kParameter, optionAnnotation)
}
}
}
}
}
}
}
context(SlashCommandBuilder)
private fun SlashCommandOptionBuilder.configureOption(guild: Guild?, instance: ApplicationCommand, kParameter: KParameter, optionAnnotation: AppOption) {
description = getEffectiveDescription(optionAnnotation)
kParameter.findAnnotation()?.let { range -> valueRange = ValueRange.ofLong(range.from, range.to) }
kParameter.findAnnotation()?.let { range -> valueRange = ValueRange.ofDouble(range.from, range.to) }
kParameter.findAnnotation()?.let { length -> lengthRange = LengthRange.of(length.min, length.max) }
kParameter.findAnnotation()?.let { channelTypesAnnotation ->
channelTypes = enumSetOf().also { types ->
types += channelTypesAnnotation.value
}
}
processAutocomplete(optionAnnotation)
usePredefinedChoices = optionAnnotation.usePredefinedChoices
choices = instance.getOptionChoices(guild, [email protected], optionName)
}
private fun SlashCommandOptionBuilder.processAutocomplete(optionAnnotation: AppOption) {
if (optionAnnotation.autocomplete.isNotEmpty()) {
autocompleteReference(optionAnnotation.autocomplete)
}
}
context(TopLevelSlashCommandBuilder)
private fun getEffectiveDescription(annotation: JDASlashCommand): String {
val joinedPath = path.getFullPath('.')
val rootLocalization = LocalizationUtils.getCommandRootLocalization(context, "$joinedPath.description")
if (rootLocalization != null) return rootLocalization
return annotation.description
}
context(SlashCommandBuilder, SlashCommandOptionBuilder)
private fun getEffectiveDescription(optionAnnotation: AppOption): String {
val joinedPath = path.getFullPath('.')
val rootLocalization = LocalizationUtils.getCommandRootLocalization(context, "$joinedPath.options.${[email protected]}.description")
if (rootLocalization != null) return rootLocalization
return optionAnnotation.description.nullIfEmpty() ?: "No description"
}
private class SlashSubcommandGroupMetadata(val name: String, val description: String) {
val subcommands: MutableMap> = hashMapOf()
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy