Skip to content

Commit

Permalink
feat(kord): hacky suggestions to replace the previous hack
Browse files Browse the repository at this point in the history
  • Loading branch information
Citymonstret committed Jan 19, 2024
1 parent db22532 commit a5229f1
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 56 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//
// MIT License
//
// Copyright (c) 2024 Incendo
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
package org.incendo.cloud.discord.slash;

import cloud.commandframework.arguments.parser.ArgumentParseResult;
import cloud.commandframework.arguments.parser.ArgumentParser;
import cloud.commandframework.context.CommandContext;
import cloud.commandframework.context.CommandInput;
import java.util.concurrent.CompletableFuture;
import org.apiguardian.api.API;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;

/**
* Hack that enables Discord parsers to return nullable values when parsing.
*
* <p>The parser will return a successful response wrapping the {@link #SENTINEL_VALUE}. This value cannot be extracted from
* the command context, as that will lead to a {@link ClassCastException} at runtime.</p>
*
* @param <C> command sender type
* @param <T> parser type
* @since 1.0.0
*/
@SuppressWarnings({"rawtypes", "unchecked"})
@API(status = API.Status.INTERNAL, since = "1.0.0")
public abstract class NullableParser<C, T> implements ArgumentParser.FutureArgumentParser<C, T> {

public static final Object SENTINEL_VALUE = new Object();

/**
* Attempts to parse the object, returning {@code null} if parsing was successful but did not map to a currently
* existing value.
*
* @param commandContext command context
* @param commandInput command input
* @return future that completes with a result or {@code null}
*/
public abstract @NonNull CompletableFuture<@Nullable ArgumentParseResult<T>> parseNullable(
@NonNull CommandContext<@NonNull C> commandContext,
@NonNull CommandInput commandInput
);

@Override
public final @NonNull CompletableFuture parseFuture(
final @NonNull CommandContext<@NonNull C> commandContext,
final @NonNull CommandInput commandInput
) {
return this.parseNullable(commandContext, commandInput).thenApply(result -> {
if (result != null) {
return result;
}
return ArgumentParseResult.success(SENTINEL_VALUE);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,17 @@ import cloud.commandframework.arguments.parser.ArgumentParseResult
import cloud.commandframework.arguments.parser.ParserDescriptor
import cloud.commandframework.context.CommandContext
import cloud.commandframework.context.CommandInput
import cloud.commandframework.kotlin.coroutines.SuspendingArgumentParser
import cloud.commandframework.kotlin.coroutines.asParserDescriptor
import dev.kord.core.entity.Attachment
import dev.kord.core.entity.Entity
import dev.kord.core.entity.Role
import dev.kord.core.entity.User
import dev.kord.core.entity.channel.Channel
import dev.kord.core.entity.interaction.GuildInteraction
import dev.kord.core.entity.interaction.InteractionCommand
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.future.future
import org.apiguardian.api.API
import kotlin.NullPointerException
import org.incendo.cloud.discord.slash.NullableParser
import java.util.concurrent.CompletableFuture

/**
* A parser which wraps a Kord option value.
Expand All @@ -48,87 +47,67 @@ import kotlin.NullPointerException
* @since 1.0.0
*/
@API(status = API.Status.STABLE, since = "1.0.0")
public fun interface KordParser<C : Any, T : Any> : SuspendingArgumentParser<C, T> {
public data class KordParser<C : Any, T : Any> internal constructor(
private val extract: suspend (
name: String,
command: InteractionCommand
) -> ArgumentParseResult<T>?
) : NullableParser<C, T>() {

public companion object {

/**
* Returns a parser which extracts a [User].
*
* The parsed user may not be accessed from the context during suggestion generation.
*/
public fun <C : Any> userParser(): ParserDescriptor<C, User> = createParser<C, User> { name, command, context ->
public fun <C : Any> userParser(): ParserDescriptor<C, User> = createParser<C, User> { name, command ->
command.users[name]?.let { ArgumentParseResult.success(it) }
?: context.ifSuggestion(name) {
ArgumentParseResult.success(context.interaction.interactionEvent.interaction.user)
}
}

/**
* Returns a parser which extracts a [Channel].
*
* The parsed channel may not be accessed from the context during suggestion generation.
*/
public fun <C : Any> channelParser(): ParserDescriptor<C, Channel> = createParser<C, Channel> { name, command, context ->
public fun <C : Any> channelParser(): ParserDescriptor<C, Channel> = createParser<C, Channel> { name, command ->
command.channels[name]?.let { ArgumentParseResult.success(it) }
?: context.ifSuggestion(name) {
(context.interaction.interactionEvent as? GuildInteraction)?.channel?.asChannel()
?.let { ArgumentParseResult.success(it) }
}
}

/**
* Returns a parser which extracts a [Role].
*
* The parsed role may not be accessed from the context during suggestion generation.
*/
public fun <C : Any> roleParser(): ParserDescriptor<C, Role> = createParser<C, Role> { name, command, context ->
public fun <C : Any> roleParser(): ParserDescriptor<C, Role> = createParser<C, Role> { name, command ->
command.roles[name]?.let { ArgumentParseResult.success(it) }
?: context.ifSuggestion(name) {
(context.interaction.interactionEvent as? GuildInteraction)?.user?.roles?.firstOrNull()
?.let { ArgumentParseResult.success(it) }
}
}

/**
* Returns a parser which extracts an [Entity].
*
* The parsed mentionable may not be accessed from the context during suggestion generation.
*/
public fun <C : Any> mentionableParser(): ParserDescriptor<C, Entity> =
createParser<C, Entity> { name, command, context ->
command.users[name]?.let { ArgumentParseResult.success(it) }
?: context.ifSuggestion(name) {
ArgumentParseResult.success(context.interaction.interactionEvent.interaction.user)
}
}
createParser<C, Entity> { name, command -> command.users[name]?.let { ArgumentParseResult.success(it) } }

/**
* Returns a parser which extracts an [Attachment].
*
* The parsed attachment may not be accessed from the context during suggestion generation.
*/
public fun <C : Any> attachmentParser(): ParserDescriptor<C, Attachment> =
createParser<C, Attachment> { name, command, _ ->
command.attachments[name]?.let { ArgumentParseResult.success(it) }
?: ArgumentParseResult.failure(NullPointerException(name))
}
createParser<C, Attachment> { name, command -> command.attachments[name]?.let { ArgumentParseResult.success(it) } }

private inline fun <C : Any, reified T : Any> createParser(parser: KordParser<C, T>): ParserDescriptor<C, T> =
parser.asParserDescriptor<C, T>()

// TODO(City): This is a terrible hack and should be removed.
private suspend inline fun <C : Any, T : Any> CommandContext<C>.ifSuggestion(
name: String,
crossinline body: suspend () -> ArgumentParseResult<T>?
): ArgumentParseResult<T> {
val result = if (isSuggestions) {
body()
} else {
null
}
return result ?: ArgumentParseResult.failure(NullPointerException(name))
}
private inline fun <C : Any, reified T : Any> createParser(
noinline extract: suspend (name: String, command: InteractionCommand) -> ArgumentParseResult<T>?
): ParserDescriptor<C, T> = ParserDescriptor.of(KordParser(extract), T::class.java)
}

/**
* Returns the result of extracting the argument from the given mapping.
*/
public suspend fun extract(name: String, command: InteractionCommand, context: CommandContext<C>): ArgumentParseResult<T>

override suspend fun invoke(commandContext: CommandContext<C>, commandInput: CommandInput): ArgumentParseResult<T> = extract(
commandInput.readString(),
commandContext.interaction.command,
commandContext
)
override fun parseNullable(
commandContext: CommandContext<C>,
commandInput: CommandInput
): CompletableFuture<ArgumentParseResult<T>?> = GlobalScope.future {
extract(commandInput.readString(), commandContext.interaction.command)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public data class AnnotatedCommands(
}
}

@Command("cat meow <cat> <target>")
@Command("cat meow <target> <cat>")
public suspend fun meow(interaction: KordInteraction, @Argument(suggestions = "cats") cat: String, target: Entity) {
interaction.deferPublicResponse().respond {
content = "$cat meows at <@${target.id}>"
Expand Down

0 comments on commit a5229f1

Please sign in to comment.