Skip to content

Add command confirmation system for sensitive actions #344

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@
import net.minecraft.util.Tuple;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.Level;
import net.mrliam2614.flan.commands.CommandCallback;
import net.mrliam2614.flan.commands.CommandConfirmation;

import java.util.ArrayList;
import java.util.Collection;
Expand All @@ -67,6 +69,9 @@
public class CommandClaim {

public static void register(CommandDispatcher<CommandSourceStack> dispatcher, CommandBuildContext buildContext, boolean dedicated) {
//MrLiam2614 - Register Command Confirmation
new CommandConfirmation();

LiteralArgumentBuilder<CommandSourceStack> builder = Commands.literal("flan")
.then(Commands.literal("reload").requires(src -> PermissionNodeHandler.INSTANCE.perm(src, PermissionNodeHandler.cmdReload, true)).executes(CommandClaim::reloadConfig))
.then(Commands.literal("add").requires(src -> PermissionNodeHandler.INSTANCE.perm(src, PermissionNodeHandler.claimCreate))
Expand Down Expand Up @@ -198,7 +203,8 @@ public static void register(CommandDispatcher<CommandSourceStack> dispatcher, Co
.executes(src -> CommandClaim.removeClaimListEntries(src, CustomInteractListScreenHandler.Type.ENTITYATTACK))))
.then(Commands.literal(CustomInteractListScreenHandler.Type.ENTITYUSE.commandKey)
.then(Commands.argument("entry", ResourceOrTagKeyArgument.resourceOrTagKey(Registries.ENTITY_TYPE)).suggests((src, b) -> CommandHelpers.claimEntryListSuggestion(src, b, CustomInteractListScreenHandler.Type.ENTITYUSE))
.executes(src -> CommandClaim.removeClaimListEntries(src, CustomInteractListScreenHandler.Type.ENTITYUSE)))))
.executes(src -> CommandClaim.removeClaimListEntries(src, CustomInteractListScreenHandler.Type.ENTITYUSE))))))
.then(Commands.literal("confirm").requires(src -> PermissionNodeHandler.INSTANCE.perm(src, PermissionNodeHandler.cmdConfirm, true)).executes(CommandClaim::confirmCommand)
);
builder.then(Commands.literal("help").executes(ctx -> CommandHelp.helpMessage(ctx, 0, builder.getArguments()))
.then(Commands.argument("page", IntegerArgumentType.integer()).executes(ctx -> CommandHelp.helpMessage(ctx, builder.getArguments())))
Expand Down Expand Up @@ -581,15 +587,21 @@ private static int switchAdminMode(CommandContext<CommandSourceStack> context) t
}

private static int adminDelete(CommandContext<CommandSourceStack> context) {
CommandSourceStack src = context.getSource();
ClaimStorage storage = ClaimStorage.get(src.getLevel());
Claim claim = storage.getClaimAt(BlockPos.containing(src.getPosition()));
if (claim == null) {
src.sendSuccess(() -> PermHelper.simpleColoredText(ConfigHandler.LANG_MANAGER.get("noClaim"), ChatFormatting.RED), false);
return 0;
}
storage.deleteClaim(claim, true, EnumEditMode.DEFAULT, src.getLevel());
src.sendSuccess(() -> PermHelper.simpleColoredText(ConfigHandler.LANG_MANAGER.get("deleteClaim"), ChatFormatting.RED), true);
CommandConfirmation.INSTANCE.requestConfirmation(context, new CommandCallback() {
@Override
public int onConfirm(CommandContext<CommandSourceStack> context) {
CommandSourceStack src = context.getSource();
ClaimStorage storage = ClaimStorage.get(src.getLevel());
Claim claim = storage.getClaimAt(BlockPos.containing(src.getPosition()));
if (claim == null) {
src.sendSuccess(() -> PermHelper.simpleColoredText(ConfigHandler.LANG_MANAGER.get("noClaim"), ChatFormatting.RED), false);
return 0;
}
storage.deleteClaim(claim, true, EnumEditMode.DEFAULT, src.getLevel());
src.sendSuccess(() -> PermHelper.simpleColoredText(ConfigHandler.LANG_MANAGER.get("deleteClaim"), ChatFormatting.RED), true);
return Command.SINGLE_SUCCESS;
}
});
return Command.SINGLE_SUCCESS;
}

Expand Down Expand Up @@ -1068,4 +1080,11 @@ private static <T> String addClaimListEntry(CommandContext<CommandSourceStack> c
});
return value.asPrintable();
}


/* Confirm Command by MrLiam2614 */
private static int confirmCommand(CommandContext<CommandSourceStack> context) throws CommandSyntaxException {
ServerPlayer player = context.getSource().getPlayerOrException();
return CommandConfirmation.INSTANCE.confirmCommand(context);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,8 @@ private void loadDefault() {
this.defaultTranslation.put("removeIgnoreEntry", "Removed %1$s from the claims ignore list %2$s");

this.defaultTranslation.put("wiki", "For more info check out the wiki:");
this.defaultTranslation.put("confirmMessage", "Run the command '/flan confirm' if you are sure you want to execute this command.");
this.defaultTranslation.put("confirmCancelled", "Your command has not been sent.");

for (ClaimPermission perm : PermissionManager.INSTANCE.getAll()) {
this.defaultTranslation.put(perm.translationKey(), this.capitalize(perm.getId().getPath()));
Expand Down Expand Up @@ -260,6 +262,7 @@ private void loadDefault() {
this.defaultTranslationArray.put("command.listAdminClaims", new String[]{"listAdminClaim", "Lists all admin claims in the current world."});
this.defaultTranslationArray.put("command.adminDelete", new String[]{"adminDelete [all <player>]", "Force deletes the current claim or deletes all claims from the specified player."});
this.defaultTranslationArray.put("command.giveClaimBlocks", new String[]{"giveClaimBlocks <amount>", "Gives a player additional claim blocks."});
this.defaultTranslationArray.put("command.confirm", new String[]{"confirm", "Confirm a command to send it."});
}

private String capitalize(String s) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ public interface PermissionNodeHandler {
String permClaimBlocksCap = "flan.claim.blocks.cap";
String permClaimBlocksBonus = "flan.claim.blocks.bonus";

String cmdConfirm = "flan.command.confirm";

PermissionNodeHandler INSTANCE = Flan.getPlatformInstance(PermissionNodeHandler.class,
"io.github.flemmli97.flan.fabric.platform.integration.permissions.PermissionNodeHandlerImpl",
"io.github.flemmli97.flan.forge.platform.integration.permissions.PermissionNodeHandlerImpl");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package net.mrliam2614.flan.commands;

import com.mojang.brigadier.context.CommandContext;
import io.github.flemmli97.flan.claim.PermHelper;
import io.github.flemmli97.flan.config.ConfigHandler;
import net.minecraft.ChatFormatting;
import net.minecraft.commands.CommandSourceStack;

/**
* Interface representing a callback mechanism for commands that require user confirmation before proceeding.
* <p>
* Provides a method to handle the confirmation action (`onConfirm`) and a default implementation to handle the
* denial or cancellation of the operation (`onDeny`).
* <p>
* If the command is called not a player (console) is automatically called the (`onConfirm`)
*/
public interface CommandCallback {

/**
* Called when the command is confirmed by the user.
*
* @param context The {@link CommandContext} for the current command execution,
* containing information about the source and arguments of the command.
* @return An integer value typically used to indicate the result of command execution.
*/
int onConfirm(CommandContext<CommandSourceStack> context);

/**
* Called when the command is denied or canceled by the user.
* This method provides a default implementation that sends a cancellation message
* to the command source using the configured language manager and displays it in red formatting.
*
* @param context The {@link CommandContext} for the current command execution,
* containing information about the source and arguments of the command.
*/
default void onDeny(CommandContext<CommandSourceStack> context) {
context.getSource().sendSuccess(() ->
PermHelper.simpleColoredText(
ConfigHandler.LANG_MANAGER.get("confirmCancelled"),
ChatFormatting.RED
), true
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package net.mrliam2614.flan.commands;

import com.mojang.brigadier.Command;
import com.mojang.brigadier.context.CommandContext;
import io.github.flemmli97.flan.claim.PermHelper;
import io.github.flemmli97.flan.config.ConfigHandler;
import net.minecraft.ChatFormatting;
import net.minecraft.commands.CommandSourceStack;

import java.util.HashMap;
import java.util.UUID;

/**
* The {@code CommandConfirmation} class manages command operations that require user confirmation before being executed.
* It maintains a registry of commands awaiting confirmation and provides mechanisms for requesting and confirming such commands.
*/
public class CommandConfirmation {
public static CommandConfirmation INSTANCE;
private HashMap<UUID, CommandCallback> awaitingConfirmation;

public CommandConfirmation() {
INSTANCE = this;
awaitingConfirmation = new HashMap<>();
}

/**
* Requests confirmation from a player for a specific command operation. If the player is active, it adds the command
* callback to a list awaiting confirmation and sends a confirmation message to the player.
*
* @param context The {@link CommandContext} containing information about the command execution,
* including the source of the command (player or console).
* @param commandCallback The {@link CommandCallback} instance representing the action to be executed upon confirmation.
*/
public void requestConfirmation(CommandContext<CommandSourceStack> context, CommandCallback commandCallback) {
if(context.getSource().getPlayer() == null){
return;
}
UUID uuid = context.getSource().getPlayer().getUUID();
awaitingConfirmation.put(uuid, commandCallback);
context.getSource().sendSuccess(() -> PermHelper.simpleColoredText(ConfigHandler.LANG_MANAGER.get("confirmMessage"), ChatFormatting.AQUA), true);
}

/**
* Processes a confirmation action for a command that is awaiting user confirmation.
* If the command source is eligible (e.g., a player), it retrieves and executes the
* associated {@link CommandCallback} if present.
*
* @param context The {@link CommandContext} containing information about the command execution,
* including the source of the command (e.g., player or console).
* @return An integer value indicating the outcome of the confirmed command execution.
* Returns 0 if no confirmation action was processed, such as when the source
* is not a player or no command callback is awaiting confirmation.
*/
public int confirmCommand(CommandContext<CommandSourceStack> context){
if(context.getSource().getPlayer() == null){
return 0;
}
UUID uuid = context.getSource().getPlayer().getUUID();
CommandCallback commandCallback = awaitingConfirmation.remove(uuid);
if(commandCallback == null){
return 0;
}
return commandCallback.onConfirm(context);
}
}
2 changes: 2 additions & 0 deletions common/src/main/resources/data/flan/lang/it_it.json
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@
"setLeaveMessage": "Imposta il titolo di uscita a %1$s",
"setLeaveSubMessage": "Imposta il sottotitolo di uscita a %1$s",
"wiki": "Per più informazioni controlla la wiki:",
"confirmMessage": "Run the command '/flan confirm' if you are sure you want to execute this command.",
"confirmCancelled": "Your command has not been sent.",
"command.help": [
"help <pagina> | (cmd <comando>)",
"Mostra tutti i comandi o le info disponibili per il comandi fornito."
Expand Down