From 87f78ccbeab166679c7faff92152d2a29cff3501 Mon Sep 17 00:00:00 2001 From: anthorob Date: Sun, 9 Mar 2025 15:29:43 -0400 Subject: [PATCH] finish abstraction for StepTask --- .../core/command/AbstractAdminCommand.java | 7 - .../core/tasks/AbstractTask.java | 5 - .../netneutrality/core/tasks/IStepTask.java | 147 ++++++++++++++++++ .../core/tasks/impl/StepTask.java | 147 ++++++++++++------ 4 files changed, 250 insertions(+), 56 deletions(-) create mode 100644 src/main/java/me/bobert/netneutrality/core/tasks/IStepTask.java diff --git a/src/main/java/me/bobert/netneutrality/core/command/AbstractAdminCommand.java b/src/main/java/me/bobert/netneutrality/core/command/AbstractAdminCommand.java index 89e901c..b98b3e1 100644 --- a/src/main/java/me/bobert/netneutrality/core/command/AbstractAdminCommand.java +++ b/src/main/java/me/bobert/netneutrality/core/command/AbstractAdminCommand.java @@ -5,8 +5,6 @@ package me.bobert.netneutrality.core.command; * https://discord.gg/qUa6zNFZsd */ -import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions; - public abstract class AbstractAdminCommand extends AbstractCommand { @Override @@ -14,11 +12,6 @@ public abstract class AbstractAdminCommand extends AbstractCommand { return true; } - @Override - public DefaultMemberPermissions getSlashCommandPermission() { - return DefaultMemberPermissions.DISABLED; - } - @Override public CommandCategory getCategory() { return CommandCategory.ADMINISTRATION; diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/AbstractTask.java b/src/main/java/me/bobert/netneutrality/core/tasks/AbstractTask.java index a6b3820..ba5d5a1 100644 --- a/src/main/java/me/bobert/netneutrality/core/tasks/AbstractTask.java +++ b/src/main/java/me/bobert/netneutrality/core/tasks/AbstractTask.java @@ -17,11 +17,6 @@ public abstract class AbstractTask implements Task { creationTime = OffsetDateTime.now(); } - @Override - public boolean isInsertable() { - return true; - } - @Override public OffsetDateTime getCreationTime() { return creationTime; diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/IStepTask.java b/src/main/java/me/bobert/netneutrality/core/tasks/IStepTask.java new file mode 100644 index 0000000..fee1dbb --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/tasks/IStepTask.java @@ -0,0 +1,147 @@ +package me.bobert.netneutrality.core.tasks; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +public interface IStepTask { + + /** + * Defines the type of input expected for a step. + */ + enum StepType { + /** + * Expects a string input from the user. + */ + STRING, + + /** + * Expects a valid role ID for the current guild. + * The role must exist in the guild where the command was executed. + */ + ROLE, + + /** + * Expects a valid emoji. + */ + EMOJI + } + + class StepResult { + private enum ResultType { + SUCCESS, FAILURE + } + + private final ResultType type; + private final String message; + private final Task nextStep; + + /** + * Represents the result of a step execution. Each step can either succeed, fail, or transition to another step. + */ + private StepResult(ResultType type, String message, Task nextStep) { + this.type = type; + this.message = message; + this.nextStep = nextStep; + } + + /** + * Indicates that the step was completed successfully, with an optional follow-up step. + * + * @param message the success message + * @param nextStep the next step in the workflow, or null if this is the final step + * @return a successful result + */ + public static StepResult success(String message, Task nextStep) { + return new StepResult(ResultType.SUCCESS, message, nextStep); + } + + /** + * Indicates that the step was completed successfully. + * + * @param message the success message + * @return a successful result without a next step + */ + public static StepResult success(String message) { + return new StepResult(ResultType.SUCCESS, message, null); + } + + /** + * Indicates that the step has failed. + * + * @param message the failure message + * @return a failed result + */ + public static StepResult failure(String message) { + return new StepResult(ResultType.FAILURE, message, null); + } + + /** + * Moves directly to the next step without displaying a message. + * + * @param nextTask the next task in the workflow + * @return a successful result that advances to the next step + */ + public static StepResult next(Task nextTask) { + return new StepResult(ResultType.SUCCESS, null, nextTask); + } + + /** + * Gets the message associated with the step result. + * + * @return the result message, or null if there is no message + */ + public String getMessage() { + return message; + } + + /** + * Gets the next step in the task workflow. + * + * @return the next step, or null if this is the last step + */ + public Task getNextStep() { + return nextStep; + } + + /** + * Checks whether the step execution was successful. + * + * @return true if the step was successful, false otherwise + */ + public boolean isSuccess() { + return type == ResultType.SUCCESS; + } + } + + /** + * Gets the type of step input expected. + * + * @return the step type + */ + StepType getStepType(); + + /** + * Provides a general description of the step. This will be displayed before asking the question. + * + * @return the step description + */ + String getStepDescription(); + + /** + * Provides the specific question for this step. + * + * @return the step question + */ + String getStepQuestion(); + + /** + * Executes the step using the provided answer. + * + * @param answer the user's input, validated against the expected step type + * @return the result of executing this step + */ + StepResult executeStep(Object answer); +} diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/impl/StepTask.java b/src/main/java/me/bobert/netneutrality/core/tasks/impl/StepTask.java index e3c9139..0c7f92a 100644 --- a/src/main/java/me/bobert/netneutrality/core/tasks/impl/StepTask.java +++ b/src/main/java/me/bobert/netneutrality/core/tasks/impl/StepTask.java @@ -10,23 +10,26 @@ import me.bobert.netneutrality.core.context.CommandContext; import me.bobert.netneutrality.core.context.MessageContext; import me.bobert.netneutrality.core.context.TaskContext; import me.bobert.netneutrality.core.tasks.AbstractTask; +import me.bobert.netneutrality.core.tasks.IStepTask; import me.bobert.netneutrality.core.tasks.TaskManager; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.Role; import net.dv8tion.jda.api.interactions.components.buttons.Button; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import net.fellbaum.jemoji.Emoji; +import net.fellbaum.jemoji.EmojiManager; import java.time.OffsetDateTime; +import java.util.Optional; import java.util.UUID; -public abstract class StepTask extends AbstractTask { - private static final Logger logger = LoggerFactory.getLogger(StepTask.class); - - public enum StepType { - STRING, ROLE, EMOJI - } - +/** + * Represents a task that consists of multiple sequential steps. The task starts with an initial message (question) + * and expects a user response. Each step can lead to another step, forming a workflow. + */ +public abstract class StepTask extends AbstractTask implements IStepTask { + private final Object stepLock = new Object(); + private CommandContext commandContext; private Guild guild; private String id = UUID.randomUUID().toString(); @@ -35,45 +38,76 @@ public abstract class StepTask extends AbstractTask { return id; } + @Override + public boolean isInsertable() { + return true; + } + + public Guild getGuild() { + return guild; + } + @Override public void replyToInteraction(CommandContext context) { + synchronized (stepLock) { + this.commandContext = context; + } + if (context.isFromGuild()) { guild = context.getGuild(); } if (guild == null && getStepType() == StepType.ROLE) { - throw new IllegalArgumentException("Step task if of type role but it was inserted in a non-guild context"); + throw new IllegalArgumentException("Step task is of type role but it was inserted in a non-guild context"); } - if (inDM()) { - context.replyEphemeral("Please follow the instructions sent in DM."); - context.sendMessageInPrivate(getStepDescription() + "\n" + getStepQuestion(), asyncContext -> assignId(asyncContext.getMessageId())); - } else { - context.reply(getStepQuestion(), asyncContext -> id = asyncContext.getMessageId()); - } - - System.out.println("step id : " + id); + context.replyEphemeral("Please follow the instructions sent in DM."); + context.sendMessageInPrivate(getStepDescription() + "\n\n" + getStepQuestion(), + asyncContext -> assignId(asyncContext.getMessageId())); } - + @Override public Lifecycle execute(TaskContext context) { - if (context instanceof MessageContext messageContext) { - Message message = messageContext.getMessage(); - String answer = message.getContentRaw(); - - if (validateAnswer(answer)) { - StepTask nextStep = executeStep(answer); - - //TODO: to finish + synchronized (stepLock) { + if (!context.getDiscordUser().equals(commandContext.getDiscordUser())) { + return Lifecycle.KEEP; } } - return Lifecycle.REMOVE_SELF; + if (context instanceof MessageContext messageContext) { + Message message = messageContext.getMessage(); + String input = message.getContentRaw(); + + + Object answer = validateInput(context, input); + if (answer == null) { + return Lifecycle.KEEP; + } + + StepResult result = executeStep(answer); + if (!result.isSuccess()) { + context.replyEphemeral(result.getMessage()); + return Lifecycle.KEEP; + } + + if (result.getMessage() != null) { + context.replyEphemeral(result.getMessage()); + } + + if (result.getNextStep() != null) { + result.getNextStep().insert(commandContext); + } + + return Lifecycle.REMOVE_SELF; + } + + throw new IllegalArgumentException("Step task is not of type MessageContext"); } @Override public boolean isExpired() { - return getCreationTime().plusMinutes(30).isBefore(OffsetDateTime.now()); + // is it really needed to expire this type of task? + return getCreationTime().plusMinutes(60).isBefore(OffsetDateTime.now()); } @Override @@ -81,8 +115,44 @@ public abstract class StepTask extends AbstractTask { return null; } - private boolean validateAnswer(String answer) { - return false; + /** + * Validates the user input against the expected step type. + * + * @param context the task execution context + * @param input the user-provided input + * @return the parsed input if valid, or null if invalid + */ + private Object validateInput(TaskContext context, String input) { + switch (getStepType()) { + case ROLE -> { + try { + Role role = guild.getRoleById(input); + if (role == null) { + context.replyEphemeral("The role doesn't exist"); + return null; + } + + return role; + } catch (NumberFormatException e) { + context.replyEphemeral("Not a valid role ID"); + return null; + } + } + case EMOJI -> { + Optional parsed = EmojiManager.getEmoji(input); + + if (parsed.isPresent()) { + return parsed.get().getEmoji(); + } + + context.replyEphemeral("Not a valid emoji"); + return null; + } + case STRING -> { + return input; + } + default -> throw new IllegalStateException("Unknown step type: " + getStepType()); + } } private void assignId(String newId) { @@ -90,21 +160,10 @@ public abstract class StepTask extends AbstractTask { throw new IllegalArgumentException("Task ID cannot be null or empty."); } + String oldId = id; synchronized (this) { this.id = newId; - TaskManager.getInstance().activateTask(this); + TaskManager.getInstance().activateTask(this, oldId); } - - this.id = newId; } - - public abstract boolean inDM(); - - public abstract StepType getStepType(); - - public abstract String getStepDescription(); - - public abstract String getStepQuestion(); - - public abstract StepTask executeStep(String answer); }