diff --git a/src/main/java/io/github/freya022/botcommands/api/localization/DefaultMessages.java b/src/main/java/io/github/freya022/botcommands/api/localization/DefaultMessages.java index 041965fcdb..ea7724c61f 100644 --- a/src/main/java/io/github/freya022/botcommands/api/localization/DefaultMessages.java +++ b/src/main/java/io/github/freya022/botcommands/api/localization/DefaultMessages.java @@ -42,7 +42,10 @@ *

Refer to {@link Localization} for more details. * * @see Localization + * + * @deprecated This has been replaced by {@link io.github.freya022.botcommands.api.core.messages.DefaultBotCommandsMessages DefaultBotCommandsMessages} */ +@Deprecated(since = "3.1.0-beta.1", forRemoval = true) public final class DefaultMessages { private final Localization localization; diff --git a/src/main/kotlin/io/github/freya022/botcommands/api/commands/ratelimit/handler/DefaultRateLimitHandler.kt b/src/main/kotlin/io/github/freya022/botcommands/api/commands/ratelimit/handler/DefaultRateLimitHandler.kt index 69ba9ff6a4..4b70301bfa 100644 --- a/src/main/kotlin/io/github/freya022/botcommands/api/commands/ratelimit/handler/DefaultRateLimitHandler.kt +++ b/src/main/kotlin/io/github/freya022/botcommands/api/commands/ratelimit/handler/DefaultRateLimitHandler.kt @@ -7,22 +7,25 @@ import io.github.freya022.botcommands.api.commands.application.ApplicationComman import io.github.freya022.botcommands.api.commands.ratelimit.RateLimitScope import io.github.freya022.botcommands.api.commands.text.TextCommandInfo import io.github.freya022.botcommands.api.core.BContext +import io.github.freya022.botcommands.api.core.messages.BotCommandsMessages +import io.github.freya022.botcommands.api.core.messages.BotCommandsMessagesFactory import io.github.freya022.botcommands.api.core.service.getService import io.github.freya022.botcommands.api.core.utils.awaitCatching import io.github.freya022.botcommands.api.core.utils.namedDefaultScope import io.github.freya022.botcommands.api.core.utils.runIgnoringResponse -import io.github.freya022.botcommands.api.localization.DefaultMessages -import io.github.freya022.botcommands.api.localization.DefaultMessagesFactory import kotlinx.coroutines.delay import kotlinx.coroutines.launch import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel +import net.dv8tion.jda.api.events.Event +import net.dv8tion.jda.api.events.interaction.GenericInteractionCreateEvent import net.dv8tion.jda.api.events.interaction.command.GenericCommandInteractionEvent import net.dv8tion.jda.api.events.interaction.component.GenericComponentInteractionCreateEvent import net.dv8tion.jda.api.events.message.MessageReceivedEvent import net.dv8tion.jda.api.interactions.callbacks.IMessageEditCallback import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback import net.dv8tion.jda.api.requests.ErrorResponse -import net.dv8tion.jda.api.utils.TimeFormat +import net.dv8tion.jda.api.utils.messages.MessageCreateData +import java.time.Instant import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.nanoseconds @@ -35,7 +38,7 @@ private val deleteScope = namedDefaultScope("Rate limit message delete", 1) * then it is sent to the user's DMs, or returns if not possible. * - Interactions are simply replying an ephemeral message to the user. * - * All messages sent to the user are localized messages from [DefaultMessages] and will be deleted when expired. + * All messages sent to the user are localized messages from [BotCommandsMessages] and will be deleted when expired. * * **Note:** The rate limit message won't be deleted in a private channel, * or if the [refill delay][ConsumptionProbe.nanosToWaitForRefill] is longer than 10 minutes. @@ -59,8 +62,8 @@ class DefaultRateLimitHandler( event.guildChannel.canTalk() -> event.channel else -> event.author.openPrivateChannel().await() } - val messages = context.getService().get(event) - val content = getRateLimitMessage(messages, probe) + val messages = context.getService().get(event) + val content = getRateLimitMessage(event, messages, probe) runIgnoringResponse(ErrorResponse.CANNOT_SEND_TO_USER) { val messageId = channel.sendMessage(content).await().idLong @@ -80,7 +83,7 @@ class DefaultRateLimitHandler( commandInfo: ApplicationCommandInfo, probe: ConsumptionProbe ) where T : GenericCommandInteractionEvent, T : IReplyCallback { - onRateLimit(context, event, probe) + onRateLimit0(context, event, probe) } override suspend fun onRateLimit( @@ -88,12 +91,17 @@ class DefaultRateLimitHandler( event: T, probe: ConsumptionProbe ) where T : GenericComponentInteractionCreateEvent, T : IReplyCallback, T : IMessageEditCallback { - onRateLimit(context, event as IReplyCallback, probe) + onRateLimit0(context, event, probe) } - private suspend fun onRateLimit(context: BContext, event: IReplyCallback, probe: ConsumptionProbe) { - val messages = context.getService().get(event) - val content = getRateLimitMessage(messages, probe) + private suspend fun onRateLimit0( + context: BContext, + event: T, + probe: ConsumptionProbe + ) where T : GenericInteractionCreateEvent, + T : IReplyCallback { + val messages = context.getService().get(event) + val content = getRateLimitMessage(event, messages, probe) val hook = event.reply(content).setEphemeral(true).await() // Only schedule delete if the interaction hook doesn't expire before // Technically this is supposed to be 15 minutes but, just to be safe @@ -106,16 +114,17 @@ class DefaultRateLimitHandler( } private fun getRateLimitMessage( - messages: DefaultMessages, + event: Event, + messages: BotCommandsMessages, probe: ConsumptionProbe - ): String { - val timestamp = TimeFormat.RELATIVE.atTimestamp(System.currentTimeMillis() + probe.nanosToWaitForRefill.floorDiv(1_000_000)) + ): MessageCreateData { + val deadline = Instant.now().plusNanos(probe.nanosToWaitForRefill) return when (scope) { - RateLimitScope.USER -> messages.getUserRateLimitMsg(timestamp) - RateLimitScope.USER_PER_GUILD -> messages.getUserRateLimitMsg(timestamp) - RateLimitScope.USER_PER_CHANNEL -> messages.getUserRateLimitMsg(timestamp) - RateLimitScope.GUILD -> messages.getGuildRateLimitMsg(timestamp) - RateLimitScope.CHANNEL -> messages.getChannelRateLimitMsg(timestamp) + RateLimitScope.USER -> messages.userRateLimited(event, deadline) + RateLimitScope.USER_PER_GUILD -> messages.userRateLimited(event, deadline) + RateLimitScope.USER_PER_CHANNEL -> messages.userRateLimited(event, deadline) + RateLimitScope.GUILD -> messages.guildRateLimited(event, deadline) + RateLimitScope.CHANNEL -> messages.channelRateLimited(event, deadline) } } } \ No newline at end of file diff --git a/src/main/kotlin/io/github/freya022/botcommands/api/core/config/BTextConfig.kt b/src/main/kotlin/io/github/freya022/botcommands/api/core/config/BTextConfig.kt index a548157652..8253734b03 100644 --- a/src/main/kotlin/io/github/freya022/botcommands/api/core/config/BTextConfig.kt +++ b/src/main/kotlin/io/github/freya022/botcommands/api/core/config/BTextConfig.kt @@ -4,9 +4,9 @@ import dev.freya02.jda.emojis.unicode.Emojis import io.github.freya022.botcommands.api.commands.text.IHelpCommand import io.github.freya022.botcommands.api.commands.text.TextPrefixSupplier import io.github.freya022.botcommands.api.commands.text.annotations.RequiresTextCommands +import io.github.freya022.botcommands.api.core.messages.BotCommandsMessages import io.github.freya022.botcommands.api.core.service.annotations.InjectedService import io.github.freya022.botcommands.api.core.utils.toImmutableList -import io.github.freya022.botcommands.api.localization.DefaultMessages import io.github.freya022.botcommands.internal.core.config.ConfigDSL import io.github.freya022.botcommands.internal.core.config.ConfigurationValue import net.dv8tion.jda.api.entities.emoji.Emoji @@ -78,7 +78,7 @@ interface BTextConfig { /** * Emoji used to indicate a user that their DMs are closed. * - * This is only used if [the closed DMs error message][DefaultMessages.getClosedDMErrorMsg] can't be sent. + * This is only used if [the closed DMs error message][BotCommandsMessages.closedDirectMessages] can't be sent. * * Default: `mailbox_closed` * diff --git a/src/main/kotlin/io/github/freya022/botcommands/api/core/messages/BotCommandsMessages.kt b/src/main/kotlin/io/github/freya022/botcommands/api/core/messages/BotCommandsMessages.kt new file mode 100644 index 0000000000..f805db00ac --- /dev/null +++ b/src/main/kotlin/io/github/freya022/botcommands/api/core/messages/BotCommandsMessages.kt @@ -0,0 +1,107 @@ +package io.github.freya022.botcommands.api.core.messages + +import io.github.freya022.botcommands.api.commands.application.slash.options.SlashCommandOption +import io.github.freya022.botcommands.api.commands.text.TopLevelTextCommandInfo +import net.dv8tion.jda.api.Permission +import net.dv8tion.jda.api.entities.channel.attribute.IAgeRestrictedChannel +import net.dv8tion.jda.api.events.GenericEvent +import net.dv8tion.jda.api.utils.messages.MessageCreateData +import java.time.Instant + +/** + * Returns the messages used by the framework, instances are produced by [BotCommandsMessagesFactory]. + * + * @see BotCommandsMessagesFactory + */ +interface BotCommandsMessages { + + /** + * @return Message to display when an uncaught exception occurs + */ + fun uncaughtException(event: GenericEvent?): MessageCreateData + + /** + * @return Message to display when the user is missing [permissions][io.github.freya022.botcommands.api.commands.annotations.UserPermissions] + */ + fun missingUserPermissions(event: GenericEvent?, permissions: Set): MessageCreateData + + /** + * @return Message to display when the bot is missing [permissions][io.github.freya022.botcommands.api.commands.annotations.BotPermissions] + */ + fun missingBotPermissions(event: GenericEvent?, permissions: Set): MessageCreateData + + /** + * @return Message to display when a text command is [only usable by the owner][io.github.freya022.botcommands.api.commands.text.annotations.RequireOwner] + */ + fun ownerOnly(event: GenericEvent?): MessageCreateData + + /** + * @return Message to display when a user has exceeded a command's [rate limit][io.github.freya022.botcommands.api.commands.annotations.RateLimit] + */ + fun userRateLimited(event: GenericEvent?, deadline: Instant): MessageCreateData + + /** + * @return Message to display when a channel has exceeded a command's [rate limit][io.github.freya022.botcommands.api.commands.annotations.RateLimit] + */ + fun channelRateLimited(event: GenericEvent?, deadline: Instant): MessageCreateData + + /** + * @return Message to display when a guild has exceeded a command's [rate limit][io.github.freya022.botcommands.api.commands.annotations.RateLimit] + */ + fun guildRateLimited(event: GenericEvent?, deadline: Instant): MessageCreateData + + /** + * @return Message to display when application commands are not loaded on the guild yet + */ + fun applicationCommandsNotAvailable(event: GenericEvent?): MessageCreateData + + /** + * @return Message to display when the command is not found + */ + fun commandNotFound(event: GenericEvent?, suggestions: Collection): MessageCreateData + + /** + * @return Message to display when a channel parameter could not be resolved + */ + fun resolverChannelNotFound(event: GenericEvent?, channelId: Long): MessageCreateData + + /** + * @return Message to display when a channel parameter could be resolved but is not accessible (such as private threads) + */ + fun resolverChannelMissingAccess(event: GenericEvent?, channelId: Long): MessageCreateData + + /** + * @return Message to display when a user parameter could not be resolved + */ + fun resolverUserNotFound(event: GenericEvent?, userId: Long): MessageCreateData + + /** + * @return Message to display when a slash command option is unresolvable (only in slash command interactions) + */ + fun slashCommandUnresolvableOption(event: GenericEvent?, option: SlashCommandOption): MessageCreateData + + /** + * @return Message to display when a User's DMs are closed (when sending help content for example) + */ + fun closedDirectMessages(event: GenericEvent?): MessageCreateData + + /** + * @return Message to display when a command is used in a NSFW [IAgeRestrictedChannel] (see [@NSFW][io.github.freya022.botcommands.api.commands.text.annotations.NSFW]) + */ + fun nsfwOnly(event: GenericEvent?): MessageCreateData + + /** + * @return Message to display when a user tries to use a component it isn't allowed to interact with + */ + fun componentNotAllowed(event: GenericEvent?): MessageCreateData + + /** + * @return Message to display when a user tries to use a component which does not exist anymore + */ + fun componentExpired(event: GenericEvent?): MessageCreateData + + /** + * @return Message to display when a user tries to use a modal which has reached timeout + */ + fun modalExpired(event: GenericEvent?): MessageCreateData +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/freya022/botcommands/api/core/messages/BotCommandsMessagesFactory.kt b/src/main/kotlin/io/github/freya022/botcommands/api/core/messages/BotCommandsMessagesFactory.kt new file mode 100644 index 0000000000..1cfaa5f566 --- /dev/null +++ b/src/main/kotlin/io/github/freya022/botcommands/api/core/messages/BotCommandsMessagesFactory.kt @@ -0,0 +1,43 @@ +@file:Suppress("DEPRECATION") + +package io.github.freya022.botcommands.api.core.messages + +import io.github.freya022.botcommands.api.core.service.annotations.InterfacedService +import io.github.freya022.botcommands.api.localization.DefaultMessagesFactory +import io.github.freya022.botcommands.api.localization.interaction.UserLocaleProvider +import io.github.freya022.botcommands.api.localization.text.TextCommandLocaleProvider +import net.dv8tion.jda.api.events.message.MessageReceivedEvent +import net.dv8tion.jda.api.interactions.Interaction +import java.util.* + +/** + * Factory of [BotCommandsMessages], the default implementation is [DefaultBotCommandsMessagesFactory], or, + * if a non-default [DefaultMessagesFactory] exists, an adapter is used. + * + * ### Complete customization + * + * Returning a [BotCommandsMessagesFactory] from a service factory will disable the default implementation, + * this will let you return a completely custom instance, + * in which you can craft entirely custom messages in any way you see fit. + */ +@InterfacedService(acceptMultiple = false) +interface BotCommandsMessagesFactory { + /** + * Retrieves a [BotCommandsMessages] instance for the given locale. + */ + fun get(locale: Locale): BotCommandsMessages + + /** + * Retrieves a [BotCommandsMessages] instance, with the locale derived from this event. + * + * By default, this uses [TextCommandLocaleProvider] to get the locale. + */ + fun get(event: MessageReceivedEvent): BotCommandsMessages + + /** + * Retrieves a [BotCommandsMessages] instance, with the locale derived from this interaction. + * + * By default, this uses [UserLocaleProvider] to get the locale. + */ + fun get(event: Interaction): BotCommandsMessages +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/freya022/botcommands/api/core/messages/DefaultBotCommandsMessages.kt b/src/main/kotlin/io/github/freya022/botcommands/api/core/messages/DefaultBotCommandsMessages.kt new file mode 100644 index 0000000000..187593966d --- /dev/null +++ b/src/main/kotlin/io/github/freya022/botcommands/api/core/messages/DefaultBotCommandsMessages.kt @@ -0,0 +1,122 @@ +package io.github.freya022.botcommands.api.core.messages + +import io.github.freya022.botcommands.api.commands.application.slash.options.SlashCommandOption +import io.github.freya022.botcommands.api.commands.text.TopLevelTextCommandInfo +import io.github.freya022.botcommands.api.core.messages.exceptions.MissingMessageTemplateException +import io.github.freya022.botcommands.api.localization.Localization +import io.github.freya022.botcommands.api.localization.LocalizationService +import io.github.freya022.botcommands.api.localization.LocalizationTemplate +import io.github.freya022.botcommands.api.localization.PermissionLocalization +import io.github.freya022.botcommands.api.localization.localize +import io.github.freya022.botcommands.internal.utils.throwArgument +import net.dv8tion.jda.api.Permission +import net.dv8tion.jda.api.events.GenericEvent +import net.dv8tion.jda.api.utils.TimeFormat +import net.dv8tion.jda.api.utils.messages.MessageCreateData +import java.time.Instant +import java.util.* +import kotlin.to + +/** + * Default implementation of [BotCommandsMessages], + * see [DefaultBotCommandsMessagesFactory] for more details. + * + * @see DefaultBotCommandsMessagesFactory + */ +open class DefaultBotCommandsMessages( + protected val permissionLocalization: PermissionLocalization, + localizationService: LocalizationService, + protected val locale: Locale, + bundleName: String, +) : BotCommandsMessages { + + protected val localization: Localization = localizationService.getInstance(bundleName, locale) + ?: throwArgument("Could not find localization files for '$bundleName'") + + override fun uncaughtException(event: GenericEvent?): MessageCreateData { + return getLocalizationTemplate("uncaught_exception").localize().toMessage() + } + + override fun missingUserPermissions(event: GenericEvent?, permissions: Set): MessageCreateData { + val localizedPermissions = permissions.joinToString(separator = ", ") { permissionLocalization.localize(it, locale) } + return getLocalizationTemplate("missing.permissions.user").localize("permissions" to localizedPermissions).toMessage() + } + + override fun missingBotPermissions(event: GenericEvent?, permissions: Set): MessageCreateData { + val localizedPermissions = permissions.joinToString(separator = ", ") { permissionLocalization.localize(it, locale) } + return getLocalizationTemplate("missing.permissions.bot").localize("permissions" to localizedPermissions).toMessage() + } + + override fun ownerOnly(event: GenericEvent?): MessageCreateData { + return getLocalizationTemplate("owner_only").localize().toMessage() + } + + override fun userRateLimited(event: GenericEvent?, deadline: Instant): MessageCreateData { + val args = "timestamp" to TimeFormat.RELATIVE.atInstant(deadline) + return getLocalizationTemplate("ratelimited.user").localize(args).toMessage() + } + + override fun channelRateLimited(event: GenericEvent?, deadline: Instant): MessageCreateData { + val timestamp = TimeFormat.RELATIVE.atInstant(deadline) + return getLocalizationTemplate("ratelimited.channel").localize("timestamp" to timestamp).toMessage() + } + + override fun guildRateLimited(event: GenericEvent?, deadline: Instant): MessageCreateData { + val timestamp = TimeFormat.RELATIVE.atInstant(deadline) + return getLocalizationTemplate("ratelimited.guild").localize("timestamp" to timestamp).toMessage() + } + + override fun applicationCommandsNotAvailable(event: GenericEvent?): MessageCreateData { + return getLocalizationTemplate("commands.application.not_available").localize().toMessage() + } + + override fun commandNotFound(event: GenericEvent?, suggestions: Collection): MessageCreateData { + val suggestionsStr = suggestions.joinToString(separator = "**, **", prefix = "**", postfix = "**") { it.name } + return getLocalizationTemplate("commands.text.not_found").localize("suggestions" to suggestionsStr).toMessage() + } + + override fun resolverChannelNotFound(event: GenericEvent?, channelId: Long): MessageCreateData { + return getLocalizationTemplate("resolver.channel.not_found").localize("channel_id" to channelId).toMessage() + } + + override fun resolverChannelMissingAccess(event: GenericEvent?, channelId: Long): MessageCreateData { + return getLocalizationTemplate("resolver.channel.missing_access").localize("channel_id" to channelId).toMessage() + } + + override fun resolverUserNotFound(event: GenericEvent?, userId: Long): MessageCreateData { + return getLocalizationTemplate("resolver.user.not_found").localize("user_id" to userId).toMessage() + } + + override fun slashCommandUnresolvableOption(event: GenericEvent?, option: SlashCommandOption): MessageCreateData { + return getLocalizationTemplate("commands.slash.option.unresolvable").localize("option_name" to option.discordName).toMessage() + } + + override fun closedDirectMessages(event: GenericEvent?): MessageCreateData { + return getLocalizationTemplate("direct_messages.closed").localize().toMessage() + } + + override fun nsfwOnly(event: GenericEvent?): MessageCreateData { + return getLocalizationTemplate("nsfw_only").localize().toMessage() + } + + override fun componentNotAllowed(event: GenericEvent?): MessageCreateData { + return getLocalizationTemplate("components.not_allowed").localize().toMessage() + } + + override fun componentExpired(event: GenericEvent?): MessageCreateData { + return getLocalizationTemplate("components.expired").localize().toMessage() + } + + override fun modalExpired(event: GenericEvent?): MessageCreateData { + return getLocalizationTemplate("modals.expired").localize().toMessage() + } + + protected fun getLocalizationTemplate(path: String): LocalizationTemplate { + val template = localization[path] + ?: throw MissingMessageTemplateException("Template '$path' could not be found, available keys: ${localization.keys}") + + return template + } + + protected fun String.toMessage(): MessageCreateData = MessageCreateData.fromContent(this) +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/freya022/botcommands/api/core/messages/DefaultBotCommandsMessagesFactory.kt b/src/main/kotlin/io/github/freya022/botcommands/api/core/messages/DefaultBotCommandsMessagesFactory.kt new file mode 100644 index 0000000000..9ab28e83b0 --- /dev/null +++ b/src/main/kotlin/io/github/freya022/botcommands/api/core/messages/DefaultBotCommandsMessagesFactory.kt @@ -0,0 +1,59 @@ +package io.github.freya022.botcommands.api.core.messages + +import io.github.freya022.botcommands.api.localization.Localization +import io.github.freya022.botcommands.api.localization.LocalizationService +import io.github.freya022.botcommands.api.localization.PermissionLocalization +import io.github.freya022.botcommands.api.localization.interaction.UserLocaleProvider +import io.github.freya022.botcommands.api.localization.text.TextCommandLocaleProvider +import net.dv8tion.jda.api.events.message.MessageReceivedEvent +import net.dv8tion.jda.api.interactions.Interaction +import java.util.* +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock + +/** + * Default implementation of [BotCommandsMessagesFactory]. + * + * Instances returned by this factory are [DefaultBotCommandsMessages], + * which uses [LocalizationService] to get a localization bundle using the provided [bundleName]. + * + * By default, an instance of this factory is registered using the default bundle name (`BotCommandsMessages`), + * you can override the default factory by providing a corresponding service factory. + * + * ### Light customization / Supporting more locales + * + * With the default values, the localization templates would be loaded from `/bc_localization/BotCommandsMessages-default.json`, + * you may change the values by creating a new `BotCommandsMessages.json`, + * you can also support more locales following by appending an underscore and the [language tag][Locale.toLanguageTag], + * such as `BotCommandsMessages_fr.json`. + * + * The localization paths must be identical to those used by [DefaultBotCommandsMessages], + * but the placeholders can be modified but must keep the same names, they can also be removed. + * + * Refer to [Localization] for mode customization details. + * + * @see Localization + */ +class DefaultBotCommandsMessagesFactory( + private val permissionLocalization: PermissionLocalization, + private val localizationService: LocalizationService, + private val textCommandLocaleProvider: TextCommandLocaleProvider, + private val userLocaleProvider: UserLocaleProvider, + private val bundleName: String = "BotCommandsMessages", +) : BotCommandsMessagesFactory { + + private val cache: MutableMap = hashMapOf() + private val lock = ReentrantLock() + + override fun get(locale: Locale): DefaultBotCommandsMessages { + cache[locale]?.let { return it } + + return lock.withLock { + cache.getOrPut(locale) { DefaultBotCommandsMessages(permissionLocalization, localizationService, locale, bundleName) } + } + } + + override fun get(event: MessageReceivedEvent): DefaultBotCommandsMessages = get(textCommandLocaleProvider.getLocale(event)) + + override fun get(event: Interaction): DefaultBotCommandsMessages = get(userLocaleProvider.getLocale(event)) +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/freya022/botcommands/api/core/messages/exceptions/MissingMessageTemplateException.kt b/src/main/kotlin/io/github/freya022/botcommands/api/core/messages/exceptions/MissingMessageTemplateException.kt new file mode 100644 index 0000000000..b17b6c19e6 --- /dev/null +++ b/src/main/kotlin/io/github/freya022/botcommands/api/core/messages/exceptions/MissingMessageTemplateException.kt @@ -0,0 +1,7 @@ +package io.github.freya022.botcommands.api.core.messages.exceptions + +/** + * Exception thrown when a localization template could not be found + * by a [BotCommandsMessages][io.github.freya022.botcommands.api.core.messages.BotCommandsMessages] instance. + */ +class MissingMessageTemplateException(message: String) : IllegalArgumentException(message) \ No newline at end of file diff --git a/src/main/kotlin/io/github/freya022/botcommands/api/core/service/annotations/InterfacedService.kt b/src/main/kotlin/io/github/freya022/botcommands/api/core/service/annotations/InterfacedService.kt index 253377a60c..107fafee43 100644 --- a/src/main/kotlin/io/github/freya022/botcommands/api/core/service/annotations/InterfacedService.kt +++ b/src/main/kotlin/io/github/freya022/botcommands/api/core/service/annotations/InterfacedService.kt @@ -14,8 +14,8 @@ import io.github.freya022.botcommands.api.components.ComponentInteractionFilter import io.github.freya022.botcommands.api.core.* import io.github.freya022.botcommands.api.core.db.ConnectionSupplier import io.github.freya022.botcommands.api.core.db.query.ParametrizedQueryFactory +import io.github.freya022.botcommands.api.core.messages.BotCommandsMessagesFactory import io.github.freya022.botcommands.api.core.service.ServiceContainer -import io.github.freya022.botcommands.api.localization.DefaultMessagesFactory import io.github.freya022.botcommands.api.localization.arguments.factories.FormattableArgumentFactory import io.github.freya022.botcommands.api.localization.providers.LocalizationMapProvider import io.github.freya022.botcommands.api.localization.readers.LocalizationMapReader @@ -37,7 +37,7 @@ import io.github.freya022.botcommands.api.localization.readers.LocalizationMapRe * @see ICoroutineEventManagerSupplier * @see JDAService * - * @see DefaultMessagesFactory + * @see BotCommandsMessagesFactory * * @see GlobalExceptionHandler * diff --git a/src/main/kotlin/io/github/freya022/botcommands/api/core/utils/JDA.kt b/src/main/kotlin/io/github/freya022/botcommands/api/core/utils/JDA.kt index 8696177c57..92362e47f5 100644 --- a/src/main/kotlin/io/github/freya022/botcommands/api/core/utils/JDA.kt +++ b/src/main/kotlin/io/github/freya022/botcommands/api/core/utils/JDA.kt @@ -1,3 +1,5 @@ +@file:Suppress("removal", "DEPRECATION") + package io.github.freya022.botcommands.api.core.utils import dev.minn.jda.ktx.coroutines.await @@ -5,7 +7,7 @@ import dev.minn.jda.ktx.messages.InlineMessage import dev.minn.jda.ktx.messages.MessageCreate import dev.minn.jda.ktx.messages.MessageEdit import io.github.freya022.botcommands.api.core.exceptions.InvalidChannelTypeException -import io.github.freya022.botcommands.api.localization.DefaultMessages +import io.github.freya022.botcommands.api.localization.PermissionLocalization import io.github.freya022.botcommands.internal.utils.deferredRestAction import io.github.freya022.botcommands.internal.utils.takeIfFinite import net.dv8tion.jda.api.JDA @@ -240,9 +242,9 @@ inline fun suppressContentWarning(block: () -> R): R { /** * Computes the missing permissions from the specified permission holder, - * If you plan on showing them, be sure to use [DefaultMessages.getPermission] + * if you plan on showing them, be sure to use [PermissionLocalization.localize]. * - * @see DefaultMessages.getPermission + * @see PermissionLocalization.localize */ fun getMissingPermissions(requiredPerms: EnumSet, permissionHolder: IPermissionHolder, channel: GuildChannel): Set = EnumSet.copyOf(requiredPerms).also { it.removeAll(permissionHolder.getPermissions(channel)) } diff --git a/src/main/kotlin/io/github/freya022/botcommands/api/localization/DefaultMessagesFactory.kt b/src/main/kotlin/io/github/freya022/botcommands/api/localization/DefaultMessagesFactory.kt index 0b2064aa7b..0a52848b2c 100644 --- a/src/main/kotlin/io/github/freya022/botcommands/api/localization/DefaultMessagesFactory.kt +++ b/src/main/kotlin/io/github/freya022/botcommands/api/localization/DefaultMessagesFactory.kt @@ -1,3 +1,5 @@ +@file:Suppress("removal", "DEPRECATION") + package io.github.freya022.botcommands.api.localization import io.github.freya022.botcommands.api.core.service.annotations.BService @@ -15,6 +17,7 @@ import java.util.* * * @see InterfacedService @InterfacedService */ +@Deprecated("Replaced by BotCommandsMessagesFactory") @InterfacedService(acceptMultiple = false) interface DefaultMessagesFactory { /** diff --git a/src/main/kotlin/io/github/freya022/botcommands/api/localization/DefaultPermissionLocalization.kt b/src/main/kotlin/io/github/freya022/botcommands/api/localization/DefaultPermissionLocalization.kt new file mode 100644 index 0000000000..20cc7ddd2b --- /dev/null +++ b/src/main/kotlin/io/github/freya022/botcommands/api/localization/DefaultPermissionLocalization.kt @@ -0,0 +1,44 @@ +package io.github.freya022.botcommands.api.localization + +import net.dv8tion.jda.api.Permission +import java.util.* +import java.util.concurrent.ConcurrentHashMap + +/** + * Default implementation for [PermissionLocalization]. + * + * The translations are queried from [bundleName], + * and the keys used are the enum name of the permission. + * + * The returned translations will try to use the nearest available locale, + * with the last fallback being [Permission.getName]. + * + * ### Light customization / Supporting more locales + * + * To support more locales, (here we supposed the [bundleName] is `Permissions`) you may create a new `Permissions.json` + * following by appending an underscore and the [language tag][Locale.toLanguageTag], + * such as `Permissions_fr.json`. + * + * The localization paths must be equal to [Permission.getName]. + * + * Refer to [Localization] for mode customization details. + */ +open class DefaultPermissionLocalization( + protected val localizationService: LocalizationService, + protected val bundleName: String = "Permissions", +) : PermissionLocalization { + + private val cache: MutableMap = ConcurrentHashMap() + + override fun localize(permission: Permission, locale: Locale): String { + val key = CacheKey(permission, locale) + return cache.getOrPut(key) { + val permissionsLocalization: Localization? = localizationService.getInstance(bundleName, locale) + + @Suppress("UsePropertyAccessSyntax") // `permission.name` targets Enum#name() which is definitely not the same + permissionsLocalization?.get(permission.name)?.localize() ?: permission.getName() + } + } + + private data class CacheKey(val permission: Permission, val locale: Locale) +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/freya022/botcommands/api/localization/LocalizableAction.kt b/src/main/kotlin/io/github/freya022/botcommands/api/localization/LocalizableAction.kt index 257a4e2623..7ec9e0c85d 100644 --- a/src/main/kotlin/io/github/freya022/botcommands/api/localization/LocalizableAction.kt +++ b/src/main/kotlin/io/github/freya022/botcommands/api/localization/LocalizableAction.kt @@ -1,6 +1,8 @@ package io.github.freya022.botcommands.api.localization import io.github.freya022.botcommands.api.core.config.BLocalizationConfig +import io.github.freya022.botcommands.api.core.messages.BotCommandsMessages +import io.github.freya022.botcommands.api.core.messages.BotCommandsMessagesFactory import io.github.freya022.botcommands.api.localization.context.LocalizationContext import io.github.freya022.botcommands.api.localization.context.PairEntry import io.github.freya022.botcommands.api.localization.context.mapToEntries @@ -43,8 +45,17 @@ interface LocalizableAction { * * @see DefaultMessagesFactory */ + @Suppress("DEPRECATION", "removal") + @Deprecated("Replaced with getBotCommandsMessages()") fun getDefaultMessages(): DefaultMessages + /** + * Retrieves a [BotCommandsMessages] instance, using a locale suitable for messages sent to the user. + * + * @see BotCommandsMessagesFactory + */ + fun getBotCommandsMessages(): BotCommandsMessages + /** * Returns the localized message at the following [path][localizationPath], * using the provided locale and parameters. diff --git a/src/main/kotlin/io/github/freya022/botcommands/api/localization/PermissionLocalization.kt b/src/main/kotlin/io/github/freya022/botcommands/api/localization/PermissionLocalization.kt new file mode 100644 index 0000000000..c06b9b2dc7 --- /dev/null +++ b/src/main/kotlin/io/github/freya022/botcommands/api/localization/PermissionLocalization.kt @@ -0,0 +1,21 @@ +package io.github.freya022.botcommands.api.localization + +import io.github.freya022.botcommands.api.core.service.annotations.InterfacedService +import net.dv8tion.jda.api.Permission +import java.util.* + +/** + * Utility service to translate [Permission] names, a [DefaultPermissionLocalization] instance is available by default, + * but can be overridden if necessary, using a service factory. + */ +@InterfacedService(acceptMultiple = false) +interface PermissionLocalization { + + /** + * Returns the given permission's name with the requested locale. + * + * If no translation is available for the requested locale, + * it is permitted to return fallbacks. + */ + fun localize(permission: Permission, locale: Locale): String +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/freya022/botcommands/api/parameters/resolvers/SlashParameterResolver.kt b/src/main/kotlin/io/github/freya022/botcommands/api/parameters/resolvers/SlashParameterResolver.kt index 1ed134d94e..436128aaae 100644 --- a/src/main/kotlin/io/github/freya022/botcommands/api/parameters/resolvers/SlashParameterResolver.kt +++ b/src/main/kotlin/io/github/freya022/botcommands/api/parameters/resolvers/SlashParameterResolver.kt @@ -6,7 +6,7 @@ import io.github.freya022.botcommands.api.commands.application.slash.annotations import io.github.freya022.botcommands.api.commands.application.slash.autocomplete.annotations.AutocompleteHandler import io.github.freya022.botcommands.api.commands.application.slash.options.SlashCommandOption import io.github.freya022.botcommands.api.commands.application.slash.options.builder.SlashCommandOptionBuilder -import io.github.freya022.botcommands.api.localization.DefaultMessages +import io.github.freya022.botcommands.api.core.messages.BotCommandsMessages import io.github.freya022.botcommands.api.parameters.ParameterResolver import net.dv8tion.jda.api.entities.Guild import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent @@ -57,7 +57,7 @@ interface SlashParameterResolver : IParameterResolver * and you should reply if this is a [SlashCommandInteractionEvent]. * * If the interaction is not replied to, - * the handler sends an [unresolvable option error message][DefaultMessages.getSlashCommandUnresolvableOptionMsg]. + * the handler sends an [unresolvable option error message][BotCommandsMessages.slashCommandUnresolvableOption]. * * @param option The option currently being resolved * @param event The corresponding event, could be a [SlashCommandInteractionEvent] or a [CommandAutoCompleteInteractionEvent] @@ -74,7 +74,7 @@ interface SlashParameterResolver : IParameterResolver * and you should reply if this is a [SlashCommandInteractionEvent]. * * If the interaction is not replied to, - * the handler sends an [unresolvable option error message][DefaultMessages.getSlashCommandUnresolvableOptionMsg]. + * the handler sends an [unresolvable option error message][BotCommandsMessages.slashCommandUnresolvableOption]. * * @param option The option currently being resolved * @param event The corresponding event, could be a [SlashCommandInteractionEvent] or a [CommandAutoCompleteInteractionEvent] diff --git a/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/ApplicationCommandListener.kt b/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/ApplicationCommandListener.kt index a3ae3f41ab..76d0243ea7 100644 --- a/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/ApplicationCommandListener.kt +++ b/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/ApplicationCommandListener.kt @@ -1,6 +1,5 @@ package io.github.freya022.botcommands.internal.commands.application -import dev.minn.jda.ktx.messages.reply_ import io.github.freya022.botcommands.api.commands.Usability.UnusableReason import io.github.freya022.botcommands.api.commands.application.ApplicationCommandFilter import io.github.freya022.botcommands.api.commands.application.annotations.RequiresApplicationCommands @@ -16,10 +15,11 @@ import io.github.freya022.botcommands.api.core.BContext import io.github.freya022.botcommands.api.core.annotations.BEventListener import io.github.freya022.botcommands.api.core.checkFilters import io.github.freya022.botcommands.api.core.entities.inputUser +import io.github.freya022.botcommands.api.core.messages.BotCommandsMessages +import io.github.freya022.botcommands.api.core.messages.BotCommandsMessagesFactory import io.github.freya022.botcommands.api.core.service.annotations.BService import io.github.freya022.botcommands.api.core.service.getService import io.github.freya022.botcommands.api.core.utils.getMissingPermissions -import io.github.freya022.botcommands.api.localization.DefaultMessagesFactory import io.github.freya022.botcommands.internal.commands.application.cache.factory.ApplicationCommandsCacheFactory import io.github.freya022.botcommands.internal.commands.application.context.message.MessageCommandInfoImpl import io.github.freya022.botcommands.internal.commands.application.context.user.UserCommandInfoImpl @@ -40,6 +40,8 @@ import net.dv8tion.jda.api.events.interaction.command.MessageContextInteractionE import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent import net.dv8tion.jda.api.events.interaction.command.UserContextInteractionEvent import net.dv8tion.jda.api.exceptions.InsufficientPermissionException +import net.dv8tion.jda.api.interactions.Interaction +import net.dv8tion.jda.api.utils.messages.MessageCreateData private val logger = KotlinLogging.logger { } @@ -48,7 +50,7 @@ private val logger = KotlinLogging.logger { } internal class ApplicationCommandListener internal constructor( private val context: BContext, private val applicationCommandsBuilder: ApplicationCommandsBuilder, - private val defaultMessagesFactory: DefaultMessagesFactory, + private val messagesFactory: BotCommandsMessagesFactory, private val localizableInteractionFactory: LocalizableInteractionFactory, private val rateLimitHandler: RateLimitHandler, filters: List, @@ -144,7 +146,7 @@ internal class ApplicationCommandListener internal constructor( } else { logger.debug { "Ignored '${event.fullCommandName}' as guild (${guild!!.id}) commands could not be updated" } } - return event.reply_(defaultMessagesFactory.get(event).applicationCommandsNotAvailableMsg, ephemeral = true).queue() + return event.reply(messagesFactory.get(event).applicationCommandsNotAvailable(event)).setEphemeral(true).queue() } //This is done so warnings are printed after the exception @@ -225,29 +227,31 @@ internal class ApplicationCommandListener internal constructor( exceptionHandler.handleException(event, e, "application command '${event.commandString}'", emptyMap(), logLevel) if (e is InsufficientPermissionException) { - event.replyExceptionMessage(defaultMessagesFactory.get(event).getBotPermErrorMsg(setOf(e.permission))) + event.replyExceptionMessage(messagesFactory.get(event).missingBotPermissions(event, setOf(e.permission))) } else { - event.replyExceptionMessage(defaultMessagesFactory.get(event).generalErrorMsg) + event.replyExceptionMessage(messagesFactory.get(event).uncaughtException(event)) } } private suspend fun canRun(event: GenericCommandInteractionEvent, applicationCommand: ApplicationCommandInfoImpl): Boolean { val usability = applicationCommand.getUsability(event.inputUser, event.messageChannel) if (usability.isNotUsable) { - val errorMessage: String = when (usability.bestReason) { - UnusableReason.OWNER_ONLY -> defaultMessagesFactory.get(event).ownerOnlyErrorMsg - UnusableReason.USER_PERMISSIONS -> { - val member = event.member ?: throwInternal("USER_PERMISSIONS got checked even if guild is null") - val missingPermissions = getMissingPermissions(applicationCommand.userPermissions, member, event.guildChannel) - defaultMessagesFactory.get(event).getUserPermErrorMsg(missingPermissions) - } - UnusableReason.BOT_PERMISSIONS -> { - val guild = event.guild ?: throwInternal("BOT_PERMISSIONS got checked even if guild is null") - val missingPermissions = getMissingPermissions(applicationCommand.botPermissions, guild.selfMember, event.guildChannel) - defaultMessagesFactory.get(event).getBotPermErrorMsg(missingPermissions) + val errorMessage = fromMessages(event) { + when (usability.bestReason) { + UnusableReason.OWNER_ONLY -> ownerOnly(event) + UnusableReason.USER_PERMISSIONS -> { + val member = event.member ?: throwInternal("USER_PERMISSIONS got checked even if guild is null") + val missingPermissions = getMissingPermissions(applicationCommand.userPermissions, member, event.guildChannel) + missingUserPermissions(event, missingPermissions) + } + UnusableReason.BOT_PERMISSIONS -> { + val guild = event.guild ?: throwInternal("BOT_PERMISSIONS got checked even if guild is null") + val missingPermissions = getMissingPermissions(applicationCommand.botPermissions, guild.selfMember, event.guildChannel) + missingBotPermissions(event, missingPermissions) + } + UnusableReason.NSFW_ONLY -> throwInternal("Discord already handles NSFW commands") + UnusableReason.HIDDEN -> throwInternal("Application commands can't be hidden") } - UnusableReason.NSFW_ONLY -> throwInternal("Discord already handles NSFW commands") - UnusableReason.HIDDEN -> throwInternal("Application commands can't be hidden") } reply(event, errorMessage) return false @@ -268,10 +272,15 @@ internal class ApplicationCommandListener internal constructor( return true } - private fun reply(event: GenericCommandInteractionEvent, msg: String) { - event.reply_(msg, ephemeral = true) + private fun reply(event: GenericCommandInteractionEvent, message: MessageCreateData) { + event.reply(message) + .setEphemeral(true) .queue(null) { throwable -> exceptionHandler.handleException(event, throwable, "interaction reply", emptyMap()) } } + + private inline fun fromMessages(event: Interaction, crossinline block: BotCommandsMessages.() -> MessageCreateData): MessageCreateData { + return messagesFactory.get(event).run(block) + } } \ No newline at end of file diff --git a/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/SlashCommandInfoImpl.kt b/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/SlashCommandInfoImpl.kt index 6e82e6bc07..95959175ca 100644 --- a/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/SlashCommandInfoImpl.kt +++ b/src/main/kotlin/io/github/freya022/botcommands/internal/commands/application/slash/SlashCommandInfoImpl.kt @@ -1,14 +1,13 @@ package io.github.freya022.botcommands.internal.commands.application.slash -import dev.minn.jda.ktx.messages.reply_ import io.github.freya022.botcommands.api.commands.INamedCommand import io.github.freya022.botcommands.api.commands.application.slash.GlobalSlashEvent import io.github.freya022.botcommands.api.commands.application.slash.GuildSlashEvent import io.github.freya022.botcommands.api.commands.application.slash.SlashCommandInfo import io.github.freya022.botcommands.api.core.BContext +import io.github.freya022.botcommands.api.core.messages.BotCommandsMessagesFactory import io.github.freya022.botcommands.api.core.service.getService import io.github.freya022.botcommands.api.core.utils.simpleNestedName -import io.github.freya022.botcommands.api.localization.DefaultMessagesFactory import io.github.freya022.botcommands.internal.* import io.github.freya022.botcommands.internal.commands.application.ApplicationCommandInfoImpl import io.github.freya022.botcommands.internal.commands.application.options.ApplicationGeneratedOption @@ -156,11 +155,10 @@ private fun onUnresolvableOption( else -> { //Only use the generic message if the user didn't handle this situation if (!event.isAcknowledged && event is SlashCommandInteractionEvent) { - val defaultMessages = option.context.getService().get(event) - event.reply_( - defaultMessages.getSlashCommandUnresolvableOptionMsg(option.discordName), - ephemeral = true - ).queue() + val defaultMessages = option.context.getService().get(event) + event.reply(defaultMessages.slashCommandUnresolvableOption(event, option)) + .setEphemeral(true) + .queue() } InsertOptionResult.ABORT diff --git a/src/main/kotlin/io/github/freya022/botcommands/internal/commands/ratelimit/handler/RateLimitHandler.kt b/src/main/kotlin/io/github/freya022/botcommands/internal/commands/ratelimit/handler/RateLimitHandler.kt index ab3f2d1052..a1067f024a 100644 --- a/src/main/kotlin/io/github/freya022/botcommands/internal/commands/ratelimit/handler/RateLimitHandler.kt +++ b/src/main/kotlin/io/github/freya022/botcommands/internal/commands/ratelimit/handler/RateLimitHandler.kt @@ -1,14 +1,13 @@ package io.github.freya022.botcommands.internal.commands.ratelimit.handler -import dev.minn.jda.ktx.messages.reply_ import io.github.bucket4j.Bucket import io.github.freya022.botcommands.api.commands.ratelimit.CancellableRateLimit import io.github.freya022.botcommands.api.core.BContext import io.github.freya022.botcommands.api.core.BotOwners import io.github.freya022.botcommands.api.core.config.BConfig +import io.github.freya022.botcommands.api.core.messages.BotCommandsMessagesFactory import io.github.freya022.botcommands.api.core.service.annotations.BService import io.github.freya022.botcommands.api.core.utils.loggerOf -import io.github.freya022.botcommands.api.localization.DefaultMessagesFactory import io.github.freya022.botcommands.internal.commands.application.ApplicationCommandInfoImpl import io.github.freya022.botcommands.internal.commands.ratelimit.CancellableRateLimitImpl import io.github.freya022.botcommands.internal.commands.ratelimit.NullCancellableRateLimit @@ -28,7 +27,7 @@ internal class RateLimitHandler internal constructor( private val context: BContext, private val botOwners: BotOwners, private val rateLimitContainer: RateLimitContainer, - private val defaultMessagesFactory: DefaultMessagesFactory, + private val messagesFactory: BotCommandsMessagesFactory, config: BConfig, ) { private val enableOwnerBypass = config.enableOwnerBypass @@ -91,8 +90,7 @@ internal class RateLimitHandler internal constructor( val rateLimitInfo = rateLimitContainer[group] ?: run { componentsListenerLogger.warn { "Could not find a rate limiter named '$group'" } - val defaultMessages = defaultMessagesFactory.get(event) - event.reply_(defaultMessages.componentExpiredErrorMsg, ephemeral = true).queue() + event.reply(messagesFactory.get(event).componentExpired(event)).setEphemeral(true).queue() return } diff --git a/src/main/kotlin/io/github/freya022/botcommands/internal/commands/text/HelpCommand.kt b/src/main/kotlin/io/github/freya022/botcommands/internal/commands/text/HelpCommand.kt index f761a19e5f..507af8c933 100644 --- a/src/main/kotlin/io/github/freya022/botcommands/internal/commands/text/HelpCommand.kt +++ b/src/main/kotlin/io/github/freya022/botcommands/internal/commands/text/HelpCommand.kt @@ -7,6 +7,7 @@ import io.github.freya022.botcommands.api.commands.text.annotations.RequiresText import io.github.freya022.botcommands.api.commands.text.provider.TextCommandManager import io.github.freya022.botcommands.api.commands.text.provider.TextCommandProvider import io.github.freya022.botcommands.api.core.config.BTextConfig +import io.github.freya022.botcommands.api.core.messages.BotCommandsMessagesFactory import io.github.freya022.botcommands.api.core.service.ConditionalServiceChecker import io.github.freya022.botcommands.api.core.service.ServiceContainer import io.github.freya022.botcommands.api.core.service.annotations.BService @@ -14,7 +15,6 @@ import io.github.freya022.botcommands.api.core.service.annotations.ConditionalSe import io.github.freya022.botcommands.api.core.service.getInterfacedServices import io.github.freya022.botcommands.api.core.service.getService import io.github.freya022.botcommands.api.core.utils.* -import io.github.freya022.botcommands.api.localization.DefaultMessagesFactory import io.github.freya022.botcommands.internal.commands.text.TextUtils.getSpacedPath import io.github.freya022.botcommands.internal.core.BContextImpl import io.github.freya022.botcommands.internal.utils.reference @@ -44,14 +44,14 @@ internal open class BuiltInHelpCommandProvider { @ConditionalOnMissingBean(IHelpCommand::class) internal open fun builtInHelpCommand( context: BContextImpl, - defaultMessagesFactory: DefaultMessagesFactory, + messagesFactory: BotCommandsMessagesFactory, textCommandsContext: TextCommandsContext, helpBuilderConsumer: HelpBuilderConsumer?, - ) = HelpCommand(context, defaultMessagesFactory, textCommandsContext, helpBuilderConsumer) + ) = HelpCommand(context, messagesFactory, textCommandsContext, helpBuilderConsumer) } internal class HelpCommand internal constructor( private val context: BContextImpl, - private val defaultMessagesFactory: DefaultMessagesFactory, + private val messagesFactory: BotCommandsMessagesFactory, private val textCommandsContext: TextCommandsContext, private val helpBuilderConsumer: HelpBuilderConsumer? ) : IHelpCommand, TextCommandProvider { @@ -118,7 +118,7 @@ internal class HelpCommand internal constructor( // Ignore and reply in channel/react if we can't send to DMs .handle(ErrorResponse.CANNOT_SEND_TO_USER) { if (event.channel.canTalk()) - event.respond(defaultMessagesFactory.get(event).closedDMErrorMsg).await() + event.channel.sendMessage(messagesFactory.get(event).closedDirectMessages(event)).await() else if (hasReactionPermissions) // May throw REACTION_BLOCKED event.message.addReaction(context.textConfig.dmClosedEmoji).await() diff --git a/src/main/kotlin/io/github/freya022/botcommands/internal/commands/text/TextCommandsListener.kt b/src/main/kotlin/io/github/freya022/botcommands/internal/commands/text/TextCommandsListener.kt index 493a42b3ad..3d47aaa461 100644 --- a/src/main/kotlin/io/github/freya022/botcommands/internal/commands/text/TextCommandsListener.kt +++ b/src/main/kotlin/io/github/freya022/botcommands/internal/commands/text/TextCommandsListener.kt @@ -10,6 +10,8 @@ import io.github.freya022.botcommands.api.core.JDAService import io.github.freya022.botcommands.api.core.annotations.BEventListener import io.github.freya022.botcommands.api.core.checkFilters import io.github.freya022.botcommands.api.core.config.BTextConfig +import io.github.freya022.botcommands.api.core.messages.BotCommandsMessages +import io.github.freya022.botcommands.api.core.messages.BotCommandsMessagesFactory import io.github.freya022.botcommands.api.core.service.ConditionalServiceChecker import io.github.freya022.botcommands.api.core.service.ServiceContainer import io.github.freya022.botcommands.api.core.service.annotations.BService @@ -17,7 +19,6 @@ import io.github.freya022.botcommands.api.core.service.annotations.ConditionalSe import io.github.freya022.botcommands.api.core.service.getService import io.github.freya022.botcommands.api.core.service.getServiceOrNull import io.github.freya022.botcommands.api.core.utils.* -import io.github.freya022.botcommands.api.localization.DefaultMessagesFactory import io.github.freya022.botcommands.internal.commands.ratelimit.handler.RateLimitHandler import io.github.freya022.botcommands.internal.commands.text.TextCommandsListener.Status.* import io.github.freya022.botcommands.internal.core.ExceptionHandler @@ -32,6 +33,7 @@ import net.dv8tion.jda.api.events.message.MessageReceivedEvent import net.dv8tion.jda.api.exceptions.InsufficientPermissionException import net.dv8tion.jda.api.requests.ErrorResponse import net.dv8tion.jda.api.requests.GatewayIntent +import net.dv8tion.jda.api.utils.messages.MessageCreateData private val logger = KotlinLogging.logger { } private val spacePattern = Regex("\\s+") @@ -42,7 +44,7 @@ private val spacePattern = Regex("\\s+") @ConditionalService(TextCommandsListener.ActivationCondition::class) internal class TextCommandsListener internal constructor( private val context: BContext, - private val defaultMessagesFactory: DefaultMessagesFactory, + private val messagesFactory: BotCommandsMessagesFactory, private val textCommandsContext: TextCommandsContextImpl, private val localizableTextCommandFactory: LocalizableTextCommandFactory, private val rateLimitHandler: RateLimitHandler, @@ -132,9 +134,9 @@ internal class TextCommandsListener internal constructor( private suspend fun handleException(event: MessageReceivedEvent, e: Throwable, msg: String) { exceptionHandler.handleException(event, e, "text command '$msg'", mapOf("Message" to event.jumpUrl)) if (e is InsufficientPermissionException) { - replyError(event, defaultMessagesFactory.get(event).getBotPermErrorMsg(setOf(e.permission))) + replyError(event, messagesFactory.get(event).missingBotPermissions(event, setOf(e.permission))) } else { - replyError(event, defaultMessagesFactory.get(event).generalErrorMsg) + replyError(event, messagesFactory.get(event).uncaughtException(event)) } } @@ -175,19 +177,22 @@ internal class TextCommandsListener internal constructor( val usability = commandInfo.getUsability(member, event.guildChannel) if (usability.isNotUsable) { - val errorMessage: String = when (usability.bestReason) { - UnusableReason.HIDDEN -> throwInternal("Hidden commands should have been ignored by ${TextCommandsListener::findCommandWithArgs.shortSignature}") - UnusableReason.OWNER_ONLY -> defaultMessagesFactory.get(event).ownerOnlyErrorMsg - UnusableReason.USER_PERMISSIONS -> { - val missingPermissions = getMissingPermissions(commandInfo.userPermissions, member, event.guildChannel) - defaultMessagesFactory.get(event).getUserPermErrorMsg(missingPermissions) - } - UnusableReason.BOT_PERMISSIONS -> { - val missingPermissions = getMissingPermissions(commandInfo.botPermissions, event.guild.selfMember, event.guildChannel) - defaultMessagesFactory.get(event).getBotPermErrorMsg(missingPermissions) + val errorMessage = fromMessages(event) { + when (usability.bestReason) { + UnusableReason.HIDDEN -> throwInternal("Hidden commands should have been ignored by ${TextCommandsListener::findCommandWithArgs.shortSignature}") + UnusableReason.OWNER_ONLY -> ownerOnly(event) + UnusableReason.USER_PERMISSIONS -> { + val missingPermissions = getMissingPermissions(commandInfo.userPermissions, member, event.guildChannel) + missingUserPermissions(event, missingPermissions) + } + UnusableReason.BOT_PERMISSIONS -> { + val missingPermissions = getMissingPermissions(commandInfo.botPermissions, event.guild.selfMember, event.guildChannel) + missingBotPermissions(event, missingPermissions) + } + UnusableReason.NSFW_ONLY -> nsfwOnly(event) } - UnusableReason.NSFW_ONLY -> defaultMessagesFactory.get(event).nsfwOnlyErrorMsg } + replyError(event, errorMessage) return false } @@ -218,13 +223,13 @@ internal class TextCommandsListener internal constructor( return ExecutionResult.OK } - private suspend fun replyError(event: MessageReceivedEvent, msg: String) { + private suspend fun replyError(event: MessageReceivedEvent, message: MessageCreateData) { val channel = when { event.guildChannel.canTalk() -> event.channel else -> event.author.openPrivateChannel().await() } - channel.sendMessage(msg) + channel.sendMessage(message) .awaitCatching() .handle(ErrorResponse.CANNOT_SEND_TO_USER) { event.message.addReaction(context.textConfig.dmClosedEmoji).await() @@ -240,11 +245,14 @@ internal class TextCommandsListener internal constructor( val suggestions = suggestionSupplier.getSuggestions(commandName, candidates) if (suggestions.isNotEmpty()) { - val suggestionsStr = suggestions.joinToString("**, **", "**", "**") { it.name } - replyError(event, defaultMessagesFactory.get(event).getCommandNotFoundMsg(suggestionsStr)) + replyError(event, messagesFactory.get(event).commandNotFound(event, suggestions)) } } + private inline fun fromMessages(event: MessageReceivedEvent, crossinline block: BotCommandsMessages.() -> MessageCreateData): MessageCreateData { + return messagesFactory.get(event).run(block) + } + internal enum class Status { /** Disabled by config */ DISABLED, diff --git a/src/main/kotlin/io/github/freya022/botcommands/internal/components/controller/ComponentsListener.kt b/src/main/kotlin/io/github/freya022/botcommands/internal/components/controller/ComponentsListener.kt index 20b1fac4ed..6d59872149 100644 --- a/src/main/kotlin/io/github/freya022/botcommands/internal/components/controller/ComponentsListener.kt +++ b/src/main/kotlin/io/github/freya022/botcommands/internal/components/controller/ComponentsListener.kt @@ -1,6 +1,5 @@ package io.github.freya022.botcommands.internal.components.controller -import dev.minn.jda.ktx.messages.reply_ import io.github.freya022.botcommands.api.commands.ratelimit.CancellableRateLimit import io.github.freya022.botcommands.api.components.ComponentInteractionFilter import io.github.freya022.botcommands.api.components.Components @@ -13,9 +12,9 @@ import io.github.freya022.botcommands.api.core.Filter import io.github.freya022.botcommands.api.core.annotations.BEventListener import io.github.freya022.botcommands.api.core.checkFilters import io.github.freya022.botcommands.api.core.config.BComponentsConfigBuilder +import io.github.freya022.botcommands.api.core.messages.BotCommandsMessagesFactory import io.github.freya022.botcommands.api.core.service.annotations.BService import io.github.freya022.botcommands.api.core.utils.simpleNestedName -import io.github.freya022.botcommands.api.localization.DefaultMessagesFactory import io.github.freya022.botcommands.internal.commands.ratelimit.handler.RateLimitHandler import io.github.freya022.botcommands.internal.components.data.ActionComponentData import io.github.freya022.botcommands.internal.components.data.PersistentComponentData @@ -36,7 +35,7 @@ private val logger = KotlinLogging.logger { } @RequiresComponents internal class ComponentsListener( private val context: BContext, - private val defaultMessagesFactory: DefaultMessagesFactory, + private val messagesFactory: BotCommandsMessagesFactory, private val localizableInteractionFactory: LocalizableInteractionFactory, private val rateLimitHandler: RateLimitHandler, filters: List, @@ -61,13 +60,13 @@ internal class ComponentsListener( ComponentController.parseComponentId(id) } val component = componentController.getActiveComponent(componentId) - ?: return@launch event.reply_(defaultMessagesFactory.get(event).componentExpiredErrorMsg, ephemeral = true).queue() + ?: return@launch event.reply(messagesFactory.get(event).componentExpired(event)).setEphemeral(true).queue() if (component !is ActionComponentData) throwInternal("Somehow retrieved a non-executable component on a component interaction: $component") if (component.filters === ComponentFilters.INVALID_FILTERS) { - return@launch event.reply_(defaultMessagesFactory.get(event).componentNotAllowedErrorMsg, ephemeral = true).queue() + return@launch event.reply(messagesFactory.get(event).componentNotAllowed(event)).setEphemeral(true).queue() } component.filters.onEach { filter -> @@ -101,7 +100,7 @@ internal class ComponentsListener( component: ActionComponentData ): Boolean { if (!component.constraints.isAllowed(event)) { - event.reply_(defaultMessagesFactory.get(event).componentNotAllowedErrorMsg, ephemeral = true).queue() + event.reply(messagesFactory.get(event).componentNotAllowed(event)).setEphemeral(true).queue() return false } @@ -139,9 +138,9 @@ internal class ComponentsListener( "Component" to event.component )) if (e is InsufficientPermissionException) { - event.replyExceptionMessage(defaultMessagesFactory.get(event).getBotPermErrorMsg(setOf(e.permission))) + event.replyExceptionMessage(messagesFactory.get(event).missingBotPermissions(event, setOf(e.permission))) } else { - event.replyExceptionMessage(defaultMessagesFactory.get(event).generalErrorMsg) + event.replyExceptionMessage(messagesFactory.get(event).uncaughtException(event)) } } } \ No newline at end of file diff --git a/src/main/kotlin/io/github/freya022/botcommands/internal/components/handler/ComponentHandlerExecutor.kt b/src/main/kotlin/io/github/freya022/botcommands/internal/components/handler/ComponentHandlerExecutor.kt index 61b2ffe05a..682efb3ce6 100644 --- a/src/main/kotlin/io/github/freya022/botcommands/internal/components/handler/ComponentHandlerExecutor.kt +++ b/src/main/kotlin/io/github/freya022/botcommands/internal/components/handler/ComponentHandlerExecutor.kt @@ -1,15 +1,14 @@ package io.github.freya022.botcommands.internal.components.handler -import dev.minn.jda.ktx.messages.reply_ import io.github.freya022.botcommands.api.components.annotations.JDAButtonListener import io.github.freya022.botcommands.api.components.annotations.JDASelectMenuListener import io.github.freya022.botcommands.api.components.annotations.RequiresComponents import io.github.freya022.botcommands.api.components.event.EntitySelectEvent import io.github.freya022.botcommands.api.components.event.StringSelectEvent import io.github.freya022.botcommands.api.components.serialization.SerializedComponentData +import io.github.freya022.botcommands.api.core.messages.BotCommandsMessagesFactory import io.github.freya022.botcommands.api.core.service.annotations.BService import io.github.freya022.botcommands.api.core.utils.simpleNestedName -import io.github.freya022.botcommands.api.localization.DefaultMessagesFactory import io.github.freya022.botcommands.internal.components.ComponentType import io.github.freya022.botcommands.internal.components.data.ActionComponentData import io.github.freya022.botcommands.internal.components.data.EphemeralComponentData @@ -33,7 +32,7 @@ private val logger = KotlinLogging.logger { } @BService @RequiresComponents internal class ComponentHandlerExecutor internal constructor( - private val defaultMessagesFactory: DefaultMessagesFactory, + private val messagesFactory: BotCommandsMessagesFactory, private val componentHandlerContainer: ComponentHandlerContainer, ) { internal suspend fun runHandler(component: ActionComponentData, event: GenericComponentInteractionCreateEvent): Boolean { @@ -61,7 +60,7 @@ internal class ComponentHandlerExecutor internal constructor( Component raw data: $userData """.trimIndent() } - event.reply_(defaultMessagesFactory.get(event).componentExpiredErrorMsg, ephemeral = true).queue() + event.reply(messagesFactory.get(event).componentExpired(event)).setEphemeral(true).queue() return false } diff --git a/src/main/kotlin/io/github/freya022/botcommands/internal/core/messages/BotCommandsMessagesDefaultMessagesAdapter.kt b/src/main/kotlin/io/github/freya022/botcommands/internal/core/messages/BotCommandsMessagesDefaultMessagesAdapter.kt new file mode 100644 index 0000000000..c5634e18b2 --- /dev/null +++ b/src/main/kotlin/io/github/freya022/botcommands/internal/core/messages/BotCommandsMessagesDefaultMessagesAdapter.kt @@ -0,0 +1,93 @@ +@file:Suppress("removal", "DEPRECATION") + +package io.github.freya022.botcommands.internal.core.messages + +import io.github.freya022.botcommands.api.commands.application.slash.options.SlashCommandOption +import io.github.freya022.botcommands.api.commands.text.TopLevelTextCommandInfo +import io.github.freya022.botcommands.api.core.messages.BotCommandsMessages +import io.github.freya022.botcommands.api.localization.DefaultMessages +import net.dv8tion.jda.api.Permission +import net.dv8tion.jda.api.events.GenericEvent +import net.dv8tion.jda.api.utils.TimeFormat +import net.dv8tion.jda.api.utils.messages.MessageCreateData +import java.time.Instant + +internal class BotCommandsMessagesDefaultMessagesAdapter internal constructor( + private val defaultMessages: DefaultMessages, +) : BotCommandsMessages { + + override fun uncaughtException(event: GenericEvent?): MessageCreateData { + return defaultMessages.generalErrorMsg.toMessage() + } + + override fun missingUserPermissions(event: GenericEvent?, permissions: Set): MessageCreateData { + return defaultMessages.getUserPermErrorMsg(permissions).toMessage() + } + + override fun missingBotPermissions(event: GenericEvent?, permissions: Set): MessageCreateData { + return defaultMessages.getBotPermErrorMsg(permissions).toMessage() + } + + override fun ownerOnly(event: GenericEvent?): MessageCreateData { + return defaultMessages.ownerOnlyErrorMsg.toMessage() + } + + override fun userRateLimited(event: GenericEvent?, deadline: Instant): MessageCreateData { + return defaultMessages.getUserRateLimitMsg(TimeFormat.RELATIVE.atInstant(deadline)).toMessage() + } + + override fun channelRateLimited(event: GenericEvent?, deadline: Instant): MessageCreateData { + return defaultMessages.getChannelRateLimitMsg(TimeFormat.RELATIVE.atInstant(deadline)).toMessage() + } + + override fun guildRateLimited(event: GenericEvent?, deadline: Instant): MessageCreateData { + return defaultMessages.getGuildRateLimitMsg(TimeFormat.RELATIVE.atInstant(deadline)).toMessage() + } + + override fun applicationCommandsNotAvailable(event: GenericEvent?): MessageCreateData { + return defaultMessages.applicationCommandsNotAvailableMsg.toMessage() + } + + override fun commandNotFound(event: GenericEvent?, suggestions: Collection): MessageCreateData { + val suggestionsStr = suggestions.joinToString(separator = "**, **", prefix = "**", postfix = "**") { it.name } + return defaultMessages.getCommandNotFoundMsg(suggestionsStr).toMessage() + } + + override fun resolverChannelNotFound(event: GenericEvent?, channelId: Long): MessageCreateData { + return defaultMessages.resolverChannelNotFoundMsg.toMessage() + } + + override fun resolverChannelMissingAccess(event: GenericEvent?, channelId: Long): MessageCreateData { + return defaultMessages.getResolverChannelMissingAccessMsg("<#$channelId>").toMessage() + } + + override fun resolverUserNotFound(event: GenericEvent?, userId: Long): MessageCreateData { + return defaultMessages.resolverUserNotFoundMsg.toMessage() + } + + override fun slashCommandUnresolvableOption(event: GenericEvent?, option: SlashCommandOption): MessageCreateData { + return defaultMessages.getSlashCommandUnresolvableOptionMsg(option.discordName).toMessage() + } + + override fun closedDirectMessages(event: GenericEvent?): MessageCreateData { + return defaultMessages.closedDMErrorMsg.toMessage() + } + + override fun nsfwOnly(event: GenericEvent?): MessageCreateData { + return defaultMessages.nsfwOnlyErrorMsg.toMessage() + } + + override fun componentNotAllowed(event: GenericEvent?): MessageCreateData { + return defaultMessages.componentNotAllowedErrorMsg.toMessage() + } + + override fun componentExpired(event: GenericEvent?): MessageCreateData { + return defaultMessages.componentExpiredErrorMsg.toMessage() + } + + override fun modalExpired(event: GenericEvent?): MessageCreateData { + return defaultMessages.modalExpiredErrorMsg.toMessage() + } + + private fun String.toMessage(): MessageCreateData = MessageCreateData.fromContent(this) +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/freya022/botcommands/internal/core/messages/BotCommandsMessagesFactoryDefaultMessagesFactoryAdapter.kt b/src/main/kotlin/io/github/freya022/botcommands/internal/core/messages/BotCommandsMessagesFactoryDefaultMessagesFactoryAdapter.kt new file mode 100644 index 0000000000..9e397b590d --- /dev/null +++ b/src/main/kotlin/io/github/freya022/botcommands/internal/core/messages/BotCommandsMessagesFactoryDefaultMessagesFactoryAdapter.kt @@ -0,0 +1,27 @@ +@file:Suppress("removal", "DEPRECATION") + +package io.github.freya022.botcommands.internal.core.messages + +import io.github.freya022.botcommands.api.core.messages.BotCommandsMessages +import io.github.freya022.botcommands.api.core.messages.BotCommandsMessagesFactory +import io.github.freya022.botcommands.api.localization.DefaultMessagesFactory +import net.dv8tion.jda.api.events.message.MessageReceivedEvent +import net.dv8tion.jda.api.interactions.Interaction +import java.util.* + +internal class BotCommandsMessagesFactoryDefaultMessagesFactoryAdapter internal constructor( + private val defaultMessagesFactory: DefaultMessagesFactory, +) : BotCommandsMessagesFactory { + + override fun get(locale: Locale): BotCommandsMessages { + return BotCommandsMessagesDefaultMessagesAdapter(defaultMessagesFactory.get(locale)) + } + + override fun get(event: MessageReceivedEvent): BotCommandsMessages { + return BotCommandsMessagesDefaultMessagesAdapter(defaultMessagesFactory.get(event)) + } + + override fun get(event: Interaction): BotCommandsMessages { + return BotCommandsMessagesDefaultMessagesAdapter(defaultMessagesFactory.get(event)) + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/freya022/botcommands/internal/core/messages/BotCommandsMessagesFactoryProvider.kt b/src/main/kotlin/io/github/freya022/botcommands/internal/core/messages/BotCommandsMessagesFactoryProvider.kt new file mode 100644 index 0000000000..0cc86de046 --- /dev/null +++ b/src/main/kotlin/io/github/freya022/botcommands/internal/core/messages/BotCommandsMessagesFactoryProvider.kt @@ -0,0 +1,76 @@ +@file:Suppress("DEPRECATION") + +package io.github.freya022.botcommands.internal.core.messages + +import io.github.classgraph.ClassGraph +import io.github.freya022.botcommands.api.core.messages.BotCommandsMessagesFactory +import io.github.freya022.botcommands.api.core.messages.DefaultBotCommandsMessagesFactory +import io.github.freya022.botcommands.api.core.service.ConditionalServiceChecker +import io.github.freya022.botcommands.api.core.service.ServiceContainer +import io.github.freya022.botcommands.api.core.service.annotations.BService +import io.github.freya022.botcommands.api.core.service.annotations.ConditionalService +import io.github.freya022.botcommands.api.core.service.getInterfacedServiceTypes +import io.github.freya022.botcommands.api.core.utils.simpleNestedName +import io.github.freya022.botcommands.api.localization.DefaultMessagesFactory +import io.github.freya022.botcommands.api.localization.LocalizationService +import io.github.freya022.botcommands.api.localization.PermissionLocalization +import io.github.freya022.botcommands.api.localization.interaction.UserLocaleProvider +import io.github.freya022.botcommands.api.localization.text.TextCommandLocaleProvider +import io.github.freya022.botcommands.internal.localization.FallbackDefaultMessagesFactory +import io.github.freya022.botcommands.internal.utils.classRef +import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.boot.autoconfigure.AutoConfiguration +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean +import org.springframework.context.annotation.Bean + +private val logger = KotlinLogging.logger { } + +@BService +@AutoConfiguration +internal open class BotCommandsMessagesFactoryProvider internal constructor() { + + @Bean + @ConditionalOnMissingBean(BotCommandsMessagesFactory::class) + @BService + @ConditionalService(ActivationCondition::class) + open fun botCommandsMessagesFactory( + defaultMessagesFactory: DefaultMessagesFactory, + permissionLocalization: PermissionLocalization, + localizationService: LocalizationService, + textCommandLocaleProvider: TextCommandLocaleProvider, + userLocaleProvider: UserLocaleProvider, + ): BotCommandsMessagesFactory { + // Check if the user has a custom factory or if the fallback factory has customized files + if (defaultMessagesFactory !is FallbackDefaultMessagesFactory || hasCustomDefaultMessages()) { + logger.warn { "${classRef()} has been deprecated and will be removed in the full release." } + return BotCommandsMessagesFactoryDefaultMessagesFactoryAdapter(defaultMessagesFactory) + } + + return DefaultBotCommandsMessagesFactory(permissionLocalization, localizationService, textCommandLocaleProvider, userLocaleProvider) + } + + private fun hasCustomDefaultMessages(): Boolean { + // The base name is guaranteed to be "DefaultMessages" as it is hardcoded in [[DefaultMessages]] + return ClassGraph() + .acceptPathsNonRecursive("bc_localization") + .scan() + .use { scan -> + scan.allResources + .any { + val path = it.path + path.startsWith("bc_localization/DefaultMessages") && !path.startsWith("bc_localization/DefaultMessages-default") + } + } + } + + internal object ActivationCondition : ConditionalServiceChecker { + override fun checkServiceAvailability(serviceContainer: ServiceContainer, checkedClass: Class<*>): String? { + val types = serviceContainer.getInterfacedServiceTypes() + if (types.isNotEmpty()) { + return "An user supplied ${classRef()} is already active (${types.first().simpleNestedName})" + } + + return null + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/freya022/botcommands/internal/localization/DefaultDefaultMessagesFactoryProvider.kt b/src/main/kotlin/io/github/freya022/botcommands/internal/localization/DefaultDefaultMessagesFactoryProvider.kt index 0572fef420..fe33f4b4e7 100644 --- a/src/main/kotlin/io/github/freya022/botcommands/internal/localization/DefaultDefaultMessagesFactoryProvider.kt +++ b/src/main/kotlin/io/github/freya022/botcommands/internal/localization/DefaultDefaultMessagesFactoryProvider.kt @@ -1,3 +1,5 @@ +@file:Suppress("removal", "DEPRECATION") + package io.github.freya022.botcommands.internal.localization import io.github.freya022.botcommands.api.core.service.ConditionalServiceChecker @@ -6,18 +8,14 @@ import io.github.freya022.botcommands.api.core.service.annotations.BService import io.github.freya022.botcommands.api.core.service.annotations.ConditionalService import io.github.freya022.botcommands.api.core.service.getInterfacedServiceTypes import io.github.freya022.botcommands.api.core.utils.simpleNestedName -import io.github.freya022.botcommands.api.localization.DefaultMessages import io.github.freya022.botcommands.api.localization.DefaultMessagesFactory import io.github.freya022.botcommands.api.localization.LocalizationService import io.github.freya022.botcommands.api.localization.interaction.UserLocaleProvider import io.github.freya022.botcommands.api.localization.text.TextCommandLocaleProvider import io.github.freya022.botcommands.internal.utils.classRef -import net.dv8tion.jda.api.events.message.MessageReceivedEvent -import net.dv8tion.jda.api.interactions.Interaction import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration -import java.util.* // I hate those names @Configuration @@ -31,27 +29,7 @@ internal open class DefaultDefaultMessagesFactoryProvider { localizationService: LocalizationService, textCommandLocaleProvider: TextCommandLocaleProvider, userLocaleProvider: UserLocaleProvider, - ): DefaultMessagesFactory = DefaultDefaultMessagesFactory(localizationService, textCommandLocaleProvider, userLocaleProvider) - - private class DefaultDefaultMessagesFactory( - private val localizationService: LocalizationService, - private val textCommandLocaleProvider: TextCommandLocaleProvider, - private val userLocaleProvider: UserLocaleProvider, - ): DefaultMessagesFactory { - private val localeDefaultMessagesMap: MutableMap = hashMapOf() - - override fun get(locale: Locale): DefaultMessages = localeDefaultMessagesMap.computeIfAbsent(locale) { - DefaultMessages(localizationService, it) - } - - override fun get(event: MessageReceivedEvent): DefaultMessages { - return get(textCommandLocaleProvider.getLocale(event)) - } - - override fun get(event: Interaction): DefaultMessages { - return get(userLocaleProvider.getLocale(event)) - } - } + ): DefaultMessagesFactory = FallbackDefaultMessagesFactory(localizationService, textCommandLocaleProvider, userLocaleProvider) internal object ActivationCondition : ConditionalServiceChecker { override fun checkServiceAvailability(serviceContainer: ServiceContainer, checkedClass: Class<*>): String? { diff --git a/src/main/kotlin/io/github/freya022/botcommands/internal/localization/FallbackDefaultMessagesFactory.kt b/src/main/kotlin/io/github/freya022/botcommands/internal/localization/FallbackDefaultMessagesFactory.kt new file mode 100644 index 0000000000..e637767228 --- /dev/null +++ b/src/main/kotlin/io/github/freya022/botcommands/internal/localization/FallbackDefaultMessagesFactory.kt @@ -0,0 +1,32 @@ +@file:Suppress("removal", "DEPRECATION") + +package io.github.freya022.botcommands.internal.localization + +import io.github.freya022.botcommands.api.localization.DefaultMessages +import io.github.freya022.botcommands.api.localization.DefaultMessagesFactory +import io.github.freya022.botcommands.api.localization.LocalizationService +import io.github.freya022.botcommands.api.localization.interaction.UserLocaleProvider +import io.github.freya022.botcommands.api.localization.text.TextCommandLocaleProvider +import net.dv8tion.jda.api.events.message.MessageReceivedEvent +import net.dv8tion.jda.api.interactions.Interaction +import java.util.* + +internal class FallbackDefaultMessagesFactory internal constructor( + private val localizationService: LocalizationService, + private val textCommandLocaleProvider: TextCommandLocaleProvider, + private val userLocaleProvider: UserLocaleProvider, +): DefaultMessagesFactory { + private val cache: MutableMap = hashMapOf() + + override fun get(locale: Locale): DefaultMessages = cache.computeIfAbsent(locale) { + DefaultMessages(localizationService, it) + } + + override fun get(event: MessageReceivedEvent): DefaultMessages { + return get(textCommandLocaleProvider.getLocale(event)) + } + + override fun get(event: Interaction): DefaultMessages { + return get(userLocaleProvider.getLocale(event)) + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/freya022/botcommands/internal/localization/PermissionLocalizationProvider.kt b/src/main/kotlin/io/github/freya022/botcommands/internal/localization/PermissionLocalizationProvider.kt new file mode 100644 index 0000000000..50046655d1 --- /dev/null +++ b/src/main/kotlin/io/github/freya022/botcommands/internal/localization/PermissionLocalizationProvider.kt @@ -0,0 +1,39 @@ +package io.github.freya022.botcommands.internal.localization + +import io.github.freya022.botcommands.api.core.service.ConditionalServiceChecker +import io.github.freya022.botcommands.api.core.service.ServiceContainer +import io.github.freya022.botcommands.api.core.service.annotations.BService +import io.github.freya022.botcommands.api.core.service.annotations.ConditionalService +import io.github.freya022.botcommands.api.core.service.getInterfacedServiceTypes +import io.github.freya022.botcommands.api.core.utils.simpleNestedName +import io.github.freya022.botcommands.api.localization.DefaultPermissionLocalization +import io.github.freya022.botcommands.api.localization.LocalizationService +import io.github.freya022.botcommands.api.localization.PermissionLocalization +import io.github.freya022.botcommands.internal.utils.classRef +import org.springframework.boot.autoconfigure.AutoConfiguration +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean +import org.springframework.context.annotation.Bean + +@BService +@AutoConfiguration +internal open class PermissionLocalizationProvider internal constructor() { + + @Bean + @ConditionalOnMissingBean(PermissionLocalization::class) + @BService + @ConditionalService(ActivationCondition::class) + open fun permissionLocalization(localizationService: LocalizationService): PermissionLocalization { + return DefaultPermissionLocalization(localizationService) + } + + internal object ActivationCondition : ConditionalServiceChecker { + override fun checkServiceAvailability(serviceContainer: ServiceContainer, checkedClass: Class<*>): String? { + val types = serviceContainer.getInterfacedServiceTypes() + if (types.isNotEmpty()) { + return "An user supplied ${classRef()} is already active (${types.first().simpleNestedName})" + } + + return null + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/freya022/botcommands/internal/localization/interaction/LocalizableInteractionFactory.kt b/src/main/kotlin/io/github/freya022/botcommands/internal/localization/interaction/LocalizableInteractionFactory.kt index 58ac8dd18e..9c51765ce0 100644 --- a/src/main/kotlin/io/github/freya022/botcommands/internal/localization/interaction/LocalizableInteractionFactory.kt +++ b/src/main/kotlin/io/github/freya022/botcommands/internal/localization/interaction/LocalizableInteractionFactory.kt @@ -1,6 +1,9 @@ +@file:Suppress("DEPRECATION") + package io.github.freya022.botcommands.internal.localization.interaction import io.github.freya022.botcommands.api.core.config.BLocalizationConfig +import io.github.freya022.botcommands.api.core.messages.BotCommandsMessagesFactory import io.github.freya022.botcommands.api.core.service.annotations.BService import io.github.freya022.botcommands.api.localization.DefaultMessagesFactory import io.github.freya022.botcommands.api.localization.LocalizationService @@ -16,7 +19,8 @@ internal class LocalizableInteractionFactory internal constructor( private val userLocaleProvider: UserLocaleProvider, private val guildLocaleProvider: GuildLocaleProvider, private val defaultMessagesFactory: DefaultMessagesFactory, + private val messagesFactory: BotCommandsMessagesFactory, ) { internal fun create(event: IReplyCallback) = - LocalizableInteractionImpl(event, localizationService, localizationConfig, userLocaleProvider, guildLocaleProvider, defaultMessagesFactory) + LocalizableInteractionImpl(event, localizationService, localizationConfig, userLocaleProvider, guildLocaleProvider, defaultMessagesFactory, messagesFactory) } \ No newline at end of file diff --git a/src/main/kotlin/io/github/freya022/botcommands/internal/localization/interaction/LocalizableInteractionImpl.kt b/src/main/kotlin/io/github/freya022/botcommands/internal/localization/interaction/LocalizableInteractionImpl.kt index 594a18438e..287f2740aa 100644 --- a/src/main/kotlin/io/github/freya022/botcommands/internal/localization/interaction/LocalizableInteractionImpl.kt +++ b/src/main/kotlin/io/github/freya022/botcommands/internal/localization/interaction/LocalizableInteractionImpl.kt @@ -1,6 +1,10 @@ +@file:Suppress("removal", "DEPRECATION") + package io.github.freya022.botcommands.internal.localization.interaction import io.github.freya022.botcommands.api.core.config.BLocalizationConfig +import io.github.freya022.botcommands.api.core.messages.BotCommandsMessages +import io.github.freya022.botcommands.api.core.messages.BotCommandsMessagesFactory import io.github.freya022.botcommands.api.localization.DefaultMessages import io.github.freya022.botcommands.api.localization.DefaultMessagesFactory import io.github.freya022.botcommands.api.localization.Localization @@ -22,6 +26,7 @@ internal class LocalizableInteractionImpl internal constructor( private val userLocaleProvider: UserLocaleProvider, private val guildLocaleProvider: GuildLocaleProvider, private val defaultMessagesFactory: DefaultMessagesFactory, + private val messagesFactory: BotCommandsMessagesFactory, ) : AbstractLocalizableAction(localizationConfig, localizationService), LocalizableInteraction { @@ -38,10 +43,16 @@ internal class LocalizableInteractionImpl internal constructor( ) } + @Suppress("DEPRECATION", "removal") + @Deprecated("Replaced with getBotCommandsMessages()") override fun getDefaultMessages(): DefaultMessages { return defaultMessagesFactory.get(deferrableCallback) } + override fun getBotCommandsMessages(): BotCommandsMessages { + return messagesFactory.get(deferrableCallback) + } + override fun getUserMessage(localizationPath: String, vararg entries: Localization.Entry): String { return getLocalizedMessage(userLocale, localizationPath, *entries) } diff --git a/src/main/kotlin/io/github/freya022/botcommands/internal/localization/text/LocalizableTextCommandFactory.kt b/src/main/kotlin/io/github/freya022/botcommands/internal/localization/text/LocalizableTextCommandFactory.kt index 665fda4148..a75a5aadd3 100644 --- a/src/main/kotlin/io/github/freya022/botcommands/internal/localization/text/LocalizableTextCommandFactory.kt +++ b/src/main/kotlin/io/github/freya022/botcommands/internal/localization/text/LocalizableTextCommandFactory.kt @@ -1,6 +1,9 @@ +@file:Suppress("DEPRECATION") + package io.github.freya022.botcommands.internal.localization.text import io.github.freya022.botcommands.api.core.config.BLocalizationConfig +import io.github.freya022.botcommands.api.core.messages.BotCommandsMessagesFactory import io.github.freya022.botcommands.api.core.service.annotations.BService import io.github.freya022.botcommands.api.localization.DefaultMessagesFactory import io.github.freya022.botcommands.api.localization.LocalizationService @@ -14,7 +17,8 @@ internal class LocalizableTextCommandFactory internal constructor( private val localizationConfig: BLocalizationConfig, private val localeProvider: TextCommandLocaleProvider, private val defaultMessagesFactory: DefaultMessagesFactory, + private val messagesFactory: BotCommandsMessagesFactory, ) { internal fun create(event: MessageReceivedEvent) = - LocalizableTextCommandImpl(event, localizationService, localizationConfig, localeProvider, defaultMessagesFactory) + LocalizableTextCommandImpl(event, localizationService, localizationConfig, localeProvider, defaultMessagesFactory, messagesFactory) } \ No newline at end of file diff --git a/src/main/kotlin/io/github/freya022/botcommands/internal/localization/text/LocalizableTextCommandImpl.kt b/src/main/kotlin/io/github/freya022/botcommands/internal/localization/text/LocalizableTextCommandImpl.kt index 4be783a7f9..70850d5e78 100644 --- a/src/main/kotlin/io/github/freya022/botcommands/internal/localization/text/LocalizableTextCommandImpl.kt +++ b/src/main/kotlin/io/github/freya022/botcommands/internal/localization/text/LocalizableTextCommandImpl.kt @@ -1,6 +1,10 @@ +@file:Suppress("removal", "DEPRECATION") + package io.github.freya022.botcommands.internal.localization.text import io.github.freya022.botcommands.api.core.config.BLocalizationConfig +import io.github.freya022.botcommands.api.core.messages.BotCommandsMessages +import io.github.freya022.botcommands.api.core.messages.BotCommandsMessagesFactory import io.github.freya022.botcommands.api.localization.DefaultMessages import io.github.freya022.botcommands.api.localization.DefaultMessagesFactory import io.github.freya022.botcommands.api.localization.Localization @@ -20,6 +24,7 @@ internal class LocalizableTextCommandImpl internal constructor( localizationConfig: BLocalizationConfig, private val localeProvider: TextCommandLocaleProvider, private val defaultMessagesFactory: DefaultMessagesFactory, + private val messagesFactory: BotCommandsMessagesFactory, ) : AbstractLocalizableAction(localizationConfig, localizationService), LocalizableTextCommand { private val locale: Locale by lazy { localeProvider.getLocale(event) } @@ -32,10 +37,16 @@ internal class LocalizableTextCommandImpl internal constructor( ) } + @Suppress("DEPRECATION", "removal") + @Deprecated("Replaced with getBotCommandsMessages()") override fun getDefaultMessages(): DefaultMessages { return defaultMessagesFactory.get(locale) } + override fun getBotCommandsMessages(): BotCommandsMessages { + return messagesFactory.get(locale) + } + override fun getGuildMessage(localizationPath: String, vararg entries: Localization.Entry): String { return getLocalizedMessage(locale, localizationPath, *entries) } diff --git a/src/main/kotlin/io/github/freya022/botcommands/internal/modals/ModalListener.kt b/src/main/kotlin/io/github/freya022/botcommands/internal/modals/ModalListener.kt index 6e6922637c..58eb24d359 100644 --- a/src/main/kotlin/io/github/freya022/botcommands/internal/modals/ModalListener.kt +++ b/src/main/kotlin/io/github/freya022/botcommands/internal/modals/ModalListener.kt @@ -1,10 +1,9 @@ package io.github.freya022.botcommands.internal.modals -import dev.minn.jda.ktx.messages.reply_ import io.github.freya022.botcommands.api.core.annotations.BEventListener import io.github.freya022.botcommands.api.core.config.BModalsConfig +import io.github.freya022.botcommands.api.core.messages.BotCommandsMessagesFactory import io.github.freya022.botcommands.api.core.service.annotations.BService -import io.github.freya022.botcommands.api.localization.DefaultMessagesFactory import io.github.freya022.botcommands.api.modals.ModalEvent import io.github.freya022.botcommands.api.modals.Modals import io.github.freya022.botcommands.api.modals.annotations.ModalHandler @@ -24,7 +23,7 @@ private val logger = KotlinLogging.logger { } @RequiresModals internal class ModalListener( private val context: BContextImpl, - private val defaultMessagesFactory: DefaultMessagesFactory, + private val messagesFactory: BotCommandsMessagesFactory, private val localizableInteractionFactory: LocalizableInteractionFactory, private val modalHandlerContainer: ModalHandlerContainer, private val modalMaps: ModalMaps, @@ -44,7 +43,7 @@ internal class ModalListener( val modalData = modalMaps.consumeModal(ModalMaps.parseModalId(jdaEvent.modalId)) if (modalData == null) { //Probably the modal expired - jdaEvent.reply_(defaultMessagesFactory.get(jdaEvent).modalExpiredErrorMsg, ephemeral = true).queue() + jdaEvent.reply(messagesFactory.get(jdaEvent).modalExpired(jdaEvent)).setEphemeral(true).queue() return@launch } @@ -73,9 +72,9 @@ internal class ModalListener( put("Modal values", event.values.associate { it.id to it.asString }) }) if (e is InsufficientPermissionException) { - event.replyExceptionMessage(defaultMessagesFactory.get(event).getBotPermErrorMsg(setOf(e.permission))) + event.replyExceptionMessage(messagesFactory.get(event).missingBotPermissions(event, setOf(e.permission))) } else { - event.replyExceptionMessage(defaultMessagesFactory.get(event).generalErrorMsg) + event.replyExceptionMessage(messagesFactory.get(event).uncaughtException(event)) } } } \ No newline at end of file diff --git a/src/main/kotlin/io/github/freya022/botcommands/internal/parameters/resolvers/AbstractUserSnowflakeResolver.kt b/src/main/kotlin/io/github/freya022/botcommands/internal/parameters/resolvers/AbstractUserSnowflakeResolver.kt index ebc2a4bda5..b2b238b439 100644 --- a/src/main/kotlin/io/github/freya022/botcommands/internal/parameters/resolvers/AbstractUserSnowflakeResolver.kt +++ b/src/main/kotlin/io/github/freya022/botcommands/internal/parameters/resolvers/AbstractUserSnowflakeResolver.kt @@ -1,6 +1,5 @@ package io.github.freya022.botcommands.internal.parameters.resolvers -import dev.minn.jda.ktx.messages.reply_ import io.github.freya022.botcommands.api.commands.application.context.user.options.UserContextCommandOption import io.github.freya022.botcommands.api.commands.application.slash.options.SlashCommandOption import io.github.freya022.botcommands.api.commands.text.BaseCommandEvent @@ -8,11 +7,11 @@ import io.github.freya022.botcommands.api.commands.text.options.TextCommandOptio import io.github.freya022.botcommands.api.components.options.ComponentOption import io.github.freya022.botcommands.api.components.serialization.SerializedComponentData import io.github.freya022.botcommands.api.core.BContext +import io.github.freya022.botcommands.api.core.messages.BotCommandsMessagesFactory import io.github.freya022.botcommands.api.core.service.getService import io.github.freya022.botcommands.api.core.traceNull import io.github.freya022.botcommands.api.core.utils.retrieveMemberByIdOrNull import io.github.freya022.botcommands.api.core.utils.retrieveUserByIdOrNull -import io.github.freya022.botcommands.api.localization.DefaultMessagesFactory import io.github.freya022.botcommands.api.parameters.ClassParameterResolver import io.github.freya022.botcommands.api.parameters.resolvers.ComponentParameterResolver import io.github.freya022.botcommands.api.parameters.resolvers.SlashParameterResolver @@ -41,8 +40,8 @@ internal sealed class AbstractUserSnowflakeResolver, ComponentParameterResolver, UserContextParameterResolver { - - private val defaultMessagesFactory: DefaultMessagesFactory = context.getService() + + private val messagesFactory: BotCommandsMessagesFactory = context.getService() final override val pattern: Pattern get() = userMentionPattern final override val testExample: String = "<@1234>" @@ -76,7 +75,7 @@ internal sealed class AbstractUserSnowflakeResolver")).queue() + event.message.reply(messagesFactory.get(event).resolverChannelMissingAccess(event, channelId)).queue() }) private suspend fun retrieveThreadChannel( - event: IReplyCallback, + event: GenericComponentInteractionCreateEvent, guild: Guild, channelId: Long ): ThreadChannel? = retrieveThreadChannel(guild, channelId, onMissingAccess = { - event.reply_(defaultMessagesFactory.get(event).getResolverChannelMissingAccessMsg("<#$channelId>"), ephemeral = true).queue() + event.reply(messagesFactory.get(event).resolverChannelMissingAccess(event, channelId)).queue() }) private suspend fun retrieveThreadChannel( diff --git a/src/main/kotlin/io/github/freya022/botcommands/internal/utils/Exceptions.kt b/src/main/kotlin/io/github/freya022/botcommands/internal/utils/Exceptions.kt index 4d16c975d4..3fd7319a49 100644 --- a/src/main/kotlin/io/github/freya022/botcommands/internal/utils/Exceptions.kt +++ b/src/main/kotlin/io/github/freya022/botcommands/internal/utils/Exceptions.kt @@ -9,6 +9,7 @@ import kotlinx.datetime.Clock import kotlinx.datetime.Instant import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback import net.dv8tion.jda.api.requests.ErrorResponse +import net.dv8tion.jda.api.utils.messages.MessageCreateData import java.lang.reflect.InvocationTargetException import kotlin.contracts.ExperimentalContracts import kotlin.contracts.contract @@ -138,7 +139,7 @@ internal fun Throwable.unwrap(): Throwable { } internal suspend fun IReplyCallback.replyExceptionMessage( - message: String + message: MessageCreateData ) = runIgnoringResponse(ErrorResponse.UNKNOWN_INTERACTION, ErrorResponse.UNKNOWN_WEBHOOK) { if (isAcknowledged) { // Give ourselves 5 seconds to delete diff --git a/src/main/resources/bc_localization/BotCommandsMessages-default.json b/src/main/resources/bc_localization/BotCommandsMessages-default.json new file mode 100644 index 0000000000..041a624cad --- /dev/null +++ b/src/main/resources/bc_localization/BotCommandsMessages-default.json @@ -0,0 +1,29 @@ +{ + "uncaught_exception": "An uncaught exception occurred and has been reported to the bot developers, please try again later", + + "missing.permissions.user": "You are not allowed to do this", + "missing.permissions.bot": "I am missing these permissions: {permissions}", + "owner_only": "Only the owner can use this", + + "ratelimited.user": "You will be able to use this {timestamp}", + "ratelimited.channel": "You will be able to use this, in this channel, {timestamp}", + "ratelimited.guild": "You will be able to use this, in this guild, {timestamp}", + + "commands.application.not_available": "Application commands are not available yet, please try again later", + "commands.text.not_found": "Unknown command, maybe you meant: {suggestions}", + + "resolver.channel.not_found": "The target channel does not exist anymore", + "resolver.channel.missing_access": "The bot cannot access {channel_id}", + "resolver.user.not_found": "The target user does not exist anymore", + + "commands.slash.option.unresolvable": "The option '{option_name}' could not be resolved.", + + "direct_messages.closed": "Unable to send you a DM, please open your DMs from this server", + + "nsfw_only": "This command can only be used in NSFW channels", + + "components.not_allowed": "You are not allowed to use this", + "components.expired": "This component is not usable anymore", + + "modals.expired": "This modal is no longer available" +} \ No newline at end of file diff --git a/src/test/kotlin/io/github/freya022/botcommands/framework/BotCommandsMessagesTests.kt b/src/test/kotlin/io/github/freya022/botcommands/framework/BotCommandsMessagesTests.kt new file mode 100644 index 0000000000..0b38830753 --- /dev/null +++ b/src/test/kotlin/io/github/freya022/botcommands/framework/BotCommandsMessagesTests.kt @@ -0,0 +1,161 @@ +@file:Suppress("DEPRECATION", "removal") + +package io.github.freya022.botcommands.framework + +import io.github.freya022.botcommands.api.core.BotCommands +import io.github.freya022.botcommands.api.core.config.registerServiceSupplier +import io.github.freya022.botcommands.api.core.messages.BotCommandsMessages +import io.github.freya022.botcommands.api.core.messages.BotCommandsMessagesFactory +import io.github.freya022.botcommands.api.core.messages.DefaultBotCommandsMessagesFactory +import io.github.freya022.botcommands.api.core.messages.exceptions.MissingMessageTemplateException +import io.github.freya022.botcommands.api.core.service.getService +import io.github.freya022.botcommands.api.core.utils.joinAsList +import io.github.freya022.botcommands.api.localization.DefaultMessages +import io.github.freya022.botcommands.api.localization.DefaultMessagesFactory +import io.github.freya022.botcommands.framework.utils.createTest +import io.github.freya022.botcommands.internal.core.messages.BotCommandsMessagesFactoryProvider +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.spyk +import net.dv8tion.jda.api.events.message.MessageReceivedEvent +import net.dv8tion.jda.api.interactions.Interaction +import net.dv8tion.jda.api.utils.messages.MessageCreateData +import org.junit.jupiter.api.assertDoesNotThrow +import java.time.Instant +import java.util.* +import kotlin.reflect.KFunction +import kotlin.test.Test +import kotlin.test.assertIsNot +import kotlin.test.assertSame +import kotlin.test.fail + +class BotCommandsMessagesTests { + + @Test + fun `Adapter is used when custom DefaultMessagesFactory type is used`() { + val context = BotCommands.createTest { + services { + registerServiceSupplier { + object : DefaultMessagesFactory { + override fun get(locale: Locale): DefaultMessages = throw UnsupportedOperationException() + override fun get(event: MessageReceivedEvent) = throw UnsupportedOperationException() + override fun get(event: Interaction) = throw UnsupportedOperationException() + } + } + } + } + + assertIsNot(context.getService()) + } + + @Test + fun `Adapter is used when custom DefaultMessages JSON exists`() { + val context = BotCommands.createTest { + services { + registerServiceSupplier { + mockk { + every { + botCommandsMessagesFactory(any(), any(), any(), any(), any()) + } answers { callOriginal() } + + every { this@mockk["hasCustomDefaultMessages"]() } returns true + } + } + } + } + + assertIsNot(context.getService()) + } + + @Test + fun `Can override autoconfiguration`() { + val expected = mockk() + val context = BotCommands.createTest { + services { + registerServiceSupplier { expected } + } + } + + val actual = assertDoesNotThrow { context.getService() } + assertSame(expected, actual) + } + + @Test + fun `All messages have defaults`() { + val context = BotCommands.createTest { + services { + // Override the autoconfiguration so we don't unexpectedly use a different implementation + registerServiceSupplier( + additionalTypes = setOf( + BotCommandsMessagesFactory::class, + ) + ) { context -> + DefaultBotCommandsMessagesFactory( + context.getService(), + context.getService(), + context.getService(), + context.getService(), + ) + } + } + } + + val templatePathSlot = slot() + val messages = spyk(context.getService().get(Locale.ROOT)) { + every { this@spyk["getLocalizationTemplate"](capture(templatePathSlot)) } answers { callOriginal() } + } + + val methodCalls = mapOf( + methodCall(messages::uncaughtException) { this(mockk()) }, + methodCall(messages::missingUserPermissions) { this(mockk(), emptySet()) }, + methodCall(messages::missingBotPermissions) { this(mockk(), emptySet()) }, + methodCall(messages::ownerOnly) { this(mockk()) }, + methodCall(messages::userRateLimited) { this(mockk(), Instant.now()) }, + methodCall(messages::channelRateLimited) { this(mockk(), Instant.now()) }, + methodCall(messages::guildRateLimited) { this(mockk(), Instant.now()) }, + methodCall(messages::applicationCommandsNotAvailable) { this(mockk()) }, + methodCall(messages::commandNotFound) { this(mockk(), emptySet()) }, + methodCall(messages::resolverChannelNotFound) { this(mockk(), 0) }, + methodCall(messages::resolverChannelMissingAccess) { this(mockk(), 0) }, + methodCall(messages::resolverUserNotFound) { this(mockk(), 0) }, + methodCall(messages::slashCommandUnresolvableOption) { + this(mockk(), mockk { + every { discordName } returns "discord_name" + }) + }, + methodCall(messages::closedDirectMessages) { this(mockk()) }, + methodCall(messages::nsfwOnly) { this(mockk()) }, + methodCall(messages::componentNotAllowed) { this(mockk()) }, + methodCall(messages::componentExpired) { this(mockk()) }, + methodCall(messages::modalExpired) { this(mockk()) }, + ) + + val missingTests = + BotCommandsMessages::class.java.declaredMethods.mapTo(hashSetOf()) { it.name } - methodCalls.keys + if (missingTests.isNotEmpty()) { + fail("The following methods are missing tests:\n" + missingTests.joinAsList()) + } + + val methodsMissingTemplate: MutableList = arrayListOf() + methodCalls.values.forEach { methodCall -> + templatePathSlot.clear() + try { + methodCall() + } catch (_: MissingMessageTemplateException) { + methodsMissingTemplate += templatePathSlot.captured + } + } + + if (methodsMissingTemplate.isNotEmpty()) { + fail("The following template keys are missing default translations:\n" + methodsMissingTemplate.joinAsList()) + } + } + + private fun > methodCall( + callableRef: F, + executor: F.() -> Unit, + ): Pair Unit> { + return callableRef.name to { executor(callableRef) } + } +} \ No newline at end of file diff --git a/src/test/kotlin/io/github/freya022/botcommands/framework/PermissionLocalizationTests.kt b/src/test/kotlin/io/github/freya022/botcommands/framework/PermissionLocalizationTests.kt new file mode 100644 index 0000000000..e48ac674fe --- /dev/null +++ b/src/test/kotlin/io/github/freya022/botcommands/framework/PermissionLocalizationTests.kt @@ -0,0 +1,58 @@ +package io.github.freya022.botcommands.framework + +import io.github.freya022.botcommands.api.core.BotCommands +import io.github.freya022.botcommands.api.core.config.registerServiceSupplier +import io.github.freya022.botcommands.api.core.service.getService +import io.github.freya022.botcommands.api.localization.DefaultPermissionLocalization +import io.github.freya022.botcommands.api.localization.LocalizationService +import io.github.freya022.botcommands.api.localization.PermissionLocalization +import io.github.freya022.botcommands.framework.utils.createTest +import io.mockk.every +import io.mockk.mockk +import net.dv8tion.jda.api.Permission +import org.junit.jupiter.api.assertDoesNotThrow +import java.util.* +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertSame + +class PermissionLocalizationTests { + + @Test + fun `Should use Permission's enum name when there are no bundles`() { + val localizationService = mockk { + every { getInstance(any(), any()) } returns null + } + + val permissionLocalization = DefaultPermissionLocalization(localizationService) + val actual = permissionLocalization.localize(Permission.VIEW_CHANNEL, Locale.FRENCH) + + assertEquals("View Channels", actual) + } + + @Test + fun `Should be able to use the default bundle`() { + val expected = "Localized permission name" + val localizationService = mockk { + every { getInstance("Permissions", Locale.FRENCH)?.get("VIEW_CHANNEL")?.localize() } returns expected + } + + val permissionLocalization = DefaultPermissionLocalization(localizationService) + val actual = permissionLocalization.localize(Permission.VIEW_CHANNEL, Locale.FRENCH) + + assertEquals(expected, actual) + } + + @Test + fun `Can override autoconfiguration`() { + val expected = mockk() + val context = BotCommands.createTest { + services { + registerServiceSupplier { expected } + } + } + + val actual = assertDoesNotThrow { context.getService() } + assertSame(expected, actual) + } +} \ No newline at end of file diff --git a/src/test/resources/bc_localization/DefaultMessages.json b/src/test/resources/bc_localization/DefaultMessages.json deleted file mode 100644 index 64c785130d..0000000000 --- a/src/test/resources/bc_localization/DefaultMessages.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "null.component.type.error.message": "Component go bruh :skull:" -} \ No newline at end of file diff --git a/src/test/resources/bc_localization/DefaultMessages_fr.json b/src/test/resources/bc_localization/DefaultMessages_fr.json deleted file mode 100644 index 8f169c4d46..0000000000 --- a/src/test/resources/bc_localization/DefaultMessages_fr.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "null.component.type.error.message": "Ce composant n'est plus utilisable" -} \ No newline at end of file