From a35d6c08ba8f68b6e2035f3bfcb4553435adb57a Mon Sep 17 00:00:00 2001 From: Gareth Coles Date: Fri, 16 Feb 2024 18:46:02 +0000 Subject: [PATCH] Two changes requested by users: - Add default-on toggle to ignore command invocations by the same bot - Add `ReactionEmoji.from(String)`` to wrap `String.toReaction()`` --- .../builders/ExtensibleBotBuilder.kt | 5 +- .../commands/chat/ChatCommandRegistry.kt | 311 +++++++++--------- .../kord/extensions/utils/_Reaction.kt | 25 +- 3 files changed, 181 insertions(+), 160 deletions(-) diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/builders/ExtensibleBotBuilder.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/builders/ExtensibleBotBuilder.kt index 7ddec49683..15fe4cc84a 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/builders/ExtensibleBotBuilder.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/builders/ExtensibleBotBuilder.kt @@ -397,7 +397,7 @@ public open class ExtensibleBotBuilder { if (koinNotStarted()) { KordExContext.startKoin { slf4jLogger(logLevel) - environmentProperties() + environmentProperties() if (File("koin.properties").exists()) { fileProperties("koin.properties") @@ -1318,6 +1318,9 @@ public open class ExtensibleBotBuilder { /** @suppress Builder that shouldn't be set directly by the user. **/ public var registryBuilder: () -> ChatCommandRegistry = { ChatCommandRegistry() } + /** Whether to ignore command invocations in messages sent by the bot. Defaults to `true`. **/ + public var ignoreSelf: Boolean = true + /** * List of command checks. * diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/chat/ChatCommandRegistry.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/chat/ChatCommandRegistry.kt index 15a701e516..a787483b5f 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/chat/ChatCommandRegistry.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/chat/ChatCommandRegistry.kt @@ -16,161 +16,170 @@ import com.kotlindiscord.kord.extensions.parser.StringParser import com.kotlindiscord.kord.extensions.utils.getLocale import dev.kord.core.Kord import dev.kord.core.event.message.MessageCreateEvent +import mu.KotlinLogging import org.koin.core.component.inject /** * A class for the registration and dispatching of message-based commands. */ public open class ChatCommandRegistry : KordExKoinComponent { - /** Current instance of the bot. **/ - public val bot: ExtensibleBot by inject() - - /** Kord instance, backing the ExtensibleBot. **/ - public val kord: Kord by inject() - - /** Chat command parser object. **/ - public open val parser: ChatCommandParser = ChatCommandParser() - - /** - * A list of all registered commands. - */ - public open val commands: MutableList> = mutableListOf() - - /** @suppress **/ - public val botSettings: ExtensibleBotBuilder by inject() - - /** Whether chat commands are enabled in the bot's settings. **/ - public open val enabled: Boolean get() = botSettings.chatCommandsBuilder.enabled - - /** - * Directly register a [ChatCommand] to this command registry. - * - * Generally speaking, you shouldn't call this directly - instead, create an [Extension] and - * call the [ChatGroupCommand.chatCommand] function in your [Extension.setup] function. - * - * This function will throw a [CommandRegistrationException] if the command has already been registered, if - * a command with the same name exists, or if a command with one of the same aliases exists. - * - * @param command The command to be registered. - * @throws CommandRegistrationException Thrown if the command could not be registered. - */ - @Throws(CommandRegistrationException::class) - public open fun add(command: ChatCommand) { - val existingCommand = commands.any { it.name == command.name } - val existingAlias: String? = commands.flatMap { - it.aliases.toList() - }.firstOrNull { command.aliases.contains(it) } - - if (existingCommand) { - throw CommandRegistrationException( - command.name, - "Chat command with this name already registered in '${command.extension.name}' extension." - ) - } - - if (existingAlias != null) { - throw CommandRegistrationException( - command.name, - "Chat command with alias '$existingAlias' already registered in '${command.extension.name}' " + - "extension." - ) - } - - if (commands.contains(command)) { - throw CommandRegistrationException( - command.name, - "Chat command already registered in '${command.extension.name}' extension." - ) - } - - commands.add(command) - } - - /** - * Directly remove a registered [ChatCommand] from this command registry. - * - * This function is used when extensions are unloaded, in order to clear out their commands. - * No exception is thrown if the command wasn't registered. - * - * @param command The command to be removed. - */ - public open fun remove(command: ChatCommand): Boolean = commands.remove(command) - - /** - * Given a [MessageCreateEvent], return the prefix that should be used for a command invocation. - * - * By default, this can be set in [ExtensibleBotBuilder] - but you can override this function in subclasses if - * needed. - */ - public open suspend fun getPrefix(event: MessageCreateEvent): String = - botSettings.chatCommandsBuilder.prefixCallback(event, botSettings.chatCommandsBuilder.defaultPrefix) - - /** - * Check whether the given string starts with a mention referring to the bot. If so, the matching mention string - * is returned, otherwise `null`. - */ - public open fun String.startsWithSelfMention(): String? { - val mention = "<@${kord.selfId.value}>" - val nickMention = "<@!${kord.selfId.value}>" - - return when { - startsWith(mention) -> mention - startsWith(nickMention) -> nickMention - - else -> null - } - } - - /** - * Handles an incoming [MessageCreateEvent] and dispatches a command invocation, if possible. - */ - public open suspend fun handleEvent(event: MessageCreateEvent) { - val message = event.message - var commandName: String? - val prefix = getPrefix(event) - var content = message.content - - if (content.isEmpty()) { - // Empty message. - return - } - - val mention = content.startsWithSelfMention() - - content = when { - // Starts with the right mention and mentions are allowed, so remove it - mention != null && botSettings.chatCommandsBuilder.invokeOnMention -> content.substring(mention.length) - - // Starts with the right prefix, so remove it - content.startsWith(prefix) -> content.substring(prefix.length) - - // Not a valid command, so stop here - else -> return - }.trim() // Remove unnecessary spaces - - commandName = content.split(" ").first() - content = content.substring(commandName.length).trim() // Remove the command name and extra whitespace - - commandName = commandName.lowercase() - - val command = getCommand(commandName, event) - val parser = StringParser(content) - - command?.call(event, commandName, parser, content) - } - - /** - * Given a command name and [MessageCreateEvent], try to find a matching command. - * - * If a command supports locale fallback, this will also attempt to resolve names via the bot's default locale. - */ - public open suspend fun getCommand(name: String, event: MessageCreateEvent): ChatCommand? { - val defaultLocale = botSettings.i18nBuilder.defaultLocale - val locale = event.getLocale() - - return commands.firstOrNull { it.getTranslatedName(locale) == name } - ?: commands.firstOrNull { it.getTranslatedAliases(locale).contains(name) } - ?: commands.firstOrNull { it.localeFallback && it.getTranslatedName(defaultLocale) == name } - ?: commands.firstOrNull { it.localeFallback && it.getTranslatedAliases(defaultLocale).contains(name) } - } + private val logger = KotlinLogging.logger { } + + /** Current instance of the bot. **/ + public val bot: ExtensibleBot by inject() + + /** Kord instance, backing the ExtensibleBot. **/ + public val kord: Kord by inject() + + /** Chat command parser object. **/ + public open val parser: ChatCommandParser = ChatCommandParser() + + /** + * A list of all registered commands. + */ + public open val commands: MutableList> = mutableListOf() + + /** @suppress **/ + public val botSettings: ExtensibleBotBuilder by inject() + + /** Whether chat commands are enabled in the bot's settings. **/ + public open val enabled: Boolean get() = botSettings.chatCommandsBuilder.enabled + + /** + * Directly register a [ChatCommand] to this command registry. + * + * Generally speaking, you shouldn't call this directly - instead, create an [Extension] and + * call the [ChatGroupCommand.chatCommand] function in your [Extension.setup] function. + * + * This function will throw a [CommandRegistrationException] if the command has already been registered, if + * a command with the same name exists, or if a command with one of the same aliases exists. + * + * @param command The command to be registered. + * @throws CommandRegistrationException Thrown if the command could not be registered. + */ + @Throws(CommandRegistrationException::class) + public open fun add(command: ChatCommand) { + val existingCommand = commands.any { it.name == command.name } + val existingAlias: String? = commands.flatMap { + it.aliases.toList() + }.firstOrNull { command.aliases.contains(it) } + + if (existingCommand) { + throw CommandRegistrationException( + command.name, + "Chat command with this name already registered in '${command.extension.name}' extension." + ) + } + + if (existingAlias != null) { + throw CommandRegistrationException( + command.name, + "Chat command with alias '$existingAlias' already registered in '${command.extension.name}' " + + "extension." + ) + } + + if (commands.contains(command)) { + throw CommandRegistrationException( + command.name, + "Chat command already registered in '${command.extension.name}' extension." + ) + } + + commands.add(command) + } + + /** + * Directly remove a registered [ChatCommand] from this command registry. + * + * This function is used when extensions are unloaded, in order to clear out their commands. + * No exception is thrown if the command wasn't registered. + * + * @param command The command to be removed. + */ + public open fun remove(command: ChatCommand): Boolean = commands.remove(command) + + /** + * Given a [MessageCreateEvent], return the prefix that should be used for a command invocation. + * + * By default, this can be set in [ExtensibleBotBuilder] - but you can override this function in subclasses if + * needed. + */ + public open suspend fun getPrefix(event: MessageCreateEvent): String = + botSettings.chatCommandsBuilder.prefixCallback(event, botSettings.chatCommandsBuilder.defaultPrefix) + + /** + * Check whether the given string starts with a mention referring to the bot. If so, the matching mention string + * is returned, otherwise `null`. + */ + public open fun String.startsWithSelfMention(): String? { + val mention = "<@${kord.selfId.value}>" + val nickMention = "<@!${kord.selfId.value}>" + + return when { + startsWith(mention) -> mention + startsWith(nickMention) -> nickMention + + else -> null + } + } + + /** + * Handles an incoming [MessageCreateEvent] and dispatches a command invocation, if possible. + */ + public open suspend fun handleEvent(event: MessageCreateEvent) { + if (botSettings.chatCommandsBuilder.ignoreSelf && event.message.author?.id == bot.kordRef.selfId) { + logger.trace { "Ignoring message ${event.message.id} as it was sent by us." } + + return + } + + val message = event.message + var commandName: String? + val prefix = getPrefix(event) + var content = message.content + + if (content.isEmpty()) { + // Empty message. + return + } + + val mention = content.startsWithSelfMention() + + content = when { + // Starts with the right mention and mentions are allowed, so remove it + mention != null && botSettings.chatCommandsBuilder.invokeOnMention -> content.substring(mention.length) + + // Starts with the right prefix, so remove it + content.startsWith(prefix) -> content.substring(prefix.length) + + // Not a valid command, so stop here + else -> return + }.trim() // Remove unnecessary spaces + + commandName = content.split(" ").first() + content = content.substring(commandName.length).trim() // Remove the command name and extra whitespace + + commandName = commandName.lowercase() + + val command = getCommand(commandName, event) + val parser = StringParser(content) + + command?.call(event, commandName, parser, content) + } + + /** + * Given a command name and [MessageCreateEvent], try to find a matching command. + * + * If a command supports locale fallback, this will also attempt to resolve names via the bot's default locale. + */ + public open suspend fun getCommand(name: String, event: MessageCreateEvent): ChatCommand? { + val defaultLocale = botSettings.i18nBuilder.defaultLocale + val locale = event.getLocale() + + return commands.firstOrNull { it.getTranslatedName(locale) == name } + ?: commands.firstOrNull { it.getTranslatedAliases(locale).contains(name) } + ?: commands.firstOrNull { it.localeFallback && it.getTranslatedName(defaultLocale) == name } + ?: commands.firstOrNull { it.localeFallback && it.getTranslatedAliases(defaultLocale).contains(name) } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Reaction.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Reaction.kt index 98e1ef2f71..d8fa54a235 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Reaction.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Reaction.kt @@ -26,23 +26,32 @@ public fun GuildEmoji.toReaction(): ReactionEmoji = ReactionEmoji.from(this) * Transform a [String] containing an emoji into a [ReactionEmoji]. * * This will attempt to parse the string as a custom emoji first and, if it can't, it'll assume you've given it a - * Unicode emoji. Custom emoji must match one of the following formats: + * Unicode emoji. + * **Note:** This may result in an invalid [ReactionEmoji], so you may prefer to use the jemoji EmojiManager to check. + * + * All custom emoji must match one of the following formats: * * * Animated emoji: `` * * Normal emoji: `<:name:id>` * - * @receiver String containing a Unicode emoji. - * @return Newly-created reaction emoji instance. + * @receiver String containing a Unicode or custom emoji. + * @return Newly created reaction emoji instance. * * @see [ReactionEmoji.Unicode] */ @Suppress("MagicNumber") public fun String.toReaction(): ReactionEmoji { - val match = CUSTOM_EMOJI_REGEX.matchEntire(this) - ?: return ReactionEmoji.Unicode(this) + val match = CUSTOM_EMOJI_REGEX.matchEntire(this) + ?: return ReactionEmoji.Unicode(this) - val groups = match.groupValues - val isAnimated = groups[1].isNotEmpty() + val groups = match.groupValues + val isAnimated = groups[1].isNotEmpty() - return ReactionEmoji.Custom(Snowflake(groups[3]), groups[2], isAnimated) + return ReactionEmoji.Custom(Snowflake(groups[3]), groups[2], isAnimated) } + +/** + * Wrapper function for the [String.toReaction] function. + */ +public fun ReactionEmoji.Companion.from(emoji: String): ReactionEmoji = + emoji.toReaction()