diff --git a/build.gradle b/build.gradle index f93d895..493492d 100644 --- a/build.gradle +++ b/build.gradle @@ -56,10 +56,10 @@ repositories { } dependencies { - implementation("me.shedaniel:linkie-core:1.0.92") { + implementation("me.shedaniel:linkie-core:1.0.94") { exclude module: "korio" } - discord_apiImplementation("com.github.Discord4J.Discord4J:discord4j-core:30ac8f4") { + discord_apiImplementation("com.github.Discord4J.Discord4J:discord4j-core:85de29f") { force true } // compile("com.github.shadorc.discord4j:discord4j-core:217336e") { diff --git a/src/discord_api/kotlin/me/shedaniel/linkie/discord/scommands/SlashCommands.kt b/src/discord_api/kotlin/me/shedaniel/linkie/discord/scommands/SlashCommands.kt index be367ea..809846a 100644 --- a/src/discord_api/kotlin/me/shedaniel/linkie/discord/scommands/SlashCommands.kt +++ b/src/discord_api/kotlin/me/shedaniel/linkie/discord/scommands/SlashCommands.kt @@ -28,6 +28,7 @@ import discord4j.core.event.domain.interaction.ChatInputInteractionEvent import discord4j.discordjson.json.ApplicationCommandData import discord4j.discordjson.json.ApplicationCommandOptionChoiceData import discord4j.discordjson.json.ApplicationCommandRequest +import discord4j.discordjson.possible.Possible import me.shedaniel.linkie.discord.handler.ThrowableHandler import me.shedaniel.linkie.discord.utils.CommandContext import me.shedaniel.linkie.discord.utils.SlashCommandBasedContext @@ -35,8 +36,8 @@ import me.shedaniel.linkie.discord.utils.dismissButton import me.shedaniel.linkie.discord.utils.event import me.shedaniel.linkie.discord.utils.extensions.getOrNull import me.shedaniel.linkie.discord.utils.replyComplex -import reactor.core.publisher.Flux import reactor.core.publisher.Mono +import java.util.stream.Collectors data class SlashCommandHandler( val responder: (event: ChatInputInteractionEvent) -> Unit, @@ -52,32 +53,29 @@ class SlashCommands( val applicationId: Long by lazy { client.restClient.applicationId.block() } val handlers = mutableMapOf() val guildHandlers = mutableMapOf() - val globalCommands: Flux by lazy { + val registeredCommands = mutableMapOf() + val registeredGuildCommands = mutableMapOf() + val globalCommands: MutableList by lazy { client.restClient.applicationService .getGlobalApplicationCommands(applicationId) .cache() + .toStream() + .collect(Collectors.toList()) } - val guildCommands = mutableMapOf>() + val guildCommands = mutableMapOf>() data class GuildCommandKey(val guildId: Snowflake, val commandName: String) - fun getGuildCommands(id: Snowflake): Flux { + fun getGuildCommands(id: Snowflake): MutableList { return guildCommands.getOrPut(id) { client.restClient.applicationService .getGuildApplicationCommands(applicationId, id.asLong()) .cache() + .toStream() + .collect(Collectors.toList()) } } - init { - /*gateway.restClient.applicationService.getGuildApplicationCommands(applicationId, testingGuild).parallel().flatMap { data -> - gateway.restClient.applicationService.deleteGuildApplicationCommand(applicationId, testingGuild, data.id().toLong()) - .doOnSuccess { - println(data.id()) - } - }.then().block()*/ - } - init { client.event { event -> val handler = when { @@ -117,26 +115,37 @@ class SlashCommands( fun guildCommand(guild: Snowflake, command: SlashCommand) { for (cmd in command.cmds) { - var registered = false - getGuildCommands(guild) - .flatMap { data -> - if (data.name() == cmd) { - registered = true - val commandData = buildData(command, cmd, data.id(), data.applicationId()) - if (data.toString() != commandData.toString()) { - println("not same $cmd guild ${guild.asString()}") - return@flatMap modifyGuildCommand(guild, command, data.id().toLong(), cmd) - } + guildHandlers[GuildCommandKey(guild, cmd)] = buildHandler(command, cmd) + + val existingCommand = registeredGuildCommands[GuildCommandKey(guild, cmd)] + + if (existingCommand != null) { + if (!equals(existingCommand, existingCommand.derive(command))) { + println("Updating guild (${guild.asString()}) command /$cmd") + modifyGuildCommand(guild, command, existingCommand.id().toLong(), cmd).subscribe { newData -> + registeredGuildCommands[GuildCommandKey(guild, cmd)] = newData } - return@flatMap Mono.empty() - }.doOnComplete { - if (!registered) { - println("yes $cmd") - registered = true - createGuildCommand(guild, command, cmd).subscribe() + } + return + } + + for (data in getGuildCommands(guild)) { + if (data.name() == cmd) { + val commandData = data.derive(command) + if (!equals(data, commandData)) { + println("Updating guild (${guild.asString()}) command /$cmd") + modifyGuildCommand(guild, command, data.id().toLong(), cmd).subscribe { newData -> + registeredGuildCommands[GuildCommandKey(guild, cmd)] = newData + } } - }.subscribe() - guildHandlers[GuildCommandKey(guild, cmd)] = buildHandler(command, cmd) + return + } + } + + println("Registering guild (${guild.asString()}) command /$cmd") + createGuildCommand(guild, command, cmd).subscribe { newData -> + registeredGuildCommands[GuildCommandKey(guild, cmd)] = newData + } } } @@ -164,28 +173,49 @@ class SlashCommands( .doOnError { errorHandler("Unable to create guild command: " + it.message) } .onErrorResume { Mono.empty() } + private fun equals(data1: ApplicationCommandData, data2: ApplicationCommandData): Boolean { + return data1.name() == data2.name() && + data1.description() == data2.description() && + data1.applicationId() == data2.applicationId() && + data1.type() == data2.type() && + data1.options() == data2.options() && + data1.defaultPermission() == data2.defaultPermission() && + data1.id() == data2.id() + } + fun globalCommand(command: SlashCommand) { for (cmd in command.cmds) { - var registered = false - globalCommands - .flatMap { data -> - if (data.name() == cmd) { - registered = true - val commandData = buildData(command, cmd, data.id(), data.applicationId()) - if (data.toString() != commandData.toString()) { - println("not same $cmd global") - return@flatMap modifyGlobalCommand(command, data.id().toLong(), cmd) - } + handlers[cmd] = buildHandler(command, cmd) + + val existingCommand = registeredCommands[cmd] + + if (existingCommand != null) { + if (!equals(existingCommand, existingCommand.derive(command))) { + println("Updating global command /$cmd") + modifyGlobalCommand(command, existingCommand.id().toLong(), cmd).subscribe { newData -> + registeredCommands[cmd] = newData } - return@flatMap Mono.empty() - }.doOnComplete { - if (!registered) { - println("yes $cmd") - registered = true - createGlobalCommand(command, cmd).subscribe() + } + return + } + + for (data in globalCommands) { + if (data.name() == cmd) { + val commandData = data.derive(command) + if (!equals(data, commandData)) { + println("Updating global command /$cmd") + modifyGlobalCommand(command, data.id().toLong(), cmd).subscribe { newData -> + registeredCommands[cmd] = newData + } } - }.subscribe() - handlers[cmd] = buildHandler(command, cmd) + return + } + } + + println("Registering global command /$cmd") + createGlobalCommand(command, cmd).subscribe { newData -> + registeredCommands[cmd] = newData + } } } @@ -242,9 +272,13 @@ class SlashCommands( .addAllOptions(command.options.map(SlashCommandOption<*>::toData)) .build() - private fun buildData(command: SlashCommand, cmd: String, id: String, applicationId: String): ApplicationCommandData = + private fun ApplicationCommandData.derive(command: SlashCommand): ApplicationCommandData = + buildData(command, name(), id(), type(), applicationId()) + + private fun buildData(command: SlashCommand, cmd: String, id: String, type: Possible, applicationId: String): ApplicationCommandData = ApplicationCommandData.builder() .id(id) + .type(type) .applicationId(applicationId) .name(cmd.also { require(it.toLowerCase() == it && "^[\\w-_]+\$".toRegex().matchEntire(it) != null) { "$it is not a valid name" } }) .description(command.description(cmd)) @@ -304,6 +338,36 @@ class SlashCommands( } return sink.suggested } + + fun removeGlobalCommand(name: String) { + val cmd = globalCommands.firstOrNull { it.name() == name.toLowerCase() } + ?: registeredCommands[name.toLowerCase()] + ?: return + println("Deleting global command /${cmd.name()}") + client.restClient.applicationService + .deleteGlobalApplicationCommand(applicationId, cmd.id().toLong()) + .doOnError { errorHandler("Unable to remove global command: " + it.message) } + .onErrorResume { Mono.empty() } + .subscribe() + globalCommands.removeIf { name.toLowerCase() == it.name() } + registeredCommands.remove(name.toLowerCase()) + handlers.remove(name.toLowerCase()) + } + + fun removeGuildCommand(guildId: Snowflake, name: String) { + val cmd = guildCommands[guildId]?.firstOrNull { it.name() == name.toLowerCase() } + ?: registeredGuildCommands[GuildCommandKey(guildId, name.toLowerCase())] + ?: return + println("Deleting guild (${guildId.asString()}) command /${cmd.name()}") + client.restClient.applicationService + .deleteGuildApplicationCommand(applicationId, guildId.asLong(), cmd.id().toLong()) + .doOnError { errorHandler("Unable to remove guild command: " + it.message) } + .onErrorResume { Mono.empty() } + .subscribe() + guildCommands[guildId]?.removeIf { name.toLowerCase() == it.name() } + registeredGuildCommands.remove(GuildCommandKey(guildId, name.toLowerCase())) + guildHandlers.remove(GuildCommandKey(guildId, name.toLowerCase())) + } } interface SlashCommandOptionSuggestionSink { @@ -589,6 +653,7 @@ interface OptionsGetter : SuggestionOptionsGetter, WeakOptionsGetter { fun of(slashCommand: SlashCommand, ctx: CommandContext, option: ApplicationCommandInteractionOption): OptionsGetter = WeakOptionsGetter.of(slashCommand, option).asStrong(ctx) + fun of(slashCommand: SlashCommand, ctx: CommandContext, value: Any?): OptionsGetter = WeakOptionsGetter.of(slashCommand, value).asStrong(ctx) } diff --git a/src/discord_api/kotlin/me/shedaniel/linkie/discord/utils/CommandContext.kt b/src/discord_api/kotlin/me/shedaniel/linkie/discord/utils/CommandContext.kt index 966f208..0688e5c 100644 --- a/src/discord_api/kotlin/me/shedaniel/linkie/discord/utils/CommandContext.kt +++ b/src/discord_api/kotlin/me/shedaniel/linkie/discord/utils/CommandContext.kt @@ -30,6 +30,7 @@ import reactor.core.publisher.Mono interface CommandContext { val prefix: String + val locale: String? val message: MessageCreator val interactionId: Snowflake val messageId: Snowflake? @@ -68,6 +69,8 @@ val CommandContext.inGuild: InGuildCommandContext get() = this@inGuild.user override val prefix: String get() = this@inGuild.prefix + override val locale: String? + get() = this@inGuild.locale override val message: MessageCreator get() = this@inGuild.message override val guild: Guild @@ -94,6 +97,8 @@ class SlashCommandBasedContext( override val prefix: String get() = "/" + override val locale: String? + get() = event.interaction.data.locale().getOrNull() override val interactionId: Snowflake get() = event.interaction.id override val messageId: Snowflake? @@ -120,6 +125,8 @@ class MessageBasedCommandContext( ) : CommandContext { override val message: MessageCreator by lazy { channel.msgCreator(this, event.message) } + override val locale: String? + get() = null override val interactionId: Snowflake get() = messageId override val messageId: Snowflake diff --git a/src/main/kotlin/me/shedaniel/linkie/discord/LinkieBot.kt b/src/main/kotlin/me/shedaniel/linkie/discord/LinkieBot.kt index 2d2036c..2836ad3 100644 --- a/src/main/kotlin/me/shedaniel/linkie/discord/LinkieBot.kt +++ b/src/main/kotlin/me/shedaniel/linkie/discord/LinkieBot.kt @@ -76,6 +76,7 @@ import me.shedaniel.linkie.discord.tricks.TricksManager import me.shedaniel.linkie.discord.utils.CommandContext import me.shedaniel.linkie.discord.utils.discriminatedName import me.shedaniel.linkie.discord.utils.event +import me.shedaniel.linkie.discord.utils.extensions.getOrNull import me.shedaniel.linkie.discord.utils.reply import me.shedaniel.linkie.discord.utils.sendMessage import me.shedaniel.linkie.discord.utils.setTimestampToNow @@ -133,6 +134,7 @@ fun main() { ) ) { val slashCommands = SlashCommands(this, LinkieThrowableHandler, ::warn) + TricksManager.listen(slashCommands) val commandManager = object : CommandManager(if (isDebug) "@" else "!") { override fun getPrefix(event: MessageCreateEvent): String { return event.guildId.orElse(null)?.let { ConfigManager[it.asLong()].prefix } ?: super.getPrefix(event) @@ -175,7 +177,7 @@ fun main() { val dispatch: ThreadMembersUpdate = ThreadMembersUpdateEvent::class.java.getDeclaredField("dispatch").also { it.isAccessible = true }.get(event) as ThreadMembersUpdate - if (dispatch.addedMembers().any { it.userId().asLong() == this.selfId.asLong() }) { + if (dispatch.addedMembers().any { it.userId().getOrNull()?.asLong() == this.selfId.asLong() }) { gateway.getChannelById(event.threadId).subscribe { channel -> if (channel is ThreadChannel) { channel.sendMessage { diff --git a/src/main/kotlin/me/shedaniel/linkie/discord/commands/AboutCommand.kt b/src/main/kotlin/me/shedaniel/linkie/discord/commands/AboutCommand.kt index c0f7b1b..1d81a00 100644 --- a/src/main/kotlin/me/shedaniel/linkie/discord/commands/AboutCommand.kt +++ b/src/main/kotlin/me/shedaniel/linkie/discord/commands/AboutCommand.kt @@ -19,6 +19,7 @@ package me.shedaniel.linkie.discord.commands import discord4j.core.`object`.entity.User import me.shedaniel.linkie.discord.OptionlessCommand import me.shedaniel.linkie.discord.gateway +import me.shedaniel.linkie.discord.lang.i18n import me.shedaniel.linkie.discord.utils.CommandContext import me.shedaniel.linkie.discord.utils.addField import me.shedaniel.linkie.discord.utils.basicEmbed @@ -29,20 +30,20 @@ import me.shedaniel.linkie.discord.utils.replyComplex object AboutCommand : OptionlessCommand { override suspend fun execute(ctx: CommandContext) { ctx.message.replyComplex { - layout { - row { - linkButton("Library Source", "https://github.com/linkie/linkie-core/") - linkButton("Bot Source", "https://github.com/linkie/linkie-discord/") - linkButton("Bot Invite", "https://discord.com/api/oauth2/authorize?client_id=472081983925780490&permissions=339008&scope=bot%20applications.commands") - } - } embed { - title("About Linkie") + title("text.about.title".i18n(ctx)) gateway.self.map(User::getAvatarUrl).block()?.also { url -> thumbnail(url) } - description = "A mappings bot created by <@430615025066049538>." - addField("License", "Apache 2.0") + description = "text.about.description".i18n(ctx) + addField("text.about.license".i18n(ctx), "Apache 2.0") basicEmbed(ctx.user) } + layout { + row { + linkButton("text.about.links.core".i18n(ctx), "https://github.com/linkie/linkie-core/") + linkButton("text.about.links.bot".i18n(ctx), "https://github.com/linkie/linkie-discord/") + linkButton("text.about.links.invite".i18n(ctx), "https://discord.com/api/oauth2/authorize?client_id=472081983925780490&permissions=339008&scope=bot%20applications.commands") + } + } } } } \ No newline at end of file diff --git a/src/main/kotlin/me/shedaniel/linkie/discord/commands/AddTrickCommand.kt b/src/main/kotlin/me/shedaniel/linkie/discord/commands/AddTrickCommand.kt index 4774dfa..93576d7 100644 --- a/src/main/kotlin/me/shedaniel/linkie/discord/commands/AddTrickCommand.kt +++ b/src/main/kotlin/me/shedaniel/linkie/discord/commands/AddTrickCommand.kt @@ -21,6 +21,7 @@ import me.shedaniel.linkie.discord.Command import me.shedaniel.linkie.discord.scommands.SlashCommandBuilderInterface import me.shedaniel.linkie.discord.scommands.args import me.shedaniel.linkie.discord.scommands.opt +import me.shedaniel.linkie.discord.scommands.string import me.shedaniel.linkie.discord.scripting.LinkieScripting import me.shedaniel.linkie.discord.tricks.ContentType import me.shedaniel.linkie.discord.tricks.Trick @@ -36,18 +37,17 @@ import java.util.* object AddTrickCommand : Command { override suspend fun SlashCommandBuilderInterface.buildCommand(slash: Boolean) { + val name = string("name", "The name of the trick") val args = args() executeCommandWithGetter { ctx, options -> - execute(ctx, options.opt(args)) + execute(ctx, options.opt(name), options.opt(args)) } } - fun execute(ctx: CommandContext, args: MutableList) { + fun execute(ctx: CommandContext, name: String, args: MutableList) { ctx.validateInGuild { - args.validateUsage(prefix, 2..Int.MAX_VALUE, "$cmd [--script] ") - val name = args.first() + args.validateUsage(prefix, 1..Int.MAX_VALUE, "$cmd [--script] ") LinkieScripting.validateTrickName(name) - args.removeAt(0) var type = ContentType.TEXT val flags = mutableListOf() val iterator = args.iterator() @@ -104,7 +104,7 @@ object AddTrickCommand : Command { message.reply { basicEmbed(user) title("Added Trick") - description = "Successfully added trick: $name" + description = "Successfully added trick: $name\nSlash commands of this trick may take a few minutes / hours to update (Discord Caching)" } } } diff --git a/src/main/kotlin/me/shedaniel/linkie/discord/commands/QueryMappingsCommand.kt b/src/main/kotlin/me/shedaniel/linkie/discord/commands/QueryMappingsCommand.kt index 2c97099..fc1ee41 100644 --- a/src/main/kotlin/me/shedaniel/linkie/discord/commands/QueryMappingsCommand.kt +++ b/src/main/kotlin/me/shedaniel/linkie/discord/commands/QueryMappingsCommand.kt @@ -162,7 +162,7 @@ open class QueryMappingsCommand( QueryMappingsExtensions.query(searchTerm, namespace.getProvider(version), user, message, maxPage, fuzzy, types) }.initiate() message.sendPages(0, maxPage.get()) { page -> - QueryMessageBuilder.buildMessage(this, searchTerm, namespace, query.value, query.mappings, page, user, maxPage.get(), fuzzy.get()) + QueryMessageBuilder.buildMessage(this, ctx.locale, searchTerm, namespace, query.value, query.mappings, page, user, maxPage.get(), fuzzy.get()) } } diff --git a/src/main/kotlin/me/shedaniel/linkie/discord/commands/QueryTranslateMappingsCommand.kt b/src/main/kotlin/me/shedaniel/linkie/discord/commands/QueryTranslateMappingsCommand.kt index abab8ef..702a806 100644 --- a/src/main/kotlin/me/shedaniel/linkie/discord/commands/QueryTranslateMappingsCommand.kt +++ b/src/main/kotlin/me/shedaniel/linkie/discord/commands/QueryTranslateMappingsCommand.kt @@ -28,6 +28,7 @@ import me.shedaniel.linkie.Method import me.shedaniel.linkie.Namespace import me.shedaniel.linkie.discord.Command import me.shedaniel.linkie.discord.MappingsQueryUtils +import me.shedaniel.linkie.discord.lang.i18n import me.shedaniel.linkie.discord.scommands.CommandOptionMeta import me.shedaniel.linkie.discord.scommands.OptionsGetter import me.shedaniel.linkie.discord.scommands.SlashCommandBuilderInterface @@ -208,7 +209,7 @@ class QueryTranslateMappingsCommand( else title("List of ${result.source.name}->${result.target.name} Mappings") buildSafeDescription { if (fuzzy.get()) { - append("**No results found for __${searchTerm}__. Displaying related results.**").appendLine().appendLine() + append("text.mappings.query.fuzzy_matched".i18n(ctx, searchTerm)).appendLine() } var isFirst = true @@ -219,18 +220,18 @@ class QueryTranslateMappingsCommand( appendLine().appendLine() } when (original) { - is Class -> buildClass(original, translated as Class) - is FieldResult -> buildField(original, translated as FieldResult) - is MethodResult -> buildMethod(original, translated as MethodResult) + is Class -> buildClass(ctx.locale, original, translated as Class) + is FieldResult -> buildField(ctx.locale, original, translated as FieldResult) + is MethodResult -> buildMethod(ctx.locale, original, translated as MethodResult) } } } } } - private fun StringBuilder.buildClass(original: Class, translated: Class) { - appendLine("**Class: ${original.optimumName} => __${translated.optimumName}__**") - append("__Name__: ") + private fun StringBuilder.buildClass(locale: String?, original: Class, translated: Class) { + appendLine("text.mappings.query.class.translate".i18n(locale, original.optimumName, translated.optimumName)) + append("text.mappings.query.name".i18n(locale)) append(original.mappedName.mapIfNotNullOrNotEquals(original.intermediaryName) { "`$it` => " } ?: "") append("`${original.intermediaryName}`") if (original.intermediaryName != translated.intermediaryName) { @@ -241,9 +242,9 @@ class QueryTranslateMappingsCommand( append(translated.mappedName.mapIfNotNullOrNotEquals(translated.intermediaryName) { " => `$it`" } ?: "") } - private fun StringBuilder.buildField(original: FieldResult, translated: FieldResult) { - appendLine("**Field: ${original.parent}#${original.field.optimumName} => __${translated.parent}#${translated.field.optimumName}__**") - append("__Name__: ") + private fun StringBuilder.buildField(locale: String?, original: FieldResult, translated: FieldResult) { + appendLine("text.mappings.query.field.translate".i18n(locale, original.parent, original.field.optimumName, translated.parent, translated.field.optimumName)) + append("text.mappings.query.name".i18n(locale)) append(original.field.mappedName.mapIfNotNullOrNotEquals(original.field.intermediaryName) { "`$it` => " } ?: "") append("`${original.field.intermediaryName}`") if (original.field.intermediaryName != translated.field.intermediaryName) { @@ -254,9 +255,9 @@ class QueryTranslateMappingsCommand( append(translated.field.mappedName.mapIfNotNullOrNotEquals(translated.field.intermediaryName) { " => `$it`" } ?: "") } - private fun StringBuilder.buildMethod(original: MethodResult, translated: MethodResult) { - appendLine("**Method: ${original.parent}#${original.method.optimumName} => __${translated.parent}#${translated.method.optimumName}__**") - append("__Name__: ") + private fun StringBuilder.buildMethod(locale: String?, original: MethodResult, translated: MethodResult) { + appendLine("text.mappings.query.method.translate".i18n(locale, original.parent, original.method.optimumName, translated.parent, translated.method.optimumName)) + append("text.mappings.query.name".i18n(locale)) append(original.method.mappedName.mapIfNotNullOrNotEquals(original.method.intermediaryName) { "`$it` => " } ?: "") append("`${original.method.intermediaryName}`") if (original.method.intermediaryName != translated.method.intermediaryName) { diff --git a/src/main/kotlin/me/shedaniel/linkie/discord/commands/RemoveTrickCommand.kt b/src/main/kotlin/me/shedaniel/linkie/discord/commands/RemoveTrickCommand.kt index a0f9a7b..c75ebc9 100644 --- a/src/main/kotlin/me/shedaniel/linkie/discord/commands/RemoveTrickCommand.kt +++ b/src/main/kotlin/me/shedaniel/linkie/discord/commands/RemoveTrickCommand.kt @@ -45,7 +45,7 @@ object RemoveTrickCommand : SimpleCommand { message.reply { basicEmbed(user) title("Removed Trick") - description = "Successfully removed trick: $trickName" + description = "Successfully removed trick: $trickName\nSlash commands of this trick may take a few minutes / hours to update (Discord Caching)" } } } diff --git a/src/main/kotlin/me/shedaniel/linkie/discord/tricks/TricksManager.kt b/src/main/kotlin/me/shedaniel/linkie/discord/tricks/TricksManager.kt index 4209fa6..a028e6c 100644 --- a/src/main/kotlin/me/shedaniel/linkie/discord/tricks/TricksManager.kt +++ b/src/main/kotlin/me/shedaniel/linkie/discord/tricks/TricksManager.kt @@ -19,6 +19,8 @@ package me.shedaniel.linkie.discord.tricks import discord4j.common.util.Snowflake import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json +import me.shedaniel.linkie.discord.asSlashCommand +import me.shedaniel.linkie.discord.scommands.SlashCommands import me.shedaniel.linkie.utils.ZipFile import me.shedaniel.linkie.utils.error import me.shedaniel.linkie.utils.info @@ -28,6 +30,8 @@ import java.util.* object TricksManager { val globalTricks = mutableMapOf() val tricks = mutableMapOf() + var slashCommands: SlashCommands? = null + private val tricksFolder get() = File(File(System.getProperty("user.dir")), "tricks").also { it.mkdirs() } private val json = Json { ignoreUnknownKeys = true @@ -63,6 +67,24 @@ object TricksManager { globalTricks.clear() globalTricks.putAll(tempGlobalTricks) save() + checkCommands(listOf()) + } + + fun checkCommands(extraChecks: List) { + val slashCommands = this.slashCommands ?: return + val tricks = (tricks.values.asSequence() + extraChecks.asSequence()).toMutableList() + TricksManager.tricks.values.forEach { trick -> + slashCommands.guildCommand(trick.guildId, TrickBasedCommand(trick) + .asSlashCommand("Run trick ${trick.name}", listOf(trick.name))) + } + val availableTricks = TricksManager.tricks.values.map { it.name } + tricks.groupBy { it.guildId }.mapValues { it.value.map { it.name }.toSet() }.forEach { (guildId, tricks) -> + ArrayList(slashCommands.getGuildCommands(Snowflake.of(guildId)) + slashCommands.registeredGuildCommands.filterKeys { it.guildId.asLong() == guildId }.values).distinctBy { it.name() }.forEach { commands -> + if (commands.description().startsWith("Run trick ") && commands.name() !in availableTricks) { + slashCommands.removeGuildCommand(Snowflake.of(guildId), commands.name()) + } + } + } } private suspend fun readGlobalTrick(function: (trick: GlobalTrick) -> Unit) { @@ -101,6 +123,7 @@ object TricksManager { require(tricks.none { it.value.name == trick.name && it.value.guildId == trick.guildId }) { "Trick with name \"${trick.name}\" already exists!" } tricks[trick.id] = trick save() + checkCommands(listOf()) } fun removeTrick(trick: Trick) { @@ -108,7 +131,13 @@ object TricksManager { val trickFile = File(tricksFolder, "${trick.id}.json") trickFile.delete() save() + checkCommands(listOf(trick)) } operator fun get(pair: Pair): Trick? = tricks.values.firstOrNull { it.name == pair.first && it.guildId == pair.second } + + fun listen(slashCommands: SlashCommands) { + this.slashCommands = slashCommands + checkCommands(listOf()) + } } \ No newline at end of file diff --git a/src/main/kotlin/me/shedaniel/linkie/discord/utils/QueryMessageBuilder.kt b/src/main/kotlin/me/shedaniel/linkie/discord/utils/QueryMessageBuilder.kt index e95d252..935248c 100644 --- a/src/main/kotlin/me/shedaniel/linkie/discord/utils/QueryMessageBuilder.kt +++ b/src/main/kotlin/me/shedaniel/linkie/discord/utils/QueryMessageBuilder.kt @@ -24,17 +24,18 @@ import me.shedaniel.linkie.MappingsContainer import me.shedaniel.linkie.MappingsMetadata import me.shedaniel.linkie.Method import me.shedaniel.linkie.Namespace +import me.shedaniel.linkie.discord.lang.i18n import me.shedaniel.linkie.getMappedDesc import me.shedaniel.linkie.utils.ResultHolder import me.shedaniel.linkie.utils.dropAndTake import me.shedaniel.linkie.utils.localiseFieldDesc object QueryMessageBuilder { - fun buildMessage(spec: EmbedCreateSpec.Builder, searchTerm: String, namespace: Namespace, results: List>, mappings: MappingsMetadata, page: Int, author: User, maxPage: Int, fuzzy: Boolean) { - buildHeader(spec, mappings, page, author, maxPage) + fun buildMessage(spec: EmbedCreateSpec.Builder, locale: String?, searchTerm: String, namespace: Namespace, results: List>, mappings: MappingsMetadata, page: Int, author: User, maxPage: Int, fuzzy: Boolean) { + buildHeader(spec, locale, mappings, page, author, maxPage) spec.buildSafeDescription { if (fuzzy) { - append("**No results found for __${searchTerm}__. Displaying related results.**").appendLine().appendLine() + append("text.mappings.query.fuzzy_matched".i18n(locale, searchTerm)).appendLine().appendLine() } var isFirst = true @@ -45,25 +46,25 @@ object QueryMessageBuilder { appendLine().appendLine() } when { - value is Class -> buildClass(this, namespace, value) + value is Class -> buildClass(this, locale, namespace, value) value is Pair<*, *> && value.second is Field -> - buildField(this, namespace, value.second as Field, value.first as Class, mappings as? MappingsContainer) + buildField(this, locale, namespace, value.second as Field, value.first as Class, mappings as? MappingsContainer) value is Pair<*, *> && value.second is Method -> - buildMethod(this, namespace, value.second as Method, value.first as Class, mappings as? MappingsContainer) + buildMethod(this, locale, namespace, value.second as Method, value.first as Class, mappings as? MappingsContainer) } } } } - fun buildHeader(spec: EmbedCreateSpec.Builder, metadata: MappingsMetadata, page: Int, author: User, maxPage: Int) = spec.apply { + fun buildHeader(spec: EmbedCreateSpec.Builder, locale: String?, metadata: MappingsMetadata, page: Int, author: User, maxPage: Int) = spec.apply { basicEmbed(author, metadata.mappingsSource?.toString()) - if (maxPage > 1) title("List of ${metadata.name} Mappings for ${metadata.version} (Page ${page + 1}/$maxPage)") - else title("List of ${metadata.name} Mappings for ${metadata.version}") + if (maxPage > 1) title("text.mappings.query.title.paged".i18n(locale, metadata.name, metadata.version, page + 1, maxPage)) + else title("text.mappings.query.title".i18n(locale, metadata.name, metadata.version)) } - fun buildClass(builder: StringBuilder, namespace: Namespace, classEntry: Class) = builder.apply { - appendLine("**Class: __${classEntry.optimumName}__**") - append("__Name__: ") + fun buildClass(builder: StringBuilder, locale: String?, namespace: Namespace, classEntry: Class) = builder.apply { + appendLine("text.mappings.query.class".i18n(locale, classEntry.optimumName)) + append("text.mappings.query.name".i18n(locale)) append(classEntry.obfName.buildString(nonEmptySuffix = " => ")) append("`${classEntry.intermediaryName}`") append(classEntry.mappedName.mapIfNotNullOrNotEquals(classEntry.intermediaryName) { " => `$it`" } ?: "") @@ -76,21 +77,23 @@ object QueryMessageBuilder { fun buildField( builder: StringBuilder, + locale: String?, namespace: Namespace, field: Field, parent: Class, mappings: MappingsContainer?, - ) = buildField(builder, namespace, field, parent.optimumName, mappings) + ) = buildField(builder, locale, namespace, field, parent.optimumName, mappings) fun buildField( builder: StringBuilder, + locale: String?, namespace: Namespace, field: Field, parent: String, mappings: MappingsContainer?, ) = builder.apply { - appendLine("**Field: $parent#__${field.optimumName}__**") - append("__Name__: ") + appendLine("text.mappings.query.field".i18n(locale, parent, field.optimumName)) + append("text.mappings.query.name".i18n(locale)) append(field.obfName.buildString(nonEmptySuffix = " => ")) append("`${field.intermediaryName}`") append(field.mappedName.mapIfNotNullOrNotEquals(field.intermediaryName) { " => `$it`" } ?: "") @@ -98,11 +101,11 @@ object QueryMessageBuilder { if (mappings == null) return@apply val mappedDesc = field.getMappedDesc(mappings) if (namespace.supportsFieldDescription()) { - appendLine().append("__Type__: ") + appendLine().append("text.mappings.query.type".i18n(locale)) append(mappedDesc.localiseFieldDesc()) } if (namespace.supportsMixin()) { - appendLine().append("__Mixin Target__: `") + appendLine().append("text.mappings.query.mixin".i18n(locale)) append("L$parent;") append(field.optimumName) append(':') @@ -128,21 +131,23 @@ object QueryMessageBuilder { fun buildMethod( builder: StringBuilder, + locale: String?, namespace: Namespace, method: Method, parent: Class, mappings: MappingsContainer?, - ) = buildMethod(builder, namespace, method, parent.optimumName, mappings) + ) = buildMethod(builder, locale, namespace, method, parent.optimumName, mappings) fun buildMethod( builder: StringBuilder, + locale: String?, namespace: Namespace, method: Method, parent: String, mappings: MappingsContainer?, ) = builder.apply { - appendLine("**Method: $parent#__${method.optimumName}__**") - append("__Name__: ") + appendLine("text.mappings.query.method".i18n(locale, parent, method.optimumName)) + append("text.mappings.query.name".i18n(locale)) append(method.obfName.buildString(nonEmptySuffix = " => ")) append("`${method.intermediaryName}`") append(method.mappedName.mapIfNotNullOrNotEquals(method.intermediaryName) { " => `$it`" } ?: "") @@ -150,7 +155,7 @@ object QueryMessageBuilder { if (mappings == null) return@apply val mappedDesc = method.getMappedDesc(mappings) if (namespace.supportsMixin()) { - appendLine().append("__Mixin Target__: `") + appendLine().append("text.mappings.query.mixin".i18n(locale)) append("L$parent;") append(method.optimumName) append(mappedDesc)