From ab1b48b5f648690e648261d9140a89872d587102 Mon Sep 17 00:00:00 2001 From: NoComment <67918617+NoComment1105@users.noreply.github.com> Date: Sat, 11 Nov 2023 14:26:40 +0000 Subject: [PATCH] Convert mappings extension to storage unit (#201) Co-authored-by: Gareth Coles --- .idea/kotlinc.xml | 2 +- extra-modules/extra-mappings/build.gradle.kts | 2 - .../extra/mappings/MappingsExtension.kt | 2217 +++++++++-------- .../arguments/MappingConversionArguments.kt | 37 +- .../mappings/builders/ExtMappingsBuilder.kt | 5 - .../configuration/MappingsConfigAdapter.kt | 39 - .../configuration/TomlMappingsConfig.kt | 62 - .../configuration/spec/CategoriesSpec.kt | 19 - .../configuration/spec/ChannelsSpec.kt | 19 - .../mappings/configuration/spec/GuildsSpec.kt | 19 - .../configuration/spec/SettingsSpec.kt | 20 - .../extra/mappings/storage/MappingsConfig.kt | 48 + .../translations/kordex/mappings.properties | 23 + .../kordex/mappings_en_GB.properties | 23 + gradle/libs.versions.toml | 3 - .../kord/extensions/testbot/TestBot.kt | 5 + 16 files changed, 1294 insertions(+), 1249 deletions(-) delete mode 100644 extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/configuration/MappingsConfigAdapter.kt delete mode 100644 extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/configuration/TomlMappingsConfig.kt delete mode 100644 extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/configuration/spec/CategoriesSpec.kt delete mode 100644 extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/configuration/spec/ChannelsSpec.kt delete mode 100644 extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/configuration/spec/GuildsSpec.kt delete mode 100644 extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/configuration/spec/SettingsSpec.kt create mode 100644 extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/storage/MappingsConfig.kt create mode 100644 extra-modules/extra-mappings/src/main/resources/translations/kordex/mappings.properties create mode 100644 extra-modules/extra-mappings/src/main/resources/translations/kordex/mappings_en_GB.properties diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index f8467b458e..1ee496d87b 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -3,4 +3,4 @@ - \ No newline at end of file + diff --git a/extra-modules/extra-mappings/build.gradle.kts b/extra-modules/extra-mappings/build.gradle.kts index e25ffda8de..272d59ca98 100644 --- a/extra-modules/extra-mappings/build.gradle.kts +++ b/extra-modules/extra-mappings/build.gradle.kts @@ -49,8 +49,6 @@ dependencies { detektPlugins(libs.detekt) detektPlugins(libs.detekt.libraries) - implementation(libs.konf.core) - implementation(libs.konf.toml) implementation(libs.bundles.logging) implementation(libs.kotlin.stdlib) diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/MappingsExtension.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/MappingsExtension.kt index c1c529e316..2330626e52 100644 --- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/MappingsExtension.kt +++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/MappingsExtension.kt @@ -4,23 +4,30 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -@file:Suppress("StringLiteralDuplication") +@file:Suppress("StringLiteralDuplication", "UnstableApiUsage") + @file:OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class) package com.kotlindiscord.kord.extensions.modules.extra.mappings +import com.kotlindiscord.kord.extensions.checks.anyGuild +import com.kotlindiscord.kord.extensions.checks.hasPermission import com.kotlindiscord.kord.extensions.checks.types.CheckContextWithCache -import com.kotlindiscord.kord.extensions.checks.types.SlashCommandCheck import com.kotlindiscord.kord.extensions.commands.Arguments import com.kotlindiscord.kord.extensions.commands.application.slash.PublicSlashCommandContext +import com.kotlindiscord.kord.extensions.commands.application.slash.ephemeralSubCommand import com.kotlindiscord.kord.extensions.commands.application.slash.publicSubCommand +import com.kotlindiscord.kord.extensions.commands.converters.impl.optionalInt +import com.kotlindiscord.kord.extensions.components.components +import com.kotlindiscord.kord.extensions.components.ephemeralStringSelectMenu import com.kotlindiscord.kord.extensions.components.forms.ModalForm import com.kotlindiscord.kord.extensions.extensions.Extension +import com.kotlindiscord.kord.extensions.extensions.ephemeralSlashCommand import com.kotlindiscord.kord.extensions.extensions.publicSlashCommand import com.kotlindiscord.kord.extensions.modules.extra.mappings.arguments.* import com.kotlindiscord.kord.extensions.modules.extra.mappings.builders.ExtMappingsBuilder import com.kotlindiscord.kord.extensions.modules.extra.mappings.enums.Channels -import com.kotlindiscord.kord.extensions.modules.extra.mappings.exceptions.UnsupportedNamespaceException +import com.kotlindiscord.kord.extensions.modules.extra.mappings.storage.MappingsConfig import com.kotlindiscord.kord.extensions.modules.extra.mappings.utils.* import com.kotlindiscord.kord.extensions.pagination.EXPAND_EMOJI import com.kotlindiscord.kord.extensions.pagination.PublicResponsePaginator @@ -28,10 +35,16 @@ import com.kotlindiscord.kord.extensions.pagination.pages.Page import com.kotlindiscord.kord.extensions.pagination.pages.Pages import com.kotlindiscord.kord.extensions.plugins.extra.MappingsPlugin import com.kotlindiscord.kord.extensions.sentry.BreadcrumbType +import com.kotlindiscord.kord.extensions.storage.StorageType +import com.kotlindiscord.kord.extensions.storage.StorageUnit +import dev.kord.common.entity.Permission +import dev.kord.common.entity.Snowflake +import dev.kord.core.behavior.GuildBehavior import dev.kord.core.event.interaction.ChatInputCommandInteractionCreateEvent import io.github.oshai.kotlinlogging.KotlinLogging import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.* import kotlinx.coroutines.newSingleThreadContext import kotlinx.coroutines.withContext import me.shedaniel.linkie.* @@ -50,1080 +63,1182 @@ private typealias InfoCommand = (suspend PublicSlashCommandContext() - val enabledNamespaces = builder.config.getEnabledNamespaces() - - enabledNamespaces.forEach { - when (it) { - "barn" -> namespaces.add(BarnNamespace) - "feather" -> namespaces.add(FeatherNamespace) - "legacy-yarn" -> namespaces.add(LegacyYarnNamespace) - "mcp" -> namespaces.add(McpNamespaceReplacement) - "mojang" -> namespaces.add(MojangNamespace) - "hashed-mojang" -> namespaces.add(MojangHashedNamespace) - "plasma" -> namespaces.add(PlasmaNamespace) - "quilt-mappings" -> namespaces.add(QuiltMappingsNamespace) - "srg-mojang" -> namespaces.add(MojangSrgNamespace) - "yarn" -> namespaces.add(YarnNamespace) - "yarrn" -> namespaces.add(YarrnNamespace) - - else -> throw UnsupportedNamespaceException(it) - } - } - - if (namespaces.isEmpty()) { - logger.warn { "No namespaces have been enabled, not registering commands." } - return - } - - Namespaces.init(LinkieConfig.DEFAULT.copy(namespaces = namespaces)) - - val barnEnabled = enabledNamespaces.contains("barn") - val featherEnabled = enabledNamespaces.contains("feather") - val legacyYarnEnabled = enabledNamespaces.contains("legacy-yarn") - val mcpEnabled = enabledNamespaces.contains("mcp") - val mojangEnabled = enabledNamespaces.contains("mojang") - val hashedMojangEnabled = enabledNamespaces.contains("hashed-mojang") - val plasmaEnabled = enabledNamespaces.contains("plasma") - val quiltMappingsEnabled = enabledNamespaces.contains("quilt-mappings") - val srgMojangEnabled = enabledNamespaces.contains("srg-mojang") - val yarnEnabled = enabledNamespaces.contains("yarn") - val yarrnEnabled = enabledNamespaces.contains("yarrn") - - val categoryCheck: SlashCommandCheck = { - allowedCategory(builder.config.getAllowedCategories(), builder.config.getBannedCategories()) - } - - val channelCheck: SlashCommandCheck = { - allowedGuild(builder.config.getAllowedChannels(), builder.config.getBannedChannels()) - } - - val guildCheck: SlashCommandCheck = { - allowedGuild(builder.config.getAllowedGuilds(), builder.config.getBannedGuilds()) - } - - val yarnChannels = Channels.entries.joinToString(", ") { "`${it.readableName}`" } - - suspend fun slashCommand( - parentName: String, - friendlyName: String, - namespace: Namespace, - arguments: () -> T, - customInfoCommand: InfoCommand = null, - ) = publicSlashCommand { - name = parentName - description = "Look up $friendlyName mappings." - - publicSubCommand(arguments) { - name = "class" - - description = "Look up $friendlyName mappings info for a class." - - check { customChecks(name, namespace) } - check(categoryCheck, channelCheck, guildCheck) - - action { - val channel = (this.arguments as? MappingWithChannelArguments)?.channel?.readableName - - queryMapping( - "class", - channel, - queryProvider = MappingsQuery::queryClasses, - pageGenerationMethod = classesToPages - ) - } - } - - publicSubCommand(arguments) { - name = "field" - - description = "Look up $friendlyName mappings info for a field." - - check { customChecks(name, namespace) } - check(categoryCheck, channelCheck, guildCheck) - - action { - val channel = (this.arguments as? MappingWithChannelArguments)?.channel?.readableName - - queryMapping( - "field", - channel, - queryProvider = MappingsQuery::queryFields, - pageGenerationMethod = ::fieldsToPages - ) - } - } - - publicSubCommand(arguments) { - name = "method" - - description = "Look up $friendlyName mappings info for a method." - - check { customChecks(name, namespace) } - check(categoryCheck, channelCheck, guildCheck) - - action { - val channel = (this.arguments as? MappingWithChannelArguments)?.channel?.readableName - - queryMapping( - "method", - channel, - queryProvider = MappingsQuery::queryMethods, - pageGenerationMethod = ::methodsToPages - ) - } - } - - publicSubCommand { - name = "info" - - description = "Get information for $friendlyName mappings." - - check { customChecks(name, namespace) } - check(categoryCheck, channelCheck, guildCheck) - - action( - customInfoCommand ?: { - val defaultVersion = namespace.defaultVersion - val allVersions = namespace.getAllSortedVersions() - - val pages = allVersions.chunked(VERSION_CHUNK_SIZE).map { - it.joinToString("\n") { version -> - if (version == defaultVersion) { - "**» $version** (Default)" - } else { - "**»** $version" - } - } - }.toMutableList() - - val versionSize = allVersions.size - pages.add( - 0, - "$friendlyName mappings are available for queries across **$versionSize** " + - "versions.\n\n" + - - "**Default version:** $defaultVersion\n" + - "**Commands:** `/$parentName class`, `/$parentName field`, `/$parentName method`\n\n" + - - "For a full list of supported $friendlyName versions, please view the rest of the " + - "pages." - ) - - val pagesObj = Pages() - val pageTitle = "Mappings info: $friendlyName" - - pages.forEach { - pagesObj.addPage( - Page { - description = it - title = pageTitle - - footer { - text = PAGE_FOOTER - icon = PAGE_FOOTER_ICON - } - } - ) - } - - val paginator = PublicResponsePaginator( - pages = pagesObj, - keepEmbed = true, - owner = event.interaction.user, - timeoutSeconds = getTimeout(), - locale = getLocale(), - interaction = interactionResponse - ) - - paginator.send() - } - ) - } - } + private val logger = KotlinLogging.logger { } + override val name: String = MappingsPlugin.PLUGIN_ID + override val bundle: String = "kordex.mappings" + + private val guildConfig = StorageUnit( + StorageType.Config, + "mappings", + "guild-config", + MappingsConfig::class + ) + + override suspend fun setup() { + // Fix issue where Linkie doesn't create its cache directory + val cacheDirectory = Path("./.linkie-cache") + if (!cacheDirectory.exists()) { + cacheDirectory.createDirectory() + } + + val yarnChannels = Channels.entries.joinToString(", ") { "`${it.readableName}`" } + + suspend fun slashCommand( + parentName: String, + friendlyName: String, + namespace: Namespace, + arguments: () -> T, + customInfoCommand: InfoCommand = null, + ) = publicSlashCommand { + name = parentName + description = "Look up $friendlyName mappings." + + publicSubCommand(arguments) { + name = "class" + + description = "Look up $friendlyName mappings info for a class." + + check { customChecks(name, namespace) } + + action { + val channel = (this.arguments as? MappingWithChannelArguments)?.channel?.readableName + + queryMapping( + "class", + channel, + queryProvider = MappingsQuery::queryClasses, + pageGenerationMethod = classesToPages + ) + } + } + + publicSubCommand(arguments) { + name = "field" + + description = "Look up $friendlyName mappings info for a field." + + check { customChecks(name, namespace) } + + action { + val channel = (this.arguments as? MappingWithChannelArguments)?.channel?.readableName + + queryMapping( + "field", + channel, + queryProvider = MappingsQuery::queryFields, + pageGenerationMethod = ::fieldsToPages + ) + } + } + + publicSubCommand(arguments) { + name = "method" + + description = "Look up $friendlyName mappings info for a method." + + check { customChecks(name, namespace) } + + action { + val channel = (this.arguments as? MappingWithChannelArguments)?.channel?.readableName + + queryMapping( + "method", + channel, + queryProvider = MappingsQuery::queryMethods, + pageGenerationMethod = ::methodsToPages + ) + } + } + + publicSubCommand { + name = "info" + + description = "Get information for $friendlyName mappings." + + check { customChecks(name, namespace) } + + action( + customInfoCommand ?: { + val defaultVersion = namespace.defaultVersion + val allVersions = namespace.getAllSortedVersions() + + val pages = allVersions.chunked(VERSION_CHUNK_SIZE).map { + it.joinToString("\n") { version -> + if (version == defaultVersion) { + "**» $version** (Default)" + } else { + "**»** $version" + } + } + }.toMutableList() + + val versionSize = allVersions.size + pages.add( + 0, + "$friendlyName mappings are available for queries across **$versionSize** " + + "versions.\n\n" + + + "**Default version:** $defaultVersion\n" + + "**Commands:** `/$parentName class`, `/$parentName field`, `/$parentName method`\n\n" + + + "For a full list of supported $friendlyName versions, please view the rest of the " + + "pages." + ) + + val pagesObj = Pages() + val pageTitle = "Mappings info: $friendlyName" + + pages.forEach { + pagesObj.addPage( + Page { + description = it + title = pageTitle + + footer { + text = PAGE_FOOTER + icon = PAGE_FOOTER_ICON + } + } + ) + } + + val paginator = PublicResponsePaginator( + pages = pagesObj, + keepEmbed = true, + owner = event.interaction.user, + timeoutSeconds = guild?.getTimeout(), + locale = getLocale(), + interaction = interactionResponse + ) + + paginator.send() + } + ) + } + } // region: Barn mappings lookups - if (barnEnabled) { - slashCommand( - "barn", - "Barn", - BarnNamespace, - ::BarnArguments + slashCommand( + "barn", + "Barn", + BarnNamespace, + ::BarnArguments + ) + + // endregion + + // region: Feather mappings lookups + + slashCommand( + "feather", + "Feather", + FeatherNamespace, + ::FeatherArguments + ) + + // endregion + + // region: Legacy Yarn mappings lookups + + slashCommand( + "lyarn", + "Legacy Yarn", + LegacyYarnNamespace, + ::LegacyYarnArguments + ) + + // endregion + + // region: MCP mappings lookups + + // Slash commands + slashCommand( + "mcp", + "MCP", + McpNamespaceReplacement, + ::MCPArguments + ) + + // endregion + + // region: Mojang mappings lookups + + slashCommand( + "mojang", + "Mojang", + MojangNamespace, + ::MojangArguments + ) { + val defaultVersion = MojangReleaseContainer.latestRelease + val allVersions = MojangNamespace.getAllSortedVersions() + + val pages = allVersions.chunked(VERSION_CHUNK_SIZE).map { + it.joinToString("\n") { version -> + if (version == defaultVersion) { + "**» $version** (Default)" + } else { + "**»** $version" + } + } + }.toMutableList() + + pages.add( + 0, + "Mojang mappings are available for queries across **${allVersions.size}** versions.\n\n" + + + "**Default version:** $defaultVersion\n\n" + + + "**Channels:** " + Channels.entries.joinToString(", ") { "`${it.readableName}`" } + + "\n" + + "**Commands:** `/mojang class`, `/mojang field`, `/mojang method`\n\n" + + + "For a full list of supported Mojang versions, please view the rest of the pages." ) + + val pagesObj = Pages() + val pageTitle = "Mappings info: Mojang" + + pages.forEach { + pagesObj.addPage( + Page { + description = it + title = pageTitle + + footer { + text = PAGE_FOOTER + icon = PAGE_FOOTER_ICON + } + } + ) + } + + val paginator = PublicResponsePaginator( + pages = pagesObj, + keepEmbed = true, + owner = event.interaction.user, + timeoutSeconds = guild?.getTimeout(), + locale = getLocale(), + interaction = interactionResponse + ) + + paginator.send() } // endregion - // region: Feather mappings lookups + // region: Hashed Mojang mappings lookups + + slashCommand( + "hashed", + "Hashed Mojang", + MojangHashedNamespace, + ::HashedMojangArguments + ) { + val defaultVersion = MojangReleaseContainer.latestRelease + val allVersions = MojangNamespace.getAllSortedVersions() + + val pages = allVersions.chunked(VERSION_CHUNK_SIZE).map { + it.joinToString("\n") { version -> + if (version == defaultVersion) { + "**» $version** (Default)" + } else { + "**»** $version" + } + } + }.toMutableList() + + pages.add( + 0, + "Hashed Mojang mappings are available for queries across **${allVersions.size}** versions.\n\n" + + + "**Default version:** $defaultVersion\n\n" + + + "**Channels:** " + Channels.entries.joinToString(", ") { "`${it.readableName}`" } + + "\n" + + "**Commands:** `/hashed class`, `/hashed field`, `/hashed method`\n\n" + + + "For a full list of supported hashed Mojang versions, please view the rest of the pages." + ) - if (featherEnabled) { - slashCommand( - "feather", - "Feather", - FeatherNamespace, - ::FeatherArguments + val pagesObj = Pages() + val pageTitle = "Mappings info: Hashed Mojang" + + pages.forEach { + pagesObj.addPage( + Page { + description = it + title = pageTitle + + footer { + text = PAGE_FOOTER + icon = PAGE_FOOTER_ICON + } + } + ) + } + + val paginator = PublicResponsePaginator( + pages = pagesObj, + keepEmbed = true, + owner = event.interaction.user, + timeoutSeconds = guild?.getTimeout(), + locale = getLocale(), + interaction = interactionResponse ) + + paginator.send() } // endregion - // region: Legacy Yarn mappings lookups - - if (legacyYarnEnabled) { - slashCommand( - "lyarn", - "Legacy Yarn", - LegacyYarnNamespace, - ::LegacyYarnArguments - ) - } - - // endregion - - // region: MCP mappings lookups - - if (mcpEnabled) { - // Slash commands - slashCommand( - "mcp", - "MCP", - McpNamespaceReplacement, - ::MCPArguments - ) - } - - // endregion - - // region: Mojang mappings lookups - - if (mojangEnabled) { - slashCommand( - "mojang", - "Mojang", - MojangNamespace, - ::MojangArguments - ) { - val defaultVersion = MojangReleaseContainer.latestRelease - val allVersions = MojangNamespace.getAllSortedVersions() - - val pages = allVersions.chunked(VERSION_CHUNK_SIZE).map { - it.joinToString("\n") { version -> - if (version == defaultVersion) { - "**» $version** (Default)" - } else { - "**»** $version" - } - } - }.toMutableList() - - pages.add( - 0, - "Mojang mappings are available for queries across **${allVersions.size}** versions.\n\n" + - - "**Default version:** $defaultVersion\n\n" + - - "**Channels:** " + Channels.entries.joinToString(", ") { "`${it.readableName}`" } + - "\n" + - "**Commands:** `/mojang class`, `/mojang field`, `/mojang method`\n\n" + - - "For a full list of supported Mojang versions, please view the rest of the pages." - ) - - val pagesObj = Pages() - val pageTitle = "Mappings info: Mojang" - - pages.forEach { - pagesObj.addPage( - Page { - description = it - title = pageTitle - - footer { - text = PAGE_FOOTER - icon = PAGE_FOOTER_ICON - } - } - ) - } - - val paginator = PublicResponsePaginator( - pages = pagesObj, - keepEmbed = true, - owner = event.interaction.user, - timeoutSeconds = getTimeout(), - locale = getLocale(), - interaction = interactionResponse - ) - - paginator.send() - } - } - - // endregion - - // region: Hashed Mojang mappings lookups - - if (hashedMojangEnabled) { - slashCommand( - "hashed", - "Hashed Mojang", - MojangHashedNamespace, - ::HashedMojangArguments - ) { - val defaultVersion = MojangReleaseContainer.latestRelease - val allVersions = MojangNamespace.getAllSortedVersions() - - val pages = allVersions.chunked(VERSION_CHUNK_SIZE).map { - it.joinToString("\n") { version -> - if (version == defaultVersion) { - "**» $version** (Default)" - } else { - "**»** $version" - } - } - }.toMutableList() - - pages.add( - 0, - "Hashed Mojang mappings are available for queries across **${allVersions.size}** versions.\n\n" + - - "**Default version:** $defaultVersion\n\n" + - - "**Channels:** " + Channels.entries.joinToString(", ") { "`${it.readableName}`" } + - "\n" + - "**Commands:** `/hashed class`, `/hashed field`, `/hashed method`\n\n" + - - "For a full list of supported hashed Mojang versions, please view the rest of the pages." - ) - - val pagesObj = Pages() - val pageTitle = "Mappings info: Hashed Mojang" - - pages.forEach { - pagesObj.addPage( - Page { - description = it - title = pageTitle - - footer { - text = PAGE_FOOTER - icon = PAGE_FOOTER_ICON - } - } - ) - } - - val paginator = PublicResponsePaginator( - pages = pagesObj, - keepEmbed = true, - owner = event.interaction.user, - timeoutSeconds = getTimeout(), - locale = getLocale(), - interaction = interactionResponse - ) - - paginator.send() - } - } - - // endregion - - // region: Plasma mappings lookups - - if (plasmaEnabled) { - slashCommand( - "plasma", - "Plasma", - PlasmaNamespace, - ::PlasmaArguments - ) - } - - // endregion - - // region: Quilt mappings lookups - - if (quiltMappingsEnabled) { - slashCommand( - "quilt", - "Quilt", - QuiltMappingsNamespace, - ::QuiltArguments - ) - } - - // endregion + // region: Plasma mappings lookups + + slashCommand( + "plasma", + "Plasma", + PlasmaNamespace, + ::PlasmaArguments + ) + + // endregion + + // region: Quilt mappings lookups + + slashCommand( + "quilt", + "Quilt", + QuiltMappingsNamespace, + ::QuiltArguments + ) + + // endregion // region: SRG Mojang mappings lookups - if (srgMojangEnabled) { - slashCommand( - "srg", - "SRG Mojang", - MojangSrgNamespace, - ::SrgMojangArguments + slashCommand( + "srg", + "SRG Mojang", + MojangSrgNamespace, + ::SrgMojangArguments + ) + + // endregion + + // region: Yarn mappings lookups + + slashCommand( + "yarn", + "Yarn", + YarnNamespace, + ::YarnArguments + ) { + val defaultVersion = YarnReleaseContainer.latestRelease + val defaultSnapshotVersion = YarnReleaseContainer.latestSnapshot + val allVersions = YarnNamespace.getAllSortedVersions() + + val pages = allVersions.chunked(VERSION_CHUNK_SIZE).map { + it.joinToString("\n") { version -> + when (version) { + defaultVersion -> "**» $version** (Default)" + defaultSnapshotVersion -> "**» $version** (Default: Snapshot)" + + else -> "**»** $version" + } + } + }.toMutableList() + + pages.add( + 0, + "Yarn mappings are available for queries across **${allVersions.size}** versions.\n\n" + + + "**Default version:** $defaultVersion\n" + + "**Default snapshot version:** $defaultSnapshotVersion\n\n" + + + "**Channels:** $yarnChannels\n" + + "**Commands:** `/yarn class`, `/yarn field`, `/yarn method`\n\n" + + + "For a full list of supported Yarn versions, please view the rest of the pages." + + + " For Legacy Yarn mappings, please see the `lyarn` command." + ) + + val pagesObj = Pages() + val pageTitle = "Mappings info: Yarn" + + pages.forEach { + pagesObj.addPage( + Page { + description = it + title = pageTitle + + footer { + text = PAGE_FOOTER + icon = PAGE_FOOTER_ICON + } + } + ) + } + + val paginator = PublicResponsePaginator( + pages = pagesObj, + keepEmbed = true, + owner = event.interaction.user, + timeoutSeconds = guild?.getTimeout(), + locale = getLocale(), + interaction = interactionResponse ) + + paginator.send() + } + + // endregion + + // region: Yarrn mappings lookups + + slashCommand( + "yarrn", + "Yarrn", + YarrnNamespace, + ::YarrnArguments + ) + + // endregion + + // region: Mapping conversions + + var enabledNamespaces = mutableListOf() + + val namespaceGetter: suspend (Snowflake?) -> Map? = { guildId -> + if (guildId == null) { + null + } else { + GuildBehavior(guildId, kord).config().namespaces.associateBy { it.lowercase() } + } + } + + val namespaceNames = mutableSetOf() + kord.guilds.flatMapMerge { it.config().namespaces.toList().asFlow() }.toSet(namespaceNames) + + val namespaces = namespaceNames.map { + when (it) { + "barn" -> BarnNamespace + "feather" -> FeatherNamespace + "hashed-mojang" -> MojangHashedNamespace + "legacy-yarn" -> LegacyYarnNamespace + "mcp" -> McpNamespaceReplacement + "mojang" -> MojangNamespace + "plasma" -> PlasmaNamespace + "quilt-mappings" -> QuiltMappingsNamespace + "srg-mojang" -> MojangSrgNamespace + "yarn" -> YarnNamespace + "yarrn" -> YarrnNamespace + + else -> error("Unknown namespace: $it") + } + } + + publicSlashCommand { + name = "convert" + description = "Convert mappings across namespaces" + + Namespaces.init(LinkieConfig.DEFAULT.copy(namespaces = namespaces)) + + publicSubCommand( + { MappingConversionArguments(namespaceGetter) } + ) { + name = "class" + description = "Convert a class mapping" + + action { + convertMapping( + "class", + MappingsQuery::queryClasses, + classMatchesToPages, + enabledNamespaces, + obfNameProvider = { obfName.preferredName }, + classNameProvider = { obfName.preferredName!! }, + descProvider = { null } + ) + } + } + + publicSubCommand( + { MappingConversionArguments(namespaceGetter) } + ) { + name = "field" + description = "Convert a field mapping" + + action { + convertMapping( + "field", + MappingsQuery::queryFields, + fieldMatchesToPages, + enabledNamespaces, + obfNameProvider = { member.obfName.preferredName }, + classNameProvider = { owner.obfName.preferredName!! }, + descProvider = { + when { + member.obfName.isMerged() -> member.getObfMergedDesc(it) + member.obfName.client != null -> member.getObfClientDesc(it) + member.obfName.server != null -> member.getObfServerDesc(it) + else -> null + } + } + ) + } + } + + publicSubCommand( + { MappingConversionArguments(namespaceGetter) } + ) { + name = "method" + description = "Convert a method mapping" + + action { + convertMapping( + "method", + MappingsQuery::queryMethods, + methodMatchesToPages, + enabledNamespaces, + obfNameProvider = { member.obfName.preferredName }, + classNameProvider = { owner.obfName.preferredName!! }, + descProvider = { + when { + member.obfName.isMerged() -> member.getObfMergedDesc(it) + member.obfName.client != null -> member.getObfClientDesc(it) + member.obfName.server != null -> member.getObfServerDesc(it) + else -> null + } + } + ) + } + } + + publicSubCommand { + name = "info" + description = "Get information about /convert and its subcommands" + + action { + val pages = mutableListOf() + pages.add( + "Mapping conversions are available for any Minecraft version with multiple mapping sets.\n\n" + + + "The version of the output is determined in this order:\n" + + "\u2022 The version specified by the command, \n" + + "\u2022 The default version of the output mapping set, \n" + + "\u2022 The default version of the input mapping set, or\n" + + "\u2022 The latest version supported by both mapping sets.\n\n" + + + "For a list of available mappings, see the next page." + ) + pages.add( + enabledNamespaces.joinToString( + prefix = "**Namespaces:** \n\n`", + separator = "`\n`", + postfix = "`" + ) + ) + + val pagesObj = Pages() + val pageTitle = "Mapping conversion info" + + pages.forEach { + pagesObj.addPage( + Page { + description = it + title = pageTitle + + footer { + text = PAGE_FOOTER + icon = PAGE_FOOTER_ICON + } + } + ) + } + + val paginator = PublicResponsePaginator( + pages = pagesObj, + keepEmbed = true, + owner = event.interaction.user, + timeoutSeconds = guild?.getTimeout(), + locale = getLocale(), + interaction = interactionResponse + ) + + paginator.send() + } + } + } + + ephemeralSlashCommand { + name = "command.mapping.name" + description = "command.mapping.description" + + check { anyGuild() } + + ephemeralSubCommand(::MappingConfigArguments) { + name = "command.mapping.timeout.name" + description = "command.mapping.timeout.description" + + check { hasPermission(Permission.ManageGuild) } + + action { + val guild = getGuild()!! + val config = guild.config() + val configUnit = guild.configUnit() + + if (arguments.timeout == null) { + respond { + content = translate( + "command.mapping.timeout.response.current", + arrayOf(config.timeout.toString()) + ) + } + + return@action + } + + config.timeout = arguments.timeout!! + configUnit.save(config) + + respond { + content = translate( + "command.mapping.timeout.response.updated", + arrayOf(config.timeout.toString()) + ) + } + } + } + + ephemeralSubCommand { + name = "command.mapping.namespace.name" + description = "command.mapping.namespace.description" + + check { hasPermission(Permission.ManageGuild) } + + action { + val guild = getGuild()!! + val config = guild.config() + val configUnit = guild.configUnit() + + var currentNamespaces: MutableList + + val context = this + + respond { + content = translate( + "command.mapping.namespace.selectmenu" + ) + components { + ephemeralStringSelectMenu { + maximumChoices = availableNamespaces.size + minimumChoices = 0 + + availableNamespaces.forEach { + option( + label = it, + value = it + ) { + default = it in config.namespaces + } + } + + action selectMenu@{ + val selectedNamespaces = event.interaction.values.toList().map { it } + + if (event.interaction.values.isEmpty()) { + config.namespaces = listOf() + configUnit.save(config) + respond { + content = context.translate( + "command.mapping.namespace.selectmenu.cleared", + ) + } + return@selectMenu + } + + currentNamespaces = mutableListOf() + currentNamespaces.addAll(selectedNamespaces) + + config.namespaces = currentNamespaces + configUnit.save(config) + // Set the namespaces for the conversion commands to update + enabledNamespaces = currentNamespaces + + respond { + content = context.translate( + "command.mapping.namespace.selectmenu.updated", + replacements = arrayOf(currentNamespaces.joinToString(", ")) + ) + } + } + } + } + } + } + } } // endregion - // region: Yarn mappings lookups - - if (yarnEnabled) { - slashCommand( - "yarn", - "Yarn", - YarnNamespace, - ::YarnArguments - ) { - val defaultVersion = YarnReleaseContainer.latestRelease - val defaultSnapshotVersion = YarnReleaseContainer.latestSnapshot - val allVersions = YarnNamespace.getAllSortedVersions() - - val pages = allVersions.chunked(VERSION_CHUNK_SIZE).map { - it.joinToString("\n") { version -> - when (version) { - defaultVersion -> "**» $version** (Default)" - defaultSnapshotVersion -> "**» $version** (Default: Snapshot)" - - else -> "**»** $version" - } - } - }.toMutableList() - - pages.add( - 0, - "Yarn mappings are available for queries across **${allVersions.size}** versions.\n\n" + - - "**Default version:** $defaultVersion\n" + - "**Default snapshot version:** $defaultSnapshotVersion\n\n" + - - "**Channels:** $yarnChannels\n" + - "**Commands:** `/yarn class`, `/yarn field`, `/yarn method`\n\n" + - - "For a full list of supported Yarn versions, please view the rest of the pages." + - - if (legacyYarnEnabled) { - " For Legacy Yarn mappings, please see the `lyarn` command." - } else { - "" - } - ) - - val pagesObj = Pages() - val pageTitle = "Mappings info: Yarn" - - pages.forEach { - pagesObj.addPage( - Page { - description = it - title = pageTitle - - footer { - text = PAGE_FOOTER - icon = PAGE_FOOTER_ICON - } - } - ) - } - - val paginator = PublicResponsePaginator( - pages = pagesObj, - keepEmbed = true, - owner = event.interaction.user, - timeoutSeconds = getTimeout(), - locale = getLocale(), - interaction = interactionResponse - ) - - paginator.send() - } - } - - // endregion - - // region: Yarrn mappings lookups - - if (yarrnEnabled) { - slashCommand( - "yarrn", - "Yarrn", - YarrnNamespace, - ::YarrnArguments - ) - } - - // endregion - - // region: Mapping conversions - - publicSlashCommand { - name = "convert" - description = "Convert mappings across namespaces" - - publicSubCommand( - { MappingConversionArguments(enabledNamespaces.associateBy { it.lowercase() }) } - ) { - name = "class" - description = "Convert a class mapping" - - action { - convertMapping( - "class", - MappingsQuery::queryClasses, - classMatchesToPages, - enabledNamespaces, - obfNameProvider = { obfName.preferredName }, - classNameProvider = { obfName.preferredName!! }, - descProvider = { null } - ) - } - } - - publicSubCommand( - { MappingConversionArguments(enabledNamespaces.associateBy { it.lowercase() }) } - ) { - name = "field" - description = "Convert a field mapping" - - action { - convertMapping( - "field", - MappingsQuery::queryFields, - fieldMatchesToPages, - enabledNamespaces, - obfNameProvider = { member.obfName.preferredName }, - classNameProvider = { owner.obfName.preferredName!! }, - descProvider = { - when { - member.obfName.isMerged() -> member.getObfMergedDesc(it) - member.obfName.client != null -> member.getObfClientDesc(it) - member.obfName.server != null -> member.getObfServerDesc(it) - else -> null - } - } - ) - } - } - - publicSubCommand( - { MappingConversionArguments(enabledNamespaces.associateBy { it.lowercase() }) } - ) { - name = "method" - description = "Convert a method mapping" - - action { - convertMapping( - "method", - MappingsQuery::queryMethods, - methodMatchesToPages, - enabledNamespaces, - obfNameProvider = { member.obfName.preferredName }, - classNameProvider = { owner.obfName.preferredName!! }, - descProvider = { - when { - member.obfName.isMerged() -> member.getObfMergedDesc(it) - member.obfName.client != null -> member.getObfClientDesc(it) - member.obfName.server != null -> member.getObfServerDesc(it) - else -> null - } - } - ) - } - } - - publicSubCommand { - name = "info" - description = "Get information about /convert and its subcommands" - - action { - val pages = mutableListOf() - pages.add( - "Mapping conversions are available for any Minecraft version with multiple mapping sets.\n\n" + - - "The version of the output is determined in this order:\n" + - "\u2022 The version specified by the command, \n" + - "\u2022 The default version of the output mapping set, \n" + - "\u2022 The default version of the input mapping set, or\n" + - "\u2022 The latest version supported by both mapping sets.\n\n" + - - "For a list of available mappings, see the next page." - ) - pages.add( - enabledNamespaces.joinToString( - prefix = "**Namespaces:** \n\n`", - separator = "`\n`", - postfix = "`" - ) - ) - - val pagesObj = Pages() - val pageTitle = "Mapping conversion info" - - pages.forEach { - pagesObj.addPage( - Page { - description = it - title = pageTitle - - footer { - text = PAGE_FOOTER - icon = PAGE_FOOTER_ICON - } - } - ) - } - - val paginator = PublicResponsePaginator( - pages = pagesObj, - keepEmbed = true, - owner = event.interaction.user, - timeoutSeconds = getTimeout(), - locale = getLocale(), - interaction = interactionResponse - ) - - paginator.send() - } - } - } - - // endregion - - logger.info { "Mappings extension set up - namespaces: " + enabledNamespaces.joinToString(", ") } - } - - private suspend fun MappingSlashCommand.queryMapping( - type: String, - channel: String? = null, - queryProvider: suspend (QueryContext) -> QueryResult, - pageGenerationMethod: (Namespace, MappingsContainer, QueryResult, Boolean) -> List>, - ) where A : MappingsMetadata, B : List<*> { - sentry.breadcrumb(BreadcrumbType.Query) { - message = "Beginning mapping lookup" - - data["type"] = type - data["channel"] = channel ?: "N/A" - data["namespace"] = arguments.namespace.id - data["query"] = arguments.query - data["version"] = arguments.version?.version ?: "N/A" - } - - newSingleThreadContext("/query $type: ${arguments.query}").use { context -> - withContext(context) { - val version = arguments.version?.version - ?: arguments.namespace.getDefaultVersion(channel) - - val provider = if (version != null) { - arguments.namespace.getProvider(version) - } else { - MappingsProvider.empty(arguments.namespace) - } - - provider.injectDefaultVersion( - arguments.namespace.getProvider(version ?: arguments.namespace.defaultVersion) - ) - - sentry.breadcrumb(BreadcrumbType.Info) { - message = "Provider resolved, with injected default version" - - data["version"] = provider.version ?: "Unknown" - } - - val query = arguments.query.replace('.', '/') - val pages: List> - - sentry.breadcrumb(BreadcrumbType.Info) { - message = "Attempting to run sanitized query" - - data["query"] = query - } - - @Suppress("TooGenericExceptionCaught") - val result = try { - queryProvider( - QueryContext( - provider = provider, - searchKey = query - ) - ) - } catch (e: NullPointerException) { - respond { - content = e.localizedMessage - } - return@withContext - } - - sentry.breadcrumb(BreadcrumbType.Info) { - message = "Generating pages for results" - - data["resultCount"] = result.value.size - } - - val container = provider.get() - - pages = pageGenerationMethod( - arguments.namespace, - container, - result, - arguments !is IntermediaryMappable || (arguments as IntermediaryMappable).mapDescriptors - ) - - if (pages.isEmpty()) { - respond { - content = "No results found" - } - - return@withContext - } - - val pagesObj = Pages("${EXPAND_EMOJI.mention} for more") - - val plural = if (type == "class") "es" else "s" - val pageTitle = "List of ${container.name} $type$plural: ${container.version}" - - val shortPages = mutableListOf() - val longPages = mutableListOf() - - pages.forEach { (short, long) -> - shortPages.add(short) - longPages.add(long) - } - - shortPages.forEach { - pagesObj.addPage( - "${EXPAND_EMOJI.mention} for more", - - Page { - description = it - title = pageTitle - - footer { - text = PAGE_FOOTER - icon = PAGE_FOOTER_ICON - } - } - ) - } - - if (shortPages != longPages) { - longPages.forEach { - pagesObj.addPage( - "${EXPAND_EMOJI.mention} for less", - - Page { - description = it - title = pageTitle - - footer { - text = PAGE_FOOTER - icon = PAGE_FOOTER_ICON - } - } - ) - } - } - - sentry.breadcrumb(BreadcrumbType.Info) { - message = "Creating and sending paginator to Discord" - } - - val paginator = PublicResponsePaginator( - pages = pagesObj, - owner = event.interaction.user, - timeoutSeconds = getTimeout(), - locale = getLocale(), - interaction = interactionResponse - ) - - paginator.send() - } - } - } - - private suspend fun ConversionSlashCommand.convertMapping( - type: String, - queryProvider: suspend (QueryContext) -> QueryResult, - pageGenerationMethod: (MappingsContainer, Map) -> List, - enabledNamespaces: List, - obfNameProvider: B.() -> String?, - classNameProvider: B.() -> String, - descProvider: B.(MappingsContainer) -> String?, - ) where A : MappingsMetadata, T : List> { - sentry.breadcrumb(BreadcrumbType.Query) { - message = "Beginning mapping conversion" - - data["type"] = type - data["query"] = arguments.query - data["inputNamespace"] = arguments.inputNamespace - data["inputChannel"] = arguments.inputChannel?.readableName ?: "N/A" - data["outputNamespace"] = arguments.outputNamespace - data["outputChannel"] = arguments.outputChannel?.readableName ?: "N/A" - data["version"] = arguments.version ?: "N/A" - } - - newSingleThreadContext("/convert $type: ${arguments.query}").use { context -> - withContext(context) { - val inputNamespace = if (arguments.inputNamespace in enabledNamespaces) { - arguments.inputNamespace.toNamespace() - } else { - returnError("Input namespace is not enabled or available") - return@withContext - } - - val outputNamespace = if (arguments.outputNamespace in enabledNamespaces) { - arguments.outputNamespace.toNamespace() - } else { - returnError("Output namespace is not enabled or available") - return@withContext - } - - val newestCommonVersion = inputNamespace.getAllSortedVersions().firstOrNull { - it in outputNamespace.getAllSortedVersions() - } ?: run { - returnError("No common version between input and output mappings") - return@withContext - } - - val inputDefault = inputNamespace.getDefaultVersion(arguments.inputChannel?.readableName) - - val outputDefault = outputNamespace.getDefaultVersion(arguments.outputChannel?.readableName) - - // try the command-provided version first - val version = arguments.version - // then try the default version for the output namespace - ?: outputDefault.takeIf { it in inputNamespace.getAllSortedVersions() } - // then try the default version for the input namespace - ?: inputDefault.takeIf { it in outputNamespace.getAllSortedVersions() } - // and if all else fails, use the newest common version - ?: newestCommonVersion - - val inputProvider = inputNamespace.getProvider(version) - val outputProvider = outputNamespace.getProvider(version) - - val inputContainer = inputProvider.getOrNull() ?: run { - returnError( - "Input mapping is not available ($version probably isn't supported by ${inputNamespace.id})" - ) - return@withContext - } - - val outputContainer = outputProvider.getOrNull() ?: run { - returnError( - "Output mapping is not available ($version probably isn't supported by ${outputNamespace.id})" - ) - return@withContext - } - - sentry.breadcrumb(BreadcrumbType.Info) { - message = "Providers and namespaces resolved" - - data["version"] = inputProvider.version ?: "Unknown" - } - - val query = arguments.query.replace('.', '/') - val pages: List - - sentry.breadcrumb(BreadcrumbType.Info) { - message = "Attempting to run sanitized query" - - data["query"] = query - } - - @Suppress("TooGenericExceptionCaught") - val inputResult = try { - queryProvider( - QueryContext( - provider = inputProvider, - searchKey = query - ) - ) - } catch (e: NullPointerException) { - returnError(e.localizedMessage) - return@withContext - } - - val outputQueries = inputResult.value.map { - it.value to obfNameProvider(it.value) - } - .filter { it.second != null } - .associate { it.first to it.second!! } - - @Suppress("TooGenericExceptionCaught") - val outputResults = outputQueries.mapValues { - try { - val classes = MappingsQuery.queryClasses( - QueryContext( - provider = outputProvider, - searchKey = classNameProvider(it.key) - ) - ) - - val clazz = classes.value.first { clazz -> - clazz.value.obfName.preferredName!! == classNameProvider(it.key) - } - .value - - val possibilities = when (type) { - "class" -> return@mapValues clazz - "method" -> clazz.methods - "field" -> clazz.fields - else -> error("`$type` isn't `class`, `field`, or `method`?????") - } - .filter { mapping -> - mapping.obfName.preferredName == it.value - } - - // NPE escapes the try block so it's ok - val inputDesc = it.key.descProvider(inputContainer)!! - - val result = possibilities.find { member -> - val desc = runCatching { member.getObfMergedDesc(outputContainer) } - .recoverCatching { member.getObfClientDesc(outputContainer) } - .recoverCatching { member.getObfServerDesc(outputContainer) } - .getOrElse { - return@find false - } - - desc == inputDesc - }!! - - MemberEntry(clazz, result) - } catch (e: NullPointerException) { - null // skip - } - } - .filterValues { it != null } - .mapValues { (_, value) -> - @Suppress("UNCHECKED_CAST") - value as B - } - - sentry.breadcrumb(BreadcrumbType.Info) { - message = "Generating pages for results" - - data["resultCount"] = outputResults.size - } - - pages = pageGenerationMethod(outputContainer, outputResults) - if (pages.isEmpty()) { - returnError("No results found") - return@withContext - } - - val pagesObj = Pages("") - - val inputName = inputContainer.name - val outputName = outputContainer.name - - val versionName = inputProvider.version ?: outputProvider.version ?: "Unknown" - - val pageTitle = "List of $inputName -> $outputName $type mappings: $versionName" - - pages.forEach { - pagesObj.addPage( - "", - - Page { - description = it - title = pageTitle - - footer { - text = PAGE_FOOTER - icon = PAGE_FOOTER_ICON - } - } - ) - } - - sentry.breadcrumb(BreadcrumbType.Info) { - message = "Creating and sending paginator to Discord" - } - - val paginator = PublicResponsePaginator( - pages = pagesObj, - owner = event.interaction.user, - timeoutSeconds = getTimeout(), - locale = getLocale(), - interaction = interactionResponse - ) - - paginator.send() - } - } - } - - private suspend fun PublicSlashCommandContext<*, *>.returnError(errorMessage: String) { - respond { - content = errorMessage - } - } - - private fun Namespace.getDefaultVersion(channel: String?): String? { - return when (this) { - is MojangNamespace, is MojangHashedNamespace -> if (channel == "snapshot") { - MojangReleaseContainer.latestSnapshot - } else { - MojangReleaseContainer.latestRelease - } - - is YarnNamespace -> if (channel == "snapshot") { - YarnReleaseContainer.latestSnapshot - } else { - YarnReleaseContainer.latestRelease - } - - else -> null - } - } - - private suspend fun getTimeout() = builder.config.getTimeout() - - private suspend fun CheckContextWithCache.customChecks( - command: String, - namespace: Namespace, - ) { - builder.commandChecks.forEach { - it(command)() - - if (!passed) { - return - } - } - - builder.namespaceChecks.forEach { - it(namespace)() - - if (!passed) { - return - } - } - } - - companion object { - private lateinit var builder: ExtMappingsBuilder - - /** @suppress: Internal function used to pass the configured builder into the extension. **/ - fun configure(builder: ExtMappingsBuilder) { - this.builder = builder - } - } + logger.info { "Mappings extension set up - namespaces: " + enabledNamespaces.joinToString(", ") } + } + + private suspend fun MappingSlashCommand.queryMapping( + type: String, + channel: String? = null, + queryProvider: suspend (QueryContext) -> QueryResult, + pageGenerationMethod: (Namespace, MappingsContainer, QueryResult, Boolean) -> List>, + ) where A : MappingsMetadata, B : List<*> { + sentry.breadcrumb(BreadcrumbType.Query) { + message = "Beginning mapping lookup" + + data["type"] = type + data["channel"] = channel ?: "N/A" + data["namespace"] = arguments.namespace.id + data["query"] = arguments.query + data["version"] = arguments.version?.version ?: "N/A" + } + + newSingleThreadContext("/query $type: ${arguments.query}").use { context -> + withContext(context) { + val version = arguments.version?.version + ?: arguments.namespace.getDefaultVersion(channel) + + val provider = if (version != null) { + arguments.namespace.getProvider(version) + } else { + MappingsProvider.empty(arguments.namespace) + } + + provider.injectDefaultVersion( + arguments.namespace.getProvider(version ?: arguments.namespace.defaultVersion) + ) + + sentry.breadcrumb(BreadcrumbType.Info) { + message = "Provider resolved, with injected default version" + + data["version"] = provider.version ?: "Unknown" + } + + val query = arguments.query.replace('.', '/') + val pages: List> + + sentry.breadcrumb(BreadcrumbType.Info) { + message = "Attempting to run sanitized query" + + data["query"] = query + } + + @Suppress("TooGenericExceptionCaught") + val result = try { + queryProvider( + QueryContext( + provider = provider, + searchKey = query + ) + ) + } catch (e: NullPointerException) { + respond { + content = e.localizedMessage + } + return@withContext + } + + sentry.breadcrumb(BreadcrumbType.Info) { + message = "Generating pages for results" + + data["resultCount"] = result.value.size + } + + val container = provider.get() + + pages = pageGenerationMethod( + arguments.namespace, + container, + result, + arguments !is IntermediaryMappable || (arguments as IntermediaryMappable).mapDescriptors + ) + + if (pages.isEmpty()) { + respond { + content = "No results found" + } + + return@withContext + } + + val pagesObj = Pages("${EXPAND_EMOJI.mention} for more") + + val plural = if (type == "class") "es" else "s" + val pageTitle = "List of ${container.name} $type$plural: ${container.version}" + + val shortPages = mutableListOf() + val longPages = mutableListOf() + + pages.forEach { (short, long) -> + shortPages.add(short) + longPages.add(long) + } + + shortPages.forEach { + pagesObj.addPage( + "${EXPAND_EMOJI.mention} for more", + + Page { + description = it + title = pageTitle + + footer { + text = PAGE_FOOTER + icon = PAGE_FOOTER_ICON + } + } + ) + } + + if (shortPages != longPages) { + longPages.forEach { + pagesObj.addPage( + "${EXPAND_EMOJI.mention} for less", + + Page { + description = it + title = pageTitle + + footer { + text = PAGE_FOOTER + icon = PAGE_FOOTER_ICON + } + } + ) + } + } + + sentry.breadcrumb(BreadcrumbType.Info) { + message = "Creating and sending paginator to Discord" + } + + val paginator = PublicResponsePaginator( + pages = pagesObj, + owner = event.interaction.user, + timeoutSeconds = guild?.getTimeout(), + locale = getLocale(), + interaction = interactionResponse + ) + + paginator.send() + } + } + } + + private suspend fun ConversionSlashCommand.convertMapping( + type: String, + queryProvider: suspend (QueryContext) -> QueryResult, + pageGenerationMethod: (MappingsContainer, Map) -> List, + enabledNamespaces: List, + obfNameProvider: B.() -> String?, + classNameProvider: B.() -> String, + descProvider: B.(MappingsContainer) -> String?, + ) where A : MappingsMetadata, T : List> { + sentry.breadcrumb(BreadcrumbType.Query) { + message = "Beginning mapping conversion" + + data["type"] = type + data["query"] = arguments.query + data["inputNamespace"] = arguments.inputNamespace + data["inputChannel"] = arguments.inputChannel?.readableName ?: "N/A" + data["outputNamespace"] = arguments.outputNamespace + data["outputChannel"] = arguments.outputChannel?.readableName ?: "N/A" + data["version"] = arguments.version ?: "N/A" + } + + newSingleThreadContext("/convert $type: ${arguments.query}").use { context -> + withContext(context) { + val inputNamespace = if (arguments.inputNamespace in enabledNamespaces) { + arguments.inputNamespace.toNamespace() + } else { + returnError("Input namespace is not enabled or available") + return@withContext + } + + val outputNamespace = if (arguments.outputNamespace in enabledNamespaces) { + arguments.outputNamespace.toNamespace() + } else { + returnError("Output namespace is not enabled or available") + return@withContext + } + + val newestCommonVersion = inputNamespace.getAllSortedVersions().firstOrNull { + it in outputNamespace.getAllSortedVersions() + } ?: run { + returnError("No common version between input and output mappings") + return@withContext + } + + val inputDefault = inputNamespace.getDefaultVersion(arguments.inputChannel?.readableName) + + val outputDefault = outputNamespace.getDefaultVersion(arguments.outputChannel?.readableName) + + // try the command-provided version first + val version = arguments.version + // then try the default version for the output namespace + ?: outputDefault.takeIf { it in inputNamespace.getAllSortedVersions() } + // then try the default version for the input namespace + ?: inputDefault.takeIf { it in outputNamespace.getAllSortedVersions() } + // and if all else fails, use the newest common version + ?: newestCommonVersion + + val inputProvider = inputNamespace.getProvider(version) + val outputProvider = outputNamespace.getProvider(version) + + val inputContainer = inputProvider.getOrNull() ?: run { + returnError( + "Input mapping is not available ($version probably isn't supported by ${inputNamespace.id})" + ) + return@withContext + } + + val outputContainer = outputProvider.getOrNull() ?: run { + returnError( + "Output mapping is not available ($version probably isn't supported by ${outputNamespace.id})" + ) + return@withContext + } + + sentry.breadcrumb(BreadcrumbType.Info) { + message = "Providers and namespaces resolved" + + data["version"] = inputProvider.version ?: "Unknown" + } + + val query = arguments.query.replace('.', '/') + val pages: List + + sentry.breadcrumb(BreadcrumbType.Info) { + message = "Attempting to run sanitized query" + + data["query"] = query + } + + @Suppress("TooGenericExceptionCaught") + val inputResult = try { + queryProvider( + QueryContext( + provider = inputProvider, + searchKey = query + ) + ) + } catch (e: NullPointerException) { + returnError(e.localizedMessage) + return@withContext + } + + val outputQueries = inputResult.value.map { + it.value to obfNameProvider(it.value) + } + .filter { it.second != null } + .associate { it.first to it.second!! } + + @Suppress("TooGenericExceptionCaught") + val outputResults = outputQueries.mapValues { + try { + val classes = MappingsQuery.queryClasses( + QueryContext( + provider = outputProvider, + searchKey = classNameProvider(it.key) + ) + ) + + val clazz = classes.value.first { clazz -> + clazz.value.obfName.preferredName!! == classNameProvider(it.key) + } + .value + + val possibilities = when (type) { + "class" -> return@mapValues clazz + "method" -> clazz.methods + "field" -> clazz.fields + else -> error("`$type` isn't `class`, `field`, or `method`?????") + } + .filter { mapping -> + mapping.obfName.preferredName == it.value + } + + // NPE escapes the try block so it's ok + val inputDesc = it.key.descProvider(inputContainer)!! + + val result = possibilities.find { member -> + val desc = runCatching { member.getObfMergedDesc(outputContainer) } + .recoverCatching { member.getObfClientDesc(outputContainer) } + .recoverCatching { member.getObfServerDesc(outputContainer) } + .getOrElse { + return@find false + } + + desc == inputDesc + }!! + + MemberEntry(clazz, result) + } catch (e: NullPointerException) { + null // skip + } + } + .filterValues { it != null } + .mapValues { (_, value) -> + @Suppress("UNCHECKED_CAST") + value as B + } + + sentry.breadcrumb(BreadcrumbType.Info) { + message = "Generating pages for results" + + data["resultCount"] = outputResults.size + } + + pages = pageGenerationMethod(outputContainer, outputResults) + if (pages.isEmpty()) { + returnError("No results found") + return@withContext + } + + val pagesObj = Pages("") + + val inputName = inputContainer.name + val outputName = outputContainer.name + + val versionName = inputProvider.version ?: outputProvider.version ?: "Unknown" + + val pageTitle = "List of $inputName -> $outputName $type mappings: $versionName" + + pages.forEach { + pagesObj.addPage( + "", + + Page { + description = it + title = pageTitle + + footer { + text = PAGE_FOOTER + icon = PAGE_FOOTER_ICON + } + } + ) + } + + sentry.breadcrumb(BreadcrumbType.Info) { + message = "Creating and sending paginator to Discord" + } + + val paginator = PublicResponsePaginator( + pages = pagesObj, + owner = event.interaction.user, + timeoutSeconds = guild?.getTimeout(), + locale = getLocale(), + interaction = interactionResponse + ) + + paginator.send() + } + } + } + + private suspend fun PublicSlashCommandContext<*, *>.returnError(errorMessage: String) { + respond { + content = errorMessage + } + } + + private fun Namespace.getDefaultVersion(channel: String?): String? { + return when (this) { + is MojangNamespace, is MojangHashedNamespace -> if (channel == "snapshot") { + MojangReleaseContainer.latestSnapshot + } else { + MojangReleaseContainer.latestRelease + } + + is YarnNamespace -> if (channel == "snapshot") { + YarnReleaseContainer.latestSnapshot + } else { + YarnReleaseContainer.latestRelease + } + + else -> null + } + } + + private suspend fun GuildBehavior.getTimeout() = config().timeout.toLong() + + private suspend fun CheckContextWithCache.customChecks( + command: String, + namespace: Namespace, + ) { + builder.commandChecks.forEach { + it(command)() + + if (!passed) { + return + } + } + + builder.namespaceChecks.forEach { + it(namespace)() + + if (!passed) { + return + } + } + } + + private fun GuildBehavior.configUnit() = + guildConfig.withGuild(id) + + private suspend fun GuildBehavior.config(): MappingsConfig { + val config = configUnit() + + return config.get() + ?: config.save(MappingsConfig()) + } + + companion object { + private lateinit var builder: ExtMappingsBuilder + + /** @suppress: Internal function used to pass the configured builder into the extension. **/ + fun configure(builder: ExtMappingsBuilder) { + this.builder = builder + } + } + + @Suppress("MagicNumber") + inner class MappingConfigArguments : Arguments() { + val timeout by optionalInt { + name = "argument.timeout.name" + description = "argument.timeout.description" + minValue = 60 + } + } } diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/MappingConversionArguments.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/MappingConversionArguments.kt index e1ca686119..323ea2d4f3 100644 --- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/MappingConversionArguments.kt +++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/MappingConversionArguments.kt @@ -8,36 +8,55 @@ package com.kotlindiscord.kord.extensions.modules.extra.mappings.arguments import com.kotlindiscord.kord.extensions.commands.Arguments import com.kotlindiscord.kord.extensions.commands.application.slash.converters.impl.optionalEnumChoice -import com.kotlindiscord.kord.extensions.commands.application.slash.converters.impl.stringChoice import com.kotlindiscord.kord.extensions.commands.converters.impl.optionalString import com.kotlindiscord.kord.extensions.commands.converters.impl.string import com.kotlindiscord.kord.extensions.modules.extra.mappings.enums.Channels import com.kotlindiscord.kord.extensions.modules.extra.mappings.utils.autocompleteVersions import com.kotlindiscord.kord.extensions.modules.extra.mappings.utils.toNamespace +import com.kotlindiscord.kord.extensions.utils.suggestStringMap +import dev.kord.common.entity.Snowflake /** * Arguments for class, field, and method conversion commands. */ @Suppress("UndocumentedPublicProperty") -class MappingConversionArguments(enabledNamespaces: Map) : Arguments() { +class MappingConversionArguments(enabledNamespaces: suspend (Snowflake?) -> Map?) : Arguments() { val query by string { name = "query" description = "Name to query mappings for" } - val inputNamespace by stringChoice { + val inputNamespace by string { name = "input" description = "The namespace to convert from" - choices(enabledNamespaces) + autoComplete { + val guildId = command.data.guildId.value + val values = enabledNamespaces(guildId) ?: emptyMap() + suggestStringMap(values) + } + + @Suppress("UnnecessaryParentheses") + validate { + failIf("Must be a valid namespace") { value !in (enabledNamespaces(context.getGuild()!!.id) ?: emptyMap()) } + } } - val outputNamespace by stringChoice { - name = "output" - description = "The namespace to convert to" + val outputNamespace by string { + name = "output" + description = "The namespace to convert to" - choices(enabledNamespaces) - } + autoComplete { + val guildId = command.data.guildId.value + val values = enabledNamespaces(guildId) ?: emptyMap() + suggestStringMap(values) + } + + @Suppress("UnnecessaryParentheses") + validate { + failIf("Must be a valid namespace") { value !in (enabledNamespaces(context.getGuild()!!.id) ?: emptyMap()) } + } + } val version by optionalString { name = "version" diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/builders/ExtMappingsBuilder.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/builders/ExtMappingsBuilder.kt index 63ee0bb9f4..521fd44fae 100644 --- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/builders/ExtMappingsBuilder.kt +++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/builders/ExtMappingsBuilder.kt @@ -7,15 +7,10 @@ package com.kotlindiscord.kord.extensions.modules.extra.mappings.builders import com.kotlindiscord.kord.extensions.checks.types.SlashCommandCheck -import com.kotlindiscord.kord.extensions.modules.extra.mappings.configuration.MappingsConfigAdapter -import com.kotlindiscord.kord.extensions.modules.extra.mappings.configuration.TomlMappingsConfig import me.shedaniel.linkie.Namespace /** Builder used for configuring the mappings extension. **/ class ExtMappingsBuilder { - /** Config adapter to use to load the mappings extension configuration. **/ - var config: MappingsConfigAdapter = TomlMappingsConfig() - /** List of checks to apply against the name of the command. **/ val commandChecks: MutableList SlashCommandCheck> = mutableListOf() diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/configuration/MappingsConfigAdapter.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/configuration/MappingsConfigAdapter.kt deleted file mode 100644 index 6186c6c61f..0000000000 --- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/configuration/MappingsConfigAdapter.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.kotlindiscord.kord.extensions.modules.extra.mappings.configuration - -import dev.kord.common.entity.Snowflake - -/** - * Simple config adapter interface, which you can implement yourself if you need some kind of alternative config - * backend. - */ -interface MappingsConfigAdapter { - /** Get a list of category IDs mappings commands are explicitly allowed in. **/ - suspend fun getAllowedCategories(): List - - /** Get a list of category IDs mappings commands are explicitly disallowed in. **/ - suspend fun getBannedCategories(): List - - /** Get a list of channel IDs mappings commands are explicitly allowed in. **/ - suspend fun getAllowedChannels(): List - - /** Get a list of channel IDs mappings commands are explicitly disallowed in. **/ - suspend fun getBannedChannels(): List - - /** Get a list of guild IDs mappings commands are explicitly allowed in. **/ - suspend fun getAllowedGuilds(): List - - /** Get a list of guild IDs mappings commands are explicitly disallowed in. **/ - suspend fun getBannedGuilds(): List - - /** Get a list of enabled mappings namespaces. **/ - suspend fun getEnabledNamespaces(): List - - /** Get the paginator timeout, in seconds. **/ - suspend fun getTimeout(): Long -} diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/configuration/TomlMappingsConfig.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/configuration/TomlMappingsConfig.kt deleted file mode 100644 index 83fb6876f1..0000000000 --- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/configuration/TomlMappingsConfig.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.kotlindiscord.kord.extensions.modules.extra.mappings.configuration - -import com.kotlindiscord.kord.extensions.modules.extra.mappings.configuration.spec.CategoriesSpec -import com.kotlindiscord.kord.extensions.modules.extra.mappings.configuration.spec.ChannelsSpec -import com.kotlindiscord.kord.extensions.modules.extra.mappings.configuration.spec.GuildsSpec -import com.kotlindiscord.kord.extensions.modules.extra.mappings.configuration.spec.SettingsSpec -import com.uchuhimo.konf.Config -import com.uchuhimo.konf.Feature -import com.uchuhimo.konf.source.toml -import dev.kord.common.entity.Snowflake -import java.io.File - -/** - * Implementation of [MappingsConfigAdapter] backed by TOML files, system properties and env vars. - * - * For more information on how this works, see the README. - */ -class TomlMappingsConfig : MappingsConfigAdapter { - private var config = Config { - addSpec(CategoriesSpec) - addSpec(ChannelsSpec) - addSpec(GuildsSpec) - addSpec(SettingsSpec) - } - .from.enabled(Feature.FAIL_ON_UNKNOWN_PATH).toml.resource("kordex/mappings/default.toml") - .from.enabled(Feature.FAIL_ON_UNKNOWN_PATH).toml.resource( - "kordex/mappings/config.toml", - optional = true - ) - - init { - if (File("config/ext/mappings.toml").exists()) { - config = config.from.enabled(Feature.FAIL_ON_UNKNOWN_PATH).toml.watchFile( - "config/ext/mappings.toml", - optional = true - ) - } - - config = config - .from.prefixed("KORDEX_MAPPINGS").env() - .from.prefixed("kordex.mappings").systemProperties() - } - - override suspend fun getAllowedCategories(): List = config[CategoriesSpec.allowed] - override suspend fun getBannedCategories(): List = config[CategoriesSpec.banned] - - override suspend fun getAllowedChannels(): List = config[ChannelsSpec.allowed] - override suspend fun getBannedChannels(): List = config[ChannelsSpec.banned] - - override suspend fun getAllowedGuilds(): List = config[GuildsSpec.allowed] - override suspend fun getBannedGuilds(): List = config[GuildsSpec.banned] - - override suspend fun getEnabledNamespaces(): List = config[SettingsSpec.namespaces] - - override suspend fun getTimeout(): Long = config[SettingsSpec.timeout] -} diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/configuration/spec/CategoriesSpec.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/configuration/spec/CategoriesSpec.kt deleted file mode 100644 index 9038a938b1..0000000000 --- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/configuration/spec/CategoriesSpec.kt +++ /dev/null @@ -1,19 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.kotlindiscord.kord.extensions.modules.extra.mappings.configuration.spec - -import com.uchuhimo.konf.ConfigSpec -import dev.kord.common.entity.Snowflake - -/** @suppress **/ -object CategoriesSpec : ConfigSpec() { - /** @suppress **/ - val allowed by required>() - - /** @suppress **/ - val banned by required>() -} diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/configuration/spec/ChannelsSpec.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/configuration/spec/ChannelsSpec.kt deleted file mode 100644 index b8544803f5..0000000000 --- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/configuration/spec/ChannelsSpec.kt +++ /dev/null @@ -1,19 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.kotlindiscord.kord.extensions.modules.extra.mappings.configuration.spec - -import com.uchuhimo.konf.ConfigSpec -import dev.kord.common.entity.Snowflake - -/** @suppress **/ -object ChannelsSpec : ConfigSpec() { - /** @suppress **/ - val allowed by required>() - - /** @suppress **/ - val banned by required>() -} diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/configuration/spec/GuildsSpec.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/configuration/spec/GuildsSpec.kt deleted file mode 100644 index 639c74a55a..0000000000 --- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/configuration/spec/GuildsSpec.kt +++ /dev/null @@ -1,19 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.kotlindiscord.kord.extensions.modules.extra.mappings.configuration.spec - -import com.uchuhimo.konf.ConfigSpec -import dev.kord.common.entity.Snowflake - -/** @suppress **/ -object GuildsSpec : ConfigSpec() { - /** @suppress **/ - val allowed by required>() - - /** @suppress **/ - val banned by required>() -} diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/configuration/spec/SettingsSpec.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/configuration/spec/SettingsSpec.kt deleted file mode 100644 index 64aaaacc83..0000000000 --- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/configuration/spec/SettingsSpec.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package com.kotlindiscord.kord.extensions.modules.extra.mappings.configuration.spec - -import com.uchuhimo.konf.ConfigSpec - -private const val DEFAULT_TIMEOUT = 300L - -/** @suppress **/ -object SettingsSpec : ConfigSpec() { - /** @suppress **/ - val namespaces by required>() - - /** @suppress **/ - val timeout by optional(DEFAULT_TIMEOUT) -} diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/storage/MappingsConfig.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/storage/MappingsConfig.kt new file mode 100644 index 0000000000..9a98010e77 --- /dev/null +++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/storage/MappingsConfig.kt @@ -0,0 +1,48 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +@file:Suppress( + "UnderscoresInNumericLiterals", + "UndocumentedPublicClass", + "UndocumentedPublicFunction", + "UndocumentedPublicProperty", +) + +package com.kotlindiscord.kord.extensions.modules.extra.mappings.storage + +import com.kotlindiscord.kord.extensions.storage.Data +import kotlinx.serialization.Serializable +import net.peanuuutz.tomlkt.TomlComment +import net.peanuuutz.tomlkt.TomlInteger + +@Serializable +@Suppress("DataClassShouldBeImmutable") +data class MappingsConfig( + @TomlComment( + "Which namespaces to allow conversions for - 'barn', 'feather', 'hashed-mojang', 'legacy-yarn'," + + "'plasma', 'quilt-mappings', 'mcp', 'mojang', 'srg-mojang', 'yarn' or 'yarrn'" + ) + var namespaces: List = + listOf( + "barn", + "feather", + "hashed-mojang", + "legacy-yarn", + "mcp", + "mojang", + "plasma", + "quilt-mappings", + "srg-mojang", + "yarn", + "yarrn", + ), + + @TomlComment( + "How long to wait before closing mappings paginators (in seconds), defaults to 5 mins" + ) + @TomlInteger(TomlInteger.Base.DEC) + var timeout: Int = 300, +) : Data diff --git a/extra-modules/extra-mappings/src/main/resources/translations/kordex/mappings.properties b/extra-modules/extra-mappings/src/main/resources/translations/kordex/mappings.properties new file mode 100644 index 0000000000..b9efcc232b --- /dev/null +++ b/extra-modules/extra-mappings/src/main/resources/translations/kordex/mappings.properties @@ -0,0 +1,23 @@ +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# + +argument.timeout.name=timeout +argument.timeout.description=Set the duration (in seconds) until the paginator buttons are removed + +command.mapping.name=mappings +command.mapping.description=Manage and view the Mappings extension settings for this server + +command.mapping.timeout.name=timeout +command.mapping.timeout.description=Set a custom timeout for the buttons on mappings paginators +command.mapping.timeout.response.updated=**Timeout updated:** {0} +command.mapping.timeout.response.current=**Current timeout:** {0} + +command.mapping.namespace.name=namespaces +command.mapping.namespace.description=Configure which namespaces can be used in conversion commands + +command.mapping.namespace.selectmenu=Select the namespaces for conversion commands +command.mapping.namespace.selectmenu.cleared=Namespaces cleared +command.mapping.namespace.selectmenu.updated=**Namespaces updated:** {0} \ No newline at end of file diff --git a/extra-modules/extra-mappings/src/main/resources/translations/kordex/mappings_en_GB.properties b/extra-modules/extra-mappings/src/main/resources/translations/kordex/mappings_en_GB.properties new file mode 100644 index 0000000000..b9efcc232b --- /dev/null +++ b/extra-modules/extra-mappings/src/main/resources/translations/kordex/mappings_en_GB.properties @@ -0,0 +1,23 @@ +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# + +argument.timeout.name=timeout +argument.timeout.description=Set the duration (in seconds) until the paginator buttons are removed + +command.mapping.name=mappings +command.mapping.description=Manage and view the Mappings extension settings for this server + +command.mapping.timeout.name=timeout +command.mapping.timeout.description=Set a custom timeout for the buttons on mappings paginators +command.mapping.timeout.response.updated=**Timeout updated:** {0} +command.mapping.timeout.response.current=**Current timeout:** {0} + +command.mapping.namespace.name=namespaces +command.mapping.namespace.description=Configure which namespaces can be used in conversion commands + +command.mapping.namespace.selectmenu=Select the namespaces for conversion commands +command.mapping.namespace.selectmenu.cleared=Namespaces cleared +command.mapping.namespace.selectmenu.updated=**Namespaces updated:** {0} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 22f3a2f042..823330535d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -44,9 +44,6 @@ koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } koin-logger = { module = "io.insert-koin:koin-logger-slf4j", version.ref = "koin" } koin-test = { module = "io.insert-koin:koin-test", version.ref = "koin" } -konf-core = { module = "com.uchuhimo:konf", version.ref = "konf" } -konf-toml = { module = "com.uchuhimo:konf-toml", version.ref = "konf" } - kord = { module = "dev.kord:kord-core-voice", version.ref = "kord" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8" } diff --git a/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/TestBot.kt b/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/TestBot.kt index 00dce80ed5..2b38823221 100644 --- a/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/TestBot.kt +++ b/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/TestBot.kt @@ -8,6 +8,7 @@ package com.kotlindiscord.kord.extensions.testbot import com.kotlindiscord.kord.extensions.ExtensibleBot import com.kotlindiscord.kord.extensions.checks.isNotBot +import com.kotlindiscord.kord.extensions.modules.extra.mappings.extMappings import com.kotlindiscord.kord.extensions.modules.extra.phishing.extPhishing import com.kotlindiscord.kord.extensions.modules.extra.pluralkit.extPluralKit import com.kotlindiscord.kord.extensions.testbot.extensions.* @@ -73,6 +74,10 @@ public suspend fun main() { extPluralKit() } + if (envOrNull("MAPPINGS_TESTING") != null) { + extMappings { } + } + add(::ArgumentTestExtension) add(::I18nTestExtension) add(::ModalTestExtension)