Skip to content

Commit

Permalink
Emoji converter: Unicode emoji support
Browse files Browse the repository at this point in the history
  • Loading branch information
gdude2002 committed Dec 1, 2023
1 parent d0ec47a commit 681eebf
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 70 deletions.
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ commons-validator = "1.7"
groovy = "3.0.19"
icu4j = "74.1"
jansi = "2.4.1"
jemoji = "1.3.2"
jsoup = "1.16.2"
junit = "5.10.1"
koin = "3.5.0"
Expand Down Expand Up @@ -37,6 +38,7 @@ detekt-libraries = { module = "io.gitlab.arturbosch.detekt:detekt-rules-librarie
groovy = { module = "org.codehaus.groovy:groovy", version.ref = "groovy" }
icu4j = { module = "com.ibm.icu:icu4j", version.ref = "icu4j" }
jansi = { module = "org.fusesource.jansi:jansi", version.ref = "jansi" }
jemoji = { module = "net.fellbaum:jemoji", version.ref = "jemoji" }
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
junit = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }

Expand Down
1 change: 1 addition & 0 deletions kord-extensions/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ dependencies {
api(libs.kord)

api(libs.bundles.logging) // Basic logging setup
api(libs.jemoji) // Basic logging setup
api(libs.kx.ser)
api(libs.sentry) // Needs to be transitive or bots will start breaking
api(libs.toml)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,93 +16,107 @@ import com.kotlindiscord.kord.extensions.modules.annotations.converters.Converte
import com.kotlindiscord.kord.extensions.modules.annotations.converters.ConverterType
import com.kotlindiscord.kord.extensions.parser.StringParser
import dev.kord.common.entity.Snowflake
import dev.kord.core.entity.GuildEmoji
import dev.kord.core.entity.Emoji
import dev.kord.core.entity.StandardEmoji
import dev.kord.core.entity.interaction.OptionValue
import dev.kord.core.entity.interaction.StringOptionValue
import dev.kord.rest.builder.interaction.OptionsBuilder
import dev.kord.rest.builder.interaction.StringChoiceBuilder
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.mapNotNull
import net.fellbaum.jemoji.EmojiManager

/**
* Argument converter for Discord [GuildEmoji] arguments.
* Argument converter for [Emoji] arguments.
*
* This converter supports specifying emojis by supplying:
*
* * The actual emoji itself
* * The emoji ID, either with or without surrounding colons
* * The emoji name, either with or without surrounding colons -
* the first matching emoji available to the bot will be used
* * The actual emoji itself, unicode or custom.
* * The custom emoji ID, either with or without surrounding colons.
* * The custom emoji name, either with or without surrounding colons,
* using the first matching emoji available to the bot.
* * The Unicode emoji name, as used by Discord.
*
* @see emoji
* @see emojiList
*/
@Converter(
"emoji",
"emoji",

types = [ConverterType.LIST, ConverterType.OPTIONAL, ConverterType.SINGLE]
types = [ConverterType.LIST, ConverterType.OPTIONAL, ConverterType.SINGLE]
)
public class EmojiConverter(
override var validator: Validator<GuildEmoji> = null
) : SingleConverter<GuildEmoji>() {
override val signatureTypeString: String = "converters.emoji.signatureType"
override val bundle: String = DEFAULT_KORDEX_BUNDLE

override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean {
val arg: String = named ?: parser?.parseNext()?.data ?: return false

val emoji: GuildEmoji = findEmoji(arg, context)
?: throw DiscordRelayedException(
context.translate("converters.emoji.error.missing", replacements = arrayOf(arg))
)

parsed = emoji
return true
}

private suspend fun findEmoji(arg: String, context: CommandContext): GuildEmoji? =
if (arg.startsWith("<a:") || arg.startsWith("<:") && arg.endsWith('>')) { // Emoji mention
val id: String = arg.substring(0, arg.length - 1).split(":").last()

try {
val snowflake = Snowflake(id)

kord.guilds.mapNotNull {
it.getEmojiOrNull(snowflake)
}.firstOrNull()
} catch (e: NumberFormatException) {
throw DiscordRelayedException(
context.translate("converters.emoji.error.invalid", replacements = arrayOf(id))
)
}
} else { // ID or name
val name = if (arg.startsWith(":") && arg.endsWith(":")) arg.substring(1, arg.length - 1) else arg

try {
val snowflake = Snowflake(name)

kord.guilds.mapNotNull {
it.getEmojiOrNull(snowflake)
}.firstOrNull()
} catch (e: NumberFormatException) { // Not an ID, let's check names
kord.guilds.mapNotNull {
it.emojis.firstOrNull { emojiObj -> emojiObj.name?.lowercase().equals(name, true) }
}.firstOrNull()
}
}

override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder =
StringChoiceBuilder(arg.displayName, arg.description).apply { required = true }

override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean {
val optionValue = (option as? StringOptionValue)?.value ?: return false

val emoji: GuildEmoji = findEmoji(optionValue, context)
?: throw DiscordRelayedException(
context.translate("converters.emoji.error.missing", replacements = arrayOf(optionValue))
)

parsed = emoji
return true
}
override var validator: Validator<Emoji> = null,
) : SingleConverter<Emoji>() {
override val signatureTypeString: String = "converters.emoji.signatureType"
override val bundle: String = DEFAULT_KORDEX_BUNDLE

override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean {
val arg: String = named ?: parser?.parseNext()?.data ?: return false

val emoji: Emoji = findEmoji(arg, context)
?: throw DiscordRelayedException(
context.translate("converters.emoji.error.missing", replacements = arrayOf(arg))
)

parsed = emoji

return true
}

private suspend fun findEmoji(arg: String, context: CommandContext): Emoji? =
if (EmojiManager.isEmoji(arg)) { // That's a Unicode emoji
StandardEmoji(arg)
} else if (arg.startsWith("<a:") || arg.startsWith("<:") && arg.endsWith('>')) { // Emoji mention
val id: String = arg.substring(0, arg.length - 1).split(":").last()

try {
val snowflake = Snowflake(id)

kord.guilds.mapNotNull {
it.getEmojiOrNull(snowflake)
}.firstOrNull()
} catch (e: NumberFormatException) {
throw DiscordRelayedException(
context.translate("converters.emoji.error.invalid", replacements = arrayOf(id))
)
}
} else { // ID or name
val name = if (arg.startsWith(":") && arg.endsWith(":")) {
arg.substring(1, arg.length - 1)
} else {
arg
}

try {
val snowflake = Snowflake(name)

kord.guilds.mapNotNull {
it.getEmojiOrNull(snowflake)
}.firstOrNull()
} catch (e: NumberFormatException) { // Not an ID, let's check names
val currentResult = kord.guilds.mapNotNull {
it.emojis.firstOrNull { emojiObj -> emojiObj.name?.lowercase().equals(name, true) }
}.firstOrNull()

currentResult ?: EmojiManager.getByDiscordAlias(arg)?.get()?.unicode?.let {
StandardEmoji(it)
}
}
}

override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder =
StringChoiceBuilder(arg.displayName, arg.description).apply { required = true }

override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean {
val optionValue = (option as? StringOptionValue)?.value ?: return false

val emoji: Emoji = findEmoji(optionValue, context)
?: throw DiscordRelayedException(
context.translate("converters.emoji.error.missing", replacements = arrayOf(optionValue))
)

parsed = emoji
return true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import dev.kord.common.entity.ChannelType
import dev.kord.common.entity.ForumTag
import dev.kord.core.behavior.channel.asChannelOfOrNull
import dev.kord.core.entity.Attachment
import dev.kord.core.entity.Emoji
import dev.kord.core.entity.GuildEmoji
import dev.kord.core.entity.channel.Channel

public class ArgumentTestExtension : Extension() {
Expand All @@ -34,6 +36,23 @@ public class ArgumentTestExtension : Extension() {
}
}

publicSlashCommand(::EmojiArguments) {
name = "test-emoji"
description = "Test the emoji converter"

action {
respond {
val type = if (arguments.emoji is GuildEmoji) {
"Guild"
} else {
"Unicode"
}

content = "$type emoji provided: `${arguments.emoji.mention}` (`${arguments.emoji.name}`)"
}
}
}

publicSlashCommand(::OptionalArgs) {
name = "optional-autocomplete"
description = "Check whether autocomplete works with an optional converter."
Expand Down Expand Up @@ -165,4 +184,11 @@ public class ArgumentTestExtension : Extension() {
requireChannelType(ChannelType.GuildText)
}
}

public inner class EmojiArguments : Arguments() {
public val emoji: Emoji by emoji {
name = "emoji"
description = "A custom or Unicode emoji"
}
}
}

0 comments on commit 681eebf

Please sign in to comment.