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)