Skip to content

Commit

Permalink
Two changes requested by users:
Browse files Browse the repository at this point in the history
- Add default-on toggle to ignore command invocations by the same bot
- Add `ReactionEmoji.from(String)`` to wrap `String.toReaction()``
  • Loading branch information
gdude2002 committed Feb 16, 2024
1 parent f847d62 commit a35d6c0
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 160 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ public open class ExtensibleBotBuilder {
if (koinNotStarted()) {
KordExContext.startKoin {
slf4jLogger(logLevel)
environmentProperties()
environmentProperties()

if (File("koin.properties").exists()) {
fileProperties("koin.properties")
Expand Down Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ChatCommand<out Arguments>> = 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<out Arguments>) {
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<out Arguments>): 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<out Arguments>? {
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<ChatCommand<out Arguments>> = 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<out Arguments>) {
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<out Arguments>): 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<out Arguments>? {
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) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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: `<a:name:id>`
* * 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()

0 comments on commit a35d6c0

Please sign in to comment.