Add a confirmation system.

This commit is contained in:
Alexander Söderberg 2020-09-22 23:03:11 +02:00
parent 77cbf15faa
commit 3f59a81836
No known key found for this signature in database
GPG key ID: C0207FF7EA146678
8 changed files with 266 additions and 3 deletions

View file

@ -32,7 +32,9 @@ import com.intellectualsites.commands.arguments.parser.ArgumentParser;
import com.intellectualsites.commands.arguments.parser.ParserParameters;
import com.intellectualsites.commands.arguments.parser.StandardParameters;
import com.intellectualsites.commands.execution.CommandExecutionHandler;
import com.intellectualsites.commands.extra.confirmation.CommandConfirmationManager;
import com.intellectualsites.commands.meta.CommandMeta;
import com.intellectualsites.commands.meta.SimpleCommandMeta;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@ -140,10 +142,15 @@ public final class AnnotationParser<C> {
/* Determine command name */
final String commandToken = commandMethod.value().split(" ")[0].split("\\|")[0];
@SuppressWarnings("ALL") final CommandManager manager = this.manager;
final SimpleCommandMeta.Builder metaBuilder = SimpleCommandMeta.builder()
.with(this.metaFactory.apply(method.getAnnotations()));
if (method.isAnnotationPresent(Confirmation.class)) {
metaBuilder.with(CommandConfirmationManager.CONFIRMATION_REQUIRED_META, "true");
}
@SuppressWarnings("ALL")
Command.Builder builder = manager.commandBuilder(commandToken,
tokens.get(commandToken).getMinor(),
this.metaFactory.apply(method.getAnnotations()));
metaBuilder.build());
final Collection<ArgumentParameterPair> arguments = this.argumentExtractor.apply(method);
final Map<String, CommandArgument<C, ?>> commandArguments = Maps.newHashMap();
final Map<CommandArgument<C, ?>, String> argumentDescriptions = Maps.newHashMap();

View file

@ -0,0 +1,37 @@
//
// MIT License
//
// Copyright (c) 2020 Alexander Söderberg
//
// 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 com.intellectualsites.commands.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Require confirmation for the command
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Confirmation {
}

View file

@ -58,7 +58,7 @@ import java.util.stream.Collectors;
* These arguments may be {@link StaticArgument literals} or variables. Command may either be required
* or optional, with the requirement that no optional argument precedes a required argument.
* <p>
* The {@link Command commands} are stored in this tree and the nodes of tree consists of the command
* The {@link Command commands} are stored in this tree and the nodes of tree consists of the command
* {@link CommandArgument arguments}. Each leaf node of the tree should containing a fully parsed
* {@link Command}. It is thus possible to walk the tree and determine whether or not the supplied
* input from a command sender constitutes a proper command.
@ -548,7 +548,7 @@ public final class CommandTree<C> {
public Node<CommandArgument<C, ?>> getNamedNode(@Nullable final String name) {
for (final Node<CommandArgument<C, ?>> node : this.getRootNodes()) {
if (node.getValue() != null && node.getValue() instanceof StaticArgument) {
final StaticArgument<C> staticArgument = (StaticArgument<C>) node.getValue();
@SuppressWarnings("unchecked") final StaticArgument<C> staticArgument = (StaticArgument<C>) node.getValue();
for (final String alias : staticArgument.getAliases()) {
if (alias.equalsIgnoreCase(name)) {
return node;

View file

@ -44,6 +44,10 @@ public final class StandardParameters {
* Command description
*/
public static final ParserParameter<String> DESCRIPTION = create("description", TypeToken.of(String.class));
/**
* Command confirmation
*/
public static final ParserParameter<Boolean> CONFIRMATION = create("confirmation", TypeToken.of(Boolean.class));
/**
* Command completions
*/

View file

@ -0,0 +1,160 @@
//
// MIT License
//
// Copyright (c) 2020 Alexander Söderberg
//
// 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 com.intellectualsites.commands.extra.confirmation;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.intellectualsites.commands.CommandManager;
import com.intellectualsites.commands.execution.CommandExecutionHandler;
import com.intellectualsites.commands.execution.postprocessor.CommandPostprocessingContext;
import com.intellectualsites.commands.execution.postprocessor.CommandPostprocessor;
import com.intellectualsites.commands.meta.SimpleCommandMeta;
import com.intellectualsites.services.types.ConsumerService;
import javax.annotation.Nonnull;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
/**
* Manager for the command confirmation system that enables the ability to add "confirmation" requirements to commands,
* such that they need to be confirmed in order to be executed.
* <p>
* To use the confirmation system, the confirmation post processor needs to be added. To to this, use
* {@link #registerConfirmationProcessor(CommandManager)}. After this is done, a confirmation command has
* been added. To do this, create a command builder and attach {@link #createConfirmationExecutionHandler()}.
* <p>
* To require a command to be confirmed, use {@link #decorate(SimpleCommandMeta.Builder)} on the command meta builder.
*
* @param <C> Command sender type
*/
public class CommandConfirmationManager<C> {
/**
* Meta data stored for commands that require confirmation
*/
public static final String CONFIRMATION_REQUIRED_META = "__REQUIRE_CONFIRMATION__";
private final Consumer<CommandPostprocessingContext<C>> notifier;
private final Consumer<C> errorNotifier;
private final Cache<C, CommandPostprocessingContext<C>> pendingCommands;
/**
* Create a new confirmation manager instance
*
* @param timeout Timeout value
* @param timeoutTimeUnit Timeout time unit
* @param notifier Notifier that gets called when a command gets added to the queue
* @param errorNotifier Notifier that gets called when someone tries to confirm a command with nothing in the queue
*/
public CommandConfirmationManager(final long timeout,
@Nonnull final TimeUnit timeoutTimeUnit,
@Nonnull final Consumer<CommandPostprocessingContext<C>> notifier,
@Nonnull final Consumer<C> errorNotifier) {
this.notifier = notifier;
this.errorNotifier = errorNotifier;
this.pendingCommands = CacheBuilder.newBuilder().expireAfterWrite(timeout, timeoutTimeUnit).concurrencyLevel(1).build();
}
private void notifyConsumer(@Nonnull final CommandPostprocessingContext<C> context) {
this.notifier.accept(context);
}
private void addPending(@Nonnull final CommandPostprocessingContext<C> context) {
this.pendingCommands.put(context.getCommandContext().getSender(), context);
}
/**
* Get a pending context if one is stored for the sender
*
* @param sender Sender
* @return Optional containing the post processing context if one has been stored, else {@link Optional#empty()}
*/
@Nonnull
public Optional<CommandPostprocessingContext<C>> getPending(@Nonnull final C sender) {
return Optional.ofNullable(this.pendingCommands.getIfPresent(sender));
}
/**
* Decorate a simple command meta builder, to require confirmation for a command
*
* @param builder Command meta builder
* @return Builder instance
*/
@Nonnull
public SimpleCommandMeta.Builder decorate(@Nonnull final SimpleCommandMeta.Builder builder) {
return builder.with(CONFIRMATION_REQUIRED_META, "true");
}
/**
* Register the confirmation processor in the command manager
*
* @param manager Command manager
*/
public void registerConfirmationProcessor(@Nonnull final CommandManager<C> manager) {
manager.registerCommandPostProcessor(new CommandConfirmationPostProcessor());
}
/**
* Create an execution handler for a confirmation command
*
* @return Handler for a confirmation command
*/
@Nonnull
public CommandExecutionHandler<C> createConfirmationExecutionHandler() {
return context -> {
final Optional<CommandPostprocessingContext<C>> pending = this.getPending(context.getSender());
if (pending.isPresent()) {
final CommandPostprocessingContext<C> postprocessingContext = pending.get();
postprocessingContext.getCommand()
.getCommandExecutionHandler()
.execute(postprocessingContext.getCommandContext());
} else {
this.errorNotifier.accept(context.getSender());
}
};
}
private final class CommandConfirmationPostProcessor implements CommandPostprocessor<C> {
@Override
public void accept(@Nonnull final CommandPostprocessingContext<C> context) {
if (!context.getCommand()
.getCommandMeta()
.getOrDefault(CONFIRMATION_REQUIRED_META, "false")
.equals("true")) {
return;
}
/* Add it to the "queue" */
addPending(context);
/* Notify the consumer that a confirmation is required */
notifyConsumer(context);
/* Interrupt */
ConsumerService.interrupt();
}
}
}

View file

@ -0,0 +1,28 @@
//
// MIT License
//
// Copyright (c) 2020 Alexander Söderberg
//
// 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.
//
/**
* Confirmation system
*/
package com.intellectualsites.commands.extra.confirmation;

View file

@ -107,6 +107,18 @@ public class SimpleCommandMeta extends CommandMeta {
private Builder() {
}
/**
* Copy all values from another command meta instance
*
* @param commandMeta Existing instance
* @return Builder instance
*/
@Nonnull
public Builder with(@Nonnull final CommandMeta commandMeta) {
commandMeta.getAll().forEach(this::with);
return this;
}
/**
* Store a new key-value pair in the meta map
*

View file

@ -26,6 +26,7 @@ package com.intellectualsites.commands;
import com.intellectualsites.commands.annotations.AnnotationParser;
import com.intellectualsites.commands.annotations.Argument;
import com.intellectualsites.commands.annotations.CommandMethod;
import com.intellectualsites.commands.annotations.Confirmation;
import com.intellectualsites.commands.annotations.Description;
import com.intellectualsites.commands.annotations.specifier.Completions;
import com.intellectualsites.commands.annotations.specifier.Range;
@ -43,6 +44,7 @@ import com.intellectualsites.commands.bukkit.CloudBukkitCapabilities;
import com.intellectualsites.commands.bukkit.parsers.WorldArgument;
import com.intellectualsites.commands.execution.AsynchronousCommandExecutionCoordinator;
import com.intellectualsites.commands.execution.CommandExecutionCoordinator;
import com.intellectualsites.commands.extra.confirmation.CommandConfirmationManager;
import com.intellectualsites.commands.meta.SimpleCommandMeta;
import com.intellectualsites.commands.paper.PaperCommandManager;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
@ -61,6 +63,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
@ -100,6 +103,14 @@ public final class BukkitTest extends JavaPlugin {
getLogger().warning("Failed to register asynchronous command completions: " + e.getMessage());
}
final CommandConfirmationManager<CommandSender> confirmationManager = new CommandConfirmationManager<>(
30,
TimeUnit.SECONDS,
c -> c.getCommandContext().getSender().sendMessage(ChatColor.RED + "Oh no. Confirm using /cloud confirm!"),
c -> c.sendMessage(ChatColor.RED + "You don't have any pending commands!")
);
confirmationManager.registerConfirmationProcessor(mgr);
final AnnotationParser<CommandSender> annotationParser
= new AnnotationParser<>(mgr, CommandSender.class, p ->
BukkitCommandMetaBuilder.builder().withDescription(p.get(StandardParameters.DESCRIPTION,
@ -185,6 +196,9 @@ public final class BukkitTest extends JavaPlugin {
.build())
.command(mgr.commandBuilder("annotationass").handler(c -> c.getSender()
.sendMessage(ChatColor.YELLOW + "Du e en ananas!")).build())
.command(mgr.commandBuilder("cloud")
.literal("confirm")
.handler(confirmationManager.createConfirmationExecutionHandler()).build())
.command(mgr.commandBuilder("cloud")
.literal("help")
.argument(StringArgument.<CommandSender>newBuilder("query").greedy()
@ -207,6 +221,7 @@ public final class BukkitTest extends JavaPlugin {
player.sendMessage(ChatColor.GOLD + "Your input was: " + ChatColor.AQUA + input + ChatColor.GREEN + " (" + number + ")");
}
@Confirmation
@CommandMethod("cloud debug")
private void doHelp() {
final Set<CloudBukkitCapabilities> capabilities = this.mgr.queryCapabilities();