diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/commands/application/slash/annotations/FileTypes.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/commands/application/slash/annotations/FileTypes.kt new file mode 100644 index 000000000..425350e13 --- /dev/null +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/commands/application/slash/annotations/FileTypes.kt @@ -0,0 +1,27 @@ +package io.github.freya022.botcommands.api.commands.application.slash.annotations + +/** + * Sets the desired file types accepted by an [Attachment][net.dv8tion.jda.api.entities.Message.Attachment] [@SlashOption][SlashOption]. + * + * @see net.dv8tion.jda.api.interactions.FileType FileType + */ +@Target(AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.RUNTIME) +annotation class FileTypes( + /** + * Extensions to filter for. + */ + vararg val extensions: String, + /** + * Accepts any kind of image file supported by the Discord client. + */ + val image: Boolean = false, + /** + * Accepts any kind of video file supported by the Discord client. + */ + val video: Boolean = false, + /** + * Accepts any kind of audio file supported by the Discord client. + */ + val audio: Boolean = false, +) diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/commands/application/slash/options/SlashCommandOption.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/commands/application/slash/options/SlashCommandOption.kt index 7908389fc..1b74714a1 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/commands/application/slash/options/SlashCommandOption.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/commands/application/slash/options/SlashCommandOption.kt @@ -4,6 +4,7 @@ import io.github.freya022.botcommands.api.commands.application.LengthRange import io.github.freya022.botcommands.api.commands.application.ValueRange import io.github.freya022.botcommands.api.commands.application.options.ApplicationCommandOption import io.github.freya022.botcommands.api.parameters.resolvers.SlashParameterResolver +import net.dv8tion.jda.api.interactions.FileType import net.dv8tion.jda.api.interactions.commands.Command import net.dv8tion.jda.api.interactions.commands.OptionType @@ -55,6 +56,11 @@ interface SlashCommandOption : ApplicationCommandOption { */ val length: LengthRange? + /** + * The file types this [Attachment][net.dv8tion.jda.api.entities.Message.Attachment] option is accepting, if it is one. + */ + val fileTypes: List + /** * Whether this option uses autocomplete */ @@ -66,4 +72,4 @@ interface SlashCommandOption : ApplicationCommandOption { * @throws IllegalStateException If this option has [no autocomplete][hasAutocomplete] */ fun invalidateAutocomplete() -} \ No newline at end of file +} diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/commands/application/slash/options/builder/SlashCommandOptionBuilder.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/commands/application/slash/options/builder/SlashCommandOptionBuilder.kt index 8106a5dac..ad25de616 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/commands/application/slash/options/builder/SlashCommandOptionBuilder.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/commands/application/slash/options/builder/SlashCommandOptionBuilder.kt @@ -13,6 +13,8 @@ import io.github.freya022.botcommands.api.commands.application.slash.autocomplet import io.github.freya022.botcommands.api.core.config.BApplicationConfigBuilder import io.github.freya022.botcommands.api.parameters.resolvers.SlashParameterResolver import io.github.freya022.botcommands.internal.core.annotations.SkipJavaReflectionOverload +import net.dv8tion.jda.api.interactions.FileType +import net.dv8tion.jda.api.interactions.IFilterableFileTypes import net.dv8tion.jda.api.interactions.commands.Command.Choice import net.dv8tion.jda.api.interactions.commands.build.OptionData import net.dv8tion.jda.api.interactions.commands.localization.LocalizationFunction @@ -85,6 +87,74 @@ interface SlashCommandOptionBuilder : ApplicationCommandOptionBuilder { */ var lengthRange: LengthRange? + /** + * The file types this [Attachment][net.dv8tion.jda.api.entities.Message.Attachment] option is accepting, if it is one; + * up to [MAX_FILE_TYPES][IFilterableFileTypes.MAX_FILE_TYPES]. + * + * @see FileType + */ + val fileTypes: FileTypeAccumulator + + /** + * Adds up to [MAX_FILE_TYPES][IFilterableFileTypes.MAX_FILE_TYPES] file extensions to filter for. + * + * @param extensions The extensions, up to [MAX_FILE_TYPES][IFilterableFileTypes.MAX_FILE_TYPES] + * + * @throws IllegalArgumentException There are more than [MAX_FILE_TYPES][IFilterableFileTypes.MAX_FILE_TYPES] extensions, + * or, an extension is empty or isn't alphanumeric + * + * @see FileType + */ + fun addFileTypeExtensions(extensions: List) { + fileTypes += extensions + } + + /** + * Adds up to [MAX_FILE_TYPES][IFilterableFileTypes.MAX_FILE_TYPES] file extensions to filter for. + * + * @param extensions The extensions, up to [MAX_FILE_TYPES][IFilterableFileTypes.MAX_FILE_TYPES] + * + * @throws IllegalArgumentException There are more than [MAX_FILE_TYPES][IFilterableFileTypes.MAX_FILE_TYPES] extensions, + * or, an extension is empty or isn't alphanumeric + * + * @see FileType + */ + fun addFileTypeExtensions(vararg extensions: String) { + fileTypes += extensions.asList() + } + + /** + * Sets up to [MAX_FILE_TYPES][IFilterableFileTypes.MAX_FILE_TYPES] file extensions to filter for. + * Leave the arguments empty to remove file type filtering. + * + * @param extensions The extensions, up to [MAX_FILE_TYPES][IFilterableFileTypes.MAX_FILE_TYPES] + * + * @throws IllegalArgumentException There are more than [MAX_FILE_TYPES][IFilterableFileTypes.MAX_FILE_TYPES] extensions, + * or, an extension is empty or isn't alphanumeric + * + * @see FileType + */ + fun setFileTypeExtensions(extensions: List) { + fileTypes.clear() + fileTypes += extensions + } + + /** + * Sets up to [MAX_FILE_TYPES][IFilterableFileTypes.MAX_FILE_TYPES] file extensions to filter for. + * Leave the arguments empty to remove file type filtering. + * + * @param extensions The extensions, up to [MAX_FILE_TYPES][IFilterableFileTypes.MAX_FILE_TYPES] + * + * @throws IllegalArgumentException There are more than [MAX_FILE_TYPES][IFilterableFileTypes.MAX_FILE_TYPES] extensions, + * or, an extension is empty or isn't alphanumeric + * + * @see FileType + */ + fun setFileTypeExtensions(vararg extensions: String) { + fileTypes.clear() + fileTypes += extensions.asList() + } + /** * Uses an existing autocomplete handler with the specified [name][AutocompleteHandler.name]. * @@ -103,4 +173,37 @@ interface SlashCommandOptionBuilder : ApplicationCommandOptionBuilder { */ @SkipJavaReflectionOverload fun autocompleteByFunction(function: KFunction>) + + class FileTypeAccumulator internal constructor() { + private val _fileTypes = arrayListOf() + internal val list: List get() = _fileTypes + + @JvmName("plusAssignExtensions") + operator fun plusAssign(extensions: Collection) { + require(list.size + extensions.size <= OptionData.MAX_FILE_TYPES) { + "Cannot filter with more than ${OptionData.MAX_FILE_TYPES} file types" + } + _fileTypes += extensions.map(FileType::ofExtension) + } + + operator fun plusAssign(extension: String) { + this += listOf(extension) + } + + @JvmName("plusAssignFileTypes") + operator fun plusAssign(extensions: Collection) { + require(list.size + extensions.size <= OptionData.MAX_FILE_TYPES) { + "Cannot filter with more than ${OptionData.MAX_FILE_TYPES} file types" + } + _fileTypes += extensions + } + + operator fun plusAssign(extension: FileType) { + this += listOf(extension) + } + + fun clear() { + _fileTypes.clear() + } + } } diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/autobuilder/SlashCommandAutoBuilder.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/autobuilder/SlashCommandAutoBuilder.kt index f09e84df9..de571dfdc 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/autobuilder/SlashCommandAutoBuilder.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/autobuilder/SlashCommandAutoBuilder.kt @@ -41,6 +41,7 @@ import io.github.freya022.botcommands.internal.parameters.ResolverContainer import io.github.freya022.botcommands.internal.utils.* import io.github.freya022.botcommands.internal.utils.ReflectionUtils.nonInstanceParameters import net.dv8tion.jda.api.entities.Guild +import net.dv8tion.jda.api.interactions.FileType import net.dv8tion.jda.api.interactions.InteractionContextType import net.dv8tion.jda.api.interactions.commands.Command as JDACommand import kotlin.reflect.KClass @@ -362,6 +363,16 @@ internal class SlashCommandAutoBuilder( parameter.findAnnotation()?.let { range -> valueRange = ValueRange.ofDouble(range.from, range.to) } parameter.findAnnotation()?.let { length -> lengthRange = LengthRange.of(length.min, length.max) } + parameter.findAnnotation()?.let { fileTypes -> + if (fileTypes.image) + this.fileTypes += FileType.IMAGE + if (fileTypes.video) + this.fileTypes += FileType.VIDEO + if (fileTypes.audio) + this.fileTypes += FileType.AUDIO + this.fileTypes += fileTypes.extensions.asList() + } + processAutocomplete(optionAnnotation) usePredefinedChoices = optionAnnotation.usePredefinedChoices diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/SlashUtils.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/SlashUtils.kt index 5cb6ca2be..7759461d1 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/SlashUtils.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/SlashUtils.kt @@ -138,6 +138,8 @@ internal object SlashUtils { } } + data.setFileTypes(option.fileTypes) + data.isRequired = option.isRequired } } diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/options/SlashCommandOptionImpl.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/options/SlashCommandOptionImpl.kt index a231c207b..a6948b0e3 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/options/SlashCommandOptionImpl.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/options/SlashCommandOptionImpl.kt @@ -9,6 +9,7 @@ import io.github.freya022.botcommands.internal.commands.application.slash.autoco import io.github.freya022.botcommands.internal.commands.application.slash.options.builder.SlashCommandOptionBuilderImpl import io.github.freya022.botcommands.internal.utils.LocalizationUtils import io.github.freya022.botcommands.internal.utils.classRef +import net.dv8tion.jda.api.interactions.FileType import net.dv8tion.jda.api.interactions.commands.Command import net.dv8tion.jda.api.interactions.commands.OptionType @@ -35,6 +36,7 @@ internal class SlashCommandOptionImpl internal constructor( override val choices: List? = optionBuilder.choices override val range: ValueRange? = optionBuilder.valueRange override val length: LengthRange? = optionBuilder.lengthRange + override val fileTypes: List = optionBuilder.fileTypes.list init { choices?.forEach { @@ -68,4 +70,4 @@ internal class SlashCommandOptionImpl internal constructor( } autocompleteHandler!!.invalidate() } -} \ No newline at end of file +} diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/options/builder/SlashCommandOptionBuilderImpl.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/options/builder/SlashCommandOptionBuilderImpl.kt index f3651bc7f..7a55ed97f 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/options/builder/SlashCommandOptionBuilderImpl.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/options/builder/SlashCommandOptionBuilderImpl.kt @@ -12,7 +12,9 @@ import io.github.freya022.botcommands.internal.commands.application.slash.builde import io.github.freya022.botcommands.internal.parameters.OptionParameter import io.github.freya022.botcommands.internal.utils.shortSignatureNoSrc import io.github.freya022.botcommands.internal.utils.throwArgument +import net.dv8tion.jda.api.interactions.FileType import net.dv8tion.jda.api.interactions.commands.Command +import net.dv8tion.jda.api.interactions.commands.build.OptionData import kotlin.reflect.KFunction internal class SlashCommandOptionBuilderImpl internal constructor( @@ -52,6 +54,8 @@ internal class SlashCommandOptionBuilderImpl internal constructor( override var lengthRange: LengthRange? = null + override val fileTypes = SlashCommandOptionBuilder.FileTypeAccumulator() + internal var autocompleteInfo: AutocompleteInfoImpl? = null private set @@ -63,4 +67,4 @@ internal class SlashCommandOptionBuilderImpl internal constructor( autocompleteInfo = context.getService()[function] ?: throwArgument("No autocomplete handler declared from: ${function.shortSignatureNoSrc}") } -} \ No newline at end of file +} diff --git a/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/AttachmentUpload.kt b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/AttachmentUpload.kt index bdd8d83e5..8e48ac65d 100644 --- a/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/AttachmentUpload.kt +++ b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/AttachmentUpload.kt @@ -2,6 +2,8 @@ package dev.freya02.botcommands.jda.ktx.components import net.dv8tion.jda.api.components.Component import net.dv8tion.jda.api.components.attachmentupload.AttachmentUpload +import net.dv8tion.jda.api.interactions.FileType +import net.dv8tion.jda.api.interactions.IFilterableFileTypes class InlineAttachmentUpload( val builder: AttachmentUpload.Builder, @@ -48,26 +50,119 @@ class InlineAttachmentUpload( builder.setMaxValues(value) } + /** + * The file types this attachment upload is accepting, up to [MAX_FILE_TYPES][IFilterableFileTypes.MAX_FILE_TYPES]. + * + * @see FileType + */ + val fileTypes = FileTypeAccumulator() + + /** + * Adds up to [MAX_FILE_TYPES][IFilterableFileTypes.MAX_FILE_TYPES] file extensions to filter for. + * + * @param extensions The extensions, up to [MAX_FILE_TYPES][IFilterableFileTypes.MAX_FILE_TYPES] + * + * @throws IllegalArgumentException There are more than [MAX_FILE_TYPES][IFilterableFileTypes.MAX_FILE_TYPES] extensions, + * or, an extension is empty or isn't alphanumeric + * + * @see FileType + */ + fun addFileTypeExtensions(extensions: List) { + fileTypes += extensions + } + + /** + * Adds up to [MAX_FILE_TYPES][IFilterableFileTypes.MAX_FILE_TYPES] file extensions to filter for. + * + * @param extensions The extensions, up to [MAX_FILE_TYPES][IFilterableFileTypes.MAX_FILE_TYPES] + * + * @throws IllegalArgumentException There are more than [MAX_FILE_TYPES][IFilterableFileTypes.MAX_FILE_TYPES] extensions, + * or, an extension is empty or isn't alphanumeric + * + * @see FileType + */ + fun addFileTypeExtensions(vararg extensions: String) { + fileTypes += extensions.asList() + } + + /** + * Sets up to [MAX_FILE_TYPES][IFilterableFileTypes.MAX_FILE_TYPES] file extensions to filter for. + * Leave the arguments empty to remove file type filtering. + * + * @param extensions The extensions, up to [MAX_FILE_TYPES][IFilterableFileTypes.MAX_FILE_TYPES] + * + * @throws IllegalArgumentException There are more than [MAX_FILE_TYPES][IFilterableFileTypes.MAX_FILE_TYPES] extensions, + * or, an extension is empty or isn't alphanumeric + * + * @see FileType + */ + fun setFileTypeExtensions(extensions: List) { + fileTypes.clear() + fileTypes += extensions + } + + /** + * Sets up to [MAX_FILE_TYPES][IFilterableFileTypes.MAX_FILE_TYPES] file extensions to filter for. + * Leave the arguments empty to remove file type filtering. + * + * @param extensions The extensions, up to [MAX_FILE_TYPES][IFilterableFileTypes.MAX_FILE_TYPES] + * + * @throws IllegalArgumentException There are more than [MAX_FILE_TYPES][IFilterableFileTypes.MAX_FILE_TYPES] extensions, + * or, an extension is empty or isn't alphanumeric + * + * @see FileType + */ + fun setFileTypeExtensions(vararg extensions: String) { + fileTypes.clear() + fileTypes += extensions.asList() + } + /** See [AttachmentUpload.Builder.build] */ fun build(): AttachmentUpload { return builder.build() } + + inner class FileTypeAccumulator { + @JvmName("plusAssignExtensions") + operator fun plusAssign(extensions: Collection) { + builder.addFileTypeExtensions(extensions) + } + + operator fun plusAssign(extension: String) { + builder.addFileTypeExtensions(extension) + } + + @JvmName("plusAssignFileTypes") + operator fun plusAssign(extensions: Collection) { + builder.addFileTypes(extensions) + } + + operator fun plusAssign(extension: FileType) { + builder.addFileTypes(extension) + } + + fun clear() { + builder.setFileTypes() + } + } } /** * Component accepting files from users, see [AttachmentUpload][net.dv8tion.jda.api.components.attachmentupload.AttachmentUpload]. * - * @param customId The custom ID of the input, see [AttachmentUpload.Builder.setCustomId] - * @param uniqueId Unique identifier of this component, see [Component.withUniqueId] - * @param required Whether the user must upload files, see [AttachmentUpload.Builder.setRequired] - * @param range Minimum and maximum amount of files a user can send, see [AttachmentUpload.Builder.setRequiredRange] - * @param block Lambda allowing further configuration + * @param customId The custom ID of the input, see [AttachmentUpload.Builder.setCustomId] + * @param uniqueId Unique identifier of this component, see [Component.withUniqueId] + * @param required Whether the user must upload files, see [AttachmentUpload.Builder.setRequired] + * @param range Minimum and maximum amount of files a user can send, see [AttachmentUpload.Builder.setRequiredRange] + * @param fileTypes The file types to accept, up to [MAX_FILE_TYPES][IFilterableFileTypes.MAX_FILE_TYPES] + * @param block Lambda allowing further configuration */ inline fun AttachmentUpload( customId: String, uniqueId: Int = -1, required: Boolean = true, range: IntRange? = null, + fileTypes: Collection = emptyList(), block: InlineAttachmentUpload.() -> Unit = {}, ): AttachmentUpload { return AttachmentUpload.create(customId) @@ -79,6 +174,7 @@ inline fun AttachmentUpload( this.required = false if (range != null) this.range = range + this.fileTypes += fileTypes block() } .build() diff --git a/buildSrc/src/main/kotlin/repositories-conventions.gradle.kts b/buildSrc/src/main/kotlin/repositories-conventions.gradle.kts index e69b970fb..bcec738b0 100644 --- a/buildSrc/src/main/kotlin/repositories-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/repositories-conventions.gradle.kts @@ -1,4 +1,14 @@ repositories { mavenCentral() mavenLocal() + + exclusiveContent { + forRepository { + maven("https://jitpack.io") + } + + filter { + includeModule("io.github.freya022", "JDA") + } + } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f80a97ab1..d48b53b8e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -54,7 +54,7 @@ jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", ver jackson-dataformat-yaml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml", version.ref = "jackson" } jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson" } java-string-similarity = { module = "info.debatty:java-string-similarity", version.ref = "java-string-similarity" } -jda = { module = "net.dv8tion:JDA", version.ref = "jda" } +jda = "io.github.freya022:JDA:3a784805d2" jda-emojis = { module = "dev.freya02:jda-emojis", version.ref = "jda-emojis" } jemoji = { module = "net.fellbaum:jemoji", version.ref = "jemoji" } jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "jetbrains-annotations" } diff --git a/test-bot/src/test/kotlin/dev/freya02/botcommands/bot/commands/slash/SlashFileTypes.kt b/test-bot/src/test/kotlin/dev/freya02/botcommands/bot/commands/slash/SlashFileTypes.kt new file mode 100644 index 000000000..e29c62c8d --- /dev/null +++ b/test-bot/src/test/kotlin/dev/freya02/botcommands/bot/commands/slash/SlashFileTypes.kt @@ -0,0 +1,55 @@ +package dev.freya02.botcommands.bot.commands.slash + +import dev.freya02.botcommands.jda.ktx.components.AttachmentUpload +import dev.freya02.botcommands.jda.ktx.messages.reply_ +import io.github.freya022.botcommands.api.commands.annotations.Command +import io.github.freya022.botcommands.api.commands.application.provider.GlobalApplicationCommandManager +import io.github.freya022.botcommands.api.commands.application.provider.GlobalApplicationCommandProvider +import io.github.freya022.botcommands.api.commands.application.slash.GuildSlashEvent +import io.github.freya022.botcommands.api.core.utils.enumSetOf +import io.github.freya022.botcommands.api.modals.Modals +import io.github.freya022.botcommands.api.modals.create +import net.dv8tion.jda.api.entities.Message.Attachment +import net.dv8tion.jda.api.interactions.FileType +import net.dv8tion.jda.api.interactions.IntegrationType +import net.dv8tion.jda.api.interactions.InteractionContextType + +@Command +class SlashFileTypes( + private val modals: Modals +) : GlobalApplicationCommandProvider { + + fun onSlashFileTypesModal(event: GuildSlashEvent) { + event.replyModal(modals.create("File types") { + label("JSON") { + child = AttachmentUpload("json") { + fileTypes += "json" + } + } + + bindTo { modalEvent -> + val jsonAttachment = modalEvent.getValue("json")!!.asAttachmentList.first() + modalEvent.reply_("File size: ${jsonAttachment.size}", ephemeral = true).queue() + } + }).queue() + } + + fun onSlashFileTypesJson(event: GuildSlashEvent, jsonAttachment: Attachment) { + event.reply_("File size: ${jsonAttachment.size}", ephemeral = true).queue() + } + + override fun declareGlobalApplicationCommands(manager: GlobalApplicationCommandManager) { + manager.slashCommand("file_types", function = null) { + integrationTypes = enumSetOf(IntegrationType.USER_INSTALL) + contexts = enumSetOf(InteractionContextType.GUILD) + + subcommand("modal", ::onSlashFileTypesModal) + + subcommand("json", ::onSlashFileTypesJson) { + option("jsonAttachment", optionName = "json") { + fileTypes += "json" + } + } + } + } +}